mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
Merge pull request #3919 from vitorpamplona/build/baseline-profile-generator
build: add a baseline profile generator (:baselineprofile)
This commit is contained in:
@@ -9,6 +9,7 @@ plugins {
|
||||
alias(libs.plugins.jetbrainsComposeCompiler)
|
||||
alias(libs.plugins.serialization)
|
||||
alias(libs.plugins.googleKsp)
|
||||
alias(libs.plugins.androidxBaselineProfile)
|
||||
}
|
||||
|
||||
fun getCurrentBranch(workingDir: java.io.File): String =
|
||||
@@ -374,6 +375,17 @@ composeCompiler {
|
||||
metricsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
// One profile for the whole app rather than per-flavour: the ingest path being
|
||||
// captured is identical in play and fdroid, and a shared profile is what the
|
||||
// fdroid build needs — it never receives Play Cloud Profiles.
|
||||
mergeIntoMain = true
|
||||
|
||||
// Keep the generated profile in source control so release builds do not depend on
|
||||
// a device being attached at build time.
|
||||
saveInSrc = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
|
||||
@@ -402,6 +414,9 @@ dependencies {
|
||||
// and no Cloud Profiles at all — there, this library is the only thing that gets
|
||||
// the shipped baseline profile into ART.
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
|
||||
// Profile produced by :baselineprofile from a real cold-start + ingest journey.
|
||||
baselineProfile(project(":baselineprofile"))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
// Hardened WebView host for sandboxed napplet/nsite rendering (origin-restricted message bridge).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
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)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.baselineprofile
|
||||
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Direction
|
||||
import androidx.test.uiautomator.Until
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Records the methods worth AOT-compiling, replacing the hand-authored
|
||||
* `amethyst/src/main/baseline-prof.txt`.
|
||||
*
|
||||
* What this captures and why: a symbol profile of a release build during cold-start
|
||||
* ingest found **42.9% of the DefaultDispatcher workers' CPU inside ART's nterp
|
||||
* interpreter** and only 10.5% in compiled app code — the app was spending 36x more
|
||||
* CPU interpreting bytecode than verifying signatures, because the shipped profile
|
||||
* covered only androidx/Compose and none of the relay client, decoder, LocalCache or
|
||||
* Jackson paths.
|
||||
*
|
||||
* So the journey is deliberately NOT just a startup trace. It launches cold and then
|
||||
* sits on the feed while events stream in, because the expensive path is ingest, and
|
||||
* ingest only runs once relays start delivering. Scrolling is included so the note
|
||||
* rendering path is captured too.
|
||||
*
|
||||
* Run with:
|
||||
* ./gradlew :amethyst:generateBaselineProfile
|
||||
*
|
||||
* Requires a connected device on API 33+ (no root needed with Macrobenchmark
|
||||
* 1.2.0-alpha06 and higher), or an `aosp` Gradle Managed Device below that.
|
||||
*/
|
||||
class BaselineProfileGenerator {
|
||||
@get:Rule
|
||||
val rule = BaselineProfileRule()
|
||||
|
||||
@Test
|
||||
fun startupAndIngest() =
|
||||
rule.collect(packageName = PACKAGE) {
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
|
||||
// Let the relay pool connect and start delivering. The ingest burst is the
|
||||
// whole point of this profile; a startup-only journey would miss it.
|
||||
device.waitForIdle()
|
||||
Thread.sleep(INGEST_SETTLE_MS)
|
||||
|
||||
// Exercise the feed rendering path over whatever arrived.
|
||||
val feed = device.findObject(By.scrollable(true))
|
||||
if (feed != null) {
|
||||
repeat(3) {
|
||||
feed.scroll(Direction.DOWN, 1.0f)
|
||||
device.waitForIdle()
|
||||
}
|
||||
feed.scroll(Direction.UP, 1.0f)
|
||||
}
|
||||
device.wait(Until.hasObject(By.pkg(PACKAGE).depth(0)), 5_000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PACKAGE = "com.vitorpamplona.amethyst"
|
||||
|
||||
/**
|
||||
* Long enough for relays to connect and deliver, short enough to stay inside the
|
||||
* memory ceiling of low-RAM test devices — an SM-T220 (3 GB) gets lmkd-killed
|
||||
* around 20 s into a release-build ingest.
|
||||
*/
|
||||
const val INGEST_SETTLE_MS = 12_000L
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ androidxJunit = "1.3.0"
|
||||
appcompat = "1.7.1"
|
||||
audiowaveform = "1.1.2"
|
||||
benchmark = "1.5.0-alpha07"
|
||||
uiautomator = "2.3.0"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coil = "3.5.0"
|
||||
composeBom = "2026.06.01"
|
||||
@@ -111,6 +112,7 @@ androidx-appfunctions = { group = "androidx.appfunctions", name = "appfunctions"
|
||||
androidx-appfunctions-service = { group = "androidx.appfunctions", name = "appfunctions-service", version.ref = "appfunctions" }
|
||||
androidx-appfunctions-compiler = { group = "androidx.appfunctions", name = "appfunctions-compiler", version.ref = "appfunctions" }
|
||||
androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark" }
|
||||
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmark" }
|
||||
androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" }
|
||||
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidxCamera" }
|
||||
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidxCamera" }
|
||||
@@ -142,6 +144,7 @@ androidx-media3-session = { group = "androidx.media3", name = "media3-session",
|
||||
androidx-media3-ui-compose-material3 = { group = "androidx.media3", name = "media3-ui-compose-material3", version.ref = "media3" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
|
||||
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
||||
androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
@@ -242,6 +245,8 @@ androidx-health-connect-client = { group = "androidx.health.connect", name = "co
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
androidBenchmark = { id = "androidx.benchmark", version.ref = "benchmark" }
|
||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||
androidTest = { id = "com.android.test", version.ref = "agp" }
|
||||
androidxBaselineProfile = { id = "androidx.baselineprofile", version.ref = "benchmark" }
|
||||
diffplugSpotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
|
||||
googleServices = { id = "com.google.gms.google-services", version.ref = "gms" }
|
||||
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
|
||||
@@ -33,6 +33,7 @@ rootProject.name = "Amethyst"
|
||||
include(":amethyst")
|
||||
include(":nappletHost")
|
||||
include(":benchmark")
|
||||
include(":baselineprofile")
|
||||
include(":quartz")
|
||||
include(":geode")
|
||||
include(":commons")
|
||||
|
||||
Reference in New Issue
Block a user