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