Compose Stability Analyzer 0.10.0: Stability Doctor and Trace-All Mode

skydovesJaewoong Eum (skydoves)||6 min read

Compose Stability Analyzer 0.10.0: Stability Doctor and Trace-All Mode

Compose Stability Analyzer has grown into a set of diagnostic tools for Jetpack Compose recomposition: gutter icons and tooltips show what the compiler predicts, the Recomposition Cascade shows what a recomposition would affect, the Live Heatmap shows what actually recomposes on a device, the Reality Check grades whether each recomposition was justified, and Recomposition Blame traces where the triggering value came from. Each tool answers its own question, but one question has remained with you: with all of these signals in front of me, what should I fix first, and what will I gain?

Version 0.10.0 answers that question. The Stability Doctor combines every signal the plugin collects into a single ranked list of prescriptions with one click fixes, and the new trace-all mode feeds it runtime data from your entire module without a single annotation.

In this article, you'll explore the two new features introduced in version 0.10.0: the Stability Doctor and trace-all mode.

Stability Doctor

The Doctor scans your project and scores every composable by combining three inputs: the static stability verdict (which parameters are unstable and why), the downstream cascade blast radius (how many composables a recomposition would drag along), and the measured runtime waste from the Reality Check (how many recompositions were caused purely by identity churn, and how long they took). The result is a prioritized list where the top entry is the fix with the highest measured payoff.

doctor

Each prescription carries a score with one of two badges:

  • ESTIMATED: computed from static analysis alone. This works with no device connected, so the Doctor is useful the moment you open the tab.
  • MEASURED: backed by live heatmap data from a running app. Measured scores are capped higher than estimated ones, so a composable with confirmed, observed waste always outranks a speculative one. The reverse also matters: a composable the compiler flags as unstable but that skips fine at runtime sinks toward the bottom, because the measurement proved the warning to be a false alarm.

Expanding a prescription shows its problem parameters, each with the static reason (for example, "has 2 mutable (var) properties"), the Reality Check grade, and where the value originates at each call site, reusing the Blame analysis. Under each cause, the Doctor offers fixes you can apply by double-clicking:

  • Change var to val on the parameter's class. The fix searches the project for write usages first and refuses to apply if any assignment exists, so it never breaks compilation.
  • Annotate the class with @Immutable or @Stable. The confirmation dialog carries the caveat that these annotations are a promise to the compiler, not a verification.
  • Add the type to your stability configuration file, for library types you cannot modify.
  • Wrap the call site argument in remember\(keys\) { ... } for silent waste parameters. This is the fix for an equals equal value that arrives as a new instance on every recomposition. The Doctor offers it only when a set of safety rules proves the transformation valid: the argument is evaluated directly in composition, contains no composable calls, and every input resolves to a parameter or an earlier local val. The keys are derived from those inputs automatically, and a preview dialog shows the exact replacement before anything changes.

To run it, open the Doctor tab in the Compose Stability Analyzer tool window (or use Code menu, Run Stability Doctor) and hit refresh. Without a device you get estimated rankings immediately. Start the recomposition heatmap and interact with your app, and rows upgrade to measured scores and re-rank automatically while the session runs. After you apply a fix, the affected prescription is re-analyzed, so you can watch it drop down the list or disappear.

Trace-all mode

Until now, runtime data flowed only for composables you annotated with @TraceRecomposition one by one. That is fine for targeted debugging, but the Doctor ranks your whole module, and a ranking built from three annotated composables misses everything else. Trace-all mode closes that gap: the compiler plugin instruments every restartable composable in the module, as if each one carried the annotation.

composeStabilityAnalyzer {
  traceAll {
    enabled.set(true)
    threshold.set(2)
    variants.set(listOf("debug"))
  }
}

The mode is opt in and debug oriented by default. Only compilations whose variant name matches one of the configured tokens are instrumented, so debug covers debug, stagingDebug, and fullDebug while release builds stay untouched, and test compilations are never instrumented. Auto-traced composables start logging from their second recomposition by default, which silences the initial composition burst of a screen: a composable that never recomposes produces no log lines, and that is exactly the population the Doctor does not need to rank. Explicit @TraceRecomposition annotations keep their own tag and threshold, and previews, inline composables, readonly composables, and property getters are excluded automatically.

Trace-all also sharpens how runtime data is matched back to your code. Log headers now carry the fully qualified name of the composable:

[Recomposition #2] UserProfile (1.20ms) (fq: com.example.profile.UserProfile) (auto)

The trailing tokens are backward compatible with older parsers, and the IDE uses them to attribute events precisely, so two composables that share a simple name across packages no longer share an inlay. With the analyzer disabled through ComposeStabilityAnalyzer.setEnabled(false), the instrumentation cost per composition is a map lookup and early returned calls, so a debug build with trace-all enabled stays usable even when you are not measuring.

Putting them together

The features in this release close the loop the plugin has been building toward. Static analysis tells you what could recompose, the heatmap tells you what does, the Reality Check tells you whether it was wasteful, and Blame tells you why. The Doctor reads all of them and answers the remaining question: which fix is worth your time, in what order, and with the fix itself one double-click away.

A practical workflow looks like this. Enable trace-all in your debug build and run the Doctor once with no device for an estimated baseline. Start the heatmap, interact with your app, and watch the rankings re-sort as measured waste arrives. Work from the top: apply the offered fix, or navigate to the composable when the fix needs human judgment, and watch the prescription disappear. When the list is quiet, your remaining recompositions are ones the measurement says are justified.

Version 0.10.0 also fixes an Android Studio freeze when starting the heatmap on large projects, stops the Gradle plugin from leaking AGP 9 onto consumers' classpaths (thanks to Valerio Pilo), and matches nullable types against the stability configuration file (thanks to David Kurzica).

You can install version 0.10.0 from the JetBrains Marketplace. You can check out the details via documentation.

As always, happy coding!

Jaewoong (skydoves)