Add summing mode to TraceSectionMetric

Test: TraceSectionMetricTest
Relnote: """
Added TraceSectionMode("label", Mode.Sum), allowing measurement of
total time spent on multiple trace sections with the same label. For
instance, TraceSectionMetric("inflate", Mode.Sum) will report a metric
`inflateMs` for the total time in a trace spent on inflation.

Also removed API requirement, as TraceSectionMetric works together
with androidx.tracing.Trace back to lower API levels, with the use of
[forceEnableAppTracing](https://developer.android.com/reference/androidx/tracing/Trace#forceEnableAppTracing()).
"""

Additionally adds tracing of cache_miss and cache_hit for observing
cost of shader compilation for b/231455742

Change-Id: Id7b68e23f5ded4d20ab21771dbf9eb96d9dcfdb7
diff --git a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
index e0b7810..69c6e99 100644
--- a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
@@ -114,8 +114,15 @@
   public final class TagKt {
   }
 
-  @RequiresApi(29) @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
-    ctor public TraceSectionMetric(String sectionName);
+  @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
+    ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode);
+  }
+
+  public enum TraceSectionMetric.Mode {
+    method public static androidx.benchmark.macro.TraceSectionMetric.Mode valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.benchmark.macro.TraceSectionMetric.Mode[] values();
+    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode First;
+    enum_constant public static final androidx.benchmark.macro.TraceSectionMetric.Mode Sum;
   }
 
 }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
index 8fc7256..98120bd 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.benchmark.macro
 
-import android.annotation.SuppressLint
 import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
 import androidx.benchmark.perfetto.PerfettoHelper
 import androidx.test.filters.MediumTest
@@ -38,43 +37,52 @@
     ).absolutePath
 
     @Test
