diff --git a/android-macrobenchmark/.gitignore b/android-macrobenchmark/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/android-macrobenchmark/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android-macrobenchmark/build.gradle b/android-macrobenchmark/build.gradle new file mode 100644 index 000000000..c5a71bd75 --- /dev/null +++ b/android-macrobenchmark/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'com.android.test' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + defaultConfig { + minSdkVersion 29 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + buildTypes { + // declare a build type (release) to match the target app's build type + performance { + debuggable = true + signingConfig = debug.signingConfig + } + } + + targetProjectPath = ":android" + properties["android.experimental.self-instrumenting"] = true +} + +androidComponents { + beforeVariants(selector().all()) { + // enable only the release buildType, since we only want to measure + // release build performance + enable = buildType == 'performance' + } +} + +dependencies { + //noinspection GradleDependency + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.10" + implementation 'androidx.test.ext:junit:1.1.2' + implementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation 'androidx.test.uiautomator:uiautomator:2.2.0' + implementation 'androidx.benchmark:benchmark-macro-junit4:1.1.0-alpha02' +} diff --git a/android-macrobenchmark/consumer-rules.pro b/android-macrobenchmark/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/android-macrobenchmark/proguard-rules.pro b/android-macrobenchmark/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/android-macrobenchmark/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android-macrobenchmark/src/main/AndroidManifest.xml b/android-macrobenchmark/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f842d64dc --- /dev/null +++ b/android-macrobenchmark/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/FrameTimingBenchmark.kt b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/FrameTimingBenchmark.kt new file mode 100644 index 000000000..a86a585b4 --- /dev/null +++ b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/FrameTimingBenchmark.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2021 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 + * + * http://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 io.github.droidkaigi.feeder.macrobenchmark + +import android.content.Intent +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.FrameTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Direction +import androidx.test.uiautomator.UiDevice +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@LargeTest +@SdkSuppress(minSdkVersion = 29) +@RunWith(AndroidJUnit4::class) +class FrameTimingBenchmark { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun start() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val device = UiDevice.getInstance(instrumentation) + benchmarkRule.measureRepeated( + packageName = "io.github.droidkaigi.feeder.debug", + metrics = listOf(FrameTimingMetric()), + // Try switching to different compilation modes to see the effect + // it has on frame timing metrics. + compilationMode = CompilationMode.None, + iterations = 10, + setupBlock = { + // Before starting to measure, navigate to the UI to be measured + val intent = Intent() + intent.setClassName("io.github.droidkaigi.feeder.debug","io.github.droidkaigi" + + ".feeder.MainActivity") + startActivityAndWait(intent) + } + ) { + val lazyColumn = device.findObject(By.scrollable(true)) + device.dumpWindowHierarchy(System.out) + device.dumpWindowHierarchy(System.err) + // Set gesture margin to avoid triggering gesture navigation + // with input events from automation. + lazyColumn.setGestureMargin(device.displayWidth / 5) + for (i in 1..3) { + lazyColumn.scroll(Direction.DOWN, 2f) + device.waitForIdle() + } + } + } + + companion object { + private const val RESOURCE_ID = "recycler" + } +} diff --git a/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/NonExportedActivityBenchmark.kt b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/NonExportedActivityBenchmark.kt new file mode 100644 index 000000000..6a859b3d8 --- /dev/null +++ b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/NonExportedActivityBenchmark.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2021 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 + * + * http://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 io.github.droidkaigi.feeder.macrobenchmark + +import android.content.Intent +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.FrameTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Direction +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit + +@LargeTest +@SdkSuppress(minSdkVersion = 29) +@RunWith(AndroidJUnit4::class) +class NonExportedActivityBenchmark { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun scroll() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val device = UiDevice.getInstance(instrumentation) + benchmarkRule.measureRepeated( + packageName = PACKAGE_NAME, + metrics = listOf(FrameTimingMetric()), + // Try switching to different compilation modes to see the effect + // it has on frame timing metrics. + compilationMode = CompilationMode.None, + iterations = 3, + setupBlock = { + // Before starting to measure, navigate to the UI to be measured + val intent = Intent() + intent.action = ACTION + // Ensures that a new activity is created every single time + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivityAndWait(intent) + // click a button to launch the target activity. + // While we use resourceId here to find the button, you could also use + // accessibility info or button text content. + val launchRecyclerActivity = device.findObject( + By.res(PACKAGE_NAME, LAUNCH_RESOURCE_ID) + ) + launchRecyclerActivity.click() + device.waitUntilActivity(RECYCLER_ACTIVITY_CLASS) + } + ) { + val recycler = device.findObject( + By.res( + PACKAGE_NAME, + RECYCLER_RESOURCE_ID + ) + ) + // Set gesture margin to avoid triggering gesture navigation + // with input events from automation. + recycler.setGestureMargin(device.displayWidth / 5) + for (i in 1..10) { + recycler.scroll(Direction.DOWN, 2f) + device.waitForIdle() + } + } + } + + companion object { + private const val PACKAGE_NAME = "io.github.droidkaigi.feeder.macrobenchmark.target" + private const val RECYCLER_ACTIVITY_CLASS = "$PACKAGE_NAME.NonExportedRecyclerActivity" + private const val ACTION = "$PACKAGE_NAME.ACTIVITY_LAUNCHER_ACTIVITY" + private const val LAUNCH_RESOURCE_ID = "launchRecyclerActivity" + private const val RECYCLER_RESOURCE_ID = "recycler" + + /** + * Waits until an [android.app.Activity] with the given `className` is visible. + */ + fun UiDevice.waitUntilActivity(className: String) { + wait( + Until.hasObject( + By.clazz(className) + ), + TimeUnit.SECONDS.toMillis(10) + ) + } + } +} diff --git a/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/SmallListStartupBenchmark.kt b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/SmallListStartupBenchmark.kt new file mode 100644 index 000000000..8f5b358ba --- /dev/null +++ b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/SmallListStartupBenchmark.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2021 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 + * + * http://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 io.github.droidkaigi.feeder.macrobenchmark + +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.filters.LargeTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@LargeTest +@RunWith(Parameterized::class) +class SmallListStartupBenchmark(private val startupMode: StartupMode) { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startup() = benchmarkRule.measureStartup( + profileCompiled = true, + startupMode = startupMode + ) { + setClassName("io.github.droidkaigi.feeder.debug","io.github.droidkaigi.feeder.MainActivity") + } + + companion object { + @Parameterized.Parameters(name = "mode={0}") + @JvmStatic + fun parameters(): List> { + return listOf(StartupMode.COLD, StartupMode.WARM) + .map { arrayOf(it) } + } + } +} diff --git a/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/StartupUtils.kt b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/StartupUtils.kt new file mode 100644 index 000000000..04595e5fa --- /dev/null +++ b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/StartupUtils.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2021 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 + * + * http://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 io.github.droidkaigi.feeder.macrobenchmark + +import android.content.Intent +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule + +const val TARGET_PACKAGE = "io.github.droidkaigi.feeder.debug" + +fun MacrobenchmarkRule.measureStartup( + profileCompiled: Boolean, + startupMode: StartupMode, + iterations: Int = 3, + setupIntent: Intent.() -> Unit = {} +) = measureRepeated( + packageName = TARGET_PACKAGE, + metrics = listOf(StartupTimingMetric()), + compilationMode = if (profileCompiled) { + CompilationMode.SpeedProfile(warmupIterations = 3) + } else { + CompilationMode.None + }, + iterations = iterations, + startupMode = startupMode +) { + pressHome() + val intent = Intent() + intent.setPackage(TARGET_PACKAGE) + setupIntent(intent) + startActivityAndWait(intent) +} diff --git a/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/TrivialStartupBenchmark.kt b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/TrivialStartupBenchmark.kt new file mode 100644 index 000000000..3c2938fe4 --- /dev/null +++ b/android-macrobenchmark/src/main/java/io/github/droidkaigi/feeder/macrobenchmark/TrivialStartupBenchmark.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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 + * + * http://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 io.github.droidkaigi.feeder.macrobenchmark + +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.filters.LargeTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@LargeTest +@RunWith(Parameterized::class) +class TrivialStartupBenchmark(private val startupMode: StartupMode) { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startup() = benchmarkRule.measureStartup( + profileCompiled = false, + startupMode = startupMode, + iterations = 3 + ) { + setClassName("io.github.droidkaigi.feeder.debug","io.github.droidkaigi.feeder.MainActivity") + } + + companion object { + @Parameterized.Parameters(name = "mode={0}") + @JvmStatic + fun parameters(): List> { + return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT) + .map { arrayOf(it) } + } + } +} diff --git a/android/build.gradle b/android/build.gradle index 3bac1cf80..cfea502e1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,6 +44,16 @@ android { mappingFileUploadEnabled false } } + performance { + matchingFallbacks = ['release'] + debuggable false + // for using Firebase debug config + applicationIdSuffix ".debug" + versionNameSuffix "-debug" + signingConfig signingConfigs.debug + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 3bef50cd2..3529295c6 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -76,6 +76,8 @@ + + + - diff --git a/android/src/performance/google-services.json b/android/src/performance/google-services.json new file mode 120000 index 000000000..b72f3da10 --- /dev/null +++ b/android/src/performance/google-services.json @@ -0,0 +1 @@ +../debug/google-services.json \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 26e0d254b..3cf32a6b7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,7 @@ include ':uicomponent-compose:main' include ':uicomponent-compose:feed' include ':uicomponent-compose:other' include ':uicomponent-compose:core' +include ':android-macrobenchmark' include ':data:repository' include ':data:api' include ':data:db'