Created
January 28, 2015 20:50
-
-
Save jspahrsummers/419266f5231832602bec to your computer and use it in GitHub Desktop.
A class for logging excessive blocking on the main thread
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
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?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
(doh) Thanks @johnboiles