Add a new SystemUIAppGc benchmark

This adds a new SystemUI benchmark that runs with 2GB max heap size.

The benchmark reports the young/old gc counts as well as the young/old
gc times.

Bug: b/518734101
Change-Id: Ibac60e21aedd7c2eb8b0e1eab0063027993944fa
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java
index 81bd699..0cf7820 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java
@@ -168,6 +168,26 @@
       return this;
     }
 
+    public Builder measureGcOldGenCount() {
+      metrics.add(BenchmarkMetric.GcOldGenCount);
+      return this;
+    }
+
+    public Builder measureGcOldGenTime() {
+      metrics.add(BenchmarkMetric.GcOldGenTime);
+      return this;
+    }
+
+    public Builder measureGcYoungGenCount() {
+      metrics.add(BenchmarkMetric.GcYoungGenCount);
+      return this;
+    }
+
+    public Builder measureGcYoungGenTime() {
+      metrics.add(BenchmarkMetric.GcYoungGenTime);
+      return this;
+    }
+
     public Builder measureInstructionCodeSize() {
       metrics.add(BenchmarkMetric.InstructionCodeSize);
       return this;
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
index 68ab9c7..4cc859f 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
@@ -26,6 +26,26 @@
   }
 
   @Override
+  public void addGcOldGenCountResult(long result) {
+    throw error();
+  }
+
+  @Override
+  public void addGcOldGenTimeResult(long result) {
+    throw error();
+  }
+
+  @Override
+  public void addGcYoungGenCountResult(long result) {
+    throw error();
+  }
+
+  @Override
+  public void addGcYoungGenTimeResult(long result) {
+    throw error();
+  }
+
+  @Override
   public void addRuntimeResult(long result) {
     throw error();
   }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
index 55d34c2..e0ac1f4 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
@@ -33,6 +33,10 @@
 
   // Consider using LongSet to eliminate duplicate results for size.
   private final LongList codeSizeResults = new LongArrayList();
+  private final LongList gcOldGenCountResults = new LongArrayList();
+  private final LongList gcOldGenTimeResults = new LongArrayList();
+  private final LongList gcYoungGenCountResults = new LongArrayList();
+  private final LongList gcYoungGenTimeResults = new LongArrayList();
   private final LongList instructionCodeSizeResults = new LongArrayList();
   private final LongList composableInstructionCodeSizeResults = new LongArrayList();
   private final LongList dex2OatSizeResults = new LongArrayList();
@@ -68,6 +72,22 @@
     return dex2OatSizeResults;
   }
 
+  public LongList getGcOldGenCountResults() {
+    return gcOldGenCountResults;
+  }
+
+  public LongList getGcOldGenTimeResults() {
+    return gcOldGenTimeResults;
+  }
+
+  public LongList getGcYoungGenCountResults() {
+    return gcYoungGenCountResults;
+  }
+
+  public LongList getGcYoungGenTimeResults() {
+    return gcYoungGenTimeResults;
+  }
+
   public LongList getRuntimeResults() {
     return runtimeResults;
   }
@@ -77,6 +97,34 @@
   }
 
   @Override
+  public void addGcOldGenCountResult(long result) {
+    verifyMetric(
+        BenchmarkMetric.GcOldGenCount, metrics.contains(BenchmarkMetric.GcOldGenCount), true);
+    gcOldGenCountResults.add(result);
+  }
+
+  @Override
+  public void addGcOldGenTimeResult(long result) {
+    verifyMetric(
+        BenchmarkMetric.GcOldGenTime, metrics.contains(BenchmarkMetric.GcOldGenTime), true);
+    gcOldGenTimeResults.add(result);
+  }
+
+  @Override
+  public void addGcYoungGenCountResult(long result) {
+    verifyMetric(
+        BenchmarkMetric.GcYoungGenCount, metrics.contains(BenchmarkMetric.GcYoungGenCount), true);
+    gcYoungGenCountResults.add(result);
+  }
+
+  @Override
+  public void addGcYoungGenTimeResult(long result) {
+    verifyMetric(
+        BenchmarkMetric.GcYoungGenTime, metrics.contains(BenchmarkMetric.GcYoungGenTime), true);
+    gcYoungGenTimeResults.add(result);
+  }
+
+  @Override
   public void addRuntimeResult(long result) {
     verifyMetric(BenchmarkMetric.RunTimeRaw, metrics.contains(BenchmarkMetric.RunTimeRaw), true);
     runtimeResults.add(result);
@@ -151,6 +199,17 @@
         "Unexpected attempt to get sub-results for benchmark without sub-benchmarks");
   }
 
