Stop Saying 'The Handler Is Stuck': A Stage-by-Stage Troubleshooting Method
Vague diagnoses like "main thread stuck" waste hours because they skip the evidence that tells you where to fix. This method replaces guesswork with a repeatable chain of proof that works across app and framework threads, and it forces engineers to distinguish delivery delay from execution cost — two problems with entirely different fixes.
A single symptom like a frozen UI or a slow system_server response can originate from a dozen different points in a Handler message's lifecycle. The wrong conclusion — "the Handler is stuck" — gives no repair direction. The right approach walks the normal main chain (post → enqueue → wait → retrieve → dispatch) and separately checks interruption side paths such as rejected enqueues, removals, lifecycle cancellations, and Looper quits.
Two built-in log signals anchor the investigation. Slow delivery measures the gap between Message.when and the actual dispatch start, pointing to preceding messages, thread scheduling delays, or queue backlogs. Slow dispatch measures the callback's own execution time, pointing to locks, I/O, synchronous Binder calls, or native blocking inside the Runnable. Neither signal is a universal monitor; a missing slow log does not prove the absence of waiting or blocking.
A fixed evidence template replaces vague hand-waving. For every stuck task, state the target message and thread, the post return value, the Message.when, the waiting evidence (delivery log, Perfetto timeline, preceding slice), the execution evidence (dispatch log, thread dump, Binder trace), and a conclusion that names the exact stage and cause while explicitly ruling out alternatives. The method applies equally to app main-thread freezes and system_server service latency, where synchronous Binder calls often chain through multiple processes and the real root cause sits on the peer side.
The most common diagnostic failure is treating Slow delivery and Slow dispatch as interchangeable; they measure completely different intervals and demand opposite investigation directions.
Logging only 'post updateUi' is nearly useless. A log that captures the target Handler, return value, calling thread, and uptime turns a mystery into a checkable fact.
Framework engineers often blame the Handler thread when a thread dump shows it stuck in BinderProxy.transact, but the real culprit is a lock or I/O wait on the peer process that never appears in the local trace.
Setting an overly aggressive Looper slow-log threshold and leaving it on distorts timing measurements because the logging itself becomes a non-trivial source of jitter.