Use this skill to generate and measure Jetpack Compose Baseline Profiles end-to-end with the AGP 8.2+ Baseline Profile Generator module and the Macrobenchmark harness. Covers writing the `BaselineProfileRule` journey for cold startup plus first-scroll, generating `baseline-prof.txt`, verifying it shipped at `assets/dexopt/baseline.prof`, measuring with `MacrobenchmarkRule` under `CompilationMode.Partial(BaselineProfileMode.Require)`, and emitting accurate time-to-fully-drawn via `ReportDrawn` / `ReportDrawnWhen` / `ReportDrawnAfter` from `androidx.activity.compose`. Compose ships unbundled, so every Compose UI app benefits — cited gains around 30% faster startup and 40% smoother first-scroll. Use when the user mentions "baseline profile", "macrobenchmark", "slow cold startup", "first-scroll jank", "StartupTimingMetric", "FrameTimingMetric", "ReportDrawn", "TTFD", or preparing a release build for performance measurement.
npx skills add https://github.com/skydoves/compose-performance-skills --skill generating-baseline-profiles
Baseline Profiles ship an AOT compilation hint list inside the APK so ART pre-compiles hot Compose code paths on install instead of relying on JIT during the first runs. Cited gains: roughly 30% faster cold startup and 40% smoother first-scroll on the journeys that were profiled. Compose ships unbundled from the platform, so every Compose UI app benefits — there is no version of Android where Compose is already AOT-compiled by the system image.
This skill is the measurement spine for the rest of the performance work. Profiles are generated with BaselineProfileRule from androidx.benchmark:benchmark-macro-junit4 and measured with MacrobenchmarkRule from the same artifact. Generation and measurement are two distinct @Test files in a separate :baselineprofile module that AGP 8.2+ scaffolds via New Module → Baseline Profile Generator.
LazyColumn / LazyVerticalGrid is janky on real devices even after stability fixes.aosp_cf_x86_64_phone-userdebug, or "ReportDrawn".../../recomposition/debugging-recompositions/SKILL.md (debug Layout Inspector) or ../tracing-recompositions-at-runtime/SKILL.md (release @TraceRecomposition).../../stability/enforcing-stability-in-ci/SKILL.md. Baseline Profiles measure runtime; stabilityCheck measures compile-time skippability.../testing-compose-in-release-mode/SKILL.md.androidx.baselineprofile Gradle plugin and a :baselineprofile module).aosp_cf_x86_64_phone-userdebug). High-end pixel devices mask perf wins; emulators with default API images are not representative.isMinifyEnabled = true, isShrinkResources = true, proguard-android-optimize.txt). Debug builds are not measurable. See ../../build/configuring-r8-for-compose/SKILL.md for R8 setup if missing.<profileable android:shell="true"/> in the app AndroidManifest.xml under <application> so the Macrobenchmark process can attach simpleperf. The android: namespace prefix is required — without it, manifest merger fails.../../stability/diagnosing-compose-stability/SKILL.md and ../../recomposition/debugging-recompositions/SKILL.md if startup is dominated by avoidable recomposition rather than initial composition.In Android Studio: File → New → New Module → Baseline Profile Generator. Pick the target application module. AGP scaffolds:
:baselineprofile module with the androidx.baselineprofile Gradle plugin applied.BaselineProfileGenerator.kt skeleton with a BaselineProfileRule @Test.StartupBenchmarks.kt skeleton with a MacrobenchmarkRule @Test.androidx.baselineprofile plugin applied in the app module too, so ./gradlew :app:generateBaselineProfile is wired up.If editing manually instead of using the template, add to :baselineprofile/build.gradle.kts:
plugins {
id("com.android.test")
id("org.jetbrains.kotlin.android")
id("androidx.baselineprofile")
}
android { targetProjectPath = ":app" }
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.3.0")
implementation("androidx.test.ext:junit:1.2.1")
implementation("androidx.test.uiautomator:uiautomator:2.3.0")
implementation("androidx.test.espresso:espresso-core:3.6.1")
}
And to :app/build.gradle.kts:
plugins { id("androidx.baselineprofile") }
dependencies { baselineProfile(project(":baselineprofile")) }
See references/macrobenchmark-harness.md for the full Gradle + manifest setup including <profileable>.
The generator is a @Test using BaselineProfileRule.collect. MUST cover startup plus at least one scroll — startup-only profiles leave first-scroll cold.
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()
@Test
fun startupAndScroll() = rule.collect(packageName = "com.example") {
startActivityAndWait()
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
feed.fling(Direction.DOWN)
device.findObject(By.res("feed_item_0"))?.click()
device.wait(Until.hasObject(By.res("detail")), 3_000)
device.pressBack()
}
}
Use Modifier.testTag("feed") in the Compose source so By.res("feed") resolves. A gesture margin of displayWidth / 5 keeps flings off the system gesture area.
./gradlew :app:generateBaselineProfile
For a specific variant: ./gradlew :app:generateReleaseBaselineProfile.
Output lands at:
app/src/<variant>/generated/baselineProfiles/baseline-prof.txt
(e.g. app/src/release/generated/baselineProfiles/baseline-prof.txt. On older AGP layouts the file may instead land at app/src/main/generated/baselineProfiles/baseline-prof.txt — AGP writes the variant-specific path; merge to main for shipping if all variants share a profile.)
Build → Analyze APK on the release .apk / .aab and confirm:
assets/dexopt/baseline.prof
assets/dexopt/baseline.profm
If those files are missing, the profile was generated but not packaged — usually because baselineProfile(project(":baselineprofile")) was not added to the app module's dependencies. Without these files in the APK, ART has nothing to AOT-compile and the perf gain is zero.
A separate @Test, in the same :baselineprofile module, using MacrobenchmarkRule. The compilation mode must be CompilationMode.Partial(BaselineProfileMode.Require) so the test fails loudly if the profile is missing rather than silently measuring an unprofiled build.
@RunWith(AndroidJUnit4::class)
class StartupBenchmarks {
@get:Rule val rule = MacrobenchmarkRule()
@Test
fun startupCompilationBaselineProfiles() = rule.measureRepeated(
packageName = "com.example",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD,
compilationMode = CompilationMode.Partial(BaselineProfileMode.Require),
) {
pressHome()
startActivityAndWait()
}
}
For an A/B comparison, add a sibling @Test with compilationMode = CompilationMode.None and compare medians. This is the only way to prove the profile moved the number.
@Test
fun feedScrollPerformance() = rule.measureRepeated(
packageName = "com.example",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
compilationMode = CompilationMode.Partial(BaselineProfileMode.Require),
) {
startActivityAndWait()
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
feed.fling(Direction.DOWN)
}
FrameTimingMetric reports frameDurationCpuMs percentiles (P50 / P90 / P95 / P99) and frameOverrunMs (negative = on time, positive = missed deadline). The headline number is P95 frameOverrunMs — the worst-case missed-deadline frame in the high tail of a scroll.
StartupTimingMetric reports timeToInitialDisplay by default. For timeToFullDisplay (TTFD) — the number that actually matches user perception — the app must call ReportDrawn once the first meaningful screen state is rendered. Without it, TTFD falls back to timeToInitialDisplay and undercounts.
import androidx.activity.compose.ReportDrawn
import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.ReportDrawnAfter
@Composable
fun Feed(state: FeedState) {
ReportDrawnWhen { state.items.isNotEmpty() }
LazyColumn { items(state.items, key = { it.id }) { SnackRow(it) } }
}
ReportDrawn — fire immediately on composition (use when the first frame is the meaningful frame).ReportDrawnWhen { predicate } — fire when the predicate first turns true (typical case: state hydrated).ReportDrawnAfter { suspend block } — fire after the suspend block completes (typical case: an explicit awaitFirstFrame()).Run on a connected physical device or aosp_cf_x86_64_phone-userdebug Cuttlefish via ./gradlew :baselineprofile:connectedReleaseAndroidTest. Report medians across iterations, not means — Macrobenchmark logs both, and means are sensitive to a single thermal-throttled run. The IDE displays a result table; the JSON lives at :baselineprofile/build/outputs/connected_android_test_additional_output/.
See references/macrobenchmark-harness.md for parsing the JSON in CI.
// WRONG
@Test fun startup() = rule.collect(packageName = "com.example") {
startActivityAndWait()
}
// WRONG because: only the startup code paths get AOT-compiled. First-scroll, list-item
// composition, and detail-screen entry stay interpreted on first run, which is exactly
// where the user perceives jank. Profile every hot user journey, not just launch.
// RIGHT
@Test fun startupAndScroll() = rule.collect(packageName = "com.example") {
startActivityAndWait()
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
feed.fling(Direction.DOWN)
}
// WRONG
compilationMode = CompilationMode.Partial(BaselineProfileMode.UseIfAvailable)
// WRONG because: UseIfAvailable silently falls back to no profile if assets/dexopt/baseline.prof
// is missing. The Macrobench then measures an unprofiled build and the developer thinks
// the profile is working when it is not. Use Require to fail loudly.
// RIGHT
compilationMode = CompilationMode.Partial(BaselineProfileMode.Require)
// WRONG
@Composable
fun Feed(state: FeedState) {
LazyColumn { items(state.items) { SnackRow(it) } }
}
// WRONG because: TTFD is reported when the first frame draws. An empty LazyColumn draws
// a frame too — TTFD then matches timeToInitialDisplay and undercounts the time the user
// actually waited for content. Add ReportDrawnWhen { state.items.isNotEmpty() }.
// RIGHT
@Composable
fun Feed(state: FeedState) {
ReportDrawnWhen { state.items.isNotEmpty() }
LazyColumn { items(state.items, key = { it.id }) { SnackRow(it) } }
}
# WRONG
"./gradlew :app:installDebug && adb shell am start … && record startup with systrace"
# WRONG because: debug builds run Compose interpreted, with Live Literals turning constants
# into getters that defeat compile-time folding. Cold-start numbers are inflated 2–4×;
# scroll FrameTiming numbers are dominated by the interpreter overhead, not by the code.
# Measurement only counts on release + R8 + real device. Cross-link
# ../testing-compose-in-release-mode/SKILL.md.
# RIGHT
"./gradlew :baselineprofile:connectedReleaseAndroidTest" on a physical device, with
release variant minified, baseline profile generated, BaselineProfileMode.Require asserting it.
# WRONG
"Mean cold startup with profile: 412 ms (5 iterations)."
# WRONG because: a single thermal-throttled iteration drags the mean. Macrobenchmark
# reports min / median / max; the median is the resilient summary. 5 iterations is also
# thin for startup variance — use ≥10 for StartupTimingMetric, ≥5 for FrameTimingMetric.
# RIGHT
"Median cold startup, 10 iterations: BaselineProfile 318 ms vs None 462 ms (–31%).
P95 frameOverrunMs scrolling 30 items: BaselineProfile –6 ms vs None +9 ms."
// WRONG
val feed = device.findObject(By.res("feed"))
feed.fling(Direction.DOWN)
// WRONG because: on gesture-nav devices, a fling that starts inside the system gesture area
// is intercepted as a back-swipe or recents-swipe and the scroll never happens.
// Macrobenchmark then measures an idle screen and reports artificially-good numbers.
// RIGHT
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
aosp_cf_x86_64_phone-userdebug Cuttlefish. High-end devices mask the perf delta; default API emulator images are not representative.CompilationMode.Partial(BaselineProfileMode.Require) in the measurement test so a missing or stale profile fails the test loudly. BaselineProfileMode.UseIfAvailable silently measures an unprofiled build.ReportDrawn / ReportDrawnWhen / ReportDrawnAfter from androidx.activity.compose to mark the meaningful first-drawn moment. Without it timeToFullDisplay undercounts.../testing-compose-in-release-mode/SKILL.md.assets/dexopt/baseline.prof must exist after ./gradlew :app:assembleRelease. If absent, the baselineProfile(project(":baselineprofile")) wiring on the app module is missing.StartupTimingMetric, ≥5 for FrameTimingMetric.CompilationMode.None so every PR can prove the profile is still moving the number.Modifier.testTag("feed") (or whatever ID the journey uses) in the Compose source rather than relying on text matchers — text changes with localization, test tags do not.:baselineprofile module exists with the androidx.baselineprofile plugin applied.:app/build.gradle.kts has baselineProfile(project(":baselineprofile")) in dependencies../gradlew :app:generateBaselineProfile produces baseline-prof.txt under app/src/<variant>/generated/baselineProfiles/ (or app/src/main/generated/baselineProfiles/).assets/dexopt/baseline.prof and assets/dexopt/baseline.profm.@Test covers cold startup AND at least one scroll fling.@Test uses CompilationMode.Partial(BaselineProfileMode.Require).ReportDrawn / ReportDrawnWhen / ReportDrawnAfter is invoked from a composable that renders only when the screen is meaningfully complete.CompilationMode.None.aosp_cf_x86_64_phone-userdebug, not a stock API emulator.androidx.activity.compose.ReportDrawn reference — https://developer.android.com/reference/kotlin/androidx/activity/compose/package-summaryreferences/macrobenchmark-harness.md — full Gradle + manifest harness setup, the metric catalogue (StartupTimingMetric, FrameTimingMetric, TraceSectionMetric, MemoryUsageMetric, PowerMetric), and how to parse the JSON output for CI dashboards.For confirming that a generated profile actually moved the recomposition count of a specific composable in release, see ../tracing-recompositions-at-runtime/SKILL.md. For why debug numbers are not measurement evidence, see ../testing-compose-in-release-mode/SKILL.md. For the build-time R8 setup that release measurement assumes, see ../../build/configuring-r8-for-compose/SKILL.md. For preventing stability regressions between profile-generation runs, see ../../stability/enforcing-stability-in-ci/SKILL.md.
Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementing any feature or bugfix, before writing implementation code
Use when you have a spec or requirements for a multi-step task, before touching code
Use when creating new skills, editing existing skills, or verifying skills work before deployment
Use when writing or improving README files. Not all READMEs are the same — provides templates and guidance matched to your audience and project type.
| Remove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's inflated symbolism, promotional language, superficial -ing analyses, vague attributions, em dash overuse, rule of three, AI vocabulary words, negative parallelisms, and excessive conjunctive phrases.
Official Opentrons Protocol API for OT-2 and Flex robots. Use when writing protocols specifically for Opentrons hardware with full access to Protocol API v2 features. Best for production Opentrons protocols, official API compatibility. For multi-vendor automation or broader equipment control use pylabrobot.
Take skydoves/generating-baseline-profiles from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.