+  @Override
+  public boolean isBenchmarkingGc() {
+    if (metrics.contains(BenchmarkMetric.GcOldGenCount)) {
+      assert metrics.contains(BenchmarkMetric.GcOldGenTime);
+      assert metrics.contains(BenchmarkMetric.GcYoungGenCount);
+      assert metrics.contains(BenchmarkMetric.GcYoungGenTime);
+      return true;
+    }
+    return false;
+  }
+
   private static void verifyMetric(BenchmarkMetric metric, boolean expected, boolean actual) {
     if (expected != actual) {
       throw new BenchmarkConfigError(
@@ -221,6 +280,24 @@
     System.out.println(BenchmarkResults.prettyMetric(name, BenchmarkMetric.Dex2OatCodeSize, bytes));
   }
 
+  private void printGcOldGenCount(long count) {
+    System.out.println(BenchmarkResults.prettyMetric(name, BenchmarkMetric.GcOldGenCount, count));
+  }
+
+  private void printGcOldGenTime(long duration) {
+    String value = BenchmarkResults.prettyTime(duration);
+    System.out.println(BenchmarkResults.prettyMetric(name, BenchmarkMetric.GcOldGenTime, value));
+  }
+
+  private void printGcYoungGenCount(long count) {
+    System.out.println(BenchmarkResults.prettyMetric(name, BenchmarkMetric.GcYoungGenCount, count));
+  }
+
+  private void printGcYoungGenTime(long duration) {
+    String value = BenchmarkResults.prettyTime(duration);
+    System.out.println(BenchmarkResults.prettyMetric(name, BenchmarkMetric.GcYoungGenTime, value));
+  }
+
   private void printResourceSize(long bytes) {
     System.out.println(BenchmarkResults.prettyMetric(name, BenchmarkMetric.ResourceSize, bytes));
   }
@@ -249,6 +326,10 @@
     }
     printCodeSizeResults(dex2OatSizeResults, failOnCodeSizeDifferences, this::printDex2OatSize);
     printCodeSizeResults(resourceSizeResults, failOnCodeSizeDifferences, this::printResourceSize);
+    printGcCountResults(gcOldGenCountResults, mode, this::printGcOldGenCount);
+    printGcTimeResults(gcOldGenTimeResults, mode, this::printGcOldGenTime);
+    printGcCountResults(gcYoungGenCountResults, mode, this::printGcYoungGenCount);
+    printGcTimeResults(gcYoungGenTimeResults, mode, this::printGcYoungGenTime);
   }
 
   private static void printCodeSizeResults(
@@ -257,6 +338,22 @@
         codeSizeResults, codeSizeResults::getLong, failOnCodeSizeDifferences, printer);
   }
 
