ios - 当主线程阻塞时,如何获得断点/日志/增加的可见性?

标签 ios multithreading macos debugging io

在对UI响应性的永无止境的追求中,我想对主线程执行阻塞操作的情况有更多的了解。

我正在寻找某种“ Debug模式”或额外的代码,钩子(Hook)或其他任何东西,借此我可以设置一个断点/日志/会被命中的东西,并允许我检查主线程“自愿”运行的情况。除了在运行循环结束时进入空闲状态以外,其他原因用于I/O(或其他任何原因)阻止。

过去,我使用runloop观察器查看了runloop周期的时钟持续时间,这对于发现问题很有用,但是到您可以检查的时候,要知道它的作用为时已晚,因为您的代码已经在运行循环的那个周期中完成了运行。

我意识到UIKit/AppKit所执行的操作仅是主线程的操作,这将导致I/O并导致主线程被阻塞,因此,在某种程度上,这是毫无希望的(例如,访问粘贴板似乎是可能会阻塞,仅执行主线程的操作),但总有总比没有好。

有人有什么好主意吗?似乎是有用的东西。在理想情况下,当应用程序的代码在Runloop上处于 Activity 状态时,您永远不会希望阻塞主线程,而这样的事情对于尽可能接近该目标将非常有帮助。

最佳答案

因此,我决定在本周末回答我自己的问题。记录下来,这种努力变得非常复杂,因此,正如肯德尔·赫尔姆斯特泰尔·格伦(Kendall Helmstetter Glen)所建议的那样,大多数阅读此问题的人可能应该只是迷惑于Instruments。对于人群中的受虐狂,请继续阅读!

从重述问题开始是最容易的。这是我想出的:

I want to be alerted to long periods of time spent in syscalls/mach_msg_trap that are not legitimate idle time. "Legitimate idle time" is defined as time spent in mach_msg_trap waiting for the next event from the OS.



同样重要的是,我并不关心花费很长时间的用户代码。使用Instruments的Time Profiler工具可以很容易地诊断和理解该问题。我想特别了解一下阻塞时间。虽然确实可以使用Time Profiler诊断出阻塞的时间,但我发现很难将其用于此目的。同样,系统跟踪仪器也可用于此类调查,但粒度极细且复杂。我想要更简单的东西-更针对此特定任务。

从一开始就可以明显看出,这里选择的工具将是Dtrace。
我首先使用CFRunLoopkCFRunLoopAfterWaiting触发的kCFRunLoopBeforeWaiting观察器开始。调用我的kCFRunLoopBeforeWaiting处理程序将指示“合法的空闲时间”的开始,而kCFRunLoopAfterWaiting处理程序将向我发出合法等待已结束的信号。我将使用Dtrace pid提供程序捕获对这些函数的调用,以将合法的空闲与阻止空闲的方式进行排序。

这种方法使我起步,但最终证明是有缺陷的。最大的问题是,许多AppKit操作是同步的,因为它们会阻止UI中的事件处理,但实际上会将RunLoop调低到调用堆栈中。 RunLoop的那些旋转不是“合法的”空闲时间(出于我的目的),因为用户在这段时间内无法与UI进行交互。可以肯定的是,它们很有值(value)-想象一下后台线程上的运行循环,观看一堆面向RunLoop的I/O,但是当主线程上发生UI时,UI仍然处于阻塞状态。例如,我将以下代码放入IBAction中,并通过按钮触发了它:
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: @"http://www.google.com/"] 
                                                   cachePolicy: NSURLRequestReloadIgnoringCacheData
                                               timeoutInterval: 60.0];    
NSURLResponse* response = nil;
NSError* err = nil;
[NSURLConnection sendSynchronousRequest: req returningResponse: &response error: &err];

该代码不会阻止RunLoop旋转-AppKit会在sendSynchronousRequest:...调用中为您旋转它-但它确实会阻止用户与UI交互,直到它返回为止。在我看来,这不是“合法的空闲”,因此我需要一种方法来找出哪些空闲。 (CFRunLoopObserver方法也存在缺陷,因为它要求对代码进行更改,而我的最终解决方案则不需要。)

我决定将UI/主线程建模为状态机。它始终处于三种状态之一:LEGIT_IDLE,RUNNING或BLOCKED,并且在程序执行时会在这些状态之间来回转换。我需要提出Dtrace探针,使我能够捕获(并测量)这些转换。我实现的最终状态机比这三个状态要复杂得多,但这是20,000英尺的 View 。

