diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f2c75d47..d84914d4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.dagger.hilt) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.devtools.ksp) + alias(libs.plugins.baselineprofile) } val properties = Properties().apply { @@ -18,6 +19,23 @@ android { namespace = "com.kiero" compileSdk = libs.versions.compileSdk.get().toInt() + signingConfigs { + getByName("debug") { + val debugKeystorePath = System.getProperty("user.home") + "/.android/debug.keystore" + storeFile = file(debugKeystorePath) + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + + create("benchmark") { + storeFile = file(System.getProperty("user.home") + "/.android/debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + defaultConfig { applicationId = "com.Kiero" minSdk = libs.versions.minSdk.get().toInt() @@ -53,6 +71,15 @@ android { "proguard-rules.pro" ) } + getByName("release") { + signingConfig = signingConfigs.getByName("debug") + + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -82,20 +109,14 @@ android { } } - signingConfigs { - getByName("debug") { - keyAlias = "androiddebugkey" - keyPassword = "android" - storeFile = File("${project.rootDir.absolutePath}/keystore/debug.keystore") - storePassword = "android" - } - } } dependencies { + implementation(libs.androidx.profileinstaller) testImplementation(libs.junit) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.bundles.test) + "baselineProfile"(project(":baselineprofile")) debugImplementation(libs.bundles.debug) diff --git a/app/src/main/java/com/kiero/presentation/kid/journey/model/StoneType.kt b/app/src/main/java/com/kiero/presentation/kid/journey/model/StoneType.kt index 32f9f9d6..4eaf9c6a 100644 --- a/app/src/main/java/com/kiero/presentation/kid/journey/model/StoneType.kt +++ b/app/src/main/java/com/kiero/presentation/kid/journey/model/StoneType.kt @@ -1,7 +1,12 @@ package com.kiero.presentation.kid.journey.model import androidx.annotation.DrawableRes +import androidx.annotation.Keep import com.kiero.R +import kotlinx.serialization.Serializable + +@Keep +@Serializable enum class StoneUiType( val serverKey: String, diff --git a/baselineProfile/.gitignore b/baselineProfile/.gitignore new file mode 100644 index 00000000..42afabfd --- /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 00000000..31a2aa3c --- /dev/null +++ b/baselineProfile/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.android.test) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.baselineprofile) +} + +android { + namespace = "com.kiero.baselineprofile" + compileSdk { + version = release(36) + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + defaultConfig { + minSdk = 28 + targetSdk = 36 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + targetProjectPath = ":app" + + flavorDimensions += listOf("version") + productFlavors { + create("parent") { dimension = "version" } + create("child") { dimension = "version" } + } + +} + +// 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 { + useConnectedDevices = true +} + +dependencies { + implementation(libs.androidx.junit) + implementation(libs.androidx.espresso.core) + implementation(libs.androidx.uiautomator) + implementation(libs.androidx.benchmark.macro.junit4) +} + +androidComponents { + onVariants { v -> + val artifactsLoader = v.artifacts.getBuiltArtifactsLoader() + v.instrumentationRunnerArguments.put( + "targetAppId", + v.testedApks.map { artifactsLoader.load(it)?.applicationId } + ) + } +} \ No newline at end of file diff --git a/baselineProfile/src/main/AndroidManifest.xml b/baselineProfile/src/main/AndroidManifest.xml new file mode 100644 index 00000000..227314ee --- /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/kiero/baselineprofile/benchmarks/StartupBenchmarks.kt b/baselineProfile/src/main/java/com/kiero/baselineprofile/benchmarks/StartupBenchmarks.kt new file mode 100644 index 00000000..a7e1a43e --- /dev/null +++ b/baselineProfile/src/main/java/com/kiero/baselineprofile/benchmarks/StartupBenchmarks.kt @@ -0,0 +1,89 @@ +package com.kiero.baselineprofile.benchmarks + +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.platform.app.InstrumentationRegistry +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 [androidx.benchmark.macro.CompilationMode.None], which represents the app with no Baseline + * Profiles optimizations, and [androidx.benchmark.macro.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() + + private val targetPackageName = InstrumentationRegistry.getArguments().getString("targetAppId") + ?: "com.kiero.parent" + + @Test + fun startupCompilationNone() = + benchmark(CompilationMode.None()) + + @Test + fun startupCompilationBaselineProfiles() = + benchmark(CompilationMode.Partial(BaselineProfileMode.Require)) + + @Test + fun startupWithBaselineProfile() = rule.measureRepeated( + packageName = targetPackageName, + metrics = listOf(StartupTimingMetric()), + iterations = 5, + startupMode = StartupMode.COLD, + compilationMode = CompilationMode.Partial() + ) { + pressHome() + startActivityAndWait() + } + private fun benchmark(compilationMode: CompilationMode) { + // The application id for the running build variant is read from the instrumentation arguments. + rule.measureRepeated( + packageName = targetPackageName, + 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/baselineProfile/src/main/java/com/kiero/baselineprofile/generator/BaselineProfileGenerator.kt b/baselineProfile/src/main/java/com/kiero/baselineprofile/generator/BaselineProfileGenerator.kt new file mode 100644 index 00000000..2c61a435 --- /dev/null +++ b/baselineProfile/src/main/java/com/kiero/baselineprofile/generator/BaselineProfileGenerator.kt @@ -0,0 +1,68 @@ +package com.kiero.baselineprofile.generator + +import androidx.benchmark.macro.junit4.BaselineProfileRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +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 [com.kiero.baselineprofile.benchmarks.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() { + // The application id for the running build variant is read from the instrumentation arguments. + rule.collect( + packageName = InstrumentationRegistry.getArguments().getString("targetAppId") + ?: throw Exception("targetAppId not passed as instrumentation runner arg"), + + // 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/build.gradle.kts b/build.gradle.kts index 5fc55f9d..76e37be8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,4 +7,6 @@ plugins { alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.devtools.ksp) apply false alias(libs.plugins.android.library) apply false + alias(libs.plugins.android.test) apply false + alias(libs.plugins.baselineprofile) apply false } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 20e2a015..316a8ed5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +org.gradle.configuration-cache=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b73a280c..4be6eaea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,10 @@ kakao = "2.20.6" # DataStore & Tink (여기 추가!) datastore = "1.0.0" tink = "1.15.0" +uiautomator = "2.2.0" +benchmarkMacroJunit4 = "1.2.4" +baselineprofile = "1.2.4" +profileinstaller = "1.3.1" [libraries] # Test @@ -117,6 +121,9 @@ kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" # DataStore & Tink androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } tink-android = { group = "com.google.crypto.tink", name = "tink-android", version.ref = "tink" } +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] android-application = { id = "com.android.application", version.ref = "agp" } @@ -127,6 +134,8 @@ devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt" } android-library = { id = "com.android.library", version.ref = "agp" } jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" } +android-test = { id = "com.android.test", version.ref = "agp" } +baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" } [bundles] diff --git a/settings.gradle.kts b/settings.gradle.kts index 4b0d9038..81fd864f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,4 +22,4 @@ dependencyResolutionManagement { rootProject.name = "Kiero" include(":app") - \ No newline at end of file +include(":baselineprofile")