How Tencent Matrix Detects iOS Main Thread Hang: RunLoop State, Annealing Backoff, and a Real Report
A detector that notices a hang only after the main thread recovers often loses the exact stack that caused it. Matrix's out-of-band monitor, annealing backoff, and all-thread capture catch the stall while it is still happening without burying the evidence in duplicate reports.
Tencent Matrix builds its iOS hang detector around RunLoop state transitions rather than FPS counters. A RunLoop Observer marks the main thread as busy from the moment it is woken or about to handle timers/sources, and clears the marker only when RunLoop reaches BeforeWaiting. A separate monitor thread checks every second whether the busy marker is still set and how long it has been set; if the gap passes 2 seconds, Matrix treats it as a main-thread hang. This split keeps detection alive even when the main thread is blocked in business code or waiting on a lock.
Once a hang is confirmed, Matrix has to decide which evidence is worth keeping. In the default mode it immediately captures the main thread's stack, and if consecutive checks return the same stack it skips the duplicate and backs off the next check using an annealing sequence: 1, 2, 3, 5, 8, 13 seconds. With early sampling enabled, it captures main-thread stacks every 50ms into a 20-entry queue and later picks the sample with the longest run of identical stack-top addresses, using the newest on a tie.
The final report is not just one stack. Matrix enters the KSCrash report path and captures every thread in the process, so the main thread's scene stack is analyzed alongside other threads as auxiliary context. A real report shows a 2.106-second block, a threshold of 2 seconds, 12 threads, and a main thread parked in Thread.sleep(forTimeInterval: 4) called from a demo validation method. CADisplayLink and sub-thread ping both measure delay after the fact but miss or misattribute the actual stall site.
Watching RunLoop transitions measures delay at the scheduling source, so it sidesteps render pipeline behavior, refresh rate, and low-power policies that distort FPS counters.
The early-sampling selection rule is a practical compromise: comparing only stack-top addresses catches the dominant blocking function without requiring the whole call chain to match every time.
Annealing is also a resource policy: writing full thread dumps while the main thread is already stuck compounds the problem, so suppressing repeated reports and lengthening the gap is part of making the monitor safe to run.