如上所述,从坏空闲中分出合法空闲并不是一件容易的事,因为两种情况都以mach_msg_trap()__CFRunLoopRun结尾。我无法在调用堆栈中找到一个简单的工件,可以用来可靠地分辨出差异。似乎对一个功能进行简单的探索不会对我有帮助。我最终使用调试器查看了合法空闲与坏空闲的各种情况下的堆栈状态。我确定在合法的空闲期间,(看似可靠)我会看到这样的调用堆栈:
#0  in mach_msg
#1  in __CFRunLoopServiceMachPort
#2  in __CFRunLoopRun
#3  in CFRunLoopRunSpecific
#4  in RunCurrentEventLoopInMode
#5  in ReceiveNextEventCommon
#6  in BlockUntilNextEventMatchingListInMode
#7  in _DPSNextEvent
#8  in -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]
#9  in -[NSApplication run]
#10 in NSApplicationMain
#11 in main

因此,我努力建立了一堆嵌套/链接的pid探针,这些探针将确定我何时到达并随后离开此状态。不幸的是,无论出于何种原因,Dtrace的pid提供程序似乎都无法普遍探测所有任意符号的进入和返回。具体来说,我无法获得有关pid000:*:__CFRunLoopServiceMachPort:returnpid000:*:_DPSNextEvent:return的探针的工作。详细信息并不重要,但是通过观察其他各种情况并跟踪特定状态,我可以确定(再次,似乎可靠)进入我并离开合法闲置状态的时间。

然后,我必须确定用于说明“运行”和“已阻止”之间区别的探针。那有点容易。最后,我选择考虑在合法的空闲时间内未发生的BSD系统调用(使用Dtrace的syscall探针)和对mach_msg_trap()的调用(使用pid探针)被阻塞。 (我确实看过Dtrace的mach_trap探针,但是它似乎没有执行我想要的操作,因此我退回到使用pid探针的方式。)

最初,我与Dtrace sched提供程序做了一些额外的工作,以实际测量实际的阻塞时间(即,线程被调度程序挂起的时间),但这增加了相当大的复杂性,我最终想到了自己:在内核中,我是否在乎线程实际上是否处于 sleep 状态?对用户来说都是一样的:它被阻塞了。”因此, final方法只是测量(syscalls || mach_msg_trap()) && !legit_idle中的所有时间,并将其称为阻塞时间。

此时,捕获长持续时间的单个内核调用(例如,对sleep(5)的调用)变得微不足道。但是,UI线程延迟更多地是由于许多小的延迟积累了对内核的多次调用(考虑成百上千次对read()或select()的调用),因此,我认为在以下情况下转储某些调用堆栈也将是可取的:事件循环单次通过中的syscall或mach_msg_trap时间的总量超过了某个阈值。我最终设置了各种计时器,并记录了在每种状态下花费的累积时间,将状态范围限定在状态机中,并在碰巧从BLOCKED状态过渡到超过某个阈值时转储警报。这种方法显然会产生容易被误解的数据,或者可能是完全的红鲱鱼(即一些随机,相对较快的系统调用,恰好使我们超过了警报阈值),但是我觉得总比没有好。

最后,Dtrace脚本最终将状态机保留在D变量中,并使用所描述的探针跟踪状态之间的转换,并在状态机转换状态时使我有机会做事(例如打印警报),基于在某些条件下。我玩了一个精心设计的示例应用程序,该应用程序执行了许多磁盘I/O,网络I/O并调用sleep(),并且能够捕获所有这三种情况,而不会分散与合法等待有关的数据的干扰。这正是我想要的。

这个解决方案显然很脆弱,而且在几乎所有方面都对造成了极大的困扰。 :)它可能对我或其他任何人都有用,也可能没有用,但这是一个有趣的练习,所以我想我应该分享这个故事以及由此产生的Dtrace脚本。也许其他人会发现它有用。我还必须承认在编写Dtrace脚本方面是相对的n00b,所以我确定我做错了100万件事。享受!

它太大了,无法在线发布,因此它由@Catfish_Man托管在此处:MainThreadBlocking.d

关于ios - 当主线程阻塞时,如何获得断点/日志/增加的可见性?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8540975/

相关文章:

ios - 如何比较两幅图像并突出差异?

ios - 为什么这个委托(delegate)不适用于 UITextView

java.nio.ByteBuffer.slice() 线程行为?

c# - ThreadPool 没有立即启动新线程

java - 调用wait()时发生IllegalMonitorStateException

ios - 在 Swift REPL 中使用 iphonesimulator sdk 和 macosx sdk 有什么区别?

ruby - OS X 10.5 下 Ruby 的常规 GEM PATHS 是什么?

iphone - UIScrollView:动画横幅效果

ios - 使用 Objective-C 下载图像到 iPad

mysql - xampp mysql 不在端口 3306 上运行