Merge "Macro Benchmark for Semantics" into androidx-main am: 8c1fc2090d

Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/3073541

Change-Id: If29f697772dbcdb9681819dc16edf63bc523f091
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index a3da118..a4cf022 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -30,6 +30,7 @@
     implementation 'androidx.viewpager2:viewpager2:1.0.0'
 
     implementation(libs.kotlinStdlib)
+    implementation(libs.material)
     implementation(project(":activity:activity-compose"))
     implementation("androidx.appcompat:appcompat:1.4.1")
     implementation("androidx.cardview:cardview:1.0.0")
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 0eadc27..d249e08 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -20,12 +20,11 @@
     <uses-permission android:name="android.permission.INTERNET"/>
 
     <application
-        android:label="Jetpack Compose Macrobenchmark Target"
         android:allowBackup="false"
-        android:supportsRtl="true"
         android:icon="@mipmap/ic_launcher"
+        android:label="Jetpack Compose Macrobenchmark Target"
+        android:supportsRtl="true"
         tools:ignore="GoogleAppIndexingWarning">
-
         <!-- Profileable to enable macrobenchmark profiling -->
         <profileable android:shell="true"/>
 
@@ -37,8 +36,8 @@
          -->
         <activity
             android:name=".TrivialStartupActivity"
-            android:label="C Trivial"
-            android:exported="true">
+            android:exported="true"
+            android:label="C Trivial">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -50,8 +49,8 @@
         </activity>
         <activity
             android:name=".StaticScrollingContentWithChromeInitialCompositionActivity"
-            android:label="C StaticScrollingWithChrome Init"
-            android:exported="true">
+            android:exported="true"
+            android:label="C StaticScrollingWithChrome Init">
             <intent-filter>
                 <action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -63,8 +62,8 @@
         </activity>
         <activity
             android:name=".TrivialStartupTracingActivity"
-            android:label="C TrivialTracing"
-            android:exported="true">
+            android:exported="true"
+            android:label="C TrivialTracing">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -92,8 +91,8 @@
         </activity>
         <activity
             android:name=".LazyColumnActivity"
-            android:label="C LazyColumn"
-            android:exported="true">
+            android:exported="true"
+            android:label="C LazyColumn">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -105,8 +104,8 @@
         </activity>
         <activity
             android:name=".FrameExperimentActivity"
-            android:label="FrameExp"
-            android:exported="true">
+            android:exported="true"
+            android:label="FrameExp">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -176,7 +175,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
         <activity
             android:name=".ViewPagerActivity"
             android:exported="true"
@@ -186,7 +184,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
         <activity
             android:name=".RecyclerViewAsCarouselActivity"
             android:exported="true"
@@ -196,7 +193,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
         <activity
             android:name=".PagerAsCarouselActivity"
             android:exported="true"
@@ -206,7 +202,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
         <activity
             android:name=".PagerActivity"
             android:exported="true"
@@ -216,7 +211,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
         <activity
             android:name=".TrivialTracingActivity"
             android:exported="true">
@@ -225,7 +219,8 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <activity android:name=".AndroidViewListActivity"
+        <activity
+            android:name=".AndroidViewListActivity"
             android:exported="true"
             android:theme="@style/Theme.AppCompat">
             <intent-filter>
@@ -233,7 +228,8 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <activity android:name=".RecyclerViewListActivity"
+        <activity
+            android:name=".RecyclerViewListActivity"
             android:exported="true"
             android:theme="@style/Theme.AppCompat">
             <intent-filter>
@@ -241,11 +237,10 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        
-	<activity
+        <activity
             android:name=".VectorsListActivity"
-            android:label="Compose vectors list"
-            android:exported="true">
+            android:exported="true"
+            android:label="Compose vectors list">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -255,11 +250,10 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
         <activity
             android:name=".CrossfadeActivity"
-            android:label="Compose Crossfade Benchmark"
-            android:exported="true">
+            android:exported="true"
+            android:label="Compose Crossfade Benchmark">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -268,9 +262,9 @@
                 <action android:name="androidx.compose.integration.macrobenchmark.target.CROSSFADE_ACTIVITY" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
