-
-
Save jspahrsummers/419266f5231832602bec to your computer and use it in GitHub Desktop.
/// Observes a run loop to detect any stalling or blocking that occurs. | |
/// | |
/// This class is thread-safe. | |
@interface GHRunLoopWatchdog : NSObject | |
/// Initializes the receiver to watch the specified run loop, using a default | |
/// stalling threshold. | |
- (id)initWithRunLoop:(CFRunLoopRef)runLoop; | |
/// Initializes the receiver to detect when the specified run loop blocks for | |
/// more than `threshold` seconds. | |
/// | |
/// This is the designated initializer for this class. | |
- (id)initWithRunLoop:(CFRunLoopRef)runLoop stallingThreshold:(NSTimeInterval)threshold; | |
/// Begins watching the receiver's run loop for stalling in the given mode. | |
/// | |
/// The receiver will automatically stop watching the run loop upon deallocation. | |
/// | |
/// mode - The mode in which to monitor the specified run loop. Use | |
/// kCFRunLoopCommonModes to watch all common run loop modes. This should | |
/// not be NULL. | |
- (void)startWatchingMode:(CFStringRef)mode; | |
/// Stops watching the receiver's run loop for stalling in the given mode. | |
/// | |
/// There is generally no need to invoke this method explicitly. | |
/// | |
/// mode - The mode in which to monitor the specified run loop. Use | |
/// kCFRunLoopCommonModes to watch all common run loop modes. This should | |
/// not be NULL. | |
- (void)stopWatchingMode:(CFStringRef)mode; | |
/// A block to invoke any time the run loop stalls. | |
/// | |
/// duration - The number of seconds that elapsed in the run loop iteration. | |
@property (copy) void (^didStallWithDuration)(NSTimeInterval duration); | |
@end |
#import "GHRunLoopWatchdog.h" | |
#include <mach/mach_time.h> | |
// The default number of seconds that must pass to consider a run loop stalled. | |
static const NSTimeInterval GHRunLoopWatchdogDefaultStallingThreshold = 0.2; | |
@interface GHRunLoopWatchdog () | |
// The run loop to watch. | |
// | |
// Despite being marked `assign`, this property is retained. | |
@property (nonatomic, assign, readonly) CFRunLoopRef runLoop; | |
// The observer used to watch the run loop. | |
// | |
// Despite being marked `assign`, this property is retained. | |
@property (nonatomic, assign, readonly) CFRunLoopObserverRef observer; | |
// The number of seconds that must pass to consider the run loop stalled. | |
@property (nonatomic, assign, readonly) NSTimeInterval threshold; | |
// The mach_absolute_time() at which the current run loop iteration was started, | |
// or 0 if there is no current iteration in progress. | |
// | |
// This property is not thread-safe, and must only be accessed from the thread | |
// that the run loop is associated with. | |
@property (nonatomic, assign) uint64_t startTime; | |
// Invoked any time the run loop stalls. | |
// | |
// duration - The number of seconds that elapsed in the run loop iteration. | |
- (void)iterationStalledWithDuration:(NSTimeInterval)duration; | |
@end | |
@implementation GHRunLoopWatchdog | |
#pragma mark Lifecycle | |
- (id)initWithRunLoop:(CFRunLoopRef)runLoop { | |
return [self initWithRunLoop:runLoop stallingThreshold:GHRunLoopWatchdogDefaultStallingThreshold]; | |
} | |
- (id)initWithRunLoop:(CFRunLoopRef)runLoop stallingThreshold:(NSTimeInterval)threshold { | |
NSParameterAssert(runLoop != NULL); | |
NSParameterAssert(threshold > 0); | |
self = [super init]; | |
if (self == nil) return nil; | |
_runLoop = (CFRunLoopRef)CFRetain(runLoop); | |
_threshold = threshold; | |
// Precalculate timebase information. | |
mach_timebase_info_data_t timebase; | |
mach_timebase_info(&timebase); | |
NSTimeInterval secondsPerMachTime = timebase.numer / timebase.denom / 1e9; | |
@weakify(self); | |
// Observe at an extremely low order so that we can catch stalling even in | |
// high-priority operations (like UI redrawing or animation). | |
_observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, INT_MIN, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { | |
@strongify(self); | |
switch (activity) { | |
// What we consider one "iteration" might start with any one of | |
// these events. | |
case kCFRunLoopEntry: | |
case kCFRunLoopBeforeTimers: | |
case kCFRunLoopAfterWaiting: | |
case kCFRunLoopBeforeSources: | |
if (self.startTime == 0) self.startTime = mach_absolute_time(); | |
break; | |
case kCFRunLoopBeforeWaiting: | |
case kCFRunLoopExit: { | |
uint64_t endTime = mach_absolute_time(); | |
if (self.startTime <= 0) { | |
break; | |
} | |
uint64_t elapsed = endTime - self.startTime; | |
NSTimeInterval duration = elapsed * secondsPerMachTime; | |
if (duration > self.threshold) [self iterationStalledWithDuration:duration]; | |
self.startTime = 0; | |
break; | |
} | |
default: | |
NSAssert(NO, @"Observer should not have been triggered for activity %i", (int)activity); | |
} | |
}); | |
if (_observer == NULL) return nil; | |
return self; | |
} | |
- (void)dealloc { | |
if (_observer != NULL) { | |
CFRunLoopObserverInvalidate(_observer); | |
CFRelease(_observer); | |
_observer = NULL; | |
} | |
if (_runLoop != NULL) { | |
CFRelease(_runLoop); | |
_runLoop = NULL; | |
} | |
} | |
#pragma mark Starting and Stopping | |
- (void)startWatchingMode:(CFStringRef)mode { | |
NSParameterAssert(mode != NULL); | |
CFRunLoopAddObserver(self.runLoop, self.observer, mode); | |
} | |
- (void)stopWatchingMode:(CFStringRef)mode { | |
NSParameterAssert(mode != NULL); | |
CFRunLoopRemoveObserver(self.runLoop, self.observer, mode); | |
} | |
#pragma mark Timing | |
- (void)iterationStalledWithDuration:(NSTimeInterval)duration { | |
#if DEBUG | |
NSLog(@"%@: iteration of run loop %p took %.f ms to execute", self, self.runLoop, (double)duration * 1000); | |
#endif | |
void (^didStall)(NSTimeInterval) = self.didStallWithDuration; | |
if (didStall != nil) didStall(duration); | |
} | |
@end |
@jspahrsummers:
i try to use and initial GHRunLoopWatchdog in AppDelegate like @dbachrach use it
do i use this right?
I put a breakpoint inside the observer and it never is executed.
@hossein1448 I put a breakpoint under case kCFRunLoopExit
and it breaks fine.
@dbachrach, @hossein1448: don't forget to retain the GHRunLoopWatchdog somewhere or it will get released when it goes out of scope. If you initialize your watchdog in application:didFinishLaunchingWithOptions:
, then you might want to create a property on your AppDelegate @property GHRunLoopWatchdog *runLoopWatchdog
then initialize it like this:
self.runLoopWatchdog = [[GHRunLoopWatchdog alloc] initWithRunLoop:CFRunLoopGetMain()];
[self.runLoopWatchdog startWatchingMode:kCFRunLoopCommonModes];
You can then do this to test that it fires:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
sleep(3);
});
(doh) Thanks @johnboiles
I'm wondering if there's a lightweight way to log what methods are stalling in UI thread. It would be very helpful for occasional stalling issues.
@johnboiles thanks
You may observe kCFRunLoopAfterWaiting and kCFRunLoopBeforeWaiting but not kCFRunLoopBeforeSources and kCFRunLoopExit to detect the blocking.
This is a great idea - thank you.
I added this to my app and from time to time I see something like:
"<GHRunLoopWatchdog: 0x7faf6343d340>: iteration of run loop 0x7faf6394d230 took 1866 ms to execute"
There are a lot of things going on with the app.
How can I tell which chunk of code (or object) causes this delay?
@hossein1448 no not yet. Will take a look again tomorrow.