You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thank you for your interest in contributing! This project welcomes bug reports, feature requests, documentation improvements, and pull requests.
4
+
5
+
## Reporting Issues
6
+
7
+
- Use the [issue templates](https://github.com/skydoves/compose-stability-analyzer/issues/new/choose) for bug reports and feature requests.
8
+
- For IDE plugin freezes or performance problems, please attach the Android Studio thread dumps (**Help → Collect Logs and Diagnostic Data**) — they make issues diagnosable in one pass.
9
+
- For Gradle plugin issues, include your Kotlin/AGP versions and the relevant `composeStabilityAnalyzer { ... }` configuration.
10
+
11
+
## Development Setup
12
+
13
+
The project requires **JDK 17+** and builds with the bundled Gradle wrapper. Kotlin and AGP versions are pinned in `gradle/libs.versions.toml`.
14
+
15
+
```bash
16
+
# Build everything and publish to Maven Local (required before building the sample app)
17
+
./gradlew spotlessApply publishToMavenLocal :app:assembleDebug -x test -PRELEASE_SIGNING_ENABLED=false
18
+
```
19
+
20
+
The IntelliJ/Android Studio plugin is a separate IntelliJ Platform build:
21
+
22
+
```bash
23
+
./gradlew :stability-runtime:publishToMavenLocal -x test -PRELEASE_SIGNING_ENABLED=false
24
+
cd compose-stability-analyzer-idea && ../gradlew buildPlugin
25
+
```
26
+
27
+
The sample app resolves the Gradle plugin from Maven Local (it shadows Maven Central for the same version), so always publish locally before building `:app`.
28
+
29
+
## Module Overview
30
+
31
+
| Module | What it is |
32
+
|--------|-----------|
33
+
|`stability-compiler`| K2 compiler plugin (FIR checks + IR instrumentation) |
- IDE plugin: `cd compose-stability-analyzer-idea && ../gradlew test`
48
+
4.**Compiler golden data**: if your change affects generated IR, regenerate snapshots with `./gradlew :compiler-tests:test -Pupdate.test.data` (the regenerating run reports failures while writing; the next run passes). Never hand-edit `.txt` snapshots or the generated JUnit classes under `compiler-tests/src/test/java/`.
49
+
50
+
## Things to Know Before Changing…
51
+
52
+
-**The logcat output format** (`stability-runtime` loggers): it is a wire protocol parsed by the IDE plugin's `LogcatParser`. Header lines may only gain *trailing* optional tokens; changing or reordering existing tokens breaks older IDE/runtime combinations.
53
+
-**Stability inference** (`stability-compiler` / IDE plugin): the compiler's and the IDE's inference must stay in sync, including the known-stable type lists.
54
+
-**AGP types in `stability-gradle`**: AGP is `compileOnly` and its types may only be referenced inside `AndroidStabilityTaskRegistrar`, so projects without AGP keep working.
55
+
-**Versions**: `gradle.properties``VERSION_NAME`, the `VERSION` constant in `StabilityAnalyzerGradlePlugin`, `gradle/libs.versions.toml`, and the IDE plugin's version/runtime dependency must move together.
56
+
57
+
## Code Reviews
58
+
59
+
All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on using pull requests.
60
+
61
+
## License
62
+
63
+
By contributing, you agree that your contributions will be licensed under the [Apache License 2.0](LICENSE).
By default, runtime recomposition data flows only for composables you annotate with [`@TraceRecomposition`](trace-recomposition.md) one by one. **Trace-all mode** instruments every restartable composable in the module automatically — as if each one carried the annotation — so the [Live Heatmap](../ide-plugin/recomposition-heatmap.md), [Reality Check](../ide-plugin/reality-check.md), and [Stability Doctor](../ide-plugin/stability-doctor.md) receive module-wide runtime data without manual annotations.
-**Debug-oriented by default**: only compilations whose variant name equals or ends with one of the `variants` tokens are instrumented — `debug` matches `debug`, `stagingDebug`, and `fullDebug`, while release builds stay untouched. Test compilations are never instrumented. For KMP/JVM targets without variants, the runtime `ComposeStabilityAnalyzer.setEnabled(...)` gate is the production safety net.
20
+
-**Explicit annotations win**: a composable with `@TraceRecomposition` keeps its own `tag`, `threshold`, and `traceStates` settings.
21
+
-**Quiet by default**: auto-traced composables only start logging from their 2nd recomposition (`threshold = 2`), so the initial composition of a screen produces no logcat flood. A composable that never *re*-composes emits nothing — which is exactly the population that needs no attention. Raise the threshold if very active composables (e.g. animations) get noisy.
22
+
-**Skips what shouldn't be traced**: `@Preview` composables, `@IgnoreStabilityReport`, inline/readonly/non-restartable composables, and property getters are excluded automatically.
23
+
-**Cheap when disabled**: with `ComposeStabilityAnalyzer.setEnabled(false)`, the residual cost per composition is a map lookup plus early-returned calls.
24
+
25
+
## Fully Qualified Names in Logs
26
+
27
+
Trace-all-capable runtimes (0.10.0+) append two trailing tokens to every log header:
-`(fq: ...)` — the fully qualified composable name, emitted for annotated and auto-traced composables alike. The IDE uses it to attribute runtime data precisely, so two composables that share a simple name across packages no longer share an inlay.
34
+
-`(auto)` — marks events from trace-all instrumentation (absent for explicitly annotated composables).
35
+
36
+
Both tokens are **trailing and optional**, so older log parsers simply ignore them — old IDE + new runtime and new IDE + old runtime combinations keep working.
37
+
38
+
## Options
39
+
40
+
| Option | Default | Description |
41
+
|--------|---------|-------------|
42
+
|`enabled`|`false`| Opt-in master switch for auto-instrumentation. |
43
+
|`threshold`|`2`| Recomposition count at which auto-traced composables start logging. |
44
+
|`variants`|`["debug"]`| Android variant/build-type name tokens (case-insensitive equals/endsWith match) that receive instrumentation. |
45
+
46
+
!!! note "Logging still requires the runtime gate"
47
+
48
+
Trace-all only decides *what gets instrumented at compile time*. No logs appear unless you also call `ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)` in your `Application` class, exactly as with `@TraceRecomposition`.
The Stability Doctor answers the question every other feature leads up to: **what should I fix first, and what will I gain?** It scans your project, scores every composable by combining the static stability verdict, the downstream [cascade](recomposition-cascade.md) blast radius, and the measured runtime waste from the [Reality Check](reality-check.md), then presents a ranked list of prescriptions with one-click fixes.
Each prescription carries a score (0–100) with one of two badges:
10
+
11
+
| Badge | Inputs | When |
12
+
|-------|--------|------|
13
+
|**ESTIMATED**| Unstable/unknown parameter count, skippability, cascade blast radius | Always available — no device needed. Capped below measured scores. |
14
+
|**MEASURED**| Observed wasted recompositions × average duration, silent-waste grades, blast radius | When a [heatmap](recomposition-heatmap.md) session has collected enough observations. |
15
+
16
+
Two ranking behaviors follow from this design:
17
+
18
+
- A composable with **confirmed, measured waste always outranks** a speculative static finding — estimated scores are capped below the measured range.
19
+
- 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.
20
+
21
+
## Prescriptions
22
+
23
+
Expanding a prescription shows its problem parameters, each with:
24
+
25
+
- The **static reason** (e.g. "Has 2 mutable (var) properties"), from in-IDE analysis.
26
+
- The **Reality Check grade** (silent waste / false alarm / justified), when live data exists.
27
+
- The **value provenance** at each call site (a `val`/`var` property, a parameter, a function call), reusing the [Blame](recomposition-blame.md) analysis.
28
+
29
+
## One-Click Fixes
30
+
31
+
Under each cause, the Doctor offers fixes you can apply by double-clicking:
32
+
33
+
| Fix | Applies to | Safety |
34
+
|-----|-----------|--------|
35
+
|**Change `var` → `val`**| The parameter's class, when it lives in your project | Searches for write usages first; refuses to apply if any assignment exists. |
36
+
|**Annotate with `@Immutable` / `@Stable`**| Project classes without an existing stability annotation | The confirmation dialog reminds you this is a *promise* to the compiler, not a verification. `@Stable` is offered instead of `@Immutable` when the class keeps `var` properties. |
37
+
|**Add to stability configuration file**| Library types you cannot modify | Skipped for platform/known-stable types and patterns already covered. |
38
+
|**Wrap argument in `remember(keys) { ... }`**| Call-site arguments of *silent waste* parameters | Offered only when safety rules prove the transformation valid (see below). |
39
+
40
+
!!! note "Remember-hoisting safety rules"
41
+
42
+
The `remember` fix is the cure for an `equals`-equal value that arrives as a new instance on every recomposition. The Doctor offers it only when **all** of the following hold: the argument is evaluated directly in composition (not inside a lambda), it is a genuine computation (not a bare reference, constant, or lambda), it contains no composable calls, and every input resolves to a caller parameter or an earlier local `val`. The remember keys are derived from those inputs automatically, and a preview dialog shows the exact replacement before anything changes. Purity cannot be proven statically — verify the expression is side-effect free before confirming.
43
+
44
+
## How to Use
45
+
46
+
1. Open **View → Tool Windows → Compose Stability Analyzer → Doctor** tab (or **Code menu → Run Stability Doctor**) and hit refresh. This works immediately with `ESTIMATED` scores — no device required.
47
+
2. For measured scores, enable [trace-all](../gradle-plugin/trace-all.md) (or annotate composables with `@TraceRecomposition`), start the [Recomposition Heatmap](recomposition-heatmap.md), and interact with your app. Rows upgrade to `MEASURED` and re-rank automatically while the session runs.
48
+
3. Double-click a row to jump to the composable; double-click a fix to apply it. After a fix is applied, the affected prescription is re-analyzed.
| Enable Stability Doctor | on | Master switch for the Doctor tab and auto-refresh. |
57
+
| Max cascade candidates | 15 | How many top-scored composables get the (expensive) downstream blast-radius analysis. |
58
+
| Auto-refresh interval | 10s | Refresh cadence while a heatmap session is running. |
59
+
| Minimum score | 5 | Prescriptions scoring below this are hidden. |
60
+
| Include test sources | off | Whether composables in test source roots are scanned. |
61
+
62
+
!!! tip "Same-named composables"
63
+
64
+
Runtime data is matched by fully qualified name when the runtime reports it (version 0.10.0+). If two composables share a simple name and the runtime data cannot be attributed precisely (older runtimes), the prescription stays `ESTIMATED` with a note rather than guessing.
0 commit comments