Add complex nested lists benchmark
Each item contains a constraint layout with an AsyncImage from coil
Test: run it
Change-Id: I6cfb7ecee9f5c22f4794875606c7e51c4002f3b2
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index f3bc589..edd93da78 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -47,6 +47,8 @@
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-tooling"))
implementation(project(":profileinstaller:profileinstaller"))
+ implementation(project(":constraintlayout:constraintlayout-compose"))
+ implementation("io.coil-kt:coil-compose:2.5.0")
}
android.defaultConfig.minSdkVersion 21
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index d54a8f1..d56e2a2 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -17,6 +17,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
+ <uses-permission android:name="android.permission.INTERNET"/>
+
<application
android:label="Jetpack Compose Macrobenchmark Target"
android:allowBackup="false"
@@ -109,6 +111,18 @@
</intent-filter>
</activity>
<activity
+ android:name=".ComplexNestedListsActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.COMPLEX_NESTED_LISTS_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity
android:name=".LazyBoxWithConstraintsActivity"
android:exported="true">
<intent-filter>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ComplexNestedListsActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ComplexNestedListsActivity.kt
new file mode 100644
index 0000000..cab2d3d
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ComplexNestedListsActivity.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2024 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 androidx.compose.integration.macrobenchmark.target
+
+import android.os.Bundle
+import android.view.Choreographer
+import android.view.View
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountBox
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Recomposer
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import coil.compose.AsyncImage
+
+class ComplexNestedListsActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MaterialTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colors.background
+ ) {
+ Greeting()
+ }
+ }
+ }
+
+ launchIdlenessTracking()
+ }
+
+ internal fun ComponentActivity.launchIdlenessTracking() {
+ val contentView: View = findViewById(android.R.id.content)
+ val callback: Choreographer.FrameCallback = object : Choreographer.FrameCallback {
+ override fun doFrame(frameTimeNanos: Long) {
+ if (Recomposer.runningRecomposers.value.any { it.hasPendingWork }) {
+ contentView.contentDescription = "COMPOSE-BUSY"
+ } else {
+ contentView.contentDescription = "COMPOSE-IDLE"
+ }
+ Choreographer.getInstance().postFrameCallback(this)
+ }
+ }
+ Choreographer.getInstance().postFrameCallback(callback)
+ }
+}
+
+@Composable
+private fun Greeting() {
+ LazyColumn(Modifier.semantics { contentDescription = "IamLazy" }) {
+ items(1000) {
+ LazyRow {
+ items(10) {
+ Video(
+ modifier = Modifier
+ .width(200.dp)
+ .height(120.dp)
+ .padding(16.dp)
+ )
+ }
+ }
+ Box(
+ modifier = Modifier
+ .height(1.dp)
+ .fillMaxWidth()
+ .background(Color.Black)
+ )
+ }
+ }
+}
+
+@Composable
+private fun Video(
+ modifier: Modifier = Modifier,
+ imageRes: Int = R.drawable.simple_image,
+ duration: String = "100",
+ onVideoClick: () -> Unit = {},
+ shimmerModifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = onVideoClick
+ )
+ ) {
+ VideoImageBox(
+ modifier = Modifier.then(shimmerModifier),
+ imageRes = imageRes,
+ duration = duration
+ )
+ }
+}
+
+@Composable
+private fun VideoImageBox(
+ modifier: Modifier,
+ imageRes: Int,
+ duration: String,
+) {
+ Card(
+ modifier = modifier
+ .aspectRatio(16f / 9)
+ .shadow(
+ elevation = 12.dp,
+ spotColor = Color.Gray,
+ shape = RoundedCornerShape(size = 12.dp)
+ )
+ ) {
+ ConstraintLayout(Modifier.fillMaxSize()) {
+ val (image, durationBox) = createRefs()
+
+ AsyncImage(
+ modifier = Modifier.constrainAs(image) {
+ top.linkTo(parent.top)
+ bottom.linkTo(parent.bottom)
+ start.linkTo(parent.start)
+ end.linkTo(parent.end)
+ width = Dimension.fillToConstraints
+ height = Dimension.fillToConstraints
+ },
+ model = imageRes,
+ contentDescription = null,
+ contentScale = ContentScale.Crop
+ )
+
+ Row(
+ modifier = Modifier.constrainAs(durationBox) {
+ bottom.linkTo(parent.bottom)
+ end.linkTo(parent.end)
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = duration,
+ color = Color.White,
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ Icon(
+ modifier = Modifier
+ .size(12.dp)
+ .padding(2.dp),
+ imageVector = Icons.Default.AccountBox,
+ contentDescription = null,
+ tint = Color.White
+ )
+ }
+ }
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/simple_image.jpg b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/simple_image.jpg
new file mode 100644
index 0000000..d2233a7
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/simple_image.jpg
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
new file mode 100644
index 0000000..003b307
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 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 androidx.compose.integration.macrobenchmark
+
+import android.content.Intent
+import android.graphics.Point
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class ComplexNestedListsScrollBenchmark {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ private lateinit var device: UiDevice
+
+ @Before
+ fun setUp() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = UiDevice.getInstance(instrumentation)
+ }
+
+ @Test
+ fun start() {
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(FrameTimingMetric()),
+ compilationMode = CompilationMode.Full(),
+ iterations = 8,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = ACTION
+ startActivityAndWait(intent)
+ }
+ ) {
+ val lazyColumn = device.findObject(By.desc(CONTENT_DESCRIPTION))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ lazyColumn.setGestureMargin(device.displayWidth / 5)
+ for (i in 1..10) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ lazyColumn.drag(Point(lazyColumn.visibleCenter.x, lazyColumn.visibleCenter.y / 3))
+ device.wait(Until.findObject(By.desc(COMPOSE_IDLE)), 3000)
+ }
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
+ private const val ACTION =
+ "androidx.compose.integration.macrobenchmark.target.COMPLEX_NESTED_LISTS_ACTIVITY"
+ private const val CONTENT_DESCRIPTION = "IamLazy"
+
+ private const val COMPOSE_IDLE = "COMPOSE-IDLE"
+ }
+}
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 35cbf8c..7c4300e 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -7500,6 +7500,35 @@
=SIQO
-----END PGP PUBLIC KEY BLOCK-----
+pub 4D176DC503FB4267
+uid Colin White <[email protected]>
+
+sub 5686B45C142551D3
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBF1EtnUBCAChtyYd/4eMAxTz5DVmO+8QOrTA1cf9bprQhtXD5pVbw8/IGKN+
+EqXmvt7AGy+4O633g7ec5iyirwCfEP+4YDv8k1LOvY9C5+tOwfK+FxAPRVc1AAB5
+q23x4yMI7aDdvN52/jqpREeBBWPcrnEIET68RApayALenjUP6Kx5K0ge9aU8uaSB
+HsVpHSr05L1nKuuqh0LDNXIYDLCpo0LnwPNfLrJ2xT6gNTwHBY5mncDy4m01rdxD
+3to/s2Uu/9Xkfz3+BJKRD2kr5txiqoW69H/qsg7u8tTOr9FhB1T21pjFZsdpzbOq
+cKd7UyD0RpyQYHmTeUIhKSI0Su7pKT+RTL9BABEBAAG0IUNvbGluIFdoaXRlIDxj
+b2xpbkBjb2xpbndoaXRlLm1lPrkBDQRdRLZ1AQgAxOP3klOByUo1Kyl1O6rZqj9e
+yts/oXtuDTISfcRkZyg6fmkhT4kpd4xLSP3xHvRwJugybyTedDHzXXCOSjl3EFfU
+GaKJuMqSKs5YjQOWk8S9BegAPeiq6PGV6gbHZQ3Xqy+XE+TLy5N96zu7th/YJraE
+NpS79sj//mJQE2d49YrxhZwtMj64X0B8/mDmED+D2cPXAoLxNh//LaV26gpOC9yV
+YTXrCq7ODPE5LyoljhBmPZxoapcn/39V6UvVSG8Dq6R5QdahDgNnCEjVPtSzGtbC
+B4zpSv/LvZkm8gKlaxUcra5clXL7p4NWGx1C1Ap4+H6U2h53lC7FuYl8k9mBNwAR
+AQABiQE2BBgBCAAgAhsMFiEE3yXTxIaP7BdxgvrWTRdtxQP7QmcFAl1P4JcACgkQ
+TRdtxQP7QmcrkAgAiSZ2VRgHTY8SV4dXXjYBO6WM2Yzamt4bO5Uqflw7gPO44AtJ
+h+Pn89iz37RnGL+bAq2MCNsgV4G+KaLJrE0c/rt/szC7UmwEJmy4DcH5B80wzBvo
+2kpN1Uyr7+1+IgHogL6HjLxccoBpawpm0AnhNYrd2ml7STBCfNmilztWGrTkerb/
+2qOY7PmgWEad7whM0OEj1OE+M2IYZUga3gxf0B94GtQNwmI4xv5qXGskTFAlM9ko
+94NfvOxEkgCGtnicN4Md74od0POEVrZxBpXuJGt60XVGvOr1iLmlpypWo+PkHhZe
+Wt45mNAFy6vraTYYCkGXQKqx1kxg+OurYgIFgw==
+=zwEJ
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 4DB7BC57DFDBCEA4
uid Timothy Wall <[email protected]>
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 2ae8411..a239024 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -431,6 +431,7 @@
<trusting group="com.beust"/>
<trusting group="org.testng"/>
</trusted-key>
+ <trusted-key id="DF25D3C4868FEC177182FAD64D176DC503FB4267" group="io.coil-kt"/>
<trusted-key id="DF3986523A6AD079C46B730BCA183FBA1E476C6E" group="com.squareup.leakcanary"/>
<trusted-key id="E01ED293981AE484403B65D7DA70BCBA6D76AD03" group="com.charleskorn.kaml"/>
<trusted-key id="E0D98C5FD55A8AF232290E58DEE12B9896F97E34" group="org.pcollections"/>