| name | investigate-compose-metrics |
| description | Investigate per-module Compose compiler metrics in this repo to find low-skippability hotspots and the underlying unstable classes. |
| disable-model-invocation | true |
| allowed-tools | ["Read","Glob","Grep","Bash(./gradlew:*)","Bash(find:*)","Bash(awk:*)","Bash(grep:*)","Bash(sed:*)","Bash(jq:*)","Bash(sort:*)","Bash(column:*)"] |
Investigate Compose Metrics
Purpose
Drill into the Compose compiler metrics that the build emits when
-PcomposeCompilerReports=true is passed. The goal is to find specific
Composables and parameter types whose stability or skippability should be
improved — not to produce a roll-up summary across modules.
This skill is investigation-only. Do not change code unless the user
explicitly asks for a fix in a follow-up turn.
1. Check whether metrics are already on disk
find . -path '*/build/compose_metrics/*-module.json'
Each module that applies the sunsetscrob.android.compose convention
plugin produces:
<mod>/build/compose_metrics/debug/<mod>-module.json
<mod>/build/compose_reports/<mod>-classes.txt
<mod>/build/compose_reports/<mod>-composables.csv
<mod>/build/compose_reports/<mod>-composables.txt
Modules expected to appear: :app, :core, :ui_common,
:feature:account, :feature:album, :feature:artist,
:feature:auth, :feature:discover, :feature:home,
:feature:scrobble, :test_helper:integration.
If only a subset is present, the previous build restored
compileDebugKotlin from the build cache and the Compose compiler
plugin did not run for the missing modules. Regenerate (step 2).
2. Generate / regenerate metrics
If metrics are missing or stale, ask the user before regenerating —
this rebuilds every Kotlin module from cold and takes several minutes.
./gradlew clean assembleDebug -PcomposeCompilerReports=true --no-build-cache
Why each flag matters:
-PcomposeCompilerReports=true — wires metricsDestination and
reportsDestination for the Compose compiler plugin
(build-logic/.../ext/ComposeConfiguration.kt). Without it, no
reports are written.
--no-build-cache — without this, Gradle may restore
compileDebugKotlin outputs from the build cache and the Compose
plugin never runs, so most modules end up with no reports.
clean — needed at least once after enabling the flag, to evict
prior compile output.
3. Read the module-level JSON with care
<mod>-module.json reports effectivelyStableClasses / totalClasses,
but this ratio looks worse than reality because the denominator
includes classes that never appear as a Composable parameter and
therefore cannot affect skippability. Treat the following as noise
when scanning <mod>-classes.txt:
- Metro-generated classes — anything ending in
MetroFactory, the
MetroContributionToAppScope.BindsMirror inner class, the assisted
Factory.Impl.
- KSerializer classes generated by
kotlinx.serialization — names
ending in .$serializer.
- Service classes (e.g.
MusicNotificationListenerService).
- ViewModel classes themselves — only their
UiState data class is
passed into Compose.
- Helper classes whose fields are obviously not Composable inputs:
MutableStateFlow, Job, CoroutineScope, Repository-typed
fields.
The signal lives in classes that actually appear as a parameter type
of an @Composable function.
4. Drill-down workflow
4a. Pick a module
Read the <mod>-module.json files (Glob + Read, or jq) and choose a
module with a low skippableComposables / totalComposables ratio. A
fast scan:
jq -r '
"\(input_filename | sub(".*/(.+)/build.*"; "\\1")): " +
"skippable=\(.skippableComposables)/\(.totalComposables), " +
"stableCls=\(.effectivelyStableClasses)/\(.totalClasses), " +
"stableArg=\(.knownStableArguments)/\(.totalArguments)"
' */build/compose_metrics/debug/*-module.json **/*/build/compose_metrics/debug/*-module.json 2>/dev/null
If the glob expansion is awkward in the user's shell, fall back to
find ... | xargs jq ....
4b. List non-skippable Composables in the chosen module
The CSV header is:
package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls
Filter rows that are real Composables (composable=1) and not
skippable (skippable=0):
awk -F, 'NR>1 && $3=="1" && $4=="0" {print $1"."$2}' \
<mod>/build/compose_reports/<mod>-composables.csv
4c. Inspect parameter types in source
For each suspect Composable, read its declaration in the source tree
(usually under feature/<mod>/src/main/kotlin/.../ui/) and note every
non-primitive parameter type, plus the receiver if any.
4d. Check why each parameter type is unstable
grep -B 1 -A 20 -E '(stable|unstable) class .*\.<TypeName> \{' \
<mod>/build/compose_reports/<mod>-classes.txt
The block ends with <runtime stability> = .... Each field line
states whether that field is stable, unstable, or runtime. The
unstable fields are the actual cause.
4e. Decide a fix per case
Common patterns in this codebase (see
.claude/rules/coding-conventions.md → "Data Class"):
- Add
@Immutable to a data class whose fields are all stable but
that the compiler conservatively marks unstable (e.g. deep generics,
inferred uncertainty across module boundaries).
- Replace
List<T> with kotlinx.collections.immutable.ImmutableList<T>.
- Hoist an unstable wiring object out of the parameter list — pass
only the data the Composable actually reads.
- Move the unstable parameter behind a lambda or a remembered state
holder so it is not part of the call's positional arguments.
Present the candidates and the suggested direction to the user. Do not
edit code in this turn unless asked.
5. What this skill does NOT do
- Modify code. Fixes are a separate request informed by the findings.
- Update the
Makefile or .github/workflows/compose_metrics.yml.
- Aggregate trends across runs. Each invocation is a single snapshot.