Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup baseline profile for Orbit library and the catalog app #525

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .idea/runConfigurations/Generate_Baseline_Profile.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions baselineprofile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
63 changes: 63 additions & 0 deletions baselineprofile/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@file:Suppress("UnstableApiUsage")

import com.android.build.api.dsl.ManagedVirtualDevice

plugins {
kotlin("android")
id("com.android.test")
id("androidx.baselineprofile")
id("org.jmailen.kotlinter")
}

android {
namespace = "kiwi.orbit.compose.baselineprofile"
compileSdk = libs.versions.compileSdk.get().toInt()

defaultConfig {
minSdk = 28 // required by the BaselineProfileRule
targetSdk = libs.versions.targetSdk.get().toInt()

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

lint {
abortOnError = true
warningsAsErrors = true
}

targetProjectPath = ":catalog"

testOptions {
animationsDisabled = true

managedDevices.devices {
create<ManagedVirtualDevice>("pixel6Api33") {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "google"
}
}
}
}

kotlinter {
reporters = arrayOf("json")
}

baselineProfile {
managedDevices += "pixel6Api33"
useConnectedDevices = false
}

dependencies {
implementation(projects.catalog.semantics)

implementation(libs.androidx.benchmark.macro)
implementation(libs.androidx.test.runner)
implementation(libs.androidx.test.uiAutomator)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package kiwi.orbit.baselineprofile

import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.filters.LargeTest
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import kiwi.orbit.compose.catalog.semantics.AlertScreenSemantics
import kiwi.orbit.compose.catalog.semantics.ButtonScreenSemantics
import kiwi.orbit.compose.catalog.semantics.DialogScreenSemantics
import kiwi.orbit.compose.catalog.semantics.MainScreenSemantics
import kiwi.orbit.compose.catalog.semantics.PillButtonScreenSemantics
import kiwi.orbit.compose.catalog.semantics.SelectFieldScreenSemantics
import kiwi.orbit.compose.catalog.semantics.SubScreenSemantics
import kiwi.orbit.compose.catalog.semantics.ToastScreenSemantics
import kiwi.orbit.compose.catalog.semantics.TopAppBarScreenSemantics
import org.junit.Rule
import org.junit.Test

/**
* 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,
* or directly with `generateBaselineProfile` Gradle task:
* ```
* ./gradlew :catalog:generateReleaseBaselineProfile -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile
* ```
* 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.
**/
@LargeTest
internal class BaselineProfileGenerator {

private companion object {
const val TIMEOUT = 5000L
}

@get:Rule
val rule = BaselineProfileRule()

@Test
fun generate() {
rule.collect(
packageName = "kiwi.orbit.compose.catalog",
maxIterations = 5,
stableIterations = 2,
) {
pressHome()
startActivityAndWait()

profileSubScreen(MainScreenSemantics.ColorsItemTag)
profileSubScreen(MainScreenSemantics.IconsItemTag)
profileSubScreen(MainScreenSemantics.IllustrationsItemTag)
profileSubScreen(MainScreenSemantics.TypographyItemTag)

profileSubScreen(MainScreenSemantics.AlertItemTag) {
device.findObject(By.res(AlertScreenSemantics.NormalTabTag)).click()
device.findObject(By.res(AlertScreenSemantics.SuppressedTabTag)).click()
device.findObject(By.res(AlertScreenSemantics.InlineTabTag)).click()
}
profileSubScreen(MainScreenSemantics.BadgeItemTag)
profileSubScreen(MainScreenSemantics.BadgeListItemTag)
profileSubScreen(MainScreenSemantics.ButtonItemTag) {
device.findObject(By.res(ButtonScreenSemantics.ButtonTabTag)).click()
device.findObject(By.res(ButtonScreenSemantics.ButtonLinkTabTag)).click()
}
profileSubScreen(MainScreenSemantics.CardItemTag)
profileSubScreen(MainScreenSemantics.CheckboxItemTag)
profileSubScreen(MainScreenSemantics.ChoiceTileItemTag)
profileSubScreen(MainScreenSemantics.CollapseItemTag)
profileSubScreen(MainScreenSemantics.DialogItemTag) {
device.findObject(By.res(DialogScreenSemantics.OrbitDialogButtonTag))
.clickAndWait(Until.newWindow(), TIMEOUT)
device.pressBack()
device.findObject(By.res(DialogScreenSemantics.M3DialogButtonTag))
.clickAndWait(Until.newWindow(), TIMEOUT)
device.pressBack()
device.findObject(By.res(DialogScreenSemantics.M3TimePickerButtonTag))
.clickAndWait(Until.newWindow(), TIMEOUT)
device.pressBack()
device.findObject(By.res(DialogScreenSemantics.M3DatePickerButtonTag))
.clickAndWait(Until.newWindow(), TIMEOUT)
device.pressBack()
}
profileSubScreen(MainScreenSemantics.EmptyStateItemTag)
profileSubScreen(MainScreenSemantics.KeyValueItemTag)
profileSubScreen(MainScreenSemantics.ListItemTag)
profileSubScreen(MainScreenSemantics.ListChoiceItemTag)
profileSubScreen(MainScreenSemantics.LoadingItemTag)
profileSubScreen(MainScreenSemantics.PillButtonItemTag) {
device.findObject(By.res(PillButtonScreenSemantics.ShowWithIconButtonTag)).click()
}
profileSubScreen(MainScreenSemantics.ProgressIndicatorItemTag)
profileSubScreen(MainScreenSemantics.RadioItemTag)
profileSubScreen(MainScreenSemantics.SeatItemTag)
profileSubScreen(MainScreenSemantics.SegmentedSwitchItemTag)
profileSubScreen(MainScreenSemantics.SelectFieldItemTag) {
device.findObject(By.res(SelectFieldScreenSemantics.CountrySelectFieldTag)).click() // open
device.findObject(By.res(SelectFieldScreenSemantics.CountrySelectFieldTag)).click() // close
Thread.sleep(1000L) // back navigation is blocked until the popup is fully closed
}
profileSubScreen(MainScreenSemantics.SliderItemTag)
profileSubScreen(MainScreenSemantics.StepperItemTag)
profileSubScreen(MainScreenSemantics.SurfaceCardItemTag)
profileSubScreen(MainScreenSemantics.SwitchItemTag)
profileSubScreen(MainScreenSemantics.TabsItemTag)
profileSubScreen(MainScreenSemantics.TagItemTag)
profileSubScreen(MainScreenSemantics.TextFieldItemTag)
profileSubScreen(MainScreenSemantics.TileItemTag)
profileSubScreen(MainScreenSemantics.TileGroupItemTag)
profileSubScreen(MainScreenSemantics.TimelineItemTag)
profileSubScreen(MainScreenSemantics.ToastItemTag) {
device.findObject(By.res(ToastScreenSemantics.ToastSignedInButtonTag)).click()
}
profileSubScreen(MainScreenSemantics.TopAppBarItemTag) {
device.findObject(By.res(TopAppBarScreenSemantics.NormalSimpleButtonTag)).click()
device.wait(Until.hasObject(By.res(TopAppBarScreenSemantics.NormalSimpleScreenTag)), TIMEOUT)
device.pressBack()
device.wait(Until.hasObject(By.res(SubScreenSemantics.Tag)), TIMEOUT)
device.findObject(By.res(TopAppBarScreenSemantics.LargeSimpleButtonTag)).click()
device.wait(Until.hasObject(By.res(TopAppBarScreenSemantics.LargeSimpleScreenTag)), TIMEOUT)
device.pressBack()
}
}
}

private fun MacrobenchmarkScope.profileSubScreen(
mainScreenItemTag: String,
profileContent: MacrobenchmarkScope.() -> Unit = {},
) {
val mainScreenScrollable = UiScrollable(UiSelector().scrollable(true))
mainScreenScrollable.scrollIntoView(UiSelector().resourceId(mainScreenItemTag))
device.wait(Until.hasObject(By.res(mainScreenItemTag)), TIMEOUT)

device.findObject(By.res(mainScreenItemTag)).click()
device.wait(Until.hasObject(By.res(SubScreenSemantics.Tag)), TIMEOUT)

profileContent()

device.pressBack()
device.wait(Until.hasObject(By.res(MainScreenSemantics.Tag)), TIMEOUT)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package kiwi.orbit.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.filters.LargeTest
import org.junit.Rule
import org.junit.Test

/**
* 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 with this Gradle task:
* ```
* ./gradlew :baselineprofile:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=Macrobenchmark
* ```
*
* 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).
**/
@LargeTest
internal 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) {
rule.measureRepeated(
packageName = "kiwi.orbit.compose.catalog",
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = 10,
setupBlock = { pressHome() },
) {
startActivityAndWait()
}
}
}
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ plugins {
kotlin("multiplatform") version "1.9.10" apply false
kotlin("plugin.serialization") version "1.9.10" apply false
id("com.android.library") version "8.1.1" apply false
id("com.android.test") version "8.1.1" apply false
id("androidx.baselineprofile") version "1.2.0-beta01" apply false
id("app.cash.paparazzi") version "1.3.1" apply false
id("com.google.firebase.appdistribution") version "4.0.0" apply false
id("com.vanniktech.maven.publish.base") version "0.25.3" apply false
Expand Down
6 changes: 5 additions & 1 deletion catalog/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
kotlin("android")
kotlin("plugin.serialization")
id("com.android.application")
id("androidx.baselineprofile")
id("org.jmailen.kotlinter")
id("com.google.firebase.appdistribution")
kotlin("plugin.parcelize")
Expand Down Expand Up @@ -127,6 +128,7 @@ dependencies {
implementation(projects.ui)
implementation(projects.icons)
implementation(projects.illustrations)
implementation(projects.catalog.semantics)

implementation(libs.androidx.core)
implementation(libs.androidx.activityCompose)
Expand All @@ -152,12 +154,14 @@ dependencies {
implementation(libs.accompanist.systemController)
implementation(libs.kiwi.navigationComposeTyped)

baselineProfile(projects.baselineprofile)

coreLibraryDesugaring(libs.android.desugarJdk)

debugImplementation(libs.compose.tooling)
debugImplementation(libs.androidx.customView)
debugImplementation(libs.androidx.customViewPoolingContainer)

lintChecks(libs.slack.composeLintChecks)
lintChecks(project(":lint"))
lintChecks(projects.lint)
}
1 change: 1 addition & 0 deletions catalog/semantics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
28 changes: 28 additions & 0 deletions catalog/semantics/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
kotlin("android")
id("com.android.library")
id("org.jmailen.kotlinter")
}

android {
namespace = "kiwi.orbit.compose.catalog.semantics"
compileSdk = libs.versions.compileSdk.get().toInt()

defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

lint {
abortOnError = true
warningsAsErrors = true
}
}

kotlinter {
reporters = arrayOf("json")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kiwi.orbit.compose.catalog.semantics

object AlertScreenSemantics {
const val NormalTabTag = "alert_screen_normal_tab_tag"
const val SuppressedTabTag = "alert_screen_suppressed_tab_tag"
const val InlineTabTag = "alert_screen_inline_tab_tag"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kiwi.orbit.compose.catalog.semantics

object ButtonScreenSemantics {
const val ButtonTabTag = "button_screen_button_tab_tag"
const val ButtonLinkTabTag = "button_screen_button_link_tab_tag"
}
Loading