Mobile Development 12 min read

Measuring and Locating Android App Jank Using JankStats and Stack‑Dump Techniques

This article explains how to quantify Android UI jank by calculating a jank rate, uses Jetpack's JankStats library to collect frame‑level metrics, and presents two practical methods—stack dumping and bytecode instrumentation—to pinpoint the slow functions causing stutter, complete with Kotlin code examples and performance considerations.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Measuring and Locating Android App Jank Using JankStats and Stack‑Dump Techniques

Jank (UI stutter) is a common but often overlooked performance issue in Android apps; it degrades user experience and can affect retention. To address it, we first need reliable metrics that go beyond simple FPS measurements.

FPS alone does not reflect perceived smoothness because occasional long frames are averaged out. We define jank rate = jank frames / total frames . A frame is considered jank when its rendering time exceeds the expected 16 ms (for a 60 Hz display). The article provides a severity table that classifies 3‑9 dropped frames as mild jank, with higher ranges for moderate, critical, and frozen jank.

To obtain the required data—total frames, jank frames, and per‑frame durations—two native approaches exist: using a custom android.util.Printer to monitor Looper dispatch times, or registering a Choreographer.FrameCallback to measure intervals between Vsync events. The recommended solution is Jetpack's JankStats library, which abstracts these details. On Android 7+ it leverages the FrameMetrics API; on older versions it falls back to OnPreDrawListener .

The following Kotlin snippet shows a minimal integration of JankStats inside an Activity:

class JankLoggingActivity : AppCompatActivity() {
    private lateinit var jankStats: JankStats
    private val jankFrameListener = JankStats.OnFrameListener { frameData ->
        // In a real app you would upload this log to a remote server
        Log.v("JankStatsSample", frameData.toString())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Initialise JankStats with the window and the listener
        jankStats = JankStats.createAndTrack(window, jankFrameListener).apply {
            // The default heuristic multiplier is 2; we raise it to 3
            jankHeuristicMultiplier = 3f
        }
        // Optionally attach a page‑level state for later analysis
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
    }

    override fun onResume() {
        super.onResume()
        jankStats.isTrackingEnabled = true
    }

    override fun onPause() {
        super.onPause()
        jankStats.isTrackingEnabled = false
    }
}

Using JankStats involves five key steps: initialise it with the window and a listener, optionally set a page‑level state (e.g., the Activity name), configure the jank threshold (default multiplier 2, here set to 3), enable/disable tracking when the Activity is foreground/background, and finally process the OnFrameListener callbacks which deliver per‑frame data that can be aggregated and uploaded.

For cross‑Activity aggregation, the article provides a JankStatsAggregator implementation that stores aggregators in a map keyed by activity name and reports total, mild, moderate, critical, and frozen jank counts. The relevant code is:

internal class JankActivityLifecycleCallback : ActivityLifecycleCallbacks {
    private val jankAggregatorMap = hashMapOf
()
    private val jankReportListener = JankStatsAggregator.OnJankReportListener { reason, totalFrames, jankFrameData ->
        jankFrameData.forEach { frameData ->
            Log.v("Activity", frameData.states.firstOrNull { it.key == "Activity" }?.value ?: "")
            val dropFrameCount = frameData.frameDurationUiNanos / singleFrameNanosDuration
            when {
                dropFrameCount <= JankMonitor.SLIGHT_JANK_MULTIPIER -> slightJankCount++
                dropFrameCount <= JankMonitor.MIDDLE_JANK_MULTIPIER -> middleJankCount++
                dropFrameCount <= JankMonitor.CRITICAL_JANK_MULTIPIER -> criticalJankCount++
                else -> frozenJankCount++
            }
        }
        Log.v("JankMonitor", "*** Jank Report ($reason), totalFrames=$totalFrames, jankFrames=${jankFrameData.size}, " +
                "slightJankCount=$slightJankCount, middleJankCount=$middleJankCount, " +
                "criticalJankCount=$criticalJankCount, frozenJankCount=$frozenJankCount")
    }

    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        activity.window.callback = object : WindowCallbackWrapper(activity.window.callback) {
            override fun onContentChanged() {
                val activityName = activity.javaClass.simpleName
                if (!jankAggregatorMap.containsKey(activityName)) {
                    val jankAggregator = JankStatsAggregator(activity.window, jankReportListener)
                    PerformanceMetricsState.getHolderForHierarchy(activity.window.decorView).state?.putState("Activity", activityName)
                    jankAggregatorMap[activityName] = jankAggregator
                }
            }
        }
    }
    // ... other lifecycle methods omitted for brevity
}

After establishing reliable jank metrics, the next step is to locate the offending code. Two mainstream approaches are discussed.

Stack‑dump approach: When a frame is identified as jank, the main‑thread stack is dumped for analysis. To avoid late‑stage dumps that miss the culprit, a watchdog thread periodically checks whether the main thread has processed a message within a timeout; if not, it triggers an immediate stack dump. This technique is used by the open‑source DoKit library.

Bytecode instrumentation approach: By inserting timing code at the entry and exit of each method, the exact execution duration of every function can be recorded. When a jank occurs, the collected timings for the recent interval are reported, allowing precise identification of the slow method. Matrix’s slow‑function detection is based on this strategy; the performance impact is minimal on high‑end devices and adds roughly 800 KB to the APK for large apps.

In summary, the article presents a complete workflow for Android performance optimisation: define a quantitative jank rate, use JankStats (or equivalent) to gather frame‑level data, aggregate results per activity, and finally apply either stack‑dump or bytecode‑instrumentation techniques to pinpoint and fix the slow functions.

mobile developmentperformanceOptimizationAndroidKotlinprofilingJank
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

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.