Kotlin Performance
Measurement-first performance work for Kotlin. Pair with kotlin-patterns for idiomatic code and android-testing for the test harness that hosts benchmarks.
If the target is a pure JVM library or CLI, most guidance transfers — skip the Android sections.
When to activate
- Investigating cold/warm/hot start latency, first-frame time, or ANR reports
- Diagnosing scroll jank, animation stutter, or Compose recomposition storms
- Tracing memory growth, GC churn, or leaks in a shipping app
- Deciding whether to enable R8 full mode, ship a Baseline Profile, or add a Startup Profile
- Benchmarking a JVM Kotlin hot path (JMH / kotlinx-benchmark) before proposing a redesign
Rules of engagement
- Measure on-device against release builds. Debug APKs run interpreted or with limited AOT; measurements are worthless. Macrobenchmark demands
release (or a benchmark buildType with signingConfig + debuggable = false + profileable).
- Change one variable at a time. Log the baseline (before) and delta (after) with the same device, thermal state, and driver.
- Cold vs warm vs hot are three different problems — pick the
StartupMode explicitly.
- Fix algorithm and data-structure issues first, then allocations, then micro-optimizations.
- Do not benchmark on emulators for latency claims. Use a physical device profile with locked CPU governor (
adb shell cmd power set-fixed-performance-mode-enabled true when supported, or Gradle-managed devices with enablePerformanceMode = true).
Outcome expectations
- Every performance claim is backed by a reproducible Macrobenchmark or JMH run with mean + p50/p90/p99.
- Baseline Profile coverage of hot paths ≥ 30%; JIT-compilation ratio during target flow is quantified.
- R8 full mode + Baseline Profile deltas measured separately, not lumped together.
- No
Thread.sleep warm-ups; iterations are enforced by the harness.
- Behavior is unchanged: correctness tests pass on the optimized build.
Workflow
- Define the symptom precisely. Cold start / TTID / TTFD, scroll frame time p99, memory retained after N cycles, or throughput ops/sec. Choose one first.
- Choose the harness.
- Cold/warm/hot start, jank, TTID/TTFD → Macrobenchmark (
androidx.benchmark:benchmark-macro-junit4).
- Function-level JVM hot path → Microbenchmark (
androidx.benchmark:benchmark-junit4) on device, or kotlinx-benchmark / JMH for pure JVM.
- Memory/leaks → LeakCanary + Android Studio Memory Profiler; heap dumps for retention.
- System-level (jank, GC, I/O) → Perfetto trace (
adb shell perfetto -c config.pbtx -o trace.perfetto-trace).
- Capture a signed baseline on the target device. Record mean/p50/p90/p99, allocation, and one Perfetto trace.
- Analyze. In Perfetto or Android Studio: find hot slices (JIT compilation, GC, layout inflation,
androidx.compose.runtime.snapshots). In Compose reports, find non-restartable/non-skippable functions.
- Apply one change. Enable R8 full mode, add a Baseline Profile, refactor a specific
@Composable, switch a Dispatchers.Default to a bounded pool — one at a time.
- Verify. Re-run the same harness on the same device profile. Compare against baseline; reject changes that regress p99 or introduce non-deterministic variance.
Symptom → first tool mapping
| Symptom |
First tool |
Second tool |
| Cold start too slow |
Macrobenchmark StartupMode.COLD + StartupTimingMetric |
Baseline Profile + Startup Profile |
| Warm/hot start slow |
Macrobenchmark WARM/HOT |
Perfetto trace on bindApplication |
| Scroll jank (p99 frame > 16 ms) |
Macrobenchmark FrameTimingMetric |
Perfetto + Compose recomposition trace |
| Compose recomposition storm |
Compose compiler report + Modifier.Node audit |
androidx.compose.runtime.tooling.CompositionData snapshot |
| Memory growth after N screens |
LeakCanary + heap dump comparison |
adb shell dumpsys meminfo <pkg> |
| Allocations/GC pressure |
Microbenchmark MetricType.Alloc |
Async Profiler + allocation profiler |
| APK size regression |
./gradlew :app:analyzeReleaseBundle |
R8 dictionary + baseline profile diff |
| Native call latency |
Microbenchmark + TraceSectionMetric("JNI transition") |
@FastNative/@CriticalNative — see android-jni-ndk |
| JVM hot path (no Android) |
kotlinx-benchmark / JMH |
Async Profiler / JFR |
Android release-build knobs (measure each in isolation)
- R8 full mode:
android.enableR8.fullMode=true in gradle.properties and switch proguardFiles from proguard-android.txt to proguard-android-optimize.txt. Typical: 5–15% smaller APK + faster startup. Full mode enables horizontal class merging, vertical class merging, and interface-method rewriting — will break some reflection-heavy libraries. Verify at runtime, not just compile.
shrinkResources = true (paired with minifyEnabled = true) — required to strip unused resources. AGP 8.7+ supports optimized resource shrinking for ~30% resource file reduction.
- Baseline Profile:
androidx.baselineprofile Gradle plugin generates baseline-prof.txt from a Macrobenchmark journey run on a nonMinified build. R8 rewrites the rules to match the minified release (introduced in Baseline Profile 8.2+; ~30% method coverage improvement).
- Startup Profile: same generator,
includeInStartupProfile = true. Rearranges DEX layout so critical startup classes land in classes.dex — measurable startup win on top of Baseline Profile.
- PGO for the app process itself: profiles collected via
adb shell pm dump-profiles <pkg> can be committed as Cloud Profile input — reserved for late-cycle tuning.
Enable together only after each has been measured separately.
Compose performance
Coroutines & Flow performance
- Dispatcher choice at the leaf, not the caller.
withContext(Dispatchers.IO) around genuine blocking I/O; Default for CPU-bound. IO has an unbounded thread pool (~64 threads by default) — starving Default with I/O work is a common jank source.
limitedParallelism(n) on Dispatchers.IO for controlled parallelism (Dispatchers.IO.limitedParallelism(4)). Prevents thread explosion under load spikes.
- Backpressure operators on
Flow:
buffer(n) — decouples producer and consumer with a bounded queue. Use when both are CPU-bound but at different rates.
conflate() — drops intermediate values, keeps only the latest. UI state pipelines.
collectLatest { } — cancels the previous block on new emission. Search-as-you-type.
debounce(ms) / sample(ms) — throttle by time. User input flows.
- Every
launch { } allocates a StandaloneCoroutine; async { } allocates a DeferredCoroutine. In a hot loop, prefer map { } on a Flow over launching per-item.
- Avoid
withContext(Dispatchers.Main) in the middle of a leaf function — the context switch cost dominates for microsecond-scale work.
Memory & leaks
- LeakCanary is now natively integrated into Android Studio Otter (2025.12+) Profiler as a "LeakCanary task"; still install the library on debug builds for continuous detection. In CI/dev, keep
LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 1) for aggressive detection.
- Typical Android leak sources:
Context/Activity/View held in singletons, static fields, or object bodies.
Fragment view listeners bound to Fragment.lifecycleOwner instead of viewLifecycleOwner.
Handler(Looper.getMainLooper()) with postDelayed referencing a View.
GlobalScope.launch { } capturing an Activity.
- Heap dumps: Android Studio → Memory Profiler → Capture Heap Dump. Filter by class, sort by "Retained Size". Compare two dumps N minutes apart to find growth.
adb shell dumpsys meminfo <pkg> gives a quick view of PSS, private clean/dirty, DEX/code. Compare across builds.
- Native memory:
libmemunreachable (adb shell dumpsys meminfo <pkg> -a shows unaccounted native RSS). NDK bugs surface here.
Perfetto tracing
Prefer Perfetto over legacy systrace:
# Interactive: https://ui.perfetto.dev — record via chrome://inspect
# Command line (device):
adb shell perfetto \
-o /data/misc/perfetto-traces/trace.perfetto-trace \
-t 20s \
-b 32mb \
sched freq idle am wm gfx view binder_driver hal dalvik camera input res
adb pull /data/misc/perfetto-traces/trace.perfetto-trace
Key tracks to inspect:
am_activity_launch_time — cold-start slice.
Choreographer#doFrame — every frame; look for slices > 16 ms (60 Hz) or > 8 ms (120 Hz).
JIT Compiling % — significant % during startup = missing Baseline Profile coverage.
HeapTaskDaemon bursts — GC pressure; correlate with allocation profile.
Compose:recompose slices (with perfettoSdkTracing = true) — recomposition count per Composable.
Add custom slices for domain workflows:
trace("MyFeature:refresh") {
// Work
}
Or TraceSectionMetric("MyFeature:%") in a Macrobenchmark for regression tracking.
JMH / kotlinx-benchmark (pure JVM)
For non-Android Kotlin (backend libraries, tooling) or when isolating a hot function:
// build.gradle.kts
plugins { id("org.jetbrains.kotlinx.benchmark") version "0.4.13" }
benchmark {
configurations { named("main") { iterations = 5; warmups = 3; iterationTime = 1.seconds } }
targets { register("main") }
}
@State(Scope.Benchmark)
open class ParseBench {
private val input = "…".toByteArray()
@Benchmark fun parseA(bh: Blackhole) { bh.consume(parseA(input)) }
@Benchmark fun parseB(bh: Blackhole) { bh.consume(parseB(input)) }
}
Rules:
- Always
Blackhole.consume(result) — otherwise JIT dead-code-eliminates the whole benchmark.
- Warmups must run enough iterations to reach steady-state JIT (3–5 typically).
- Compare with
benchstat-style stats, not point comparisons.
Quick review checklist
- Measurement uses
release/benchmark buildType, not debug
- Macrobenchmark
StartupMode is explicit; Frame metrics use realistic user actions (UiAutomator scroll, not Thread.sleep)
- Baseline Profile includes the actual entry Activity and dependency framework calls (auth SDK, image loader, network client init)
- R8 mapping file is preserved (
obfuscated.map) for crash symbolication
- Compose compiler report has been read; unstable public data classes are annotated or added to the stability config
- Every claimed improvement has a p50/p90/p99 delta, not just mean
- Trace or heap-dump files are committed alongside PR notes for reviewer verification
- No
System.currentTimeMillis()-based DIY timers — Macrobenchmark or trace() sections only
Common anti-patterns
- Measuring on debug builds → interpreter overhead dominates; conclusions are wrong.
- Enabling R8 full mode without testing → reflection-heavy libraries (Gson, Retrofit, Room without keep rules, older Moshi) break at runtime with
NoSuchMethodException. Ship staged rollout.
- Baseline Profile generated on a non-representative journey → covers the wrong methods. Use realistic user flows, not synthetic loops.
SharingStarted.Eagerly on repository Flows → keeps upstream alive forever, leaks work and memory. Use WhileSubscribed(5_000).
Dispatchers.IO for CPU-bound work → starves the shared thread pool; use Default or a limitedParallelism slice.
- Unstable lambdas captured in a hot
@Composable → busts skipping. Hoist state or convert to remember { }-scoped values.
for (i in 0..list.size - 1) in a hot loop with an Iterable → boxes Int; use forEach / for (item in list) for concrete lists.
Resources
Load on demand:
- references/macrobenchmark-and-microbenchmark.md — end-to-end setup, StartupTimingMetric, FrameTimingMetric, TraceSectionMetric, allocation metrics, Gradle-managed devices, running on Firebase Test Lab; load when setting up the harness
- references/baseline-and-startup-profiles.md — Baseline Profile Gradle plugin, generation via BaselineProfileRule, Startup Profile
includeInStartupProfile, coverage measurement, Cloud Profile pipeline; load when creating or auditing profiles
- references/compose-recomposition.md — compiler stability report, strong skipping,
Modifier.Node, LazyColumn key stability, derivedStateOf, Perfetto recomposition tracing; load when Compose scroll or animation is the bottleneck
- references/coroutines-and-flow-performance.md — dispatcher pool sizing,
limitedParallelism, backpressure operator selection, Flow overhead vs Channel vs suspend, cold-hot conversion cost; load when async pipelines are slow or bursty
- references/memory-and-leaks.md — LeakCanary integration, heap dump comparison, retained size analysis, Android memory model (PSS/RSS/SwapPss/DEX/code), native memory triage; load when diagnosing OOM, growth, or leak reports
- references/perfetto-and-tracing.md — Perfetto config, custom trace sections, Compose SDK tracing, common track cheatsheet, offline trace analysis with the Trace Processor; load when a symptom needs system-level attribution
- references/jvm-benchmarking.md — kotlinx-benchmark / JMH setup, dead-code elimination, warmup strategy, Blackhole usage, comparing runs, running on CI; load when the hot path is non-Android or must be isolated from the runtime
1---2name: kotlin-performance3description: Measurement-first performance workflow for Kotlin on JVM and Android: Macrobenchmark, Microbenchmark, Baseline Profiles, Startup Profiles, R8 full mode, Compose recomposition analysis, coroutine/Flow throughput, Perfetto/JIT tracing, LeakCanary heap analysis. Use when investigating cold-start latency, jank, scroll frames, memory growth, or when tuning R8/PGO output for an Android app or JVM Kotlin library.4license: MIT5---67# Kotlin Performance89Measurement-first performance work for Kotlin. Pair with `kotlin-patterns` for idiomatic code and `android-testing` for the test harness that hosts benchmarks.1011If the target is a pure JVM library or CLI, most guidance transfers — skip the Android sections.1213## When to activate1415- Investigating cold/warm/hot start latency, first-frame time, or ANR reports16- Diagnosing scroll jank, animation stutter, or Compose recomposition storms17- Tracing memory growth, GC churn, or leaks in a shipping app18- Deciding whether to enable R8 full mode, ship a Baseline Profile, or add a Startup Profile19- Benchmarking a JVM Kotlin hot path (JMH / kotlinx-benchmark) before proposing a redesign2021---2223## Rules of engagement2425- **Measure on-device against release builds.** Debug APKs run interpreted or with limited AOT; measurements are worthless. Macrobenchmark demands `release` (or a `benchmark` buildType with `signingConfig` + `debuggable = false` + `profileable`).26- **Change one variable at a time.** Log the baseline (before) and delta (after) with the same device, thermal state, and driver.27- **Cold vs warm vs hot** are three different problems — pick the `StartupMode` explicitly.28- **Fix algorithm and data-structure issues first**, then allocations, then micro-optimizations.29- **Do not benchmark on emulators for latency claims.** Use a physical device profile with locked CPU governor (`adb shell cmd power set-fixed-performance-mode-enabled true` when supported, or Gradle-managed devices with `enablePerformanceMode = true`).3031---3233## Outcome expectations3435- Every performance claim is backed by a reproducible Macrobenchmark or JMH run with mean + p50/p90/p99.36- Baseline Profile coverage of hot paths ≥ 30%; JIT-compilation ratio during target flow is quantified.37- R8 full mode + Baseline Profile deltas measured separately, not lumped together.38- No `Thread.sleep` warm-ups; iterations are enforced by the harness.39- Behavior is unchanged: correctness tests pass on the optimized build.4041---4243## Workflow44451. **Define the symptom precisely.** Cold start / TTID / TTFD, scroll frame time p99, memory retained after N cycles, or throughput ops/sec. Choose one first.462. **Choose the harness.**47 - Cold/warm/hot start, jank, TTID/TTFD → **Macrobenchmark** (`androidx.benchmark:benchmark-macro-junit4`).48 - Function-level JVM hot path → **Microbenchmark** (`androidx.benchmark:benchmark-junit4`) on device, or **kotlinx-benchmark** / JMH for pure JVM.49 - Memory/leaks → LeakCanary + Android Studio Memory Profiler; heap dumps for retention.50 - System-level (jank, GC, I/O) → Perfetto trace (`adb shell perfetto -c config.pbtx -o trace.perfetto-trace`).513. **Capture a signed baseline** on the target device. Record mean/p50/p90/p99, allocation, and one Perfetto trace.524. **Analyze**. In Perfetto or Android Studio: find hot slices (JIT compilation, GC, layout inflation, `androidx.compose.runtime.snapshots`). In Compose reports, find non-restartable/non-skippable functions.535. **Apply one change.** Enable R8 full mode, add a Baseline Profile, refactor a specific `@Composable`, switch a `Dispatchers.Default` to a bounded pool — one at a time.546. **Verify.** Re-run the same harness on the same device profile. Compare against baseline; reject changes that regress p99 or introduce non-deterministic variance.5556---5758## Symptom → first tool mapping5960| Symptom | First tool | Second tool |61|---------|------------|-------------|62| Cold start too slow | Macrobenchmark `StartupMode.COLD` + `StartupTimingMetric` | Baseline Profile + Startup Profile |63| Warm/hot start slow | Macrobenchmark `WARM`/`HOT` | Perfetto trace on `bindApplication` |64| Scroll jank (p99 frame > 16 ms) | Macrobenchmark `FrameTimingMetric` | Perfetto + Compose recomposition trace |65| Compose recomposition storm | Compose compiler report + `Modifier.Node` audit | `androidx.compose.runtime.tooling.CompositionData` snapshot |66| Memory growth after N screens | LeakCanary + heap dump comparison | `adb shell dumpsys meminfo <pkg>` |67| Allocations/GC pressure | Microbenchmark `MetricType.Alloc` | Async Profiler + allocation profiler |68| APK size regression | `./gradlew :app:analyzeReleaseBundle` | R8 dictionary + baseline profile diff |69| Native call latency | Microbenchmark + `TraceSectionMetric("JNI transition")` | `@FastNative`/`@CriticalNative` — see `android-jni-ndk` |70| JVM hot path (no Android) | kotlinx-benchmark / JMH | Async Profiler / JFR |7172---7374## Android release-build knobs (measure each in isolation)7576- **R8 full mode**: `android.enableR8.fullMode=true` in `gradle.properties` **and** switch `proguardFiles` from `proguard-android.txt` to `proguard-android-optimize.txt`. Typical: 5–15% smaller APK + faster startup. Full mode enables horizontal class merging, vertical class merging, and interface-method rewriting — will break some reflection-heavy libraries. Verify at runtime, not just compile.77- **`shrinkResources = true`** (paired with `minifyEnabled = true`) — required to strip unused resources. AGP 8.7+ supports **optimized resource shrinking** for ~30% resource file reduction.78- **Baseline Profile**: `androidx.baselineprofile` Gradle plugin generates `baseline-prof.txt` from a Macrobenchmark journey run on a `nonMinified` build. R8 rewrites the rules to match the minified release (introduced in Baseline Profile 8.2+; ~30% method coverage improvement).79- **Startup Profile**: same generator, `includeInStartupProfile = true`. Rearranges DEX layout so critical startup classes land in `classes.dex` — measurable startup win on top of Baseline Profile.80- **PGO for the app process itself**: profiles collected via `adb shell pm dump-profiles <pkg>` can be committed as Cloud Profile input — reserved for late-cycle tuning.8182Enable together only after each has been measured separately.8384---8586## Compose performance8788- Enable the Compose compiler stability/metrics report:89 ```kotlin90 composeCompiler {91 reportsDestination = layout.buildDirectory.dir("compose_compiler")92 metricsDestination = layout.buildDirectory.dir("compose_compiler")93 stabilityConfigurationFile = rootProject.layout.projectDirectory.file("compose_stability.conf")94 }95 ```96 Review the `*-classes.txt` output: every class not marked `stable` breaks skipping. Fix by marking stable in the config file, using `@Immutable`/`@Stable`, or wrapping in a stable holder.97- **Strong skipping mode** (Compose Compiler 1.5.4+, default in newer AGP): treats unstable parameters as `@Stable` when they're equal by structural equality. Reduces recomposition for common patterns (e.g. lambdas with unstable captures) but must be verified with a trace — some hot Composables still need explicit fixes.98- **Recomposition tracing**: Macrobenchmark 1.4+ supports `perfettoSdkTracing = true` — recompositions appear in the Perfetto trace as named slices. Look for slice counts that exceed expected recomposition rounds per user interaction.99- Modifier chain hot paths: prefer `Modifier.Node` API (Compose 1.7+) for custom modifiers over `Modifier.composed { }`, which allocates on every recomposition.100- Lists: `LazyColumn` with unstable `key = { … }` lambda captures triggers full recomposition. Prefer stable, comparable keys (`item.id`); avoid `key = { it.hashCode() }`.101102---103104## Coroutines & Flow performance105106- **Dispatcher choice at the leaf, not the caller.** `withContext(Dispatchers.IO)` around genuine blocking I/O; `Default` for CPU-bound. `IO` has an unbounded thread pool (~64 threads by default) — starving `Default` with I/O work is a common jank source.107- **`limitedParallelism(n)`** on `Dispatchers.IO` for controlled parallelism (`Dispatchers.IO.limitedParallelism(4)`). Prevents thread explosion under load spikes.108- **Backpressure operators** on `Flow`:109 - `buffer(n)` — decouples producer and consumer with a bounded queue. Use when both are CPU-bound but at different rates.110 - `conflate()` — drops intermediate values, keeps only the latest. UI state pipelines.111 - `collectLatest { }` — cancels the previous block on new emission. Search-as-you-type.112 - `debounce(ms)` / `sample(ms)` — throttle by time. User input flows.113- Every `launch { }` allocates a `StandaloneCoroutine`; `async { }` allocates a `DeferredCoroutine`. In a hot loop, prefer `map { }` on a `Flow` over launching per-item.114- Avoid `withContext(Dispatchers.Main)` in the middle of a leaf function — the context switch cost dominates for microsecond-scale work.115116---117118## Memory & leaks119120- **LeakCanary** is now natively integrated into Android Studio Otter (2025.12+) Profiler as a "LeakCanary task"; still install the library on debug builds for continuous detection. In CI/dev, keep `LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 1)` for aggressive detection.121- Typical Android leak sources:122 - `Context`/`Activity`/`View` held in singletons, static fields, or `object` bodies.123 - `Fragment` view listeners bound to `Fragment.lifecycleOwner` instead of `viewLifecycleOwner`.124 - `Handler(Looper.getMainLooper())` with `postDelayed` referencing a `View`.125 - `GlobalScope.launch { }` capturing an Activity.126- **Heap dumps**: Android Studio → Memory Profiler → Capture Heap Dump. Filter by class, sort by "Retained Size". Compare two dumps N minutes apart to find growth.127- `adb shell dumpsys meminfo <pkg>` gives a quick view of PSS, private clean/dirty, DEX/code. Compare across builds.128- Native memory: `libmemunreachable` (`adb shell dumpsys meminfo <pkg> -a` shows unaccounted native RSS). NDK bugs surface here.129130---131132## Perfetto tracing133134Prefer Perfetto over legacy `systrace`:135136```bash137# Interactive: https://ui.perfetto.dev — record via chrome://inspect138# Command line (device):139adb shell perfetto \140 -o /data/misc/perfetto-traces/trace.perfetto-trace \141 -t 20s \142 -b 32mb \143 sched freq idle am wm gfx view binder_driver hal dalvik camera input res144adb pull /data/misc/perfetto-traces/trace.perfetto-trace145```146147Key tracks to inspect:148149- `am_activity_launch_time` — cold-start slice.150- `Choreographer#doFrame` — every frame; look for slices > 16 ms (60 Hz) or > 8 ms (120 Hz).151- `JIT Compiling %` — significant % during startup = missing Baseline Profile coverage.152- `HeapTaskDaemon` bursts — GC pressure; correlate with allocation profile.153- `Compose:recompose` slices (with `perfettoSdkTracing = true`) — recomposition count per Composable.154155Add custom slices for domain workflows:156157```kotlin158trace("MyFeature:refresh") {159 // Work160}161```162163Or `TraceSectionMetric("MyFeature:%")` in a Macrobenchmark for regression tracking.164165---166167## JMH / kotlinx-benchmark (pure JVM)168169For non-Android Kotlin (backend libraries, tooling) or when isolating a hot function:170171```kotlin172// build.gradle.kts173plugins { id("org.jetbrains.kotlinx.benchmark") version "0.4.13" }174benchmark {175 configurations { named("main") { iterations = 5; warmups = 3; iterationTime = 1.seconds } }176 targets { register("main") }177}178```179180```kotlin181@State(Scope.Benchmark)182open class ParseBench {183 private val input = "…".toByteArray()184 @Benchmark fun parseA(bh: Blackhole) { bh.consume(parseA(input)) }185 @Benchmark fun parseB(bh: Blackhole) { bh.consume(parseB(input)) }186}187```188189Rules:190191- Always `Blackhole.consume(result)` — otherwise JIT dead-code-eliminates the whole benchmark.192- Warmups must run enough iterations to reach steady-state JIT (3–5 typically).193- Compare with `benchstat`-style stats, not point comparisons.194195---196197## Quick review checklist198199- Measurement uses `release`/`benchmark` buildType, not `debug`200- Macrobenchmark `StartupMode` is explicit; Frame metrics use realistic user actions (`UiAutomator` scroll, not `Thread.sleep`)201- Baseline Profile includes the actual entry Activity **and** dependency framework calls (auth SDK, image loader, network client init)202- R8 mapping file is preserved (`obfuscated.map`) for crash symbolication203- Compose compiler report has been read; unstable public data classes are annotated or added to the stability config204- Every claimed improvement has a p50/p90/p99 delta, not just mean205- Trace or heap-dump files are committed alongside PR notes for reviewer verification206- No `System.currentTimeMillis()`-based DIY timers — Macrobenchmark or `trace()` sections only207208---209210## Common anti-patterns211212- **Measuring on debug builds** → interpreter overhead dominates; conclusions are wrong.213- **Enabling R8 full mode without testing** → reflection-heavy libraries (Gson, Retrofit, Room without keep rules, older Moshi) break at runtime with `NoSuchMethodException`. Ship staged rollout.214- **Baseline Profile generated on a non-representative journey** → covers the wrong methods. Use realistic user flows, not synthetic loops.215- **`SharingStarted.Eagerly` on repository `Flow`s** → keeps upstream alive forever, leaks work and memory. Use `WhileSubscribed(5_000)`.216- **`Dispatchers.IO` for CPU-bound work** → starves the shared thread pool; use `Default` or a `limitedParallelism` slice.217- **Unstable lambdas captured in a hot `@Composable`** → busts skipping. Hoist state or convert to `remember { }`-scoped values.218- **`for (i in 0..list.size - 1)` in a hot loop with an `Iterable`** → boxes `Int`; use `forEach` / `for (item in list)` for concrete lists.219220---221222## Resources223224Load on demand:225226- [references/macrobenchmark-and-microbenchmark.md](references/macrobenchmark-and-microbenchmark.md) — end-to-end setup, StartupTimingMetric, FrameTimingMetric, TraceSectionMetric, allocation metrics, Gradle-managed devices, running on Firebase Test Lab; load when setting up the harness227- [references/baseline-and-startup-profiles.md](references/baseline-and-startup-profiles.md) — Baseline Profile Gradle plugin, generation via BaselineProfileRule, Startup Profile `includeInStartupProfile`, coverage measurement, Cloud Profile pipeline; load when creating or auditing profiles228- [references/compose-recomposition.md](references/compose-recomposition.md) — compiler stability report, strong skipping, `Modifier.Node`, `LazyColumn` key stability, `derivedStateOf`, Perfetto recomposition tracing; load when Compose scroll or animation is the bottleneck229- [references/coroutines-and-flow-performance.md](references/coroutines-and-flow-performance.md) — dispatcher pool sizing, `limitedParallelism`, backpressure operator selection, `Flow` overhead vs `Channel` vs suspend, cold-hot conversion cost; load when async pipelines are slow or bursty230- [references/memory-and-leaks.md](references/memory-and-leaks.md) — LeakCanary integration, heap dump comparison, retained size analysis, Android memory model (PSS/RSS/SwapPss/DEX/code), native memory triage; load when diagnosing OOM, growth, or leak reports231- [references/perfetto-and-tracing.md](references/perfetto-and-tracing.md) — Perfetto config, custom trace sections, Compose SDK tracing, common track cheatsheet, offline trace analysis with the Trace Processor; load when a symptom needs system-level attribution232- [references/jvm-benchmarking.md](references/jvm-benchmarking.md) — kotlinx-benchmark / JMH setup, dead-code elimination, warmup strategy, Blackhole usage, comparing runs, running on CI; load when the hot path is non-Android or must be isolated from the runtime