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'