Baseline Profiles, Macrobenchmark & Startup Optimization
Eliminate jank and slash startup time with AOT compilation hints and systematic benchmarking
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Baseline profiles are files that tell the Android Runtime (ART) which parts of your app to compile ahead-of-time during installation, rather than just-in-time during execution. Macrobenchmark is a testing library that measures real-world performance metrics (startup time, frame timing) on actual devices. Together, they form the foundation of Android startup optimization — profiles eliminate JIT jank, and benchmarks prove the improvement with hard numbers.
Real-world relevance
A news app had a 2.1-second cold start on mid-range devices. Perfetto traces revealed: 600ms in ContentProvider initialization (6 libraries auto-initializing via separate ContentProviders), 400ms in Dagger setup, 300ms in first Compose frame JIT compilation, and 800ms loading initial data synchronously. The fixes: (1) Migrated to App Startup library — consolidated 6 ContentProviders into 1, saving 450ms. (2) Generated baseline profiles covering startup + first article scroll — eliminated Compose JIT jank, saving 280ms. (3) Parallelized data loading with async/coroutines and showed skeleton UI — TTID dropped to 600ms with TTFD at 1.1s. Total improvement: 2.1s to 0.6s TTID.
Key points
- What baseline profiles do (AOT compilation hints) — Android normally uses JIT compilation — code is compiled during execution, causing jank on first use. Baseline profiles tell ART which methods and classes to AOT (ahead-of-time) compile during app installation. This eliminates JIT overhead for critical paths, improving startup by 15-40% and reducing frame drops during initial interactions.
- Generating baseline profiles with Macrobenchmark — Use the Macrobenchmark library with BaselineProfileRule to generate profiles. Write a test that exercises critical user journeys (startup, scrolling, navigation). Run on a physical device or emulator with userdebug build. The test outputs a baseline-prof.txt file listing hot methods/classes. Copy this to src/main/baseline-prof.txt in your app module.
- BaselineProfileRule & profile types — BaselineProfileRule generates profiles by tracing app execution. Startup profiles cover app launch to first frame. Interaction profiles cover scrolling, navigation, and common actions. Combine both for maximum coverage. The rule automatically handles app installation, launch, and trace collection. Generated profiles are human-readable text files listing methods and classes.
- Measuring cold/warm/hot start times — Cold start: process not in memory, full initialization required (worst case). Warm start: process exists but Activity is recreated. Hot start: Activity is brought to front. Macrobenchmark measures all three via StartupMode.COLD, WARM, HOT. Aim for cold start < 500ms for good UX. Measure on low-end devices for realistic numbers — flagship phones hide performance issues.
- Startup tracing with Perfetto — Perfetto provides nanosecond-precision tracing of app startup. Capture traces via System Tracing in Developer Options, Macrobenchmark (auto-captures), or adb shell perfetto. Open traces at ui.perfetto.dev. Look for: bindApplication duration, Activity creation, first draw, ContentProvider initialization, Dagger/Hilt injection time. Perfetto reveals exactly WHERE time is spent.
- App Startup library (Initializer pattern) — The App Startup library (androidx.startup) replaces individual ContentProviders (used by many libraries for auto-init) with a single ContentProvider that lazily initializes components. Reduces cold start time by eliminating the overhead of multiple ContentProvider.onCreate() calls. Define Initializer classes with dependencies and register in the manifest.
- Reducing DEX count & method references — Each DEX file has a 65,536 method reference limit. More DEX files = more classloader overhead at startup. Reduce methods by: enabling R8 (removes unused code), using implementation instead of api in Gradle (limits transitive deps), removing unused libraries, and using multidex only when necessary. Monitor with APK Analyzer's DEX viewer.
- Compose baseline profiles & pre-compilation — Jetpack Compose benefits enormously from baseline profiles because Compose's runtime has many methods that are JIT-compiled on first use, causing jank. The Compose libraries ship with their own baseline profiles (since 1.2.0), but app-specific profiles for YOUR composables add further improvement. ProfileInstaller auto-applies profiles on first install.
- Macrobenchmark setup & metrics — Add a :macrobenchmark module with the macrobenchmark plugin. Tests extend MacrobenchmarkRule. Key metrics: StartupTimingMetric (time-to-initial-display, time-to-fully-drawn), FrameTimingMetric (frame duration, jank percentage), TraceSectionMetric (custom trace sections). Run benchmarks on CI to detect regressions. Use compilationMode = CompilationMode.DEFAULT for realistic results.
- Time-to-initial-display vs time-to-fully-drawn — TTID measures when the first frame is drawn — controlled by the system. TTFD measures when all async content is loaded — you must call reportFullyDrawn() after data loads. Google Play Vitals tracks both. Optimize TTID by deferring heavy init. Optimize TTFD by parallelizing data loading. Show skeletons/placeholders between TTID and TTFD.
- Lazy initialization & deferred work — Defer any initialization not needed for the first frame: analytics SDKs, image loaders, feature flag services. Use lazy { } for heavy singletons. Prefer Dispatchers.Default for CPU-intensive init over Dispatchers.Main. Move disk I/O to background — SharedPreferences.apply() on main thread during startup is a common bottleneck shown by StrictMode.
Code example
// 1. Module setup — :macrobenchmark/build.gradle.kts
plugins {
id("com.android.test")
id("androidx.baselineprofile")
}
android {
namespace = "com.example.benchmark"
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.2.3")
implementation("androidx.test.ext:junit:1.1.5")
}
// 2. Baseline Profile Generator
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generateStartupProfile() {
rule.collect(
packageName = "com.example.app",
maxIterations = 5,
stableIterations = 3
) {
// Cold start — measures startup path
pressHome()
startActivityAndWait()
// Critical user journey — scroll main feed
device.findObject(By.res("feed_list"))
.also { list ->
list.setGestureMargin(device.displayWidth / 5)
list.fling(Direction.DOWN)
list.fling(Direction.DOWN)
}
// Navigate to detail screen
device.findObject(By.res("article_card"))
.click()
device.waitForIdle()
}
}
}
// 3. Startup Benchmark
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupCold() {
rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
compilationMode = CompilationMode.DEFAULT(),
startupMode = StartupMode.COLD,
iterations = 10
) {
pressHome()
startActivityAndWait()
}
}
@Test
fun startupWithBaselineProfile() {
rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
compilationMode = CompilationMode.Partial(
baselineProfiles = listOf("baseline-prof.txt")
),
startupMode = StartupMode.COLD,
iterations = 10
) {
pressHome()
startActivityAndWait()
}
}
@Test
fun scrollPerformance() {
rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric()),
compilationMode = CompilationMode.DEFAULT(),
iterations = 5
) {
pressHome()
startActivityAndWait()
val list = device.findObject(By.res("feed_list"))
list.setGestureMargin(device.displayWidth / 5)
repeat(3) {
list.fling(Direction.DOWN)
device.waitForIdle()
}
}
}
}
// 4. App Startup library — replace multiple ContentProviders
class AnalyticsInitializer : Initializer<Analytics> {
override fun create(context: Context): Analytics {
return Analytics.init(context, BuildConfig.ANALYTICS_KEY)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList() // No dependencies
}
}
class ImageLoaderInitializer : Initializer<ImageLoader> {
override fun create(context: Context): ImageLoader {
return ImageLoader.Builder(context)
.diskCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder(context)
.maxSizePercent(0.25)
.build()
}
.build()
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(AnalyticsInitializer::class.java)
}
}
// AndroidManifest.xml — App Startup provider
// <provider
// android:name="androidx.startup.InitializationProvider"
// android:authorities="${applicationId}.androidx-startup"
// android:exported="false"
// tools:node="merge">
// <meta-data
// android:name="com.example.app.AnalyticsInitializer"
// android:value="androidx.startup" />
// <meta-data
// android:name="com.example.app.ImageLoaderInitializer"
// android:value="androidx.startup" />
// </provider>
// 5. reportFullyDrawn() for TTFD tracking
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: MainViewModel by viewModels()
setContent {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is UiState.Loading -> SkeletonScreen()
is UiState.Success -> {
MainScreen(data = (uiState as UiState.Success).data)
// Report fully drawn after content is ready
LaunchedEffect(Unit) {
reportFullyDrawn()
}
}
is UiState.Error -> ErrorScreen()
}
}
}
}Line-by-line walkthrough
- 1. BaselineProfileRule() — JUnit rule that manages device setup, app installation, and trace collection for profile generation
- 2. rule.collect(packageName, maxIterations = 5, stableIterations = 3) — runs the profile collection; stops after results stabilize for 3 consecutive iterations or reaches 5 max
- 3. pressHome(); startActivityAndWait() — simulates cold start by going home first, then launching the app and waiting for the first frame to render
- 4. device.findObject(By.res('feed_list')).fling(Direction.DOWN) — exercises the scroll path so those methods get included in the baseline profile
- 5. MacrobenchmarkRule().measureRepeated(...) — runs the benchmark multiple times and reports statistical results (median, P50, P90, P99)
- 6. StartupTimingMetric() — measures time-to-initial-display (TTID) and time-to-fully-drawn (TTFD) in milliseconds with high precision
- 7. CompilationMode.DEFAULT() — uses whatever profiles are installed, simulating real-world conditions; CompilationMode.None() is fully interpreted (worst case)
- 8. Initializer.create(context) — App Startup calls this lazily when the component is first needed; dependencies() declares initialization ordering
- 9. tools:node='merge' — merges App Startup's ContentProvider with any library-declared providers; use tools:node='remove' to disable a library's auto-initialization
- 10. reportFullyDrawn() — signals to the system (and Macrobenchmark) that all async content is loaded and the screen is fully interactive; tracked as TTFD in Play Vitals
- 11. LaunchedEffect(Unit) { reportFullyDrawn() } — calls reportFullyDrawn when the Success composable first enters composition; Unit key ensures it runs only once
Spot the bug
// Startup benchmark — always shows 0ms
@Test
fun benchmarkStartup() {
rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric()), // Wrong metric
compilationMode = CompilationMode.Full(), // Unrealistic
startupMode = StartupMode.HOT, // Not cold start
iterations = 1 // Too few iterations
) {
startActivityAndWait() // Missing pressHome()
}
}
// App Startup — circular dependency
class AInitializer : Initializer<A> {
override fun create(context: Context): A = A()
override fun dependencies() = listOf(BInitializer::class.java)
}
class BInitializer : Initializer<B> {
override fun create(context: Context): B = B()
override fun dependencies() = listOf(AInitializer::class.java)
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Baseline Profiles (developer.android.com)
- Macrobenchmark Guide (developer.android.com)
- App Startup Library (developer.android.com)
- Perfetto Tracing (perfetto.dev)
- App Startup Time (developer.android.com)