diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 090b103eb..49c51b871 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("kotlinx-serialization") id("kotlin-parcelize") id("androidx.navigation.safeargs.kotlin") + alias(libs.plugins.baselineprofile) } android { @@ -135,4 +136,8 @@ dependencies { /* Room */ ksp(libs.room.compiler) implementation(libs.room) + + /* Baseline profile generation */ + implementation(libs.androidx.profileinstaller) + "baselineProfile"(project(":baselineprofile")) } diff --git a/baselineProfile/.gitignore b/baselineProfile/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/baselineProfile/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/baselineProfile/build.gradle.kts b/baselineProfile/build.gradle.kts new file mode 100644 index 000000000..576e95bc7 --- /dev/null +++ b/baselineProfile/build.gradle.kts @@ -0,0 +1,52 @@ +import com.android.build.api.dsl.ManagedVirtualDevice + +plugins { + alias(libs.plugins.androidTest) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.baselineprofile) +} + +android { + namespace = "com.example.baselineprofile" + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + defaultConfig { + minSdk = 28 + targetSdk = 34 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + targetProjectPath = ":app" + + testOptions.managedDevices.devices { + create("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.test.junit) + implementation(libs.androidx.test.espressoCore) + implementation(libs.androidx.uiautomator) + implementation(libs.androidx.benchmark.macro.junit4) +} diff --git a/baselineProfile/src/main/AndroidManifest.xml b/baselineProfile/src/main/AndroidManifest.xml new file mode 100644 index 000000000..227314eeb --- /dev/null +++ b/baselineProfile/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/baselineProfile/src/main/java/com/github/libretube/baselineprofile/BaselineProfileGenerator.kt b/baselineProfile/src/main/java/com/github/libretube/baselineprofile/BaselineProfileGenerator.kt new file mode 100644 index 000000000..d4a2eae4a --- /dev/null +++ b/baselineProfile/src/main/java/com/github/libretube/baselineprofile/BaselineProfileGenerator.kt @@ -0,0 +1,66 @@ +package com.github.libretube.baselineprofile + +import androidx.benchmark.macro.junit4.BaselineProfileRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This test class generates a basic startup baseline profile for the target package. + * + * We recommend you start with this but add important user flows to the profile to improve their performance. + * Refer to the [baseline profile documentation](https://d.android.com/topic/performance/baselineprofiles) + * for more information. + * + * You can run the generator with the "Generate Baseline Profile" run configuration in Android Studio or + * the equivalent `generateBaselineProfile` gradle task: + * ``` + * ./gradlew :app:generateReleaseBaselineProfile + * ``` + * The run configuration runs the Gradle task and applies filtering to run only the generators. + * + * Check [documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args) + * for more information about available instrumentation arguments. + * + * After you run the generator, you can verify the improvements running the [StartupBenchmarks] benchmark. + * + * When using this class to generate a baseline profile, only API 33+ or rooted API 28+ are supported. + * + * The minimum required version of androidx.benchmark to generate a baseline profile is 1.2.0. + **/ +@RunWith(AndroidJUnit4::class) +@LargeTest +class BaselineProfileGenerator { + + @get:Rule + val rule = BaselineProfileRule() + + @Test + fun generate() { + // This example works only with the variant with application id `com.github.libretube`." + rule.collect( + packageName = "com.github.libretube", + + // See: https://d.android.com/topic/performance/baselineprofiles/dex-layout-optimizations + includeInStartupProfile = true + ) { + // This block defines the app's critical user journey. Here we are interested in + // optimizing for app startup. But you can also navigate and scroll through your most important UI. + + // Start default activity for your app + pressHome() + startActivityAndWait() + + // TODO Write more interactions to optimize advanced journeys of your app. + // For example: + // 1. Wait until the content is asynchronously loaded + // 2. Scroll the feed content + // 3. Navigate to detail screen + + // Check UiAutomator documentation for more information how to interact with the app. + // https://d.android.com/training/testing/other-components/ui-automator + } + } +} \ No newline at end of file diff --git a/baselineProfile/src/main/java/com/github/libretube/baselineprofile/StartupBenchmarks.kt b/baselineProfile/src/main/java/com/github/libretube/baselineprofile/StartupBenchmarks.kt new file mode 100644 index 000000000..56b06dd46 --- /dev/null +++ b/baselineProfile/src/main/java/com/github/libretube/baselineprofile/StartupBenchmarks.kt @@ -0,0 +1,75 @@ +package com.github.libretube.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 org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * 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). + **/ +@RunWith(AndroidJUnit4::class) +@LargeTest +class StartupBenchmarks { + + @get:Rule + val rule = MacrobenchmarkRule() + + @Test + fun startupCompilationNone() = + benchmark(CompilationMode.None()) + + @Test + fun startupCompilationBaselineProfiles() = + benchmark(CompilationMode.Partial(BaselineProfileMode.Require)) + + private fun benchmark(compilationMode: CompilationMode) { + // This example works only with the variant with application id `com.github.libretube`." + rule.measureRepeated( + packageName = "com.github.libretube", + metrics = listOf(StartupTimingMetric()), + compilationMode = compilationMode, + startupMode = StartupMode.COLD, + 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 + } + ) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 0d67f12f8..039c61334 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,6 +25,10 @@ allprojects { plugins { id("com.google.devtools.ksp") version("1.9.22-1.0.16") apply false + alias(libs.plugins.androidTest) apply false + alias(libs.plugins.jetbrainsKotlinAndroid) apply false + alias(libs.plugins.baselineprofile) apply false + alias(libs.plugins.androidApplication) apply false } tasks.register("clean") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75afde213..99ec19a3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,6 @@ [versions] appcompat = "1.6.1" core = "1.12.0" -gradle = "8.2.0" kotlin = "1.9.22" lifecycle = "2.6.2" constraintlayout = "2.1.4" @@ -25,6 +24,11 @@ kotlinxRetrofit = "1.0.0" media3 = "1.2.0" activity = "1.8.2" fragment = "1.6.2" +agp = "8.2.0" +uiautomator = "2.2.0" +benchmarkMacroJunit4 = "1.2.2" +baselineprofile = "1.2.2" +profileinstaller = "1.3.1" [libraries] androidx-activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } @@ -32,7 +36,7 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version androidx-core = { group = "androidx.core", name = "core", version.ref = "core" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-fragment = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" } -gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } +gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } @@ -65,3 +69,12 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-serialization-retrofit = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "kotlinxRetrofit" } +androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } +androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } +androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } + +[plugins] +androidTest = { id = "com.android.test", version.ref = "agp" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" } +androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0b3cea543..5171ec3e8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,12 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + rootProject.name = "LibreTube" include(":app") +include(":baselineprofile")