Mobile Development 22 min read

Detecting and Analyzing Android UI Thread Lag with Looper Logging and Choreographer

This article explains why Android UI lag hurts user experience, reviews traditional adb‑based detection methods, introduces a new in‑app monitoring system that leverages Looper log matching and Choreographer.FrameCallback to capture precise stack traces, and describes its implementation, performance impact, data processing pipeline, and real‑world results.

Tencent TDS Service
Tencent TDS Service
Tencent TDS Service
Detecting and Analyzing Android UI Thread Lag with Looper Logging and Choreographer

Implementation Background

App smoothness is a key user‑experience metric; on Android, diverse device configurations and long‑lived codebases often cause UI‑thread stalls that lead to noticeable jank, prompting the need for reliable detection and diagnosis.

Existing Solutions

Previously, common approaches used adb commands such as adb shell dumpsys SurfaceFlinger and adb shell dumpsys gfxinfo to collect frame‑rate or per‑frame draw times. While useful for regression testing, these methods suffer from several drawbacks:

Hard to reproduce real‑user jank scenarios.

Require extensive automation scripts and cannot cover many suspect cases.

Do not capture static‑page stalls.

Provide no immediate app‑state information when a stall occurs.

New Solution

To overcome the limitations, a comprehensive Android lag‑monitoring system was designed with two core capabilities: (1) real‑time detection of UI‑thread stalls and recording of app state (stack trace, CPU, memory, I/O); (2) uploading the collected data to a backend platform for analysis and visualization.

Method 3: Looper Log Matching

The UI thread runs a Looper.loop() method that processes messages. By installing a custom Printer via Looper.getMainLooper().setMessageLogging(), the system logs a start marker (">>>>> Dispatching") and an end marker ("<<<<< Finished"). Measuring the time between these two logs reveals whether a message took longer than the 16.6 ms frame budget.

public static void loop() { ... msg.target.dispatchMessage(msg); ... }

Setting the logger is done as follows:

public class BlockDetectByPrinter { public static void start() { Looper.getMainLooper().setMessageLogging(new Printer() { @Override public void println(String x) { if (x.startsWith(START)) LogMonitor.getInstance().startMonitor(); if (x.startsWith(END)) LogMonitor.getInstance().removeMonitor(); } }); } }

LogMonitor Implementation

public class LogMonitor { private static LogMonitor sInstance = new LogMonitor(); private HandlerThread mLogThread = new HandlerThread("log"); private Handler mIoHandler; private static final long TIME_BLOCK = 1000L; private LogMonitor() { mLogThread.start(); mIoHandler = new Handler(mLogThread.getLooper()); } public static LogMonitor getInstance() { return sInstance; } public boolean isMonitor() { return mIoHandler.hasCallbacks(mLogRunnable); } public void startMonitor() { mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK); } public void removeMonitor() { mIoHandler.removeCallbacks(mLogRunnable); } private static Runnable mLogRunnable = new Runnable() { @Override public void run() { StringBuilder sb = new StringBuilder(); for (StackTraceElement s : Looper.getMainLooper().getThread().getStackTrace()) { sb.append(s.toString()).append("
"); } Log.e("TAG", sb.toString()); } };

Method 4: Choreographer.FrameCallback

Android emits a VSYNC signal every ~16 ms. By registering a Choreographer.FrameCallback, the interval between consecutive frames can be measured; a gap larger than 16.6 ms indicates a stall. The callback also starts or stops the LogMonitor as needed.

public class BlockDetectByChoreographer { public static void start() { Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { long lastFrameTimeNanos = 0; @Override public void doFrame(long frameTimeNanos) { if (lastFrameTimeNanos != 0) { long diffMs = TimeUnit.MILLISECONDS.convert(frameTimeNanos - lastFrameTimeNanos, TimeUnit.NANOSECONDS); if (diffMs > 16.6f) { if (LogMonitor.getInstance().isMonitor()) LogMonitor.getInstance().removeMonitor(); LogMonitor.getInstance().startMonitor(); } } lastFrameTimeNanos = frameTimeNanos; Choreographer.getInstance().postFrameCallback(this); } }); } }

Comparison of the Four Approaches

Both Looper‑based and Choreographer‑based methods reliably capture stalls, but the latter also provides real‑time FPS and dropped‑frame statistics, making it the preferred solution in production.

High‑Frequency Stack Sampling

To improve diagnostic accuracy, the system samples the main‑thread stack at a 52 ms interval during a detected stall (threshold 80 ms). This yields multiple snapshots per stall, allowing developers to pinpoint the exact offending code path.

Performance tests on a Vivo X9 device showed negligible overhead: ~0.1 ms per second on the UI thread, 0.1 % CPU increase, ~1 MB additional heap, and daily log files under 100 KB.

Data Processing Pipeline

Collected logs are uploaded daily, de‑duplicated, classified, and stored. High‑frequency stacks are hashed; the most frequent stack per stall is kept as the representative. Stacks are then clustered by outermost or innermost frames, depending on the analysis goal.

System Flow

Users (0.2 % gray‑scale) receive the monitor; logs are uploaded, parsed, and displayed on a dashboard that groups stalls by version, frequency, and key code locations. Automatic rules trigger TAPD bug creation for high‑frequency or high‑latency stalls, closing the loop from detection to regression testing.

Practical Impact

Deployed in WeRead, Enterprise WeChat, and QQ Mail, the system has captured over ten thousand distinct stalls in three months, generated ~500 bug tickets, and helped resolve more than 200 performance issues.

Acknowledgements

Special thanks to the engineering team members who contributed to the Android lag‑monitoring component.

mobile developmentAndroidLooperchoreographerUI lag
Tencent TDS Service
Written by

Tencent TDS Service

TDS Service offers client and web front‑end developers and operators an intelligent low‑code platform, cross‑platform development framework, universal release platform, runtime container engine, monitoring and analysis platform, and a security‑privacy compliance suite.

0 followers
Reader feedback

How this landed with the community

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.