The Two Performance Levers You're Probably Not Using
Generating Baseline Profiles with Macrobenchmark
// :benchmark/build.gradle.kts
plugins {
id("com.android.test")
id("org.jetbrains.kotlin.android")
id("androidx.baselineprofile")
}
android {
namespace = "com.example.benchmark"
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
baselineProfile {
useConnectedDevices = true
}
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.3.3")
implementation("androidx.test.uiautomator:uiautomator:2.3.0")
}
// :app/build.gradle.kts
plugins {
id("androidx.baselineprofile")
}
dependencies {
baselineProfile(project(":benchmark"))
}
baselineProfile {
automaticGenerationDuringBuild = true // Generate on every release build
saveInSrc = true // Commit profile to source control
}Writing Profile Generator Rules
@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: measure from launch to first frame
pressHome()
startActivityAndWait()
// Wait for content to load
device.wait(
Until.hasObject(By.res("article_list")),
10_000
)
}
}
@Test
fun generateCriticalJourneyProfile() {
rule.collect(
packageName = "com.example.app",
maxIterations = 5,
stableIterations = 3
) {
startActivityAndWait()
// Navigate to article detail
device.wait(Until.hasObject(By.res("article_list")), 10_000)
device.findObject(By.res("article_card")).click()
device.wait(Until.hasObject(By.res("article_content")), 5_000)
// Scroll through article
device.findObject(By.res("article_scroll")).also {
it.scroll(Direction.DOWN, 1.0f)
it.scroll(Direction.DOWN, 1.0f)
}
// Navigate to search
device.pressBack()
device.findObject(By.res("search_button")).click()
device.wait(Until.hasObject(By.res("search_field")), 5_000)
device.findObject(By.res("search_field")).text = "kotlin"
device.wait(Until.hasObject(By.res("search_results")), 5_000)
}
}
}
// Generate profiles:
// ./gradlew :app:generateBaselineProfile
// Output: app/src/main/baseline-prof.txtR8: Beyond Default Shrinking
// build.gradle.kts -- release build type
android {
buildTypes {
release {
isMinifyEnabled = true // Enable R8 shrinking + optimization
isShrinkResources = true // Remove unused resources too
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
// proguard-rules.pro -- production-grade configuration
# ---- Kotlin Serialization ----
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class com.example.app.model.**$$serializer { *; }
-keepclassmembers class com.example.app.model.** {
*** Companion;
}
# ---- Retrofit ----
-keepattributes Signature, Exceptions
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
-dontwarn retrofit2.**
# ---- Room ----
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-dontwarn androidx.room.paging.**
# ---- Compose: keep Stability metadata for compiler optimizations ----
-keep class androidx.compose.runtime.** { *; }
# ---- Aggressive optimizations ----
-optimizationpasses 5
-repackageclasses ''
-allowaccessmodification
-mergeinterfacesaggressivelyMeasuring Impact: Before and After
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupCompilationNone() = startup(CompilationMode.None())
@Test
fun startupCompilationPartial() = startup(
CompilationMode.Partial(
baselineProfileMode = BaselineProfileMode.Require
)
)
@Test
fun startupCompilationFull() = startup(CompilationMode.Full())
private fun startup(compilationMode: CompilationMode) {
rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(
StartupTimingMetric(),
TraceSectionMetric("firstComposition"),
TraceSectionMetric("dataLoaded")
),
iterations = 10,
startupMode = StartupMode.COLD,
compilationMode = compilationMode
) {
pressHome()
startActivityAndWait()
device.wait(Until.hasObject(By.res("article_list")), 10_000)
}
}
}
// Typical results (Pixel 7, release build):
//
// CompilationMode | TTID (median) | TTFD (median)
// ---------------------|---------------|---------------
// None (JIT only) | 847ms | 1,423ms
// Partial (Baseline) | 512ms (-39%) | 891ms (-37%)
// Full (AOT all) | 478ms (-43%) | 824ms (-42%)
//
// APK size (before R8): 28.4 MB
// APK size (after R8): 11.7 MB (-59%)Key Takeaways
- 1Baseline Profiles AOT-compile your critical code paths during installation, delivering 30-40% faster cold starts with zero application code changes.
- 2Generate profiles with Macrobenchmark by scripting your most important user journeys -- cold start, main navigation, and search.
- 3R8 with aggressive configuration (repackaging, optimization passes, resource shrinking) can reduce APK size by 40-60% beyond default settings.
- 4Always benchmark before and after: use MacrobenchmarkRule with StartupTimingMetric for cold, warm, and hot start measurements.
- 5Commit baseline-prof.txt to source control and regenerate it when critical user flows change to keep profiles accurate.
- 6Baseline Profiles and R8 are complementary: profiles speed up runtime execution, R8 shrinks and optimizes the bytecode itself.
Frequently Asked
Do Baseline Profiles increase APK size?
Minimally, typically 50-200KB. The startup improvement far outweighs the size increase. R8 optimization often reduces overall APK size, offsetting profile size.
How often should I update profiles?
Update profiles for each major release. Regenerate when adding significant features. Profile changes don't require user updates - profiles apply at next install.
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.