DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on • Originally published at mvpfactory.io

Android Baseline Profiles and Macrobenchmark in 2026

---
title: "Android Baseline Profiles: Measuring Real Startup Gains with Macrobenchmark"
published: true
description: "A hands-on guide to cutting cold start times 20-40% with Baseline Profiles, dex layout optimization, and a CI workflow that catches regressions before your users do."
tags: android, kotlin, performance, architecture
canonical_url: https://blog.mvp-factory.com/android-baseline-profiles-measuring-real-startup-gains
---

## What you will learn

By the end of this tutorial, you will understand exactly how Baseline Profiles cut cold start times through ART's AOT pipeline, how dex layout optimization delivers gains most teams miss entirely, and how to set up a Macrobenchmark CI workflow that catches regressions before they ship. I will walk you through concrete before/after numbers from a production Jetpack Compose app — not theory, working measurements.

## Prerequisites

- Android Studio Hedgehog or later
- AGP 8.0+ with Macrobenchmark library
- A physical device (emulators produce unreliable startup numbers due to variable CPU throttling)
- Familiarity with Gradle and basic Kotlin

## Step 1: Understand the compilation pipeline

Most teams treat Baseline Profiles as "add the Gradle plugin and forget it." Let me show you why that leaves 200-400ms on the table.

When you ship a Baseline Profile, you provide a curated list of classes and methods that ART should AOT-compile *before* the user opens your app. Without profiles, ART interprets bytecode first and only JIT-compiles hot methods after repeated execution. Three paths exist:

| Path | Trigger | Latency impact |
|------|---------|----------------|
| **Cloud Profiles** | Play Store aggregates from early adopters | Eliminates first-launch JIT penalty |
| **Baseline Profiles** | Bundled in APK/AAB via ProfileInstaller | Covers day-one installs before cloud data exists |
| **On-device PGO** | ART JIT runtime profiling | Adapts to individual usage over time |

Cloud profiles lag behind new releases by days. Baseline Profiles fill that gap from the very first install.

## Step 2: Unlock dex layout optimization

Here is the part the docs do not mention prominently: dex layout matters *a lot*. When R8 arranges your dex files, Baseline Profile rules inform which classes get grouped into contiguous memory pages, reducing disk I/O during cold start.

I isolated dex layout gains from AOT compilation gains on mid-range devices (Pixel 4a, Samsung A53):

| Configuration | Median cold start (ms) | Improvement |
|--------------|----------------------|-------------|
| AOT only (no layout opt) | 538 | — |
| AOT + dex layout opt | 491 | **-8.7%** |

On high-end devices, the gain was 3.7%. The slower the NAND, the more expensive the page faults. This is free performance — you just need current profile rules.

## Step 3: Measure with Macrobenchmark

Here is the minimal setup to get this working. I profiled a Jetpack Compose app with 12 Hilt modules and ~40 screens:

Enter fullscreen mode Exit fullscreen mode


kotlin
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

@test
fun startupCompilationBaselineProfiles() {
benchmarkRule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = listOf(StartupTimingMetric()),
iterations = 10,
compilationMode = CompilationMode.Partial(
baselineProfileMode = BaselineProfileMode.Require
),
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
// Navigate your CRITICAL user journeys here
device.waitForIdle()
}
}


Notice `BaselineProfileMode.Require` — this fails the benchmark if profiles are missing. Exactly what you want in CI. The results:

| Configuration | Median (ms) | P95 (ms) | Improvement |
|--------------|------------|----------|-------------|
| No profiles | 847 | 1,240 | Baseline |
| Stale profile | 712 | 1,080 | 16% |
| Current profile | 538 | 780 | 36% |
| Current + dex layout | 491 | 710 | **42%** |

That 16% vs 42% gap? Entirely explained by stale rules missing new code paths.

## Step 4: Set up CI that catches regressions

Profiles decay as code changes. Here is the workflow that actually holds up:

1. **Generate** — run Macrobenchmark profile generators on every release branch merge
2. **Validate** — `BaselineProfileMode.Require` fails CI if profiles are absent
3. **Benchmark** — compare cold start P50/P95 against the previous release baseline
4. **Alert** — flag regressions exceeding 10% in any metric

Run this on a dedicated Firebase Test Lab device pool with locked thermal state. I applied this exact workflow to [HealthyDesk](https://play.google.com/store/apps/details?id=com.healthydesk), a break reminder app I use daily — the profiling pipeline caught a 12% regression from a new onboarding flow before it ever shipped.

## Gotchas

**Stale profile rules** are the most common offender. If your generator does not exercise current navigation paths, new screens launch fully interpreted. Regenerate every release cycle.

**Incomplete journey coverage** kills your gains. Profile the first 30 seconds of real usage — login, home feed, first interaction. If you only profile `MainActivity.onCreate()`, you cover maybe 30% of startup-critical code.

**ProfileInstaller version mismatches** are the sneakiest problem. The `androidx.profileinstaller` version must match your AGP version's profile format. A mismatch silently skips installation. Verify with:

Enter fullscreen mode Exit fullscreen mode


bash
adb shell dumpsys package your.app | grep prof


If you see `status=no` after install, your profiles are not being applied.

## Conclusion

Baseline Profiles are not a set-and-forget optimization. Regenerate every release, profile complete user journeys (not just Activity launches), and validate installation on-device with `BaselineProfileMode.Require`. The difference between a stale 16% improvement and a current 42% improvement is the discipline of your CI pipeline — not the complexity of the tooling.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)