+  private void printGcCountResults(LongList gcCountResults, ResultMode mode, LongConsumer printer) {
+    if (!gcCountResults.isEmpty()) {
+      long sum = gcCountResults.stream().mapToLong(l -> l).sum();
+      long result = mode == ResultMode.SUM ? sum : sum / gcCountResults.size();
+      printer.accept(result);
+    }
+  }
+
+  private void printGcTimeResults(LongList gcTimeResults, ResultMode mode, LongConsumer printer) {
+    if (!gcTimeResults.isEmpty()) {
+      long sum = gcTimeResults.stream().mapToLong(l -> l).sum();
+      long result = mode == ResultMode.SUM ? sum : sum / gcTimeResults.size();
+      printer.accept(result);
+    }
+  }
+
   private static void printCodeSizeResults(
       Collection<?> codeSizeResults,
       IntToLongFunction getter,
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
index f85812c..9f334ea 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
@@ -29,6 +29,30 @@
           i -> result.getCodeSizeResults().getLong(i));
       addPropertyIfValueDifferentFromRepresentative(
           resultObject,
+          "gc_old_count",
+          iteration,
+          result.getGcOldGenCountResults(),
+          i -> result.getGcOldGenCountResults().getLong(i));
+      addPropertyIfValueDifferentFromRepresentative(
+          resultObject,
+          "gc_old_time",
+          iteration,
+          result.getGcOldGenTimeResults(),
+          i -> result.getGcOldGenTimeResults().getLong(i));
+      addPropertyIfValueDifferentFromRepresentative(
+          resultObject,
+          "gc_young_count",
+          iteration,
+          result.getGcYoungGenCountResults(),
+          i -> result.getGcYoungGenCountResults().getLong(i));
+      addPropertyIfValueDifferentFromRepresentative(
+          resultObject,
+          "gc_young_time",
+          iteration,
+          result.getGcYoungGenTimeResults(),
+          i -> result.getGcYoungGenTimeResults().getLong(i));
+      addPropertyIfValueDifferentFromRepresentative(
+          resultObject,
           "ins_code_size",
           iteration,
           result.getInstructionCodeSizeResults(),
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
index 7d9219d..01ca4a6 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
@@ -37,6 +37,30 @@
   }
 
   @Override
+  public void addGcOldGenCountResult(long result) {
+    throw addGcResultError();
+  }
+
+  @Override
+  public void addGcOldGenTimeResult(long result) {
+    throw addGcResultError();
+  }
+
+  @Override
+  public void addGcYoungGenCountResult(long result) {
+    throw addGcResultError();
+  }
+
+  @Override
+  public void addGcYoungGenTimeResult(long result) {
+    throw addGcResultError();
+  }
+
+  private Unreachable addGcResultError() {
+    throw new Unreachable("Unexpected attempt to add gc result for warmup run");
+  }
+
+  @Override
   public void addRuntimeResult(long result) {
     runtimeResults.add(result);
   }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
index 880456e..68ab73a 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
@@ -74,6 +74,7 @@
   private boolean enableContainerDex = false;
   private boolean enableDex2Oat = true;
   private boolean enableDex2OatVerification = true;
+  private boolean enableGcTracking = false;
   private boolean runtimeOnly = false;
   private int warmupIterations = 1;
   private final List<String> programPackages = new ArrayList<>();
@@ -125,6 +126,11 @@
     return this;
   }
 
+  public AppDumpBenchmarkBuilder setEnableGcTracking(boolean enableGcTracking) {
+    this.enableGcTracking = enableGcTracking;
+    return this;
+  }
+
   public AppDumpBenchmarkBuilder setName(String name) {
     this.name = name;
     return this;
@@ -196,6 +202,13 @@
         builder.measureResourceSize();
       }
     }
+    if (enableGcTracking) {
+      builder
+          .measureGcOldGenCount()
+          .measureGcOldGenTime()
+          .measureGcYoungGenCount()
+          .measureGcYoungGenTime();
+    }
     return builder.build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/internal/benchmarks/appdumps/SystemUIBenchmarks.java b/src/test/java/com/android/tools/r8/internal/benchmarks/appdumps/SystemUIBenchmarks.java
index a8b5ae4..2845d1c 100644
--- a/src/test/java/com/android/tools/r8/internal/benchmarks/appdumps/SystemUIBenchmarks.java
+++ b/src/test/java/com/android/tools/r8/internal/benchmarks/appdumps/SystemUIBenchmarks.java
@@ -48,6 +48,16 @@
             .setFromRevision(16457)
             .buildR8(SystemUIBenchmarks::configure),
         AppDumpBenchmarkBuilder.builder()
