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"/>