iOS Main Thread Lag Monitoring with RunLoop and QiLagMonitor
This article explains how to monitor iOS main‑thread lag using RunLoop observers and the QiLagMonitor tool, detailing RunLoop modes, observer activities, and providing Objective‑C code for creating semaphores, observers, and a background monitoring loop that captures stack traces when the main thread stalls.
After studying a performance‑monitoring talk, the author summarizes iOS performance‑monitoring practices and introduces a series of articles. This first article focuses on the thread‑lag monitoring module of the QiLagMonitor tool.
The iOS main thread runs a runloop that registers five default modes: kCFRunLoopDefaultMode , UITrackingRunLoopMode , UIInitializationRunLoopMode , GSEventReceiveRunLoopMode , and kCFRunLoopCommonModes . The article lists each mode and its purpose in a table.
Name
Purpose
kCFRunLoopDefaultMode
App's default mode, usually where the main thread runs.
UITrackingRunLoopMode
Used for ScrollView touch tracking, ensuring UI scrolling isn’t affected by other modes.
UIInitializationRunLoopMode
The first mode entered when the app launches; not used after startup.
GSEventReceiveRunLoopMode
Internal mode for receiving system events, rarely needed.
kCFRunLoopCommonModes
A placeholder mode that represents a set of common modes (e.g., Default and UI modes).
The article explains that Apple publicly provides two common modes: NSDefaultRunLoopMode (alias kCFRunLoopDefaultMode ) and NSRunLoopCommonModes (alias kCFRunLoopCommonModes ), which are used for monitoring.
RunLoop observer activities are defined as follows:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};The corresponding meanings are listed in another table:
State
Meaning
kCFRunLoopEntry
Entry point of the runloop.
kCFRunLoopBeforeTimers
Before any timers are processed.
kCFRunLoopBeforeSources
Before any sources are processed.
kCFRunLoopBeforeWaiting
Before the runloop sleeps, waiting for a source or timer.
kCFRunLoopAfterWaiting
After the runloop wakes up but before processing the waking event.
kCFRunLoopExit
Exit of the runloop after processing.
kCFRunLoopAllActivities
All runloop states.
QiLagMonitor monitors main‑thread lag by:
Creating a RunLoop observer ( runLoopObserver ) and a dispatch semaphore ( dispatchSemaphore ) for synchronization.
Adding the observer to the main runloop’s common mode.
Starting a background thread that runs a continuous loop, waiting on the semaphore with a timeout (e.g., 88 ms). If the runloop stays in BeforeSources or AfterWaiting beyond the threshold, the monitor records the current call stack.
Key code snippets:
// Create a semaphore to ensure synchronized operations
dispatchSemaphore = dispatch_semaphore_create(0); // Dispatch Semaphore guarantees sync
// Create a RunLoop observer
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context); static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
QiLagMonitor *lagMonitor = (__bridge QiLagMonitor*)info;
lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
} // Add observer to the main runloop's common mode
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes); // Background monitoring loop
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore,
dispatch_time(DISPATCH_TIME_NOW, STUCKMONITORRATE * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!self->runLoopObserver) {
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
if (self->runLoopActivity == kCFRunLoopBeforeSources ||
self->runLoopActivity == kCFRunLoopAfterWaiting) {
if (++self->timeoutCount < 3) { continue; }
NSLog(@"monitor trigger");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSString *stackStr = [QiCallStack callStackWithType:QiCallStackTypeMain];
QiCallStackModel *model = [[QiCallStackModel alloc] init];
model.stackStr = stackStr;
model.isStuck = YES;
[[[QiLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}];
});
}
}
self->timeoutCount = 0;
}
});When the observer detects that the main thread’s runloop remains in the critical states for longer than the defined threshold, the monitor captures the stack trace and stores it for later analysis, enabling developers to identify and address performance bottlenecks.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.