Add an interop scroll benchmark
A RecyclerView of ComposeViews and a LazyList of views
for comparison. The implemenation also matches with the
plain RecyclerView and LazyList benchmarks for additional
comparison.
Test: RecyclerViewListScrollBenchmark, AndroidViewListScrollBenchmark
Change-Id: I25ad0df6ce2d18b0a7c1a4610cc6f77012304eb8
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index 167f64b..8849087 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -18,10 +18,13 @@
}
dependencies {
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
kotlinPlugin(project(":compose:compiler:compiler"))
implementation(libs.kotlinStdlib)
implementation(project(":activity:activity-compose"))
+ implementation("androidx.appcompat:appcompat:1.4.1")
+ implementation("androidx.cardview:cardview:1.0.0")
// old version of common-java8 conflicts with newer version, because both have
// DefaultLifecycleEventObserver.
// Outside of androidx this is resolved via constraint added to lifecycle-common,
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index d997cfc..fe02b94 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -111,5 +111,21 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <activity android:name=".AndroidViewListActivity"
+ android:exported="true"
+ android:theme="@style/Theme.AppCompat">
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.ANDROIDVIEW_LIST_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity android:name=".RecyclerViewListActivity"
+ android:exported="true"
+ android:theme="@style/Theme.AppCompat">
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.RECYCLERVIEW_LIST_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/AndroidViewListActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/AndroidViewListActivity.kt
new file mode 100644
index 0000000..7524bc4
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/AndroidViewListActivity.kt
@@ -0,0 +1,72 @@
+/*
+ * 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
+ *
+ * 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.LayoutInflater
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.viewinterop.AndroidView
+
+/**
+ * This activity uses AndroidViews inside a LazyColumn. This helps us benchmark
+ * a common point of migration between views and Compose. The implementation is designed to
+ * match the LazyColumnActivity and the RecyclerViewActivity for comparison.
+ */
+class AndroidViewListActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 3000)
+
+ setContent {
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth().semantics { contentDescription = "IamLazy" }
+ ) {
+ items(List(itemCount) { Entry("Item $it") }) {
+ ListRow(it)
+ }
+ }
+ }
+
+ launchIdlenessTracking()
+ }
+
+ companion object {
+ const val EXTRA_ITEM_COUNT = "ITEM_COUNT"
+ }
+}
+
+@Composable
+private fun ListRow(entry: Entry) {
+ AndroidView(
+ factory = { context ->
+ val layoutInflator = LayoutInflater.from(context)
+ layoutInflator.inflate(R.layout.recycler_row, null, false)
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) { view ->
+ view.findViewById<AppCompatTextView>(R.id.content).text = entry.contents
+ }
+}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/RecyclerViewListActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/RecyclerViewListActivity.kt
new file mode 100644
index 0000000..693b7ac
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/RecyclerViewListActivity.kt
@@ -0,0 +1,118 @@
+/*
+ * 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
+ *
+ * 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.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Card
+import androidx.compose.material.Checkbox
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.unit.dp
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * This activity uses ComposeViews inside a RecyclerView. This helps us benchmark
+ * a common point of migration between views and Compose. The implementation is designed to
+ * match the LazyColumnActivity and the RecyclerViewActivity for comparison.
+ */
+class RecyclerViewListActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_recyclerview)
+ val recycler = findViewById<RecyclerView>(R.id.recycler)
+ val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 3000)
+
+ val adapter = EntryAdapter(entries(itemCount))
+ recycler.layoutManager = LinearLayoutManager(this)
+ recycler.adapter = adapter
+
+ launchIdlenessTracking()
+ }
+
+ private fun entries(size: Int) = List(size) {
+ Entry("Item $it")
+ }
+
+ companion object {
+ const val EXTRA_ITEM_COUNT = "ITEM_COUNT"
+ }
+}
+
+class EntryViewHolder(val composeView: ComposeView) : RecyclerView.ViewHolder(composeView) {
+ init {
+ composeView.setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+ )
+ }
+
+ fun bind(entry: Entry) {
+ composeView.setContent {
+ MaterialTheme {
+ ListRow(entry)
+ }
+ }
+ }
+}
+
+class EntryAdapter(
+ private val entries: List<Entry>,
+) : RecyclerView.Adapter<EntryViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryViewHolder {
+ return EntryViewHolder(ComposeView(parent.context))
+ }
+
+ override fun onViewRecycled(holder: EntryViewHolder) {
+ // Dispose the underlying Composition of the ComposeView
+ // when RecyclerView has recycled this ViewHolder
+ holder.composeView.disposeComposition()
+ }
+
+ override fun onBindViewHolder(holder: EntryViewHolder, position: Int) {
+ val entry = entries[position]
+ holder.bind(entry)
+ }
+
+ override fun getItemCount(): Int = entries.size
+}
+
+@Composable
+private fun ListRow(entry: Entry) {
+ Card(modifier = Modifier.padding(8.dp)) {
+ Row {
+ Text(
+ text = entry.contents,
+ modifier = Modifier.padding(16.dp)
+ )
+ Spacer(modifier = Modifier.weight(1f, fill = true))
+ Checkbox(
+ checked = false,
+ onCheckedChange = {},
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_recyclerview.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_recyclerview.xml
new file mode 100644
index 0000000..8242676
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_recyclerview.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 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.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
new file mode 100644
index 0000000..7e77ae3
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2020 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.
+ -->
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp">
+ <androidx.appcompat.widget.LinearLayoutCompat
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ tools:text="Sample text" />
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp" />
+ </androidx.appcompat.widget.LinearLayoutCompat>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/AndroidViewListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/AndroidViewListScrollBenchmark.kt
new file mode 100644
index 0000000..edc54b5
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/AndroidViewListScrollBenchmark.kt
@@ -0,0 +1,89 @@
+/*
+ * 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
+ *
+ * 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.FrameTimingGfxInfoMetric
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import androidx.testutils.createCompilationParams
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class AndroidViewListScrollBenchmark(
+ private val compilationMode: CompilationMode
+) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ private lateinit var device: UiDevice
+
+ @Before
+ fun setUp() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = UiDevice.getInstance(instrumentation)
+ }
+
+ @Test
+ fun scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 10,
+ 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(0, 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.ANDROIDVIEW_LIST_ACTIVITY"
+ private const val CONTENT_DESCRIPTION = "IamLazy"
+
+ private const val COMPOSE_IDLE = "COMPOSE-IDLE"
+
+ @Parameterized.Parameters(name = "compilation={0}")
+ @JvmStatic
+ fun parameters() = createCompilationParams()
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/RecyclerViewListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/RecyclerViewListScrollBenchmark.kt
new file mode 100644
index 0000000..e4b5857
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/RecyclerViewListScrollBenchmark.kt
@@ -0,0 +1,86 @@
+/*
+ * 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
+ *
+ * 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.FrameTimingGfxInfoMetric
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.createCompilationParams
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class RecyclerViewListScrollBenchmark(
+ private val compilationMode: CompilationMode
+) {
+ @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(), FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 10,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = ACTION
+ startActivityAndWait(intent)
+ }
+ ) {
+ val recycler = device.findObject(By.res(PACKAGE_NAME, RESOURCE_ID))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ recycler.setGestureMargin(device.displayWidth / 5)
+ repeat(10) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ recycler.drag(Point(0, recycler.visibleCenter.y / 3))
+ device.waitForIdle()
+ }
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
+ private const val ACTION =
+ "androidx.compose.integration.macrobenchmark.target.RECYCLERVIEW_LIST_ACTIVITY"
+ private const val RESOURCE_ID = "recycler"
+
+ @Parameterized.Parameters(name = "compilation={0}")
+ @JvmStatic
+ fun parameters() = createCompilationParams()
+ }
+}