-	</activity>
-
-	<activity android:name=".PagerOfLazyGridActivity"
+        </activity>
+        <activity
+            android:name=".PagerOfLazyGridActivity"
             android:exported="true"
             android:theme="@style/Theme.AppCompat">
             <intent-filter>
@@ -278,5 +272,20 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <activity
+            android:name=".FormFillingActivity"
+            android:exported="true"
+            android:label="Compose Form Filling Benchmark"
+            android:launchMode="singleInstance"
+            android:theme="@style/Theme.AppCompat.Light">
+            <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.FORM_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/FormFillingActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FormFillingActivity.kt
new file mode 100644
index 0000000..2958dfe
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FormFillingActivity.kt
@@ -0,0 +1,286 @@
+/*
+ * 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.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.EditText
+import android.widget.LinearLayout
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.util.trace
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+class FormFillingActivity : ComponentActivity() {
+    private lateinit var lazyListState: LazyListState
+    private lateinit var formView: FormView
+    private lateinit var type: String
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val rowHeightDp: Dp
+        val fontSize: TextUnit
+        when (intent.getIntExtra(MODE, 0)) {
+            FRAME_MEASUREMENT_MODE -> {
+                // Larger number of rows to stress the system while measuring frame info.
+                rowHeightDp = 30.dp
+                fontSize = 5.sp
+            }
+            CREATE_ANI_MODE -> {
+                // Smaller number of rows so that we have no dropped frames.
+                rowHeightDp = 100.dp
+                fontSize = 10.sp
+            }
+            else -> error("Invalid Mode")
+        }
+
+        type = checkNotNull(intent.getStringExtra(TYPE)) { "No type specified." }
+        when (type) {
+            COMPOSE ->
+                setContent {
+                    lazyListState = rememberLazyListState()
+                    FormComposable(lazyListState, rowHeightDp, fontSize)
+                }
+            VIEW -> {
+                val rowHeightPx = rowHeightDp.value * resources.displayMetrics.densityDpi / 160f
+                formView = FormView(this, rowHeightPx, fontSize)
+                setContentView(formView)
+            }
+            else -> error("Unknown Type")
+        }
+    }
+
+    override fun onNewIntent(intent: Intent) {
+        when (type) {
+            COMPOSE -> lazyListState.requestScrollToItem(lazyListState.firstVisibleItemIndex + 100)
+            VIEW -> formView.scrollToPosition(formView.lastVisibleItemIndex + 100)
+            else -> error("Unknown Type")
+        }
+        super.onNewIntent(intent)
+    }
+
+    @Composable
+    private fun FormComposable(lazyListState: LazyListState, rowHeight: Dp, fontSize: TextUnit) {
+        val textStyle = LocalTextStyle.current.copy(fontSize = fontSize)
+        LazyColumn(state = lazyListState) {
+            items(data.size) { index ->
+                val person = data[index]
+                Row(
+                    modifier =
+                        Modifier.height(rowHeight).semantics {
+                            customActions =
+                                listOf(CustomAccessibilityAction("customAction") { false })
+                        }
+                ) {
+                    BasicTextField(
+                        value = person.title,
+                        onValueChange = { person.title = it },
+                        textStyle = textStyle
+                    )
+                    BasicTextField(
+                        value = person.firstName,
+                        onValueChange = { person.firstName = it },
+                        textStyle = textStyle
+                    )
+                    BasicTextField(
+                        value = person.middleName,
+                        onValueChange = { person.middleName = it },
+                        textStyle = textStyle
+                    )
+                    BasicTextField(
+                        value = person.lastName,
+                        onValueChange = { person.lastName = it },
+                        textStyle = textStyle
+                    )
+                    BasicTextField(
+                        value = person.age.toString(),
+                        onValueChange = { person.age = it.toInt() },
+                        textStyle = textStyle
+                    )
+                }
+            }
+        }
+    }
+
+    private class FormView(context: Context, rowHeight: Float, fontSize: TextUnit) :
+        RecyclerView(context) {
+        private val linearLayoutManager: LinearLayoutManager
+
+        init {
+            setHasFixedSize(true)
+            linearLayoutManager = LinearLayoutManager(context, VERTICAL, false)
+            layoutManager = linearLayoutManager
+            adapter = DemoAdapter(data, rowHeight, fontSize)
+        }
+
+        val lastVisibleItemIndex: Int
+            get() = linearLayoutManager.findLastVisibleItemPosition()
+
+        override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
+            return trace(CREATE_ANI_TRACE) { super.createAccessibilityNodeInfo() }
+        }
+
+        override fun sendAccessibilityEvent(eventType: Int) {
+            return trace(ACCESSIBILITY_EVENT_TRACE) { super.sendAccessibilityEvent(eventType) }
+        }
+    }
+
+    private class RowView(context: Context, content: (RowView) -> Unit) : LinearLayout(context) {
+        init {
+            gravity = Gravity.CENTER_VERTICAL
+            content(this)
+        }
+
+        override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
+            return trace(CREATE_ANI_TRACE) { super.createAccessibilityNodeInfo() }
+        }
+
+        override fun sendAccessibilityEvent(eventType: Int) {
+            return trace(ACCESSIBILITY_EVENT_TRACE) { super.sendAccessibilityEvent(eventType) }
+        }
+    }
+
+    @SuppressLint("AppCompatCustomView")
+    private class EditTextView(context: Context, fontSize: Float) : EditText(context) {
+        init {
+            textSize = fontSize
+            gravity = Gravity.CENTER_VERTICAL
+        }
+
+        fun replaceText(newText: String) {
+            text.replace(0, length(), newText, 0, newText.length)
+        }
+
+        override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
+            return trace(CREATE_ANI_TRACE) { super.createAccessibilityNodeInfo() }
+        }
+
+        override fun sendAccessibilityEvent(eventType: Int) {
+            return trace(ACCESSIBILITY_EVENT_TRACE) { super.sendAccessibilityEvent(eventType) }
+        }
+    }
+
+    private class DemoAdapter(
+        val data: List<FormData>,
+        val rowHeightPx: Float,
+        textSize: TextUnit
+    ) : Adapter<DemoAdapter.DemoViewHolder>() {
+
+        private class DemoViewHolder(
+            val title: EditTextView,
+            val firstName: EditTextView,
+            val middleName: EditTextView,
+            val lastName: EditTextView,
+            val age: EditTextView,
+            itemRoot: View
+        ) : ViewHolder(itemRoot)
+
+        val textSize = textSize.value
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DemoViewHolder {
+            val title = EditTextView(parent.context, textSize)
+            val firstName = EditTextView(parent.context, textSize)
+            val middleName = EditTextView(parent.context, textSize)
+            val lastName = EditTextView(parent.context, textSize)
+            val age = EditTextView(parent.context, textSize)
+
+            return DemoViewHolder(
+                title,
+                firstName,
+                middleName,
+                lastName,
+                age,
+                RowView(parent.context) {
+                    it.minimumHeight = rowHeightPx.fastRoundToInt()
+                    it.addView(title)
+                    it.addView(firstName)
+                    it.addView(middleName)
+                    it.addView(lastName)
+                    it.addView(age)
+                }
+            )
+        }
+
+        override fun onBindViewHolder(holder: DemoViewHolder, position: Int) {
+            val formData = data.elementAt(position)
+            holder.title.replaceText(formData.title)
+            holder.firstName.replaceText(formData.firstName)
+            holder.middleName.replaceText(formData.middleName)
+            holder.lastName.replaceText(formData.lastName)
+            holder.age.replaceText(formData.age.toString())
+        }
+
+        override fun getItemCount(): Int = data.size
+    }
+
+    private data class FormData(
+        var title: String = "",
+        var firstName: String = "",
+        var middleName: String = "",
+        var lastName: String = "",
+        var age: Int = 0,
+    )
+
+    private companion object {
+        private const val TYPE = "TYPE"
+        private const val COMPOSE = "Compose"
+        private const val VIEW = "View"
+        private const val MODE = "MODE"
+        private const val CREATE_ANI_MODE = 1
+        private const val FRAME_MEASUREMENT_MODE = 2
+        private const val CREATE_ANI_TRACE = "createAccessibilityNodeInfo"
+        private const val ACCESSIBILITY_EVENT_TRACE = "sendAccessibilityEvent"
+        private val data by lazy {
+            List(200000) {
+                FormData(
+                    title = "Mr",
+                    firstName = "John $it",
+                    middleName = "Ace $it",
+                    lastName = "Doe $it",
+                    age = it
+                )
+            }
+        }
+    }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FormFillingBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FormFillingBenchmark.kt
new file mode 100644
index 0000000..9b05e73
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FormFillingBenchmark.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.app.Instrumentation
+import android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES
+import android.content.Intent
+import android.os.Build.VERSION_CODES.N
+import android.provider.Settings.Secure
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.TraceSectionMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.Configurator
+import androidx.test.uiautomator.UiDevice
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SdkSuppress(minSdkVersion = N)
+@LargeTest
+@RunWith(Parameterized::class)
+class FormFillingBenchmark(private var talkbackEnabled: Boolean, private val type: String) {
+
+    @get:Rule val benchmarkRule = MacrobenchmarkRule()
+    private lateinit var instrumentation: Instrumentation
+    private var previousTalkbackSettings: String? = null
+    private lateinit var device: UiDevice
+
+    @Test
+    fun createAccessibilityNodeInfo() {
+        if (!talkbackEnabled) return
+        benchmarkRule.measureRepeated(
+            packageName = PACKAGE,
+            metrics =
+                @OptIn(ExperimentalMetricApi::class)
+                listOf(
+                    TraceSectionMetric(
+                        sectionName = CREATE_ANI_TRACE,
+                        mode = TraceSectionMetric.Mode.Sum
+                    ),
+                    TraceSectionMetric(
+                        sectionName = ACCESSIBILITY_EVENT_TRACE,
+                        mode = TraceSectionMetric.Mode.Sum
+                    )
+                ),
+            iterations = 10,
+            setupBlock = {
+                if (iteration == 0) {
+                    startActivityAndWait(
+                        Intent()
+                            .setAction("$PACKAGE.$ACTIVITY")
+                            .putExtra(TYPE, type)
+                            .putExtra(MODE, CREATE_ANI_MODE)
+                    )
+                    device.waitForIdle()
+
+                    // Run one iteration to allow the scroll position to stabilize, and to remove
+                    // the effect of the initial frame which draws the accessibility focus box.
+                    performScrollAndWait(millis = 10_000)
+                }
+            },
+            measureBlock = {
+
+                // Scroll and pause to allow all frames to complete, for the accessibility events
+                // to be sent, for talkback to assign focus, and finally for talkback to trigger
+                // createAccessibilityNodeInfo calls which is the thing we want to measure.
+                performScrollAndWait(millis = 10_000)
+            }
+        )
+    }
+
+    @Test
+    fun frameInfo() {
+        benchmarkRule.measureRepeated(
+            packageName = PACKAGE,
+            metrics = listOf(FrameTimingMetric()),
+            iterations = 10,
+            setupBlock = {
+                if (iteration == 0) {
+                    startActivityAndWait(
+                        Intent()
+                            .setAction("$PACKAGE.$ACTIVITY")
+                            .putExtra(TYPE, type)
+                            .putExtra(MODE, FRAME_MEASUREMENT_MODE)
+                    )
+                    Thread.sleep(2_000)
+                    device.waitForIdle()
+
+                    // Run one iteration to allow the scroll position to stabilize, and to remove
+                    // the effect of the initial frame which draws the accessibility focus box.
+                    performScrollAndWait(millis = 20)
+                }
+            },
+            measureBlock = {
+                // Instead of using an animation to scroll (Where the number of frames triggered
+                // is not deterministic, we attempt to scroll 100 times with an aim to scroll once
+                // every frame deadline of 20ms.
+                repeat(100) { performScrollAndWait(millis = 20) }
+                Thread.sleep(10_000)
+            }
+        )
+    }
+
+    @Before
+    fun setUp() {
+        Configurator.getInstance().uiAutomationFlags = FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES
+        instrumentation = InstrumentationRegistry.getInstrumentation()
+        device = UiDevice.getInstance(instrumentation)
+        if (talkbackEnabled) {
+            previousTalkbackSettings = instrumentation.enableTalkback()
+            // Wait for talkback to turn on.
+            Thread.sleep(2_000)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        if (talkbackEnabled) {
+            instrumentation.disableTalkback(previousTalkbackSettings)
+            // Wait for talkback to turn off.
+            Thread.sleep(2_000)
+        }
+    }
+
+    private fun performScrollAndWait(millis: Long) {
+        // We don't use UI Automator to scroll because UI Automator itself is an accessibility
+        // service, and this affects the benchmark. Instead we send an event to the activity that
+        // requests it to scroll.
+        instrumentation.context.startActivity(
+            Intent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setAction("$PACKAGE.$ACTIVITY")
+        )
+
+        // Pause to allow all frames to complete, for the accessibility events to be sent,
+        // for talkback to assign focus, and finally for talkback to trigger
+        // createAccessibilityNodeInfo calls which is the thing we want to measure.
+        Thread.sleep(millis)
+    }
+
+    companion object {
+        private const val PACKAGE = "androidx.compose.integration.macrobenchmark.target"
+        private const val ACTIVITY = "FORM_ACTIVITY"
+        private const val TYPE = "TYPE"
+        private const val COMPOSE = "Compose"
+        private const val VIEW = "View"
+        const val MODE = "MODE"
+        const val CREATE_ANI_MODE = 1
+        const val FRAME_MEASUREMENT_MODE = 2
+        const val CREATE_ANI_TRACE = "createAccessibilityNodeInfo"
+        const val ACCESSIBILITY_EVENT_TRACE = "sendAccessibilityEvent"
+
+        // Manually set up LastPass on the device and use these parameters when running locally.
+        // @Parameterized.Parameters(name = "LastPassEnabled=true, type={1}")
+        // @JvmStatic
+        // fun parameters() = mutableListOf<Array<Any>>().also {
+        //    for (type in arrayOf(COMPOSE, VIEW)) {
+        //        it.add(arrayOf(false, type))
+        //    }
+        // }
+
+        @Parameterized.Parameters(name = "TalkbackEnabled={0}, type={1}")
+        @JvmStatic
+        fun parameters() =
+            mutableListOf<Array<Any>>().also {
+                for (talkbackEnabled in arrayOf(false, true)) {
+                    for (type in arrayOf(COMPOSE, VIEW)) {
+                        it.add(arrayOf(talkbackEnabled, type))
+                    }
+                }
+            }
+    }
+}
+
+private fun Instrumentation.enableTalkback(): String? {
+    val talkback =
+        "com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService"
+    val previousTalkbackSettings =
+        Secure.getString(context.contentResolver, Secure.ENABLED_ACCESSIBILITY_SERVICES)
+    UiDevice.getInstance(this)
+        .executeShellCommand("settings put secure enabled_accessibility_services $talkback")
+    return previousTalkbackSettings
+}
+
+private fun Instrumentation.disableTalkback(previousTalkbackSettings: String? = null): String {
+    return UiDevice.getInstance(this)
+        .executeShellCommand(
+            if (previousTalkbackSettings == null || previousTalkbackSettings == "") {
+                "settings delete secure enabled_accessibility_services"
+            } else {
+                "settings put secure enabled_accessibility_services $previousTalkbackSettings"
+            }
+        )
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index bc2bca0..496c90e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -208,7 +208,7 @@
     // flaky, so we use this callback to test accessibility events.
     @VisibleForTesting
     internal var onSendAccessibilityEvent: (AccessibilityEvent) -> Boolean = {
-        view.parent.requestSendAccessibilityEvent(view, it)
+        trace("sendAccessibilityEvent") { view.parent.requestSendAccessibilityEvent(view, it) }
     }
 
     private val accessibilityManager: AccessibilityManager =
@@ -1519,7 +1519,7 @@
             event.contentDescription = contentDescription.fastJoinToString(",")
         }
 
-        return trace("sendEvent") { sendEvent(event) }
+        return sendEvent(event)
     }
 
     /**
@@ -2239,47 +2239,45 @@
         try {
             val subtreeChangedSemanticsNodesIds = MutableIntSet()
             for (notification in boundsUpdateChannel) {
-                trace("AccessibilityLoopIteration") {
-                    if (isEnabled) {
-                        for (i in subtreeChangedLayoutNodes.indices) {
-                            val layoutNode = subtreeChangedLayoutNodes.valueAt(i)
-                            trace("sendSubtreeChangeAccessibilityEvents") {
-                                sendSubtreeChangeAccessibilityEvents(
-                                    layoutNode,
-                                    subtreeChangedSemanticsNodesIds
-                                )
-                            }
-                            trace("sendTypeViewScrolledAccessibilityEvent") {
-                                sendTypeViewScrolledAccessibilityEvent(layoutNode)
-                            }
+                if (isEnabled) {
+                    for (i in subtreeChangedLayoutNodes.indices) {
+                        val layoutNode = subtreeChangedLayoutNodes.valueAt(i)
+                        trace("sendSubtreeChangeAccessibilityEvents") {
+                            sendSubtreeChangeAccessibilityEvents(
+                                layoutNode,
+                                subtreeChangedSemanticsNodesIds
+                            )
                         }
-                        subtreeChangedSemanticsNodesIds.clear()
-                        // When the bounds of layout nodes change, we will not always get semantics
-                        // change notifications because bounds is not part of semantics. And bounds
-                        // change from a layout node without semantics will affect the global bounds
-                        // of it children which has semantics. Bounds change will affect which nodes
-                        // are covered and which nodes are not, so the currentSemanticsNodes is not
-                        // up to date anymore.
-                        // After the subtree events are sent, accessibility services will get the
-                        // current visible/invisible state. We also try to do semantics tree diffing
-                        // to send out the proper accessibility events and update our copy here so
-                        // that
-                        // our incremental changes (represented by accessibility events) are
-                        // consistent
-                        // with accessibility services. That is: change - notify - new change -
-                        // notify, if we don't do the tree diffing and update our copy here, we will
-                        // combine old change and new change, which is missing finer-grained
-                        // notification.
-                        if (!checkingForSemanticsChanges) {
-                            checkingForSemanticsChanges = true
-                            handler.post(semanticsChangeChecker)
+                        trace("sendTypeViewScrolledAccessibilityEvent") {
+                            sendTypeViewScrolledAccessibilityEvent(layoutNode)
                         }
                     }
-                    subtreeChangedLayoutNodes.clear()
-                    pendingHorizontalScrollEvents.clear()
-                    pendingVerticalScrollEvents.clear()
-                    delay(SendRecurringAccessibilityEventsIntervalMillis)
+                    subtreeChangedSemanticsNodesIds.clear()
+                    // When the bounds of layout nodes change, we will not always get semantics
+                    // change notifications because bounds is not part of semantics. And bounds
+                    // change from a layout node without semantics will affect the global bounds
+                    // of it children which has semantics. Bounds change will affect which nodes
+                    // are covered and which nodes are not, so the currentSemanticsNodes is not
+                    // up to date anymore.
+                    // After the subtree events are sent, accessibility services will get the
+                    // current visible/invisible state. We also try to do semantics tree diffing
+                    // to send out the proper accessibility events and update our copy here so
+                    // that
+                    // our incremental changes (represented by accessibility events) are
+                    // consistent
+                    // with accessibility services. That is: change - notify - new change -
+                    // notify, if we don't do the tree diffing and update our copy here, we will
+                    // combine old change and new change, which is missing finer-grained
+                    // notification.
+                    if (!checkingForSemanticsChanges) {
+                        checkingForSemanticsChanges = true
+                        handler.post(semanticsChangeChecker)
+                    }
                 }
+                subtreeChangedLayoutNodes.clear()
+                pendingHorizontalScrollEvents.clear()
+                pendingVerticalScrollEvents.clear()
+                delay(SendRecurringAccessibilityEventsIntervalMillis)
             }
         } finally {
             subtreeChangedLayoutNodes.clear()