Add FrameExperimentBenchmark

Bug: 322232828
Test: FrameExperimentBenchmark

Change-Id: Ie8ddb7c3cde481d92a0cc7f0df532cd83fec6f0c
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index d56e2a2..1873374 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -91,6 +91,19 @@
             </intent-filter>
         </activity>
         <activity
+            android:name=".FrameExperimentActivity"
+            android:label="FrameExp"
+            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.FRAME_EXPERIMENT_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
             android:name=".BaselineProfileActivity"
             android:exported="true">
             <intent-filter>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt
new file mode 100644
index 0000000..7c4f269
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.os.Bundle
+import android.os.Trace
+import android.view.View
+import androidx.activity.ComponentActivity
+
+private const val FRAME_ADDED_WORK_MS = 20L
+private const val FRAME_BASELINE_WORK_MS = 10L
+private const val FRAME_COUNT = 100
+
+// NOTE: Keep in sync with FrameExperimentBenchmark!!
+private enum class FrameMode(val id: Int) {
+    Fast(0),
+    PrefetchEveryFrame(1),
+    WorkDuringEveryFrame(2),
+    PrefetchSomeFrames(3),
+}
+
+private class FrameExperimentView(context: Context, val mode: FrameMode) : View(context) {
+
+    init {
+        setOnClickListener {
+            remainingFrames = FRAME_COUNT - 1
+            invalidate()
+        }
+    }
+    var remainingFrames = 0
+
+    fun work(durationMs: Long = FRAME_ADDED_WORK_MS, label: String = "Added item work") {
+        Trace.beginSection(label)
+
+        // spin!
+        val endTime = System.nanoTime() + durationMs * 1_000_000
+        @Suppress("ControlFlowWithEmptyBody")
+        while (System.nanoTime() < endTime) {}
+
+        Trace.endSection()
+    }
+
+    val paintA = Paint().apply { setColor(Color.LTGRAY) }
+    val paintB = Paint().apply { setColor(Color.WHITE) }
+
+    override fun onDraw(canvas: Canvas) {
+        if (mode == FrameMode.WorkDuringEveryFrame) {
+            work()
+        }
+        super.onDraw(canvas)
+
+        work(durationMs = FRAME_BASELINE_WORK_MS, "Baseline work frame $remainingFrames")
+
+        // small rect to reduce flicker
+        canvas.drawRect(
+            0f, 0f, 200f, 200f,
+            if (remainingFrames % 2 == 0) paintA else paintB
+        )
+
+        if (remainingFrames >= 1) {
+            remainingFrames--
+            invalidate()
+
+            if (mode == FrameMode.PrefetchEveryFrame ||
+                (mode == FrameMode.PrefetchSomeFrames && remainingFrames % 5 == 0)) {
+                this.post {
+                    work()
+                }
+            }
+        }
+    }
+}
+
+class FrameExperimentActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val frameModeId = intent.getIntExtra(EXTRA_FRAME_MODE, defaultMode.id)
+        val frameMode = FrameMode.values().first { it.id == frameModeId }
+
+        setContentView(
+            FrameExperimentView(this, frameMode)
+        )
+    }
+
+    companion object {
+        const val EXTRA_FRAME_MODE = "FRAME_MODE"
+        private val defaultMode = FrameMode.Fast
+    }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt
new file mode 100644
index 0000000..de91407
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt
@@ -0,0 +1,85 @@
+/*
+ * 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 androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.createCompilationParams
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Benchmark for experimenting with synthetic frame patterns/durations and how
+ * they show up in metrics
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FrameExperimentBenchmark {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    // NOTE: Keep in sync with FrameExperimentActivity!!
+    private enum class FrameMode(val id: Int) {
+        Fast(0),
+        PrefetchEveryFrame(1),
+        WorkDuringEveryFrame(2),
+        PrefetchSomeFrames(3),
+    }
+
+    @Test
+    fun fast() = benchmark(FrameMode.Fast)
+    @Test
+    fun prefetchEveryFrame() = benchmark(FrameMode.PrefetchEveryFrame)
+    @Test
+    fun workDuringEveryFrame() = benchmark(FrameMode.WorkDuringEveryFrame)
+    @Test
+    fun prefetchSomeFrames() = benchmark(FrameMode.PrefetchSomeFrames)
+
+    private fun benchmark(mode: FrameMode) {
+        benchmarkRule.measureRepeated(
+            packageName = PACKAGE_NAME,
+            metrics = listOf(FrameTimingMetric()),
+            compilationMode = CompilationMode.DEFAULT,
+            iterations = 1,
+            setupBlock = {
+                val intent = Intent()
+                intent.action = ACTION
+                intent.putExtra("FRAME_MODE", mode.id)
+                startActivityAndWait(intent)
+            }
+        ) {
+            device.click(device.displayWidth / 2, device.displayHeight / 2)
+            Thread.sleep(4_000) // empirically enough to produce expected frames
+        }
+    }
+
+    companion object {
+        private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
+        private const val ACTION =
+            "androidx.compose.integration.macrobenchmark.target.FRAME_EXPERIMENT_ACTIVITY"
+
+        @Parameterized.Parameters(name = "compilation={0}")
+        @JvmStatic
+        fun parameters() = createCompilationParams()
+    }
+}