Files
amethyst/baselineprofile/build.gradle.kts
T
Vitor PamplonaandClaude Opus 5 4c606c2571 build: add a baseline profile generator (:baselineprofile)
Follow-up to the hand-authored amethyst/src/main/baseline-prof.txt, which was a
stopgap: whole-package wildcards that compile methods which never run. This adds
the machinery to produce a real profile from a recorded journey.

  ./gradlew :amethyst:generateBaselineProfile

New :baselineprofile module (com.android.test + androidx.baselineprofile) with a
BaselineProfileRule journey, and :amethyst applies the plugin and consumes it.
Notes on the wiring, since three things needed working out:

- com.android.test must be applied WITHOUT a version. AGP is already on the
  buildscript classpath, so `alias(libs.plugins.androidTest)` fails with
  "already on the classpath with an unknown version".
- targetSdk belongs in defaultConfig, not testOptions, for a test module.
- :amethyst has a `channel` dimension, so the test module needs
  missingDimensionStrategy("channel", "play") or the dependency is ambiguous.

The generator ran on a connected device: Macrobenchmark 1.5.0-alpha07 supports
non-rooted generation on API 33+, so no root or AOSP emulator was needed. It
produced 31,497 rules (3.1 MB).

WHAT THE GENERATED PROFILE DOES AND DOES NOT COVER — it captures cold-start on a
LOGGED-OUT app, not the ingest burst. The generator installs the release
applicationId (com.vitorpamplona.amethyst), which is a fresh install with no
account, so the journey recorded a login screen. Measured on the output: zero
rules for justConsume and only 33 of 31,497 rules marked hot. Capturing ingest
needs a journey against a logged-in app, which needs a seeded test identity —
a design decision (a key in the repo is not acceptable), so it is left open.

Both profiles are therefore kept, because they cover different things: the
hand-authored one covers ingest (measured: nterp 42.9% -> 4.2% of ingest worker
CPU, ~1.6x more ingest per core-second), the generated one covers startup and
class loading.

Verified they both reach the shipping artifact by inspecting assets/dexopt/
baseline.prof across variants:

  11,425 bytes  neither profile
  16,341 bytes  hand-authored only        (benchmark build type)
  25,541 bytes  hand-authored + generated (RELEASE variant)

The generated profile only lands in `release`; the custom `benchmark` build type
gets just the hand-authored file, because the plugin wires generated profiles
into release variants. That is why the earlier ingest measurements — taken on
the benchmark build type — could not see it. The runtime effect of the generated
half is NOT measured here: the release APK is unsigned and could not be
installed on the test device.

The 3.1 MB generated file is committed on purpose (saveInSrc), so release builds
do not need a device attached at build time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 18:37:14 -04:00

51 lines
1.6 KiB
Kotlin

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.test")
alias(libs.plugins.androidxBaselineProfile)
}
android {
namespace = "com.vitorpamplona.amethyst.baselineprofile"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
// Baseline Profile generation needs API 28+; non-rooted generation needs API 33+.
minSdk = 28
targetSdk = libs.versions.android.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// :amethyst has a `channel` dimension (play/fdroid). The recorded journey is the
// same either way — ingest does not differ by store — so record against play and
// let the consumer's mergeIntoMain share one profile with both flavours.
missingDimensionStrategy("channel", "play")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
// The app whose journeys are recorded.
targetProjectPath = ":amethyst"
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
baselineProfile {
// Generate against a connected device. Macrobenchmark 1.2.0-alpha06+ can do this
// without root on API 33+; below that it needs root or an `aosp` Gradle Managed Device.
useConnectedDevices = true
}
dependencies {
implementation(libs.androidx.junit)
implementation(libs.androidx.runner)
implementation(libs.androidx.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}