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(mihonx.plugins.spotless)
alias(libs.plugins.aboutLibraries) alias(libs.plugins.aboutLibraries)
alias(libs.plugins.androidx.baselineProfile)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
} }
@@ -82,6 +83,8 @@ android {
signingConfig = debug.signingConfig signingConfig = debug.signingConfig
isProfileable = true
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = true)}\"") buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = true)}\"")
@@ -110,8 +113,6 @@ android {
create("benchmark") { create("benchmark") {
initWith(release) initWith(release)
isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark" versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark" applicationIdSuffix = ".benchmark"
@@ -197,7 +198,14 @@ kotlin {
} }
} }
baselineProfile {
baselineProfileOutputDir = "baselineProfiles"
mergeIntoMain = true
}
dependencies { dependencies {
baselineProfile(projects.baselineProfile)
implementation(projects.i18n) implementation(projects.i18n)
implementation(projects.core.archive) implementation(projects.core.archive)
implementation(projects.core.common) 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.NewUpdateScreen
import eu.kanade.tachiyomi.ui.more.OnboardingScreen import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.system.dpToPx 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.isNavigationBarNeedsScrim
import eu.kanade.tachiyomi.util.system.updaterEnabled import eu.kanade.tachiyomi.util.system.updaterEnabled
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
@@ -256,9 +257,11 @@ class MainActivity : BaseActivity() {
HandleOnNewIntent(context = context, navigator = navigator) HandleOnNewIntent(context = context, navigator = navigator)
CheckForUpdates() if (!isBenchmarkBuildType) {
ShowOnboarding() CheckForUpdates()
ShowDonationCampaign() ShowOnboarding()
ShowDonationCampaign()
}
} }
} }
@@ -21,3 +21,6 @@ val isReleaseBuildType: Boolean
val isFossBuildType: Boolean val isFossBuildType: Boolean
inline get() = BuildConfig.BUILD_TYPE == "foss" 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.kmp.library) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.test) 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.firebase.crashlytics) apply false
alias(libs.plugins.google.services) apply false alias(libs.plugins.google.services) apply false
alias(libs.plugins.kotlin.compose.compiler) 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-activity-compose = "1.13.0"
androidx-annotation = "1.10.0" androidx-annotation = "1.10.0"
androidx-appCompat = "1.7.1" androidx-appCompat = "1.7.1"
androidx-baselineProfile = "1.5.0-alpha06"
androidx-benchmark = "1.4.1" androidx-benchmark = "1.4.1"
androidx-biometric = "1.2.0-alpha05" androidx-biometric = "1.2.0-alpha05"
androidx-compose-bom = "2026.06.00" 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-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recyclerView" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidx-sqlite" } 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-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-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-test-uiautomator" }
androidx-viewPager = { module = "androidx.viewpager:viewpager", version.ref = "androidx-viewPager" } androidx-viewPager = { module = "androidx.viewpager:viewpager", version.ref = "androidx-viewPager" }
androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } 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] [plugins]
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" }
android-application = { id = "com.android.application", version.ref = "android-gradle" } 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-kmp-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "android-gradle" }
android-library = { id = "com.android.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" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" }
google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } 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" } 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" rootProject.name = "Mihon"
include(":app") include(":app")
include(":baseline-profile")
include(":core-metadata") include(":core-metadata")
include(":core:archive") include(":core:archive")
include(":core:common") include(":core:common")
include(":data") include(":data")
include(":domain") include(":domain")
include(":i18n") include(":i18n")
include(":macrobenchmark")
include(":presentation-core") include(":presentation-core")
include(":presentation-widget") include(":presentation-widget")
include(":source-api") include(":source-api")