What do Baseline Profiles and R8 actually do?
Generating Baseline Profiles with Macrobenchmark
com.android.test module, the androidx.baselineprofile plugin, and targetProjectPath pointing at the app. The sample below is that route.
Two facts save an afternoon of confusion:
- The device. The generator needs a rooted device or emulator, with one exception: "you can generate the Baseline Profile on devices running Android 13 (API 33) and higher without root access".
- The build type. "To keep installation turnaround during development low, Baseline Profiles are only installed for release builds." A debug build will never show the improvement.
On AGP 9 the module does not apply the kotlin-android plugin. The earlier version of this sample did, and AGP 9 rejects it; the reason is in the version catalogs and convention plugins guide.// :baseline-profile/build.gradle.kts
plugins {
id("com.android.test")
id("androidx.baselineprofile")
}
android {
namespace = "com.example.baselineprofile"
// The module the profile is generated for
targetProjectPath = ":app"
}
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.5.0")
}
// :app/build.gradle.kts
plugins {
id("com.android.application")
id("androidx.baselineprofile")
}
dependencies {
implementation("androidx.profileinstaller:profileinstaller:1.4.1")
baselineProfile(project(":baseline-profile"))
}
baselineProfile {
// Regenerate the profile for each release assembly
automaticGenerationDuringBuild = true
// Store it under src/<variant>/generated/baselineProfiles
saveInSrc = true
}Writing Profile Generator Rules
BaselineProfileRule. Each collect call runs the block repeatedly until the profile is stable. Per the reference, maxIterations defaults to 15 and stableIterations to 3.
Split startup from everything else. Android's guidance: "Make sure that any tests related to app startup are in a rule block with includeInStartupProfile set to true". That flag feeds a second artifact, the Startup Profile. Startup profiles "are used at compile time to optimize DEX layout for faster startup times, rather than for on-device optimization", and the overview says plainly: "We recommend using both Baseline Profiles and startup profiles to fully optimize app startup."
One rule surprises people who know R8: "When capturing a Baseline Profile with a test, your app must not be obfuscated." The release build still is. R8 rewrites the profile rules to match the obfuscated names when it builds the release, and "The Baseline Profile Gradle Plugin automatically handles this configuration for you."
The earlier version of this article said the output is app/src/main/baseline-prof.txt. With the plugin it is src/<variant>/generated/baselineProfiles/baseline-prof.txt.@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun startup() = rule.collect(
packageName = "com.example.app",
// Startup code also goes into the Startup Profile
includeInStartupProfile = true,
) {
pressHome()
startActivityAndWait()
}
@Test
fun criticalJourney() = rule.collect(
packageName = "com.example.app",
) {
startActivityAndWait()
// Drive the journeys that matter here: open a detail
// screen, scroll the main list, run a search.
}
}
// Generate from the command line:
// ./gradlew :app:generateBaselineProfileR8: What an Old Rules File Gets Wrong
optimization block, and "The updated DSL enables both code and resource optimization." isMinifyEnabled and isShrinkResources are documented as the legacy DSL for lower versions. The same page records that the non-optimizing default file, proguard-android.txt, is no longer supported.
- Some options do nothing. The global options page lists what "R8 ignores the following options without reporting a warning", and the list includes -optimizationpasses and -mergeinterfacesaggressively. This article used to recommend both under the heading of aggressive optimization. The advice from Android: "Because they have no effect on R8 output, remove them from your proguard-rules.pro file to keep your configuration easier to understand and maintain."
- Two more are already on. For -repackageclasses: "Since Android Gradle Plugin (AGP) 9.1, this is the default configuration for apps." For -allowaccessmodification: "Since Android Gradle Plugin (AGP) 8.2, this is the default configuration if you enable full optimization with R8".
That leaves keep rules, and the rule for those is to write fewer. "Well-defined keep rules are as specific as possible" (Android). The old sample kept every class in androidx.compose.runtime, which is the opposite, and repeated rules for Retrofit, Room and kotlinx.serialization that those libraries are expected to bring themselves: "Libraries designed to perform well on Android should avoid reflection where possible and embed consumer keep rules when necessary".// app/build.gradle.kts, AGP 9.3 and higher
android {
buildTypes {
release {
optimization {
enable = true // Code and resource optimization
}
}
}
}
# proguard-rules.pro: what to delete from an old file
#
# -optimizationpasses 5 ignored by R8
# -mergeinterfacesaggressively ignored by R8
# -repackageclasses '' default for apps since AGP 9.1
# -allowaccessmodification default since AGP 8.2
# -keep class androidx.compose.runtime.** { *; } package-wide keep
#
# What is left is specific to your own code, for example a class
# you reach by reflection:
-keepclassmembers class com.example.MyClass {
void someSpecificMethod();
}Measuring Impact: Before and After
StartupTimingMetric reports two numbers (reference): timeToInitialDisplayMs, the "Time from the system receiving a launch intent to rendering the first frame of the destination Activity", and timeToFullDisplayMs, which is only reported if the app calls reportFullyDrawn().
The mode matters. BaselineProfileMode.Require is documented as "Require the BaselineProfile methods/classes from the target app to be pre-compiled", where UseIfAvailable only includes them when a profile and the ProfileInstaller library are both present (reference). A benchmark that claims to measure the profile should require it. What the numbers mean against the thresholds Google Play applies is covered in the startup optimization guide, and running the benchmark on every change belongs in CI.
One sourced figure, for calibration and not as a promise: Android credits the R8 rewriting of profile rules that arrived in AGP 8.2 with an increase in profile coverage of methods of about 30% and in app performance of about 15%.@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupNoCompilation() = startup(CompilationMode.None())
@Test
fun startupWithBaselineProfile() = startup(
CompilationMode.Partial(
baselineProfileMode = BaselineProfileMode.Require,
warmupIterations = 0,
)
)
private fun startup(compilationMode: CompilationMode) {
rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = 10,
) {
pressHome()
startActivityAndWait()
}
}
}Key Takeaways
- 1Baseline Profiles improve code execution speed by about 30% from the first launch, according to Android. Measure your own app before quoting a number.
- 2Profiles are only installed for release builds, and the generator needs a rooted device or one on API 33 or higher.
- 3Mark startup tests with includeInStartupProfile so the same run also produces a Startup Profile.
- 4R8 full mode has been the default since AGP 8.0. From AGP 9.3, optimization is enabled with the optimization block.
- 5Delete -optimizationpasses and -mergeinterfacesaggressively from old rules files. R8 ignores them without a warning.
- 6Keep rules should be as specific as possible. A package-wide keep undoes the optimization you turned on.
Frequently Asked
Do I have to regenerate the Baseline Profile for every release?
It should match the code you ship. Setting automaticGenerationDuringBuild to true triggers the generation of a new profile for each release assembly, at the cost of a device or emulator in the build. The alternative is to run the generateBaselineProfile task when critical journeys change and commit the result with saveInSrc.
Can I generate a Baseline Profile from an obfuscated build?
No. Android states that when capturing a Baseline Profile with a test, your app must not be obfuscated, so the rules match real method signatures. The release build stays obfuscated: R8 rewrites the profile rules to the obfuscated names, and the Baseline Profile Gradle plugin sets up the non-minified variant for you.
Which ProGuard options does R8 ignore?
Android lists the options R8 ignores without reporting a warning, including -optimizationpasses, -mergeinterfacesaggressively, -overloadaggressively, -dontpreverify and -verbose. They have no effect on the output, so remove them. Two other common ones, -repackageclasses and -allowaccessmodification, are already the default on current Android Gradle plugin versions.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.