-    fun activityThreadMain() = verifySingleMetric(
+    fun activityThreadMain() = verifyFirstSum(
         tracePath = api24ColdStart,
         packageName = Packages.TEST,
         sectionName = "ActivityThreadMain",
-        expectedMs = 12.639
+        expectedFirstMs = 12.639
     )
 
     @Test
-    fun activityStart() = verifySingleMetric(
+    fun activityStart() = verifyFirstSum(
         tracePath = api24ColdStart,
         packageName = Packages.TEST,
         sectionName = "activityStart",
-        expectedMs = 81.979
+        expectedFirstMs = 81.979
     )
 
     @Test
-    fun startActivityAndWait() = verifySingleMetric(
+    fun startActivityAndWait() = verifyFirstSum(
         tracePath = api24ColdStart,
         packageName = Packages.TEST,
         sectionName = "startActivityAndWait",
-        expectedMs = 1_110.689
+        expectedFirstMs = 1_110.689
     )
 
     @Test
-    fun launching() = verifySingleMetric(
+    fun launching() = verifyFirstSum(
         tracePath = api24ColdStart,
         packageName = Packages.TEST,
         sectionName = "launching: androidx.benchmark.integration.macrobenchmark.target",
-        expectedMs = 269.947
+        expectedFirstMs = 269.947
     )
 
     @Test
-    fun section1_2() = verifySingleMetric(
+    fun section1_2() = verifyFirstSum(
         tracePath = commasInSliceNames,
         packageName = Packages.TARGET,
         sectionName = "section1,2",
-        expectedMs = 0.006615
+        expectedFirstMs = 0.006615
+    )
+
+    @Test
+    fun multiSection() = verifyFirstSum(
+        tracePath = api24ColdStart,
+        packageName = Packages.TARGET,
+        sectionName = "inflate",
+        expectedFirstMs = 13.318, // first inflation
+        expectedSumMs = 43.128 // total inflation
     )
 
     companion object {
@@ -85,16 +93,16 @@
             apiLevel = 24
         )
 
-        @SuppressLint("NewApi") // we use a fixed trace - ignore for TraceSectionMetric
-        private fun verifySingleMetric(
+        private fun verifyMetric(
             tracePath: String,
             packageName: String,
             sectionName: String,
+            mode: TraceSectionMetric.Mode,
             expectedMs: Double
         ) {
             assumeTrue(PerfettoHelper.isAbiSupported())
 
-            val metric = TraceSectionMetric(sectionName)
+            val metric = TraceSectionMetric(sectionName, mode)
             val expectedKey = sectionName + "Ms"
             metric.configure(packageName = packageName)
 
@@ -108,5 +116,28 @@
             assertEquals(setOf(expectedKey), iterationResult.singleMetrics.keys)
             assertEquals(expectedMs, iterationResult.singleMetrics[expectedKey]!!, 0.001)
         }
+
+        private fun verifyFirstSum(
+            tracePath: String,
+            packageName: String,
+            sectionName: String,
+            expectedFirstMs: Double,
+            expectedSumMs: Double = expectedFirstMs // default implies only one matching section
+        ) {
+            verifyMetric(
+                tracePath = tracePath,
+                packageName = packageName,
+                sectionName = sectionName,
+                mode = TraceSectionMetric.Mode.First,
+                expectedMs = expectedFirstMs
+            )
+            verifyMetric(
+                tracePath = tracePath,
+                packageName = packageName,
+                sectionName = sectionName,
+                mode = TraceSectionMetric.Mode.Sum,
+                expectedMs = expectedSumMs
+            )
+        }
     }
 }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index db769c5..e8fac44 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -269,19 +269,38 @@
 }
 
 /**
- * Captures the time taken by a trace section - a named begin / end pair matching the provided name.
+ * Captures the time taken by named trace section - a named begin / end pair matching the provided
+ * [sectionName].
  *
- * Always selects the first instance of a trace section captured during a measurement.
+ * Select how matching sections are resolved into a duration metric with [mode].
  *
  * @see androidx.tracing.Trace.beginSection
  * @see androidx.tracing.Trace.endSection
  * @see androidx.tracing.trace
  */
-@RequiresApi(29) // Remove once b/182386956 fixed, as app tag may be needed for this to work.
 @ExperimentalMetricApi
 public class TraceSectionMetric(
-    private val sectionName: String
+    private val sectionName: String,
+    private val mode: Mode = Mode.First
 ) : Metric() {
+    enum class Mode {
+        /**
+         * Captures the duration of the first instance of `sectionName` in the trace.
+         *
+         * When this mode is used, no measurement will be reported if the named section does
+         * not appear in the trace.
+         */
+        First,
+
+        /**
+         * Captures the sum of all instances of `sectionName` in the trace.
+         *
+         * When this mode is used, a measurement of `0` will be reported if the named section
+         * does not appear in the trace
+         */
+        Sum
+    }
+
     internal override fun configure(packageName: String) {
     }
 
@@ -296,16 +315,36 @@
         captureInfo: CaptureInfo,
         perfettoTraceProcessor: PerfettoTraceProcessor
     ): IterationResult {
-        val slice = perfettoTraceProcessor.querySlices(sectionName).firstOrNull()
-        return if (slice == null) {
-            IterationResult.EMPTY
-        } else IterationResult(
-            singleMetrics = mapOf(
-                sectionName + "Ms" to slice.dur / 1_000_000.0
-            ),
-            sampledMetrics = emptyMap(),
-            timelineRangeNs = slice.ts..slice.endTs
-        )
+        val slices = perfettoTraceProcessor.querySlices(sectionName)
+
+        return when (mode) {
+            Mode.First -> {
+                val slice = slices.firstOrNull()
+                if (slice == null) {
+                    IterationResult.EMPTY
+                } else IterationResult(
+                    singleMetrics = mapOf(
+                        sectionName + "Ms" to slice.dur / 1_000_000.0
+                    ),
+                    sampledMetrics = emptyMap(),
+                    timelineRangeNs = slice.ts..slice.endTs
+                )
+            }
+            Mode.Sum -> {
+                // note, this duration assumes non-reentrant slices
+                val durMs = slices.sumOf { it.dur } / 1_000_000.0
+                IterationResult(
+                    singleMetrics = mapOf(sectionName + "Ms" to durMs),
+                    sampledMetrics = emptyMap(),
+                    timelineRangeNs = if (slices.isEmpty()) {
+                        null
+                    } else {
+                        // parens added to make ktlint happy
+                        (slices.minOf { it.ts })..(slices.maxOf { it.endTs })
+                    }
+                )
+            }
+        }
     }
 }
 
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
index cb9e113..e76e719 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -17,16 +17,20 @@
 package androidx.compose.integration.macrobenchmark
 
 import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.ExperimentalMetricApi
 import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.TraceSectionMetric
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.test.filters.LargeTest
 import androidx.testutils.createStartupCompilationParams
+import androidx.testutils.getStartupMetrics
 import androidx.testutils.measureStartup
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
+@OptIn(ExperimentalMetricApi::class)
 @LargeTest
 @RunWith(Parameterized::class)
 class SmallListStartupBenchmark(
@@ -36,10 +40,25 @@
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
+    /**
+     * Temporary, tracking for b/231455742
+     *
+     * Note that this tracing only exists on more recent API levels
+     */
+    private val metrics = getStartupMetrics() + if (startupMode == StartupMode.COLD) {
+        listOf(
+            TraceSectionMetric("cache_hit", TraceSectionMetric.Mode.Sum),
+            TraceSectionMetric("cache_miss", TraceSectionMetric.Mode.Sum)
+        )
+    } else {
+        emptyList()
+    }
+
     @Test
     fun startup() = benchmarkRule.measureStartup(
         compilationMode = compilationMode,
         startupMode = startupMode,
+        metrics = metrics,
         packageName = "androidx.compose.integration.macrobenchmark.target"
     ) {
         action = "androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY"
diff --git a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
index 7d0807c..4366f743 100644
--- a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
+++ b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
@@ -20,6 +20,7 @@
 import android.os.Build
 import androidx.benchmark.macro.BaselineProfileMode
 import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.Metric
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingLegacyMetric
 import androidx.benchmark.macro.StartupTimingMetric
@@ -84,10 +85,11 @@
     startupMode: StartupMode,
     packageName: String,
     iterations: Int = 10,
+    metrics: List<Metric> = getStartupMetrics(),
     setupIntent: Intent.() -> Unit = {}
 ) = measureRepeated(
     packageName = packageName,
-    metrics = getStartupMetrics(),
+    metrics = metrics,
     compilationMode = compilationMode,
     iterations = iterations,
     startupMode = startupMode,