Mobile Development 8 min read

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.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
iOS Main Thread Lag Monitoring with RunLoop and QiLagMonitor

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.

mobile developmentiOSperformance-monitoringRunLoopQiLagMonitorThread Lag
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.