Merge pull request #158 from android/tm/perf-improve-benchmarks

Inspect and Improve Performance with Baseline Profiles
pull/171/head
Jolanda Verhoef 2 years ago committed by GitHub
commit 1733939841
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -117,7 +117,7 @@ jobs:
disable-animations: true disable-animations: true
disk-size: 1500M disk-size: 1500M
heap-size: 512M heap-size: 512M
script: ./gradlew connectedProdDebugAndroidTest -x :benchmark:connectedBenchmarkAndroidTest script: ./gradlew connectedProdDebugAndroidTest -x :benchmark:connectedProdBenchmarkAndroidTest
- name: Upload test reports - name: Upload test reports
if: always() if: always()

@ -62,8 +62,6 @@ android {
proguardFiles("benchmark-rules.pro") proguardFiles("benchmark-rules.pro")
// FIXME enabling minification breaks access to demo backend. // FIXME enabling minification breaks access to demo backend.
isMinifyEnabled = false isMinifyEnabled = false
// Keep the build type debuggable so we can attach a debugger if needed.
isDebuggable = true
applicationIdSuffix = ".benchmark" applicationIdSuffix = ".benchmark"
} }
} }

File diff suppressed because it is too large Load Diff

@ -39,10 +39,13 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
@ -60,7 +63,11 @@ import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation
import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_DESTINATIONS import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_DESTINATIONS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(
ExperimentalMaterial3Api::class,
ExperimentalLayoutApi::class,
ExperimentalComposeUiApi::class
)
@Composable @Composable
fun NiaApp(windowSizeClass: WindowSizeClass) { fun NiaApp(windowSizeClass: WindowSizeClass) {
NiaTheme { NiaTheme {
@ -74,7 +81,9 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
NiaBackground { NiaBackground {
Scaffold( Scaffold(
modifier = Modifier, modifier = Modifier.semantics {
testTagsAsResourceId = true
},
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
bottomBar = { bottomBar = {

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import com.google.samples.apps.nowinandroid.Flavor
import com.google.samples.apps.nowinandroid.FlavorDimension
import com.google.samples.apps.nowinandroid.configureFlavors
plugins { plugins {
id("nowinandroid.android.test") id("nowinandroid.android.test")
@ -25,7 +28,6 @@ android {
defaultConfig { defaultConfig {
minSdk = 23 minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy("contentType", "demo")
} }
buildTypes { buildTypes {
@ -33,12 +35,18 @@ android {
// release build (for example, with minification on). It's signed with a debug key // release build (for example, with minification on). It's signed with a debug key
// for easy local/CI testing. // for easy local/CI testing.
val benchmark by creating { val benchmark by creating {
isDebuggable = false // Keep the build type debuggable so we can attach a debugger if needed.
isDebuggable = true
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release") matchingFallbacks.add("release")
} }
} }
// Use the same flavor dimensions as the application to allow generating Baseline Profiles on prod,
// which is more close to what will be shipped to users (no fake data), but has ability to run the
// benchmarks on demo, so we benchmark on stable data.
configureFlavors(this)
targetProjectPath = ":app" targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true experimentalProperties["android.experimental.self-instrumenting"] = true
} }

@ -0,0 +1,25 @@
/*
* Copyright 2022 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
*
* https://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 com.google.samples.apps.nowinandroid
import com.google.samples.apps.nowinandroid.benchmark.BuildConfig
/**
* Convenience parameter to use proper package name with regards to build type and build flavor.
*/
const val PACKAGE_NAME =
"com.google.samples.apps.nowinandroid.${BuildConfig.FLAVOR}.${BuildConfig.BUILD_TYPE}"

@ -19,7 +19,12 @@ package com.google.samples.apps.nowinandroid.baselineprofile
import androidx.benchmark.macro.ExperimentalBaselineProfilesApi import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
import androidx.benchmark.macro.junit4.BaselineProfileRule import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.foryou.forYouScrollFeedDownUp
import com.google.samples.apps.nowinandroid.foryou.forYouSelectAuthors
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
import com.google.samples.apps.nowinandroid.interests.interestsScrollPeopleDownUp
import com.google.samples.apps.nowinandroid.interests.interestsScrollTopicsDownUp
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -31,27 +36,30 @@ class BaselineProfileGenerator {
@get:Rule val baselineProfileRule = BaselineProfileRule() @get:Rule val baselineProfileRule = BaselineProfileRule()
@Test @Test
fun startup() = fun generate() =
baselineProfileRule.collectBaselineProfile( baselineProfileRule.collectBaselineProfile(PACKAGE_NAME) {
packageName = "com.google.samples.apps.nowinandroid.demo.benchmark"
) {
pressHome()
// This block defines the app's critical user journey. Here we are interested in // 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 // optimizing for app startup. But you can also navigate and scroll
// through your most important UI. // through your most important UI.
pressHome()
startActivityAndWait() startActivityAndWait()
// Scroll the feed critical user journey
forYouWaitForContent()
forYouSelectAuthors()
forYouScrollFeedDownUp()
// Navigate to interests screen
device.findObject(By.text("Interests")).click()
device.waitForIdle() device.waitForIdle()
device.run { interestsScrollTopicsDownUp()
findObject(By.text("Interests"))
.click() // Navigate to people tab
waitForIdle() device.findObject(By.text("People")).click()
findObject(By.text("Accessibility")).scroll(Direction.DOWN, 2000f) device.waitForIdle()
waitForIdle()
findObject(By.text("People")).click() interestsScrollPeopleDownUp()
waitForIdle()
findObject(By.textStartsWith("Android")).scroll(Direction.DOWN, 2000f)
waitForIdle()
}
} }
} }

@ -0,0 +1,44 @@
/*
* Copyright 2022 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
*
* https://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 com.google.samples.apps.nowinandroid.foryou
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
fun MacrobenchmarkScope.forYouWaitForContent() {
// Wait until content is loaded
device.wait(Until.hasObject(By.text("What are you interested in?")), 30_000)
}
fun MacrobenchmarkScope.forYouSelectAuthors() {
val authors = device.findObject(By.res("forYou:authors"))
// select some authors to show some feed content
repeat(3) { index ->
val author = authors.children[index % authors.childCount]
author.click()
device.waitForIdle()
}
}
fun MacrobenchmarkScope.forYouScrollFeedDownUp() {
val feedList = device.findObject(By.res("forYou:feed"))
feedList.fling(Direction.DOWN)
device.waitForIdle()
feedList.fling(Direction.UP)
}

@ -0,0 +1,56 @@
/*
* Copyright 2022 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
*
* https://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 com.google.samples.apps.nowinandroid.foryou
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4ClassRunner::class)
class ScrollForYouFeedBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun scrollFeedCompilationNone() = scrollFeed(CompilationMode.None())
@Test
fun scrollFeedCompilationBaselineProfile() = scrollFeed(CompilationMode.Partial())
private fun scrollFeed(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric()),
compilationMode = compilationMode,
iterations = 10,
startupMode = StartupMode.COLD,
setupBlock = {
// Start the app
pressHome()
startActivityAndWait()
}
) {
forYouWaitForContent()
forYouSelectAuthors()
forYouScrollFeedDownUp()
}
}

@ -0,0 +1,35 @@
/*
* Copyright 2022 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
*
* https://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 com.google.samples.apps.nowinandroid.interests
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
fun MacrobenchmarkScope.interestsScrollTopicsDownUp() {
val topicsList = device.findObject(By.res("interests:topics"))
topicsList.fling(Direction.DOWN)
device.waitForIdle()
topicsList.fling(Direction.UP)
}
fun MacrobenchmarkScope.interestsScrollPeopleDownUp() {
val peopleList = device.findObject(By.res("interests:people"))
peopleList.fling(Direction.DOWN)
device.waitForIdle()
peopleList.fling(Direction.UP)
}

@ -26,6 +26,8 @@ import androidx.benchmark.macro.StartupMode.WARM
import androidx.benchmark.macro.StartupTimingMetric import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -74,7 +76,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
fun startupFullCompilation() = startup(CompilationMode.Full()) fun startupFullCompilation() = startup(CompilationMode.Full())
private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = "com.google.samples.apps.nowinandroid", packageName = PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()), metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode, compilationMode = compilationMode,
iterations = 10, iterations = 10,
@ -84,5 +86,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
} }
) { ) {
startActivityAndWait() startActivityAndWait()
// Waits until the content is ready to capture Time To Full Display
forYouWaitForContent()
} }
} }

@ -43,6 +43,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@ -65,7 +66,7 @@ fun AuthorsCarousel(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyRow( LazyRow(
modifier = modifier, modifier = modifier.testTag("forYou:authors"),
contentPadding = PaddingValues(24.dp), contentPadding = PaddingValues(24.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp) horizontalArrangement = Arrangement.spacedBy(24.dp)
) { ) {

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.foryou package com.google.samples.apps.nowinandroid.feature.foryou
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.annotation.IntRange import androidx.annotation.IntRange
@ -59,6 +60,7 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz
import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -66,6 +68,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -74,7 +78,9 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.trace
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnPreDraw
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton
@ -161,8 +167,31 @@ fun ForYouScreen(
else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1) else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1)
} }
// Workaround to call Activity.reportFullyDrawn from Jetpack Compose.
// This code should be called when the UI is ready for use
// and relates to Time To Full Display.
val interestsLoaded =
interestsSelectionState !is ForYouInterestsSelectionUiState.Loading
val feedLoaded = feedState !is ForYouFeedUiState.Loading
if (interestsLoaded && feedLoaded) {
val localView = LocalView.current
// We use Unit to call reportFullyDrawn only on the first recomposition,
// however it will be called again if this composable goes out of scope.
// Activity.reportFullyDrawn() has its own check for this
// and is safe to call multiple times though.
LaunchedEffect(Unit) {
// We're leveraging the fact, that the current view is directly set as content of Activity.
val activity = localView.context as? Activity ?: return@LaunchedEffect
// To be sure not to call in the middle of a frame draw.
localView.doOnPreDraw { activity.reportFullyDrawn() }
}
}
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.testTag("forYou:feed"),
) { ) {
InterestsSelection( InterestsSelection(
interestsSelectionState = interestsSelectionState, interestsSelectionState = interestsSelectionState,
@ -213,7 +242,8 @@ private fun LazyListScope.InterestsSelection(
NiaLoadingWheel( NiaLoadingWheel(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentSize(), .wrapContentSize()
.testTag("forYou:loading"),
contentDesc = stringResource(id = R.string.for_you_loading), contentDesc = stringResource(id = R.string.for_you_loading),
) )
} }
@ -285,7 +315,7 @@ private fun TopicSelection(
interestsSelectionState: ForYouInterestsSelectionUiState.WithInterestsSelection, interestsSelectionState: ForYouInterestsSelectionUiState.WithInterestsSelection,
onTopicCheckedChanged: (String, Boolean) -> Unit, onTopicCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) = trace("TopicSelection") {
LazyHorizontalGrid( LazyHorizontalGrid(
rows = GridCells.Fixed(3), rows = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
@ -324,7 +354,7 @@ private fun SingleTopicButton(
imageUrl: String, imageUrl: String,
isSelected: Boolean, isSelected: Boolean,
onClick: (String, Boolean) -> Unit onClick: (String, Boolean) -> Unit
) { ) = trace("SingleTopicButton") {
Surface( Surface(
modifier = Modifier modifier = Modifier
.width(312.dp) .width(312.dp)

@ -27,6 +27,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
@ -39,7 +40,9 @@ fun TopicsTabContent(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn( LazyColumn(
modifier = modifier.padding(horizontal = 16.dp), modifier = modifier
.padding(horizontal = 16.dp)
.testTag("interests:topics"),
contentPadding = PaddingValues(top = 8.dp) contentPadding = PaddingValues(top = 8.dp)
) { ) {
topics.forEach { followableTopic -> topics.forEach { followableTopic ->
@ -69,7 +72,9 @@ fun AuthorsTabContent(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn( LazyColumn(
modifier = modifier.padding(horizontal = 16.dp), modifier = modifier
.padding(horizontal = 16.dp)
.testTag("interests:people"),
contentPadding = PaddingValues(top = 8.dp) contentPadding = PaddingValues(top = 8.dp)
) { ) {
authors.forEach { followableAuthor -> authors.forEach { followableAuthor ->

Loading…
Cancel
Save