+            .setName("SystemUIAppGc")
+            .setDumpDependencyPath(dir)
+            .setEnableGcTracking(true)
+            .setEnableResourceShrinking(true)
+            // TODO(b/373550435): Update dex2oat to enable checking absence of verification errors
+            //  on SystemUI.
+            .setEnableDex2OatVerification(false)
+            .setFromRevision(16457)
+            .buildR8(SystemUIBenchmarks::configure),
+        AppDumpBenchmarkBuilder.builder()
             .setName("SystemUIAppPartial")
             .setDumpDependencyPath(dir)
             .setEnableResourceShrinking(true)
diff --git a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
index ca13cdf..323ed73 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper.DexVm.Kind;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
+import com.android.tools.r8.benchmarks.gc.CaptureGcResult;
 import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.CustomConversionVersion;
 import com.android.tools.r8.dex.ApplicationReader;
@@ -1849,7 +1850,15 @@
       BenchmarkResults benchmarkResults)
       throws CompilationFailedException {
     long start = 0;
+    CaptureGcResult startGcResult = null;
     if (benchmarkResults != null) {
+      if (benchmarkResults.isBenchmarkingGc()) {
+        // Try to run gc before starting the benchmark so that previous benchmark iterations do not
+        // inadvertently impact gc during the current iteration.
+        System.gc();
+        System.gc();
+        startGcResult = CaptureGcResult.capture();
+      }
       start = System.nanoTime();
     }
     R8Command command = commandBuilder.build();
@@ -1860,7 +1869,16 @@
     } finally {
       if (benchmarkResults != null) {
         long end = System.nanoTime();
-        benchmarkResults.addRuntimeResult(end - start);
+        long runtimeResult = end - start;
+        benchmarkResults.addRuntimeResult(runtimeResult);
+        if (startGcResult != null) {
+          CaptureGcResult endGcResult = CaptureGcResult.capture();
+          CaptureGcResult delta = startGcResult.computeDelta(endGcResult);
+          benchmarkResults.addGcOldGenCountResult(delta.getOldCount());
+          benchmarkResults.addGcOldGenTimeResult(delta.getOldTimeNanos());
+          benchmarkResults.addGcYoungGenCountResult(delta.getYoungCount());
+          benchmarkResults.addGcYoungGenTimeResult(delta.getYoungTimeNanos());
+        }
       }
     }
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java
index ff81615..170dee1 100644
--- a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java
+++ b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java
@@ -10,6 +10,10 @@
   ComposableInstructionCodeSize,
   DexSegmentsCodeSize,
   Dex2OatCodeSize,
+  GcOldGenCount,
+  GcOldGenTime,
+  GcYoungGenCount,
+  GcYoungGenTime,
   StartupTime,
   ResourceSize;
 
diff --git a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
index e3c7bc6..9cd8404 100644
--- a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
+++ b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
@@ -11,6 +11,14 @@
 
 public interface BenchmarkResults {
 
+  void addGcOldGenCountResult(long result);
+
+  void addGcOldGenTimeResult(long result);
+
+  void addGcYoungGenCountResult(long result);
+
+  void addGcYoungGenTimeResult(long result);
+
   // Append a runtime result. This may be summed or averaged depending on the benchmark set up.
   void addRuntimeResult(long result);
 
@@ -38,6 +46,10 @@
     return true;
   }
 
+  default boolean isBenchmarkingGc() {
+    return false;
+  }
+
   void printResults(ResultMode resultMode, boolean failOnCodeSizeDifferences);
 
   void writeResults(Path path, BenchmarkResults warmupResults) throws IOException;
