Refactor baseline profiles generation and regenerate (#3434)

This commit is contained in:
AntsyLich
2026-06-22 00:50:24 +06:00
committed by GitHub
parent 3bb734dde7
commit b3e190c627
20 changed files with 63270 additions and 37972 deletions
+10 -2
View File
@@ -13,6 +13,7 @@ plugins {
alias(mihonx.plugins.spotless)
alias(libs.plugins.aboutLibraries)
alias(libs.plugins.androidx.baselineProfile)
alias(libs.plugins.kotlin.serialization)
}
@@ -82,6 +83,8 @@ android {
signingConfig = debug.signingConfig
isProfileable = true
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = true)}\"")
@@ -110,8 +113,6 @@ android {
create("benchmark") {
initWith(release)
isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark"
@@ -197,7 +198,14 @@ kotlin {
}
}
baselineProfile {
baselineProfileOutputDir = "baselineProfiles"
mergeIntoMain = true
}
dependencies {
baselineProfile(projects.baselineProfile)
implementation(projects.i18n)
implementation(projects.core.archive)
implementation(projects.core.common)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -92,6 +92,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.isBenchmarkBuildType
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
import eu.kanade.tachiyomi.util.system.updaterEnabled
import eu.kanade.tachiyomi.util.view.setComposeContent
@@ -256,11 +257,13 @@ class MainActivity : BaseActivity() {
HandleOnNewIntent(context = context, navigator = navigator)
if (!isBenchmarkBuildType) {
CheckForUpdates()
ShowOnboarding()
ShowDonationCampaign()
}
}
}
val startTime = System.currentTimeMillis()
splashScreen?.setKeepOnScreenCondition {
@@ -21,3 +21,6 @@ val isReleaseBuildType: Boolean
val isFossBuildType: Boolean
inline get() = BuildConfig.BUILD_TYPE == "foss"
val isBenchmarkBuildType: Boolean
inline get() = BuildConfig.BUILD_TYPE.contains("nonMinified") || BuildConfig.BUILD_TYPE.contains("benchmark")
+50
View File
@@ -0,0 +1,50 @@
import com.android.build.api.dsl.ManagedVirtualDevice
plugins {
alias(mihonx.plugins.android.test)
alias(libs.plugins.androidx.baselineProfile)
}
android {
namespace = "mihon.baselineprofile"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
targetProjectPath = ":app"
// This code creates the gradle managed device used to generate baseline profiles.
// To use GMD please invoke generation through the command line:
// ./gradlew :app:generateBaselineProfile
testOptions.managedDevices.allDevices {
@Suppress("UnstableApiUsage")
create<ManagedVirtualDevice>("pixel6Api34") {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "google"
}
}
}
// This is the configuration block for the Baseline Profile plugin.
// You can specify to run the generators on a managed devices or connected devices.
baselineProfile {
managedDevices += "pixel6Api34"
useConnectedDevices = false
}
dependencies {
implementation(libs.androidx.benchmark.macroJunit4)
implementation(libs.androidx.test.junit)
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.test.uiautomator)
}
androidComponents {
onVariants { variant ->
val artifactsLoader = variant.artifacts.getBuiltArtifactsLoader()
val applicationId = variant.testedApks.map { artifactsLoader.load(it)?.applicationId }
variant.instrumentationRunnerArguments.put("targetAppId", applicationId)
}
}
@@ -0,0 +1 @@
<manifest />
@@ -0,0 +1,46 @@
package mihon.baselineprofile
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect(TARGET_PACKAGE_NAME) {
pressHome()
startActivityAndWait()
device.waitAndClick(By.text("Updates"))
device.waitForIdle()
device.waitAndClick(By.text("History"))
device.waitForIdle()
device.waitAndClick(By.text("Browse"))
device.waitForIdle()
device.waitAndClick(By.text("Extensions"))
device.waitForIdle()
device.waitAndClick(By.text("More"))
device.waitForIdle()
}
}
}
private fun UiDevice.waitAndClick(by: BySelector) {
wait(Until.findObject(by), 60_000).click()
}
@@ -0,0 +1,101 @@
package mihon.baselineprofile
import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance from a cold state.
*/
@RunWith(AndroidJUnit4ClassRunner::class)
class ColdStartupBenchmark : AbstractStartupBenchmark(StartupMode.COLD)
/**
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance from a warm state.
*/
@RunWith(AndroidJUnit4ClassRunner::class)
class WarmStartupBenchmark : AbstractStartupBenchmark(StartupMode.WARM)
/**
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance from a hot state.
*/
@RunWith(AndroidJUnit4ClassRunner::class)
class HotStartupBenchmark : AbstractStartupBenchmark(StartupMode.HOT)
/**
* This test class benchmarks the speed of app startup.
* Run this benchmark to verify how effective a Baseline Profile is.
* It does this by comparing [CompilationMode.None], which represents the app with no Baseline
* Profiles optimizations, and [CompilationMode.Partial], which uses Baseline Profiles.
*
* Run this benchmark to see startup measurements and captured system traces for verifying
* the effectiveness of your Baseline Profiles. You can run it directly from Android
* Studio as an instrumentation test, or run all benchmarks for a variant, for example benchmarkRelease,
* with this Gradle task:
* ```
* ./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest
* ```
*
* You should run the benchmarks on a physical device, not an Android emulator, because the
* emulator doesn't represent real world performance and shares system resources with its host.
*
* For more information, see the [Macrobenchmark documentation](https://d.android.com/macrobenchmark#create-macrobenchmark)
* and the [instrumentation arguments documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args).
**/
abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupCompilationNone() =
benchmark(CompilationMode.None())
@Test
fun startupCompilationBaselineProfilesDisabled() =
benchmark(CompilationMode.Partial(BaselineProfileMode.Disable))
@Test
fun startupCompilationBaselineProfiles() =
benchmark(CompilationMode.Partial(BaselineProfileMode.Require))
@Test
fun startupCompilationFull() = benchmark(CompilationMode.Full())
private fun benchmark(compilationMode: CompilationMode) {
// The application id for the running build variant is read from the instrumentation arguments.
rule.measureRepeated(
packageName = TARGET_PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = startupMode,
iterations = 10,
setupBlock = {
pressHome()
},
measureBlock = {
startActivityAndWait()
// TODO Add interactions to wait for when your app is fully drawn.
// The app is fully drawn when Activity.reportFullyDrawn is called.
// For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
// from the AndroidX Activity library.
// Check the UiAutomator documentation for more information on how to
// interact with the app.
// https://d.android.com/training/testing/other-components/ui-automator
}
)
}
}
@@ -0,0 +1,35 @@
package mihon.baselineprofile
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect(
packageName = TARGET_PACKAGE_NAME,
includeInStartupProfile = true
) {
pressHome()
startActivityAndWait()
}
}
}
private fun UiDevice.waitAndClick(by: BySelector) {
wait(Until.findObject(by), 60_000).click()
}
@@ -0,0 +1,7 @@
package mihon.baselineprofile
import androidx.test.platform.app.InstrumentationRegistry
internal val TARGET_PACKAGE_NAME: String
inline get() = InstrumentationRegistry.getArguments().getString("targetAppId")
?: throw Exception("targetAppId not passed as instrumentation runner arg")
+1
View File
@@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.android.kmp.library) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.androidx.baselineProfile) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.kotlin.compose.compiler) apply false
+4 -2
View File
@@ -5,6 +5,7 @@ android-gradle = "9.2.1"
androidx-activity-compose = "1.13.0"
androidx-annotation = "1.10.0"
androidx-appCompat = "1.7.1"
androidx-baselineProfile = "1.5.0-alpha06"
androidx-benchmark = "1.4.1"
androidx-biometric = "1.2.0-alpha05"
androidx-compose-bom = "2026.06.00"
@@ -106,7 +107,7 @@ androidx-profileInstaller = { module = "androidx.profileinstaller:profileinstall
androidx-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recyclerView" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidx-sqlite" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" }
androidx-test-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-test-junit" }
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-test-uiautomator" }
androidx-viewPager = { module = "androidx.viewpager:viewpager", version.ref = "androidx-viewPager" }
androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
@@ -185,9 +186,10 @@ xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", v
[plugins]
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" }
android-application = { id = "com.android.application", version.ref = "android-gradle" }
android-test = { id = "com.android.test", version.ref = "android-gradle" }
android-kmp-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "android-gradle" }
android-library = { id = "com.android.library", version.ref = "android-gradle" }
android-test = { id = "com.android.test", version.ref = "android-gradle" }
androidx-baselineProfile = { id = "androidx.baselineprofile", version.ref = "androidx-baselineProfile" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" }
google-services = { id = "com.google.gms.google-services", version.ref = "google-services" }
kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-gradle" }
-11
View File
@@ -1,11 +0,0 @@
# Baseline profiles
The baseline profile for this app is located at [`app/src/main/baseline-prof.txt`](../app/src/main/baseline-prof.txt).
It contains rules that enable AOT compilation of the critical user path taken during app launch.
For more information on baseline profiles, read [this document](https://developer.android.com/studio/profile/baselineprofiles).
> Note: The baseline profile needs to be re-generated for release builds that touch code which changes app startup.
To generate the baseline profile, select the `devBenchmark` build variant and run the
`BaselineProfileGenerator` benchmark test on an AOSP Android Emulator.
Then copy the resulting baseline profile from the emulator to [`app/src/main/baseline-prof.txt`](../app/src/main/baseline-prof.txt).
-39
View File
@@ -1,39 +0,0 @@
plugins {
alias(mihonx.plugins.android.test)
}
android {
namespace = "tachiyomi.macrobenchmark"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["androidx.benchmark.enabledRules"] = "BaselineProfile"
}
buildTypes {
// This benchmark buildType is used for benchmarking, and should function like your
// release build (for example, with minification on). It's signed with a debug key
// for easy local/CI testing.
create("benchmark") {
isDebuggable = true
signingConfig = getByName("debug").signingConfig
matchingFallbacks.add("release")
}
}
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation(libs.androidx.test.junit)
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.benchmark.macroJunit4)
}
androidComponents {
beforeVariants(selector().all()) {
it.enable = it.buildType == "benchmark"
}
}
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
@@ -1,33 +0,0 @@
package tachiyomi.macrobenchmark
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.uiautomator.By
import org.junit.Rule
import org.junit.Test
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun generate() = baselineProfileRule.collect(
packageName = "eu.kanade.tachiyomi.benchmark",
profileBlock = {
pressHome()
startActivityAndWait()
device.findObject(By.text("Updates")).click()
device.findObject(By.text("History")).click()
// TODO: automate storage permissions and possibly open manga details screen too?
// device.findObject(By.text("Browse")).click()
// device.findObject(By.text("Extensions")).click()
// device.swipe(150, 150, 50, 150, 1)
device.findObject(By.text("More")).click()
device.findObject(By.text("Settings")).click()
},
)
}
@@ -1,89 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tachiyomi.macrobenchmark
import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance from a cold state.
*/
@RunWith(AndroidJUnit4ClassRunner::class)
class ColdStartupBenchmark : AbstractStartupBenchmark(StartupMode.COLD)
/**
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance from a warm state.
*/
@RunWith(AndroidJUnit4ClassRunner::class)
class WarmStartupBenchmark : AbstractStartupBenchmark(StartupMode.WARM)
/**
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance from a hot state.
*/
@RunWith(AndroidJUnit4ClassRunner::class)
class HotStartupBenchmark : AbstractStartupBenchmark(StartupMode.HOT)
/**
* Base class for benchmarks with different startup modes.
* Enables app startups from various states of baseline profile or [CompilationMode]s.
*/
abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startupNoCompilation() = startup(CompilationMode.None())
@Test
fun startupBaselineProfileDisabled() = startup(
CompilationMode.Partial(
baselineProfileMode = BaselineProfileMode.Disable,
warmupIterations = 1,
),
)
@Test
fun startupBaselineProfile() = startup(
CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require),
)
@Test
fun startupFullCompilation() = startup(CompilationMode.Full())
private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = "eu.kanade.tachiyomi.benchmark",
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
iterations = 10,
startupMode = startupMode,
setupBlock = {
pressHome()
},
) {
startActivityAndWait()
}
}
+1 -1
View File
@@ -30,13 +30,13 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "Mihon"
include(":app")
include(":baseline-profile")
include(":core-metadata")
include(":core:archive")
include(":core:common")
include(":data")
include(":domain")
include(":i18n")
include(":macrobenchmark")
include(":presentation-core")
include(":presentation-widget")
include(":source-api")