Ship a baseline profile, generated on the emulator because the phone cannot collect one

The profile is real: 13193 lines in app/src/main/baseline-prof.txt, 584 of them the app's own classes, bundled into the release APK as assets/dexopt/baseline.prof. It is produced by a new :baselineprofile test module that walks the first journey - roots screen, one volume opened - under the androidx.baselineprofile plugin, and :app now depends on androidx.profileinstaller so the bundled profile is applied at install time.

Two device facts decided how it was made. Collection requires API 33+ (BaselineProfileRule refuses Android 12 outright), so it ran on the API 35 AVD that was already in ~/.android/avd; that emulator is software-rendered and needs androidx.benchmark.suppressErrors=EMULATOR for any macrobenchmark. And the release build needed profileable android:shell=true, which is why app/src/release/AndroidManifest.xml exists and says why: the same APK is both the thing measured and the thing shipped, so the profile's class names are the ones in the shipped dex.

The effect is not measured, and PERFORMANCE.md says so: 7404 ms median without the profile against 9005 ms with it, run to run, because a TCG emulator varies by more than a profile can buy. What remains is one run on a physical API 33+ phone, with the two compile commands documented. The emulator did settle the other open question - FrameTimingMetric there reports frameDurationCpuMs and frameOverrunMs, the metrics this phone cannot produce at all.

:baselineprofile is compiled by tools/verify.sh --release like :benchmark, so it cannot rot between device runs.
This commit is contained in:
2026-10-17 17:06:08 +02:00
parent 0a3ff88247
commit 4846454645
12 changed files with 13377 additions and 6 deletions
+55
View File
@@ -0,0 +1,55 @@
plugins {
// No version for AGP: `com.android.application` in :app has already put it on the build's plugin
// classpath. The version is the one in the catalog.
id("com.android.test")
alias(libs.plugins.androidx.baselineprofile)
}
/**
* Generates the app's baseline profile from a device run.
*
* The profile is a list of the classes and methods that the first journey touches, so the platform can
* AOT-compile exactly those at install time instead of guessing. `:app` bundles whatever ends up in
* `app/src/main/baseline-prof.txt`; this module is how that file is produced, and how it is produced
* again when the startup path changes.
*
* ./gradlew :baselineprofile:generateBaselineProfile
*
* It profiles the app's `benchmark` build type rather than `release`: same sources and the same R8
* rules, so the same class names, but with `<profileable android:shell="true"/>` in its manifest, which
* profiling requires and which a shipped release build does not carry.
*/
android {
namespace = "de.softwareapp_hb.filemanager.baselineprofile"
compileSdk = 36
defaultConfig {
minSdk = 26
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
targetProjectPath = ":app"
// Test modules pick their target variant by name in AGP 9 (see :benchmark's build file), so
// declaring one called `benchmark` is what makes this profile :app's benchmark build.
buildTypes {
create("benchmark") {
// A declared build type starts unsigned; an unsigned test APK cannot be installed.
initWith(getByName("debug"))
}
}
experimentalProperties["android.experimental.self-instrumenting"] = true
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation(libs.androidx.benchmark.macro.junit4)
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.test.runner)
}
@@ -0,0 +1,49 @@
package de.softwareapp_hb.filemanager.baselineprofile
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* The journey that becomes the baseline profile.
*
* Profile generation is not a benchmark: it records which classes and methods the *first* journey
* touches, so the platform can AOT-compile them when the app is installed instead of profiling by
* running the app. So this walks what a new user walks — the roots screen, and one volume opened — and
* nothing else. Opening a thousand-file folder here would profile the listing code, which is a different
* measurement, and adding search would profile code the first second never runs.
*
* Generated with:
*
* ./gradlew :baselineprofile:generateBaselineProfile
*
* The result lands in `app/src/main/baseline-prof.txt`; see PERFORMANCE.md for the number it buys.
*/
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun startup() = baselineProfileRule.collect(packageName = APP) {
pressHome()
startActivityAndWait()
// The roots screen is the first frame; opening internal storage is the first interaction.
device.wait(Until.findObject(By.text(INTERNAL_STORAGE)), TIMEOUT_MS)?.click()
device.waitForIdle()
device.wait(Until.hasObject(By.text(INTERNAL_STORAGE)), TIMEOUT_MS)
}
private companion object {
/** The `benchmark` build type keeps the app's own id; see :app's build file. */
const val APP = "de.softwareapp_hb.filemanager"
const val INTERNAL_STORAGE = "Internal storage"
const val TIMEOUT_MS = 5_000L
}
}