diff --git a/.github/workflows/android.yml b/.github/workflows/android_snapshot.yml similarity index 69% rename from .github/workflows/android.yml rename to .github/workflows/android_snapshot.yml index 01bb16313..bdd6e886c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android_snapshot.yml @@ -1,4 +1,4 @@ -name: Android CI +name: Android SNAPSHOT CI on: push: @@ -11,7 +11,7 @@ on: - 'fastlane/**' - '!.github/workflows/**' pull_request: - branches: [ "master", "ci-1.14.0-rc04-benchmark" ] + branches: [ "master" ] workflow_dispatch: jobs: @@ -32,10 +32,7 @@ jobs: run: chmod +x gradlew - name: Generate baseline profile - run: ./gradlew :benchmark:Pixel5Api31StableChannelRichCodecBenchmarkAndroidTest --rerun-tasks -P android.testInstrumentationRunnerArguments.class=com.m3u.benchmark.BaselineProfileGenerator - - - name: Copy and Rename baseline profile - run: cp benchmark/build/outputs/managed_device_android_test_additional_output/benchmark/flavours/stableChannelRichCodec/Pixel5Api31/BaselineProfileGenerator_generateBaselineProfile-baseline-prof.txt androidApp/src/main/baseline-prof.txt + run: ./gradlew :androidApp:generateSnapshotChannelRichCodecReleaseBaselineProfile - name: Build with Gradle run: ./gradlew :androidApp:assembleSnapshotChannelRichCodecRelease diff --git a/.idea/runConfigurations/M3UAndroid___benchmark_Pixel5Api31BenchmarkAndroidTest_.xml b/.idea/runConfigurations/M3UAndroid___benchmark_Pixel5Api31BenchmarkAndroidTest_.xml deleted file mode 100644 index eaa7eb42c..000000000 --- a/.idea/runConfigurations/M3UAndroid___benchmark_Pixel5Api31BenchmarkAndroidTest_.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 4c3619b62..6bf20edd1 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -21,7 +21,6 @@ android { versionName = "1.14.0-rc03" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArguments["androidx.benchmark.profiling.mode"] = "MethodTracing" } flavorDimensions += setOf("channel", "codec") productFlavors { @@ -131,6 +130,9 @@ dependencies { implementation(project(":feature:playlist-configuration")) implementation(project(":feature:crash")) + implementation(libs.androidx.profileinstaller) + baselineProfile(project(":baselineprofile")) + implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.androidx.activity.compose) @@ -143,6 +145,7 @@ dependencies { implementation(libs.androidx.core.splashscreen) implementation(libs.google.dagger.hilt) + baselineProfile(project(":baselineprofile")) ksp(libs.google.dagger.hilt.compiler) implementation(libs.androidx.hilt.navigation.compose) diff --git a/annotation/build.gradle.kts b/annotation/build.gradle.kts index 5bd54b44a..4d31c0bb5 100644 --- a/annotation/build.gradle.kts +++ b/annotation/build.gradle.kts @@ -1,5 +1,18 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { - kotlin("jvm") + alias(libs.plugins.org.jetbrains.kotlin.jvm) +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } } dependencies { diff --git a/benchmark/.gitignore b/baselineprofile/.gitignore similarity index 100% rename from benchmark/.gitignore rename to baselineprofile/.gitignore diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts new file mode 100644 index 000000000..9661d0829 --- /dev/null +++ b/baselineprofile/build.gradle.kts @@ -0,0 +1,61 @@ +@file:Suppress("UnstableApiUsage") + +import com.android.build.api.dsl.ManagedVirtualDevice + +plugins { + alias(libs.plugins.com.android.test) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.androidx.baselineprofile) +} + +android { + namespace = "com.m3u.baselineprofile" + compileSdk = 34 + defaultConfig { + minSdk = 28 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + kotlinOptions { + jvmTarget = "17" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + targetProjectPath = ":androidApp" + + flavorDimensions += listOf("channel", "codec") + productFlavors { + create("stableChannel") { dimension = "channel" } + create("snapshotChannel") { dimension = "channel" } + create("richCodec") { dimension = "codec" } + create("liteCodec") { dimension = "codec" } + } + + // This code creates the gradle managed device used to generate baseline profiles. + // To use GMD please invoke generation through the command line: + // ./gradlew :androidApp:generateBaselineProfile + testOptions.managedDevices.devices { + create("pixel6Api34") { + device = "Pixel 6" + apiLevel = 34 + systemImageSource = "aosp" + } + } +} + +// 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.ext.junit) + implementation(libs.androidx.test.espresso.espresso.core) + implementation(libs.androidx.test.uiautomator.uiautomator) + implementation(libs.androidx.benchmark.benchmark.macro.junit4) +} diff --git a/benchmark/src/main/AndroidManifest.xml b/baselineprofile/src/main/AndroidManifest.xml similarity index 100% rename from benchmark/src/main/AndroidManifest.xml rename to baselineprofile/src/main/AndroidManifest.xml diff --git a/baselineprofile/src/main/java/com/m3u/baselineprofile/BaselineProfileGenerator.kt b/baselineprofile/src/main/java/com/m3u/baselineprofile/BaselineProfileGenerator.kt new file mode 100644 index 000000000..19cc3029d --- /dev/null +++ b/baselineprofile/src/main/java/com/m3u/baselineprofile/BaselineProfileGenerator.kt @@ -0,0 +1,67 @@ +package com.m3u.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 :androidApp: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.m3u.androidApp.snapshot`." + rule.collect( + packageName = "com.m3u.androidApp", + + // 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/m3u/baselineprofile/StartupBenchmarks.kt b/baselineprofile/src/main/java/com/m3u/baselineprofile/StartupBenchmarks.kt new file mode 100644 index 000000000..6c09aa317 --- /dev/null +++ b/baselineprofile/src/main/java/com/m3u/baselineprofile/StartupBenchmarks.kt @@ -0,0 +1,75 @@ +package com.m3u.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.m3u.androidApp.snapshot`." + rule.measureRepeated( + packageName = "com.m3u.androidApp.snapshot", + 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/benchmark/build.gradle.kts b/benchmark/build.gradle.kts deleted file mode 100644 index 508d4c087..000000000 --- a/benchmark/build.gradle.kts +++ /dev/null @@ -1,87 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -import com.android.build.api.dsl.ManagedVirtualDevice - -plugins { - alias(libs.plugins.com.android.test) - alias(libs.plugins.org.jetbrains.kotlin.android) -} - -android { - namespace = "com.m3u.benchmark" - compileSdk = 34 - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - defaultConfig { - minSdk = 26 - targetSdk = 33 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - flavorDimensions += setOf("channel", "codec") - productFlavors { - create("stableChannel") { - dimension = "channel" - } - create("snapshotChannel") { - dimension = "channel" - } - create("richCodec") { - dimension = "codec" - } - create("liteCodec") { - dimension = "codec" - } - } - - testOptions { - managedDevices { - allDevices { - // Add Gradle task - // :benchmark:Pixel5Api31BenchmarkAndroidTest --rerun-tasks -P - // android.testInstrumentationRunnerArguments.class=com.m3u.benchmark.BaselineProfileGenerator - create("Pixel5Api31", ManagedVirtualDevice::class) { - device = "Pixel 5" - apiLevel = 31 - systemImageSource = "aosp" - } - } - } - } - - 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 += listOf("release") - } - } - - targetProjectPath = ":androidApp" - experimentalProperties["android.experimental.self-instrumenting"] = true -} - -dependencies { - implementation(libs.androidx.test.ext.junit) - implementation(libs.androidx.test.espresso.espresso.core) - implementation(libs.androidx.test.uiautomator.uiautomator) - implementation(libs.androidx.benchmark.benchmark.macro.junit4) -} - -androidComponents { - beforeVariants(selector().all()) { - it.enable = it.buildType == "benchmark" - } -} \ No newline at end of file diff --git a/benchmark/src/main/java/com/m3u/benchmark/BaselineProfileGenerator.kt b/benchmark/src/main/java/com/m3u/benchmark/BaselineProfileGenerator.kt deleted file mode 100644 index b90e65460..000000000 --- a/benchmark/src/main/java/com/m3u/benchmark/BaselineProfileGenerator.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.m3u.benchmark - -import androidx.annotation.RequiresApi -import androidx.benchmark.macro.junit4.BaselineProfileRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -@RequiresApi(28) -class BaselineProfileGenerator { - @get:Rule - val baselineRule = BaselineProfileRule() - - @Test - fun generateBaselineProfile() = baselineRule.collect( - packageName = "com.m3u.androidApp" - ) { - pressHome() - startActivityAndWait() - } -} \ No newline at end of file diff --git a/benchmark/src/main/java/com/m3u/benchmark/ExampleStartupBenchmark.kt b/benchmark/src/main/java/com/m3u/benchmark/ExampleStartupBenchmark.kt deleted file mode 100644 index b492b51f9..000000000 --- a/benchmark/src/main/java/com/m3u/benchmark/ExampleStartupBenchmark.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.m3u.benchmark - -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 org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ExampleStartupBenchmark { - @get:Rule - val benchmarkRule = MacrobenchmarkRule() - - @Test - fun startUpWithBaseline() = startUp(CompilationMode.Partial()) - - @Test - fun startUpWithoutBaseline() = startUp(CompilationMode.None()) - - private fun startUp(mode: CompilationMode) = benchmarkRule.measureRepeated( - packageName = "com.m3u.androidApp", - metrics = listOf(StartupTimingMetric()), - iterations = 3, - startupMode = StartupMode.COLD, - compilationMode = mode - ) { - pressHome() - startActivityAndWait() - } -} diff --git a/build.gradle.kts b/build.gradle.kts index 977b9ee7c..e80827a66 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ plugins { alias(libs.plugins.com.android.test) apply false alias(libs.plugins.org.jetbrains.kotlin.serialization) apply false alias(libs.plugins.org.jetbrains.kotlin.jvm) apply false + alias(libs.plugins.androidx.baselineprofile) apply false } subprojects { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6aaafb276..8689e597d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ chucker = "4.0.0" logback = "3.0.0" lottie-compose = "6.4.0" nextLib = "0.7.1" +profileinstaller = "1.4.1" slf4j-api = "2.0.13" squareup-retrofit2 = "2.11.0" @@ -47,7 +48,7 @@ androidx-test-ext-junit = "1.2.1" espresso-core = "3.6.1" com-google-android-material = "1.12.0" androidx-test-uiautomator = "2.3.0" -androidx-benchmark = "1.3.0" +androidx-benchmark = "1.3.3" androidx-graphics-shapes = "1.0.1" minabox = "1.7.1" @@ -89,6 +90,7 @@ androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" } +androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" } androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "androidx-tv" } androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx-startup" } @@ -170,11 +172,11 @@ symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-ap [plugins] com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } com-android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } +com-android-test = { id = "com.android.test", version.ref = "android-gradle-plugin" } org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } org-jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } com-google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "google-dagger" } com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-plugin" } -com-android-test = { id = "com.android.test", version.ref = "android-gradle-plugin" } androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidx-benchmark" } org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file diff --git a/processor/build.gradle.kts b/processor/build.gradle.kts index dc27070ee..34e00dd67 100644 --- a/processor/build.gradle.kts +++ b/processor/build.gradle.kts @@ -1,8 +1,21 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { - kotlin("jvm") + alias(libs.plugins.org.jetbrains.kotlin.jvm) alias(libs.plugins.com.google.devtools.ksp) } +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } +} + ksp { arg("autoserviceKsp.verify", "true") arg("autoserviceKsp.verbose", "true") diff --git a/settings.gradle.kts b/settings.gradle.kts index ca1f55403..36b4c1636 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,8 +29,7 @@ include( ":feature:channel", ":feature:crash" ) -include(":benchmark") +include(":baselineprofile") include(":i18n") include(":codec:lite", ":codec:rich") -include(":annotation") -include(":processor") +include(":annotation", ":processor")