diff --git a/src/test/testbase/java/com/android/tools/r8/benchmarks/gc/CaptureGcResult.java b/src/test/testbase/java/com/android/tools/r8/benchmarks/gc/CaptureGcResult.java
new file mode 100644
index 0000000..33993fe
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/benchmarks/gc/CaptureGcResult.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.benchmarks.gc;
+
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.util.List;
+
+public class CaptureGcResult {
+
+  private final long oldCount;
+  private final long oldTimeNanos;
+  private final long youngCount;
+  private final long youngTimeNanos;
+
+  public CaptureGcResult(long oldCount, long oldTimeNanos, long youngCount, long youngTimeNanos) {
+    this.oldCount = oldCount;
+    this.oldTimeNanos = oldTimeNanos;
+    this.youngCount = youngCount;
+    this.youngTimeNanos = youngTimeNanos;
+  }
+
+  /** Captures a snapshot of the current GC state. */
+  public static CaptureGcResult capture() {
+    List<GarbageCollectorMXBean> beans = ManagementFactory.getGarbageCollectorMXBeans();
+    long youngCount = 0;
+    long youngTimeMs = 0;
+    long oldCount = 0;
+    long oldTimeMs = 0;
+    for (GarbageCollectorMXBean bean : beans) {
+      long count = bean.getCollectionCount();
+      long time = bean.getCollectionTime();
+      if (count == -1) {
+        continue; // Collection count/time is not supported by this JVM
+      }
+      String collectorName = bean.getName();
+      if (GcUtils.isConcurrentGcCollector(collectorName)) {
+        // Concurrent gc currently not tracked.
+      } else if (GcUtils.isOldGcCollector(collectorName)) {
+        oldCount += count;
+        oldTimeMs += time;
+      } else if (GcUtils.isYoungGcCollector(collectorName)) {
+        youngCount += count;
+        youngTimeMs += time;
+      } else {
+        throw new RuntimeException("Unrecognized collector: " + bean.getName());
+      }
+    }
+    return new CaptureGcResult(
+        oldCount, oldTimeMs * 1_000_000, youngCount, youngTimeMs * 1_000_000);
+  }
+
+  /** Computes the difference (delta) between this snapshot and a later snapshot. */
+  public CaptureGcResult computeDelta(CaptureGcResult after) {
+    return new CaptureGcResult(
+        after.oldCount - oldCount,
+        after.oldTimeNanos - oldTimeNanos,
+        after.youngCount - youngCount,
+        after.youngTimeNanos - youngTimeNanos);
+  }
+
+  public long getOldCount() {
+    return oldCount;
+  }
+
+  public long getOldTimeNanos() {
+    return oldTimeNanos;
+  }
+
+  public long getYoungCount() {
+    return youngCount;
+  }
+
+  public long getYoungTimeNanos() {
+    return youngTimeNanos;
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/benchmarks/gc/GcUtils.java b/src/test/testbase/java/com/android/tools/r8/benchmarks/gc/GcUtils.java
new file mode 100644
index 0000000..ae3e69a
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/benchmarks/gc/GcUtils.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.benchmarks.gc;
+
+public class GcUtils {
+
+  public static boolean isConcurrentGcCollector(String name) {
+    return name.equals("G1 Concurrent GC");
+  }
+
+  public static boolean isOldGcCollector(String name) {
+    return name.equals("G1 Old Generation");
+  }
+
+  public static boolean isYoungGcCollector(String name) {
+    return name.equals("G1 Young Generation");
+  }
+}
diff --git a/tools/perf.py b/tools/perf.py
index d37b463..a77afec 100755
--- a/tools/perf.py
+++ b/tools/perf.py
@@ -140,6 +140,9 @@
     'SystemUIApp': {
         'targets': ['r8-full']
     },
+    'SystemUIAppGc': {
+        'targets': ['r8-full']
+    },
     'SystemUIAppPartial': {
         'targets': ['r8-full']
     },
diff --git a/tools/run_benchmark.py b/tools/run_benchmark.py
index 315a9c2..70fcde5 100755
--- a/tools/run_benchmark.py
+++ b/tools/run_benchmark.py
@@ -229,6 +229,9 @@
     if 'AGSA' in options.benchmark:
         xms = '32g'
         xmx = '32g'
+    elif options.benchmark == 'SystemUIAppGc':
+        xms = '2g'
+        xmx = '2g'
     if options.heap_size:
         xms = options.heap_size
         xmx = options.heap_size