Merge commit '39c661bbccaf9c252dde77da9d8a42724209d2f2' into dev-release

Change-Id: I57af52d498b045f5b3e1bf2e416dab16a6ae3bd9
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index d6185c6..5265c49 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration:2.1.1",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration:2.1.2",
   "configuration_format_version": 101,
   "required_compilation_api_level": 30,
   "synthesized_library_classes_package_prefix": "j$.",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
index a576966..a3cd551 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration_minimal:2.1.1",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration_minimal:2.1.2",
   "configuration_format_version": 101,
   "required_compilation_api_level": 24,
   "synthesized_library_classes_package_prefix": "j$.",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
index 19db3fa..05c530d 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration_nio:2.1.1",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration_nio:2.1.2",
   "configuration_format_version": 101,
   "required_compilation_api_level": 30,
   "synthesized_library_classes_package_prefix": "j$.",
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 3c858a9..316020f 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -449,7 +449,7 @@
       options.reporter.failIfPendingErrors();
       // Supply info to all additional resource consumers.
       if (!(programConsumer instanceof ConvertedCfFiles)) {
-        supplyAdditionalConsumers(appView);
+        supplyAdditionalConsumers(appView, virtualFiles);
       }
     } finally {
       timing.end();
@@ -651,7 +651,7 @@
   }
 
   @SuppressWarnings("DefaultCharset")
-  public static void supplyAdditionalConsumers(AppView<?> appView) {
+  public static void supplyAdditionalConsumers(AppView<?> appView, List<VirtualFile> virtualFiles) {
     InternalOptions options = appView.options();
     Reporter reporter = options.reporter;
     appView.getArtProfileCollection().supplyConsumers(appView);
@@ -729,7 +729,7 @@
     if (options.buildMetadataConsumer != null) {
       assert appView.hasClassHierarchy();
       options.buildMetadataConsumer.accept(
-          BuildMetadataFactory.create(appView.withClassHierarchy()));
+          BuildMetadataFactory.create(appView.withClassHierarchy(), virtualFiles));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index dc35e67..b58266c 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -80,6 +80,7 @@
   private final DexString primaryClassDescriptor;
   private final DexString primaryClassSynthesizingContextDescriptor;
   private DebugRepresentation debugRepresentation;
+  private boolean startup = false;
 
   VirtualFile(int id, AppView<?> appView) {
     this(id, appView, null, null, StartupProfile.empty());
@@ -169,6 +170,14 @@
     return debugRepresentation;
   }
 
+  public void setStartup() {
+    startup = true;
+  }
+
+  public boolean isStartup() {
+    return startup;
+  }
+
   public static String deriveCommonPrefixAndSanityCheck(List<String> fileNames) {
     Iterator<String> nameIterator = fileNames.iterator();
     String first = nameIterator.next();
@@ -1466,6 +1475,7 @@
       boolean isSingleStartupDexFile = hasSpaceForTransaction(virtualFile, options);
       if (isSingleStartupDexFile) {
         virtualFile.commitTransaction();
+        virtualFile.setStartup();
       } else {
         virtualFile.abortTransaction();
 
@@ -1473,6 +1483,7 @@
         MultiStartupDexDistributor distributor =
             MultiStartupDexDistributor.get(options, startupProfile);
         distributor.distribute(classPartioning.getStartupClasses(), this, virtualFile, cycler);
+        cycler.filesForDistribution.forEach(VirtualFile::setStartup);
 
         options.reporter.warning(
             createStartupClassesOverflowDiagnostic(cycler.filesForDistribution.size()));
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index b5b5299..20de02a 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -62,6 +62,7 @@
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
@@ -186,7 +187,7 @@
       }
       globalsConsumer.finished(appView);
     }
-    ApplicationWriter.supplyAdditionalConsumers(appView);
+    ApplicationWriter.supplyAdditionalConsumers(appView, Collections.emptyList());
   }
 
   private void writeClassCatchingErrors(
diff --git a/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java b/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
index be06792..486abda 100644
--- a/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
+++ b/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
@@ -4,13 +4,24 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.Version;
+import com.android.tools.r8.dex.VirtualFile;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
 
 public class BuildMetadataFactory {
 
-  @SuppressWarnings("UnusedVariable")
-  public static R8BuildMetadata create(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return R8BuildMetadataImpl.builder().setVersion(Version.LABEL).build();
+  public static R8BuildMetadata create(
+      AppView<? extends AppInfoWithClassHierarchy> appView, List<VirtualFile> virtualFiles) {
+    InternalOptions options = appView.options();
+    return R8BuildMetadataImpl.builder()
+        .setOptions(new R8OptionsImpl(options))
+        .setBaselineProfileRewritingOptions(R8BaselineProfileRewritingOptionsImpl.create(options))
+        .setResourceOptimizationOptions(R8ResourceOptimizationOptionsImpl.create(options))
+        .setStartupOptimizationOptions(
+            R8StartupOptimizationOptionsImpl.create(options, virtualFiles))
+        .setVersion(Version.LABEL)
+        .build();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptions.java b/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptions.java
new file mode 100644
index 0000000..7598aee
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptions.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public interface R8BaselineProfileRewritingOptions {}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptionsImpl.java
new file mode 100644
index 0000000..1585469
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptionsImpl.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.utils.InternalOptions;
+
+public class R8BaselineProfileRewritingOptionsImpl implements R8BaselineProfileRewritingOptions {
+
+  private R8BaselineProfileRewritingOptionsImpl() {}
+
+  public static R8BaselineProfileRewritingOptionsImpl create(InternalOptions options) {
+    if (options.getArtProfileOptions().getArtProfilesForRewriting().isEmpty()) {
+      return null;
+    }
+    return new R8BaselineProfileRewritingOptionsImpl();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
index e186575..c5b0dd6 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
@@ -4,15 +4,52 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializer;
 
 @KeepForApi
 public interface R8BuildMetadata {
 
   static R8BuildMetadata fromJson(String json) {
-    return new Gson().fromJson(json, R8BuildMetadataImpl.class);
+    return new GsonBuilder()
+        .excludeFieldsWithoutExposeAnnotation()
+        .registerTypeAdapter(R8Options.class, deserializeTo(R8OptionsImpl.class))
+        .registerTypeAdapter(
+            R8BaselineProfileRewritingOptions.class,
+            deserializeTo(R8BaselineProfileRewritingOptionsImpl.class))
+        .registerTypeAdapter(
+            R8KeepAttributesOptions.class, deserializeTo(R8KeepAttributesOptionsImpl.class))
+        .registerTypeAdapter(
+            R8ResourceOptimizationOptions.class,
+            deserializeTo(R8ResourceOptimizationOptionsImpl.class))
+        .registerTypeAdapter(
+            R8StartupOptimizationOptions.class,
+            deserializeTo(R8StartupOptimizationOptionsImpl.class))
+        .create()
+        .fromJson(json, R8BuildMetadataImpl.class);
   }
 
+  private static <T> JsonDeserializer<T> deserializeTo(Class<T> implClass) {
+    return (element, type, context) -> context.deserialize(element, implClass);
+  }
+
+  R8Options getOptions();
+
+  /**
+   * @return null if baseline profile rewriting is disabled.
+   */
+  R8BaselineProfileRewritingOptions getBaselineProfileRewritingOptions();
+
+  /**
+   * @return null if resource optimization is disabled.
+   */
+  R8ResourceOptimizationOptions getResourceOptimizationOptions();
+
+  /**
+   * @return null if startup optimization is disabled.
+   */
+  R8StartupOptimizationOptions getStartupOptizationOptions();
+
   String getVersion();
 
   String toJson();
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadataImpl.java
index 1032696..d59db11 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadataImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadataImpl.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.google.gson.Gson;
+import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
 
 @UsedByReflection(
@@ -20,10 +21,36 @@
     fieldAnnotatedByClassConstant = SerializedName.class)
 public class R8BuildMetadataImpl implements R8BuildMetadata {
 
+  @Expose
+  @SerializedName("options")
+  private final R8Options options;
+
+  @Expose
+  @SerializedName("baselineProfileRewritingOptions")
+  private final R8BaselineProfileRewritingOptions baselineProfileRewritingOptions;
+
+  @Expose
+  @SerializedName("resourceOptimizationOptions")
+  private final R8ResourceOptimizationOptions resourceOptimizationOptions;
+
+  @Expose
+  @SerializedName("startupOptimizationOptions")
+  private final R8StartupOptimizationOptions startupOptimizationOptions;
+
+  @Expose
   @SerializedName("version")
   private final String version;
 
-  public R8BuildMetadataImpl(String version) {
+  public R8BuildMetadataImpl(
+      R8Options options,
+      R8BaselineProfileRewritingOptions baselineProfileRewritingOptions,
+      R8ResourceOptimizationOptions resourceOptimizationOptions,
+      R8StartupOptimizationOptions startupOptimizationOptions,
+      String version) {
+    this.options = options;
+    this.baselineProfileRewritingOptions = baselineProfileRewritingOptions;
+    this.resourceOptimizationOptions = resourceOptimizationOptions;
+    this.startupOptimizationOptions = startupOptimizationOptions;
     this.version = version;
   }
 
@@ -32,6 +59,26 @@
   }
 
   @Override
+  public R8Options getOptions() {
+    return options;
+  }
+
+  @Override
+  public R8BaselineProfileRewritingOptions getBaselineProfileRewritingOptions() {
+    return baselineProfileRewritingOptions;
+  }
+
+  @Override
+  public R8ResourceOptimizationOptions getResourceOptimizationOptions() {
+    return resourceOptimizationOptions;
+  }
+
+  @Override
+  public R8StartupOptimizationOptions getStartupOptizationOptions() {
+    return startupOptimizationOptions;
+  }
+
+  @Override
   public String getVersion() {
     return version;
   }
@@ -43,15 +90,47 @@
 
   public static class Builder {
 
+    private R8Options options;
+    private R8BaselineProfileRewritingOptions baselineProfileRewritingOptions;
+    private R8ResourceOptimizationOptions resourceOptimizationOptions;
+    private R8StartupOptimizationOptions startupOptimizationOptions;
     private String version;
 
+    public Builder setOptions(R8Options options) {
+      this.options = options;
+      return this;
+    }
+
+    public Builder setBaselineProfileRewritingOptions(
+        R8BaselineProfileRewritingOptions baselineProfileRewritingOptions) {
+      this.baselineProfileRewritingOptions = baselineProfileRewritingOptions;
+      return this;
+    }
+
+    public Builder setResourceOptimizationOptions(
+        R8ResourceOptimizationOptions resourceOptimizationOptions) {
+      this.resourceOptimizationOptions = resourceOptimizationOptions;
+      return this;
+    }
+
+    public Builder setStartupOptimizationOptions(
+        R8StartupOptimizationOptions startupOptimizationOptions) {
+      this.startupOptimizationOptions = startupOptimizationOptions;
+      return this;
+    }
+
     public Builder setVersion(String version) {
       this.version = version;
       return this;
     }
 
     public R8BuildMetadataImpl build() {
-      return new R8BuildMetadataImpl(version);
+      return new R8BuildMetadataImpl(
+          options,
+          baselineProfileRewritingOptions,
+          resourceOptimizationOptions,
+          startupOptimizationOptions,
+          version);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptions.java b/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptions.java
new file mode 100644
index 0000000..b590c7e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptions.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public interface R8KeepAttributesOptions {
+
+  boolean isAnnotationDefaultKept();
+
+  boolean isEnclosingMethodKept();
+
+  boolean isExceptionsKept();
+
+  boolean isInnerClassesKept();
+
+  boolean isLocalVariableTableKept();
+
+  boolean isLocalVariableTypeTableKept();
+
+  boolean isMethodParametersKept();
+
+  boolean isPermittedSubclassesKept();
+
+  boolean isRuntimeInvisibleAnnotationsKept();
+
+  boolean isRuntimeInvisibleParameterAnnotationsKept();
+
+  boolean isRuntimeInvisibleTypeAnnotationsKept();
+
+  boolean isRuntimeVisibleAnnotationsKept();
+
+  boolean isRuntimeVisibleParameterAnnotationsKept();
+
+  boolean isRuntimeVisibleTypeAnnotationsKept();
+
+  boolean isSignatureKept();
+
+  boolean isSourceDebugExtensionKept();
+
+  boolean isSourceDirKept();
+
+  boolean isSourceFileKept();
+
+  boolean isStackMapTableKept();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptionsImpl.java
new file mode 100644
index 0000000..3e12905
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptionsImpl.java
@@ -0,0 +1,218 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+@UsedByReflection(
+    description = "Keep and preserve @SerializedName for correct (de)serialization",
+    constraints = {KeepConstraint.LOOKUP},
+    constrainAnnotations = @AnnotationPattern(constant = SerializedName.class),
+    kind = KeepItemKind.CLASS_AND_FIELDS,
+    fieldAccess = {FieldAccessFlags.PRIVATE},
+    fieldAnnotatedByClassConstant = SerializedName.class)
+public class R8KeepAttributesOptionsImpl implements R8KeepAttributesOptions {
+
+  @Expose
+  @SerializedName("isAnnotationDefaultKept")
+  private final boolean isAnnotationDefaultKept;
+
+  @Expose
+  @SerializedName("isEnclosingMethodKept")
+  private final boolean isEnclosingMethodKept;
+
+  @Expose
+  @SerializedName("isExceptionsKept")
+  private final boolean isExceptionsKept;
+
+  @Expose
+  @SerializedName("isInnerClassesKept")
+  private final boolean isInnerClassesKept;
+
+  @Expose
+  @SerializedName("isLocalVariableTableKept")
+  private final boolean isLocalVariableTableKept;
+
+  @Expose
+  @SerializedName("isLocalVariableTypeTableKept")
+  private final boolean isLocalVariableTypeTableKept;
+
+  @Expose
+  @SerializedName("isMethodParametersKept")
+  private final boolean isMethodParametersKept;
+
+  @Expose
+  @SerializedName("isPermittedSubclassesKept")
+  private final boolean isPermittedSubclassesKept;
+
+  @Expose
+  @SerializedName("isRuntimeInvisibleAnnotationsKept")
+  private final boolean isRuntimeInvisibleAnnotationsKept;
+
+  @Expose
+  @SerializedName("isRuntimeInvisibleParameterAnnotationsKept")
+  private final boolean isRuntimeInvisibleParameterAnnotationsKept;
+
+  @Expose
+  @SerializedName("isRuntimeInvisibleTypeAnnotationsKept")
+  private final boolean isRuntimeInvisibleTypeAnnotationsKept;
+
+  @Expose
+  @SerializedName("isRuntimeVisibleAnnotationsKept")
+  private final boolean isRuntimeVisibleAnnotationsKept;
+
+  @Expose
+  @SerializedName("isRuntimeVisibleParameterAnnotationsKept")
+  private final boolean isRuntimeVisibleParameterAnnotationsKept;
+
+  @Expose
+  @SerializedName("isRuntimeVisibleTypeAnnotationsKept")
+  private final boolean isRuntimeVisibleTypeAnnotationsKept;
+
+  @Expose
+  @SerializedName("isSignatureKept")
+  private final boolean isSignatureKept;
+
+  @Expose
+  @SerializedName("isSourceDebugExtensionKept")
+  private final boolean isSourceDebugExtensionKept;
+
+  @Expose
+  @SerializedName("isSourceDirKept")
+  private final boolean isSourceDirKept;
+
+  @Expose
+  @SerializedName("isSourceFileKept")
+  private final boolean isSourceFileKept;
+
+  @Expose
+  @SerializedName("isStackMapTableKept")
+  private final boolean isStackMapTableKept;
+
+  public R8KeepAttributesOptionsImpl(ProguardKeepAttributes keepAttributes) {
+    this.isAnnotationDefaultKept = keepAttributes.annotationDefault;
+    this.isEnclosingMethodKept = keepAttributes.enclosingMethod;
+    this.isExceptionsKept = keepAttributes.exceptions;
+    this.isInnerClassesKept = keepAttributes.innerClasses;
+    this.isLocalVariableTableKept = keepAttributes.localVariableTable;
+    this.isLocalVariableTypeTableKept = keepAttributes.localVariableTypeTable;
+    this.isMethodParametersKept = keepAttributes.methodParameters;
+    this.isPermittedSubclassesKept = keepAttributes.permittedSubclasses;
+    this.isRuntimeInvisibleAnnotationsKept = keepAttributes.runtimeInvisibleAnnotations;
+    this.isRuntimeInvisibleParameterAnnotationsKept =
+        keepAttributes.runtimeInvisibleParameterAnnotations;
+    this.isRuntimeInvisibleTypeAnnotationsKept = keepAttributes.runtimeInvisibleTypeAnnotations;
+    this.isRuntimeVisibleAnnotationsKept = keepAttributes.runtimeVisibleAnnotations;
+    this.isRuntimeVisibleParameterAnnotationsKept =
+        keepAttributes.runtimeVisibleParameterAnnotations;
+    this.isRuntimeVisibleTypeAnnotationsKept = keepAttributes.runtimeVisibleTypeAnnotations;
+    this.isSignatureKept = keepAttributes.signature;
+    this.isSourceDebugExtensionKept = keepAttributes.sourceDebugExtension;
+    this.isSourceDirKept = keepAttributes.sourceDir;
+    this.isSourceFileKept = keepAttributes.sourceFile;
+    this.isStackMapTableKept = keepAttributes.stackMapTable;
+  }
+
+  @Override
+  public boolean isAnnotationDefaultKept() {
+    return isAnnotationDefaultKept;
+  }
+
+  @Override
+  public boolean isEnclosingMethodKept() {
+    return isEnclosingMethodKept;
+  }
+
+  @Override
+  public boolean isExceptionsKept() {
+    return isExceptionsKept;
+  }
+
+  @Override
+  public boolean isInnerClassesKept() {
+    return isInnerClassesKept;
+  }
+
+  @Override
+  public boolean isLocalVariableTableKept() {
+    return isLocalVariableTableKept;
+  }
+
+  @Override
+  public boolean isLocalVariableTypeTableKept() {
+    return isLocalVariableTypeTableKept;
+  }
+
+  @Override
+  public boolean isMethodParametersKept() {
+    return isMethodParametersKept;
+  }
+
+  @Override
+  public boolean isPermittedSubclassesKept() {
+    return isPermittedSubclassesKept;
+  }
+
+  @Override
+  public boolean isRuntimeInvisibleAnnotationsKept() {
+    return isRuntimeInvisibleAnnotationsKept;
+  }
+
+  @Override
+  public boolean isRuntimeInvisibleParameterAnnotationsKept() {
+    return isRuntimeInvisibleParameterAnnotationsKept;
+  }
+
+  @Override
+  public boolean isRuntimeInvisibleTypeAnnotationsKept() {
+    return isRuntimeInvisibleTypeAnnotationsKept;
+  }
+
+  @Override
+  public boolean isRuntimeVisibleAnnotationsKept() {
+    return isRuntimeVisibleAnnotationsKept;
+  }
+
+  @Override
+  public boolean isRuntimeVisibleParameterAnnotationsKept() {
+    return isRuntimeVisibleParameterAnnotationsKept;
+  }
+
+  @Override
+  public boolean isRuntimeVisibleTypeAnnotationsKept() {
+    return isRuntimeVisibleTypeAnnotationsKept;
+  }
+
+  @Override
+  public boolean isSignatureKept() {
+    return isSignatureKept;
+  }
+
+  @Override
+  public boolean isSourceDebugExtensionKept() {
+    return isSourceDebugExtensionKept;
+  }
+
+  @Override
+  public boolean isSourceDirKept() {
+    return isSourceDirKept;
+  }
+
+  @Override
+  public boolean isSourceFileKept() {
+    return isSourceFileKept;
+  }
+
+  @Override
+  public boolean isStackMapTableKept() {
+    return isStackMapTableKept;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8Options.java b/src/main/java/com/android/tools/r8/metadata/R8Options.java
new file mode 100644
index 0000000..42fe2fc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8Options.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public interface R8Options {
+
+  /**
+   * @return null if no ProGuard configuration is provided.
+   */
+  R8KeepAttributesOptions getKeepAttributesOptions();
+
+  int getMinApiLevel();
+
+  boolean isAccessModificationEnabled();
+
+  boolean isDebugModeEnabled();
+
+  boolean isProGuardCompatibilityModeEnabled();
+
+  boolean isObfuscationEnabled();
+
+  boolean isOptimizationsEnabled();
+
+  boolean isShrinkingEnabled();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8OptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/R8OptionsImpl.java
new file mode 100644
index 0000000..a65dfc7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8OptionsImpl.java
@@ -0,0 +1,110 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+@UsedByReflection(
+    description = "Keep and preserve @SerializedName for correct (de)serialization",
+    constraints = {KeepConstraint.LOOKUP},
+    constrainAnnotations = @AnnotationPattern(constant = SerializedName.class),
+    kind = KeepItemKind.CLASS_AND_FIELDS,
+    fieldAccess = {FieldAccessFlags.PRIVATE},
+    fieldAnnotatedByClassConstant = SerializedName.class)
+public class R8OptionsImpl implements R8Options {
+
+  @Expose
+  @SerializedName("keepAttributesOptions")
+  private final R8KeepAttributesOptions keepAttributesOptions;
+
+  @Expose
+  @SerializedName("minApiLevel")
+  private final int minApiLevel;
+
+  @Expose
+  @SerializedName("isAccessModificationEnabled")
+  private final boolean isAccessModificationEnabled;
+
+  @Expose
+  @SerializedName("isDebugModeEnabled")
+  private final boolean isDebugModeEnabled;
+
+  @Expose
+  @SerializedName("isProGuardCompatibilityModeEnabled")
+  private final boolean isProGuardCompatibilityModeEnabled;
+
+  @Expose
+  @SerializedName("isObfuscationEnabled")
+  private final boolean isObfuscationEnabled;
+
+  @Expose
+  @SerializedName("isOptimizationsEnabled")
+  private final boolean isOptimizationsEnabled;
+
+  @Expose
+  @SerializedName("isShrinkingEnabled")
+  private final boolean isShrinkingEnabled;
+
+  public R8OptionsImpl(InternalOptions options) {
+    this.keepAttributesOptions =
+        options.hasProguardConfiguration()
+            ? new R8KeepAttributesOptionsImpl(
+                options.getProguardConfiguration().getKeepAttributes())
+            : null;
+    this.minApiLevel = options.getMinApiLevel().getLevel();
+    this.isAccessModificationEnabled = options.isAccessModificationEnabled();
+    this.isDebugModeEnabled = options.debug;
+    this.isProGuardCompatibilityModeEnabled = options.forceProguardCompatibility;
+    this.isObfuscationEnabled = options.isMinifying();
+    this.isOptimizationsEnabled = options.isOptimizing();
+    this.isShrinkingEnabled = options.isShrinking();
+  }
+
+  @Override
+  public R8KeepAttributesOptions getKeepAttributesOptions() {
+    return keepAttributesOptions;
+  }
+
+  @Override
+  public int getMinApiLevel() {
+    return minApiLevel;
+  }
+
+  @Override
+  public boolean isAccessModificationEnabled() {
+    return isAccessModificationEnabled;
+  }
+
+  @Override
+  public boolean isDebugModeEnabled() {
+    return isDebugModeEnabled;
+  }
+
+  @Override
+  public boolean isProGuardCompatibilityModeEnabled() {
+    return isProGuardCompatibilityModeEnabled;
+  }
+
+  @Override
+  public boolean isObfuscationEnabled() {
+    return isObfuscationEnabled;
+  }
+
+  @Override
+  public boolean isOptimizationsEnabled() {
+    return isOptimizationsEnabled;
+  }
+
+  @Override
+  public boolean isShrinkingEnabled() {
+    return isShrinkingEnabled;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java b/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
new file mode 100644
index 0000000..693286e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public interface R8ResourceOptimizationOptions {
+
+  boolean isOptimizedShrinkingEnabled();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptionsImpl.java
new file mode 100644
index 0000000..c9cc341
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptionsImpl.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.ResourceShrinkerConfiguration;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+@UsedByReflection(
+    description = "Keep and preserve @SerializedName for correct (de)serialization",
+    constraints = {KeepConstraint.LOOKUP},
+    constrainAnnotations = @AnnotationPattern(constant = SerializedName.class),
+    kind = KeepItemKind.CLASS_AND_FIELDS,
+    fieldAccess = {FieldAccessFlags.PRIVATE},
+    fieldAnnotatedByClassConstant = SerializedName.class)
+public class R8ResourceOptimizationOptionsImpl implements R8ResourceOptimizationOptions {
+
+  @Expose
+  @SerializedName("isOptimizedShrinkingEnabled")
+  private final boolean isOptimizedShrinkingEnabled;
+
+  private R8ResourceOptimizationOptionsImpl(
+      ResourceShrinkerConfiguration resourceShrinkerConfiguration) {
+    this.isOptimizedShrinkingEnabled = resourceShrinkerConfiguration.isOptimizedShrinking();
+  }
+
+  public static R8ResourceOptimizationOptionsImpl create(InternalOptions options) {
+    if (options.androidResourceProvider == null) {
+      return null;
+    }
+    return new R8ResourceOptimizationOptionsImpl(options.resourceShrinkerConfiguration);
+  }
+
+  @Override
+  public boolean isOptimizedShrinkingEnabled() {
+    return isOptimizedShrinkingEnabled;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptions.java b/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptions.java
new file mode 100644
index 0000000..a57b01e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptions.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public interface R8StartupOptimizationOptions {
+
+  int getNumberOfStartupDexFiles();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptionsImpl.java
new file mode 100644
index 0000000..07b0a0f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptionsImpl.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2024, 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.metadata;
+
+import com.android.tools.r8.dex.VirtualFile;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+
+@UsedByReflection(
+    description = "Keep and preserve @SerializedName for correct (de)serialization",
+    constraints = {KeepConstraint.LOOKUP},
+    constrainAnnotations = @AnnotationPattern(constant = SerializedName.class),
+    kind = KeepItemKind.CLASS_AND_FIELDS,
+    fieldAccess = {FieldAccessFlags.PRIVATE},
+    fieldAnnotatedByClassConstant = SerializedName.class)
+public class R8StartupOptimizationOptionsImpl implements R8StartupOptimizationOptions {
+
+  @Expose
+  @SerializedName("numberOfStartupDexFiles")
+  private final int numberOfStartupDexFiles;
+
+  public R8StartupOptimizationOptionsImpl(List<VirtualFile> virtualFiles) {
+    this.numberOfStartupDexFiles =
+        (int) virtualFiles.stream().filter(VirtualFile::isStartup).count();
+  }
+
+  public static R8StartupOptimizationOptionsImpl create(
+      InternalOptions options, List<VirtualFile> virtualFiles) {
+    if (options.getStartupOptions().getStartupProfileProviders().isEmpty()) {
+      return null;
+    }
+    return new R8StartupOptimizationOptionsImpl(virtualFiles);
+  }
+
+  @Override
+  public int getNumberOfStartupDexFiles() {
+    return numberOfStartupDexFiles;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsAnalysis.java
index 564ef5f..db497a7 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsAnalysis.java
@@ -222,7 +222,7 @@
       if (!ParameterRemovalUtils.canRemoveUnusedParameter(appView, method, argument.getIndex())) {
         return;
       }
-      if (!argumentValue.getType().isClassType() || argumentValue.hasDebugUsers()) {
+      if (argumentValue.hasDebugUsers()) {
         return;
       }
       Value usedValue;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java
index 445e0b0..ce5e106 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java
@@ -56,15 +56,7 @@
   public boolean stackMapTable = false;
   public boolean permittedSubclasses = false;
 
-  private ProguardKeepAttributes() {
-  }
-
-  public static ProguardKeepAttributes filterOnlySignatures() {
-    ProguardKeepAttributes result = new ProguardKeepAttributes();
-    result.applyPatterns(KEEP_ALL);
-    result.signature = false;
-    return result;
-  }
+  private ProguardKeepAttributes() {}
 
   /**
    * Implements ProGuards attribute matching rules.
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 44c289f..32658ea 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -704,6 +704,9 @@
     if (desugarGraphConsumer != null) {
       desugarGraphConsumer.finished();
     }
+    if (resourceShrinkerConfiguration.getDebugConsumer() != null) {
+      resourceShrinkerConfiguration.getDebugConsumer().finished(reporter);
+    }
   }
 
   public boolean shouldDesugarNests() {
diff --git a/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java b/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
index 47742f0..d9ad4ff 100644
--- a/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
@@ -88,12 +88,16 @@
     return new ShrinkerDebugReporter() {
       @Override
       public void debug(Supplier<String> logSupplier) {
-        consumer.accept(logSupplier.get(), diagnosticsHandler);
+        // The default usage of shrinkerdebug in the legacy resource shrinker does not add
+        // new lines. Add these to make it consistent with the normal usage of StringConsumer.
+        consumer.accept(logSupplier.get() + "\n", diagnosticsHandler);
       }
 
       @Override
       public void info(Supplier<String> logProducer) {
-        consumer.accept(logProducer.get(), diagnosticsHandler);
+        // The default usage of shrinkerdebug in the legacy resource shrinker does not add
+        // new lines. Add these to make it consistent with the normal usage of StringConsumer.
+        consumer.accept(logProducer.get() + "\n", diagnosticsHandler);
       }
 
       @Override
diff --git a/src/test/examplesJava17/records/EmptyRecord.java b/src/test/examplesJava17/records/EmptyRecord.java
deleted file mode 100644
index 5d16e44..0000000
--- a/src/test/examplesJava17/records/EmptyRecord.java
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) 2020, 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 records;
-
-public class EmptyRecord {
-
-  record Empty() {}
-
-  public static void main(String[] args) {
-    System.out.println(new Empty());
-  }
-}
diff --git a/src/test/examplesJava17/records/EmptyRecordAnnotation.java b/src/test/examplesJava17/records/EmptyRecordAnnotation.java
deleted file mode 100644
index 350e8ce..0000000
--- a/src/test/examplesJava17/records/EmptyRecordAnnotation.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2021, 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 records;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-public class EmptyRecordAnnotation {
-
-  record Empty() {}
-
-  @Retention(RetentionPolicy.RUNTIME)
-  @interface ClassAnnotation {
-    Class<? extends Record> theClass();
-  }
-
-  @ClassAnnotation(theClass = Record.class)
-  public static void annotatedMethod1() {}
-
-  @ClassAnnotation(theClass = Empty.class)
-  public static void annotatedMethod2() {}
-
-  public static void main(String[] args) throws Exception {
-    Class<?> annotatedMethod1Content =
-        EmptyRecordAnnotation.class
-            .getDeclaredMethod("annotatedMethod1")
-            .getAnnotation(ClassAnnotation.class)
-            .theClass();
-    System.out.println(annotatedMethod1Content);
-    Class<?> annotatedMethod2Content =
-        EmptyRecordAnnotation.class
-            .getDeclaredMethod("annotatedMethod2")
-            .getAnnotation(ClassAnnotation.class)
-            .theClass();
-    System.out.println(annotatedMethod2Content);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java b/src/test/examplesJava17/records/EmptyRecordAnnotationTest.java
similarity index 64%
rename from src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java
rename to src/test/examplesJava17/records/EmptyRecordAnnotationTest.java
index 84f3ea9..1759f97 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordAnnotationTest.java
+++ b/src/test/examplesJava17/records/EmptyRecordAnnotationTest.java
@@ -2,14 +2,16 @@
 // 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.desugar.records;
+package records;
 
-
+import com.android.tools.r8.JdkClassFileProvider;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.utils.StringUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -17,14 +19,11 @@
 @RunWith(Parameterized.class)
 public class EmptyRecordAnnotationTest extends TestBase {
 
-  private static final String RECORD_NAME = "EmptyRecordAnnotation";
-  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
-  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
   private static final String EXPECTED_RESULT_NATIVE_OR_PARTIALLY_DESUGARED_RECORD =
-      StringUtils.lines("class java.lang.Record", "class records.EmptyRecordAnnotation$Empty");
+      StringUtils.lines("class java.lang.Record", "class records.EmptyRecordAnnotationTest$Empty");
   private static final String EXPECTED_RESULT_DESUGARED_RECORD =
       StringUtils.lines(
-          "class com.android.tools.r8.RecordTag", "class records.EmptyRecordAnnotation$Empty");
+          "class com.android.tools.r8.RecordTag", "class records.EmptyRecordAnnotationTest$Empty");
 
   private final TestParameters parameters;
 
@@ -45,8 +44,8 @@
   public void testJvm() throws Exception {
     parameters.assumeJvmTestParameters();
     testForJvm(parameters)
-        .addProgramClassFileData(PROGRAM_DATA)
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED_RESULT_NATIVE_OR_PARTIALLY_DESUGARED_RECORD);
   }
 
@@ -54,10 +53,10 @@
   public void testD8() throws Exception {
     parameters.assumeDexRuntime();
     testForD8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
+        .addInnerClassesAndStrippedOuter(getClass())
         .setMinApi(parameters)
         .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .run(parameters.getRuntime(), TestClass.class)
         .applyIf(
             isRecordsFullyDesugaredForD8(parameters),
             r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED_RECORD),
@@ -68,23 +67,54 @@
   public void testR8() throws Exception {
     parameters.assumeR8TestParameters();
     testForR8(parameters.getBackend())
-        .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-        .addProgramClassFileData(PROGRAM_DATA)
+        .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
+        .addInnerClassesAndStrippedOuter(getClass())
         .setMinApi(parameters)
-        .addKeepRules("-keep class records.EmptyRecordAnnotation { *; }")
+        .addKeepRules("-keep class records.EmptyRecordAnnotationTest$TestClass { *; }")
         .addKeepRules("-keepattributes *Annotation*")
-        .addKeepRules("-keep class records.EmptyRecordAnnotation$Empty")
-        .addKeepMainRule(MAIN_TYPE)
+        .addKeepRules("-keep class records.EmptyRecordAnnotationTest$Empty")
+        .addKeepMainRule(TestClass.class)
         // This is used to avoid renaming com.android.tools.r8.RecordTag.
         .applyIf(
             isRecordsFullyDesugaredForR8(parameters),
             b -> b.addKeepRules("-keep class java.lang.Record"))
         .compile()
         .applyIf(parameters.isCfRuntime(), r -> r.inspect(RecordTestUtils::assertRecordsAreRecords))
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .run(parameters.getRuntime(), TestClass.class)
         .applyIf(
             isRecordsFullyDesugaredForR8(parameters),
             r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED_RECORD),
             r -> r.assertSuccessWithOutput(EXPECTED_RESULT_NATIVE_OR_PARTIALLY_DESUGARED_RECORD));
   }
+
+  record Empty() {}
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface ClassAnnotation {
+    Class<? extends Record> theClass();
+  }
+
+  public class TestClass {
+
+    @ClassAnnotation(theClass = Record.class)
+    public static void annotatedMethod1() {}
+
+    @ClassAnnotation(theClass = Empty.class)
+    public static void annotatedMethod2() {}
+
+    public static void main(String[] args) throws Exception {
+      Class<?> annotatedMethod1Content =
+          TestClass.class
+              .getDeclaredMethod("annotatedMethod1")
+              .getAnnotation(ClassAnnotation.class)
+              .theClass();
+      System.out.println(annotatedMethod1Content);
+      Class<?> annotatedMethod2Content =
+          TestClass.class
+              .getDeclaredMethod("annotatedMethod2")
+              .getAnnotation(ClassAnnotation.class)
+              .theClass();
+      System.out.println(annotatedMethod2Content);
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java b/src/test/examplesJava17/records/EmptyRecordTest.java
similarity index 78%
rename from src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
rename to src/test/examplesJava17/records/EmptyRecordTest.java
index 9c88c93..963a514 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
+++ b/src/test/examplesJava17/records/EmptyRecordTest.java
@@ -2,10 +2,11 @@
 // 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.desugar.records;
+package records;
 
 import static org.junit.Assume.assumeFalse;
 
+import com.android.tools.r8.JdkClassFileProvider;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
@@ -21,13 +22,10 @@
 @RunWith(Parameterized.class)
 public class EmptyRecordTest extends TestBase {
 
-  private static final String RECORD_NAME = "EmptyRecord";
-  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
-  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
   private static final String EXPECTED_RESULT_D8 = StringUtils.lines("Empty[]");
   private static final String EXPECTED_RESULT_R8_MINIFICATION = StringUtils.lines("a[]");
   private static final String EXPECTED_RESULT_R8_NO_MINIFICATION =
-      StringUtils.lines("EmptyRecord$Empty[]");
+      StringUtils.lines("EmptyRecordTest$Empty[]");
 
   @Parameter(0)
   public boolean enableMinification;
@@ -55,8 +53,8 @@
     assumeFalse("Only applicable for R8", enableMinification);
     parameters.assumeJvmTestParameters();
     testForJvm(parameters)
-        .addProgramClassFileData(PROGRAM_DATA)
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED_RESULT_D8);
   }
 
@@ -64,10 +62,10 @@
   public void testD8() throws Exception {
     assumeFalse("Only applicable for R8", enableMinification || enableRepackaging);
     testForD8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
+        .addInnerClassesAndStrippedOuter(getClass())
         .setMinApi(parameters)
         .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED_RESULT_D8);
   }
 
@@ -76,11 +74,11 @@
     parameters.assumeDexRuntime();
     parameters.assumeR8TestParameters();
     testForR8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .addKeepMainRule(MAIN_TYPE)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .addKeepMainRule(TestClass.class)
         .applyIf(
             parameters.isCfRuntime(),
-            testBuilder -> testBuilder.addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp)))
+            testBuilder -> testBuilder.addLibraryProvider(JdkClassFileProvider.fromSystemJdk()))
         .addDontObfuscateUnless(enableMinification)
         .applyIf(enableRepackaging, b -> b.addKeepRules("-repackageclasses p"))
         .setMinApi(parameters)
@@ -88,10 +86,19 @@
         .applyIf(
             parameters.isCfRuntime(),
             compileResult -> compileResult.inspect(RecordTestUtils::assertRecordsAreRecords))
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(
             enableMinification
                 ? EXPECTED_RESULT_R8_MINIFICATION
                 : EXPECTED_RESULT_R8_NO_MINIFICATION);
   }
+
+  record Empty() {}
+
+  public class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(new Empty());
+    }
+  }
 }
diff --git a/src/test/examplesJava17/records/RecordInstanceOf.java b/src/test/examplesJava17/records/RecordInstanceOf.java
deleted file mode 100644
index 591192c..0000000
--- a/src/test/examplesJava17/records/RecordInstanceOf.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2020, 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 records;
-
-public class RecordInstanceOf {
-
-  record Empty() {}
-
-  record Person(String name, int age) {}
-
-  public static void main(String[] args) {
-    Empty empty = new Empty();
-    Person janeDoe = new Person("Jane Doe", 42);
-    Object o = new Object();
-    System.out.println(janeDoe instanceof java.lang.Record);
-    System.out.println(empty instanceof java.lang.Record);
-    System.out.println(o instanceof java.lang.Record);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java b/src/test/examplesJava17/records/RecordInstanceOfTest.java
similarity index 64%
rename from src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
rename to src/test/examplesJava17/records/RecordInstanceOfTest.java
index 301210b..0fdb15f 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
+++ b/src/test/examplesJava17/records/RecordInstanceOfTest.java
@@ -2,8 +2,9 @@
 // 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.desugar.records;
+package records;
 
+import com.android.tools.r8.JdkClassFileProvider;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -17,9 +18,6 @@
 @RunWith(Parameterized.class)
 public class RecordInstanceOfTest extends TestBase {
 
-  private static final String RECORD_NAME = "RecordInstanceOf";
-  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
-  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
   private static final String EXPECTED_RESULT = StringUtils.lines("true", "true", "false");
 
   private final TestParameters parameters;
@@ -41,18 +39,18 @@
   public void testJvm() throws Exception {
     parameters.assumeJvmTestParameters();
     testForJvm(parameters)
-        .addProgramClassFileData(PROGRAM_DATA)
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
 
   @Test
   public void testD8() throws Exception {
     testForD8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
+        .addInnerClassesAndStrippedOuter(getClass())
         .setMinApi(parameters)
         .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
 
@@ -61,18 +59,34 @@
     parameters.assumeR8TestParameters();
     R8FullTestBuilder builder =
         testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
+            .addInnerClassesAndStrippedOuter(getClass())
             .setMinApi(parameters)
-            .addKeepMainRule(MAIN_TYPE);
+            .addKeepMainRule(TestClass.class);
     if (parameters.isCfRuntime()) {
       builder
-          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .run(parameters.getRuntime(), MAIN_TYPE)
+          .run(parameters.getRuntime(), TestClass.class)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
+    builder.run(parameters.getRuntime(), TestClass.class).assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  record Empty() {}
+
+  record Person(String name, int age) {}
+
+  public class TestClass {
+
+    public static void main(String[] args) {
+      Empty empty = new Empty();
+      Person janeDoe = new Person("Jane Doe", 42);
+      Object o = new Object();
+      System.out.println(janeDoe instanceof java.lang.Record);
+      System.out.println(empty instanceof java.lang.Record);
+      System.out.println(o instanceof java.lang.Record);
+    }
   }
 }
diff --git a/src/test/examplesJava17/records/RecordTestUtils.java b/src/test/examplesJava17/records/RecordTestUtils.java
new file mode 100644
index 0000000..6c501c0
--- /dev/null
+++ b/src/test/examplesJava17/records/RecordTestUtils.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2021, 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 records;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+
+public class RecordTestUtils {
+
+  public static void assertRecordsAreRecords(CodeInspector inspector) {
+    for (FoundClassSubject clazz : inspector.allClasses()) {
+      if (clazz.getDexProgramClass().superType.toString().equals("java.lang.Record")) {
+        assertTrue(clazz.getDexProgramClass().isRecord());
+      }
+    }
+  }
+
+  public static void assertNoJavaLangRecord(CodeInspector inspector, TestParameters parameters) {
+    if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.V)) {
+      assertFalse(inspector.clazz("java.lang.RecordTag").isPresent());
+    } else {
+      assertFalse(inspector.clazz("java.lang.Record").isPresent());
+    }
+  }
+}
diff --git a/src/test/examplesJava17/records/UnusedRecordField.java b/src/test/examplesJava17/records/UnusedRecordField.java
deleted file mode 100644
index 1d184cd..0000000
--- a/src/test/examplesJava17/records/UnusedRecordField.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2021, 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 records;
-
-public class UnusedRecordField {
-
-  Record unusedInstanceField;
-
-  void printHello() {
-    System.out.println("Hello!");
-  }
-
-  public static void main(String[] args) {
-    new UnusedRecordField().printHello();
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java b/src/test/examplesJava17/records/UnusedRecordFieldTest.java
similarity index 67%
rename from src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java
rename to src/test/examplesJava17/records/UnusedRecordFieldTest.java
index e845260..0de5cba 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordFieldTest.java
+++ b/src/test/examplesJava17/records/UnusedRecordFieldTest.java
@@ -2,8 +2,9 @@
 // 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.desugar.records;
+package records;
 
+import com.android.tools.r8.JdkClassFileProvider;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -18,9 +19,6 @@
 @RunWith(Parameterized.class)
 public class UnusedRecordFieldTest extends TestBase {
 
-  private static final String RECORD_NAME = "UnusedRecordField";
-  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
-  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
   private static final String EXPECTED_RESULT = StringUtils.lines("Hello!");
 
   @Parameter(0)
@@ -39,18 +37,18 @@
   public void testJvm() throws Exception {
     parameters.assumeJvmTestParameters();
     testForJvm(parameters)
-        .addProgramClassFileData(PROGRAM_DATA)
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .run(parameters.getRuntime(), UnusedRecordField.class)
         .assertSuccessWithOutput(EXPECTED_RESULT);
-    }
+  }
 
   @Test
   public void testD8() throws Exception {
     testForD8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
+        .addInnerClassesAndStrippedOuter(getClass())
         .setMinApi(parameters)
         .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .run(parameters.getRuntime(), UnusedRecordField.class)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
 
@@ -59,19 +57,34 @@
     parameters.assumeR8TestParameters();
     R8FullTestBuilder builder =
         testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
+            .addInnerClassesAndStrippedOuter(getClass())
             .setMinApi(parameters)
-            .addKeepRules("-keep class records.UnusedRecordField { *; }")
-            .addKeepMainRule(MAIN_TYPE);
+            .addKeepRules("-keep class records.UnusedRecordFieldTest$UnusedRecordField { *; }")
+            .addKeepMainRule(UnusedRecordField.class);
     if (parameters.isCfRuntime()) {
       builder
-          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .run(parameters.getRuntime(), MAIN_TYPE)
+          .run(parameters.getRuntime(), UnusedRecordField.class)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
+    builder
+        .run(parameters.getRuntime(), UnusedRecordField.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  public static class UnusedRecordField {
+
+    Record unusedInstanceField;
+
+    void printHello() {
+      System.out.println("Hello!");
+    }
+
+    public static void main(String[] args) {
+      new UnusedRecordField().printHello();
+    }
   }
 }
diff --git a/src/test/examplesJava17/records/UnusedRecordMethod.java b/src/test/examplesJava17/records/UnusedRecordMethod.java
deleted file mode 100644
index 342b178..0000000
--- a/src/test/examplesJava17/records/UnusedRecordMethod.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) 2021, 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 records;
-
-public class UnusedRecordMethod {
-
-  Record unusedInstanceMethod(Record unused) {
-    return null;
-  }
-
-  void printHello() {
-    System.out.println("Hello!");
-  }
-
-  public static void main(String[] args) {
-    new UnusedRecordMethod().printHello();
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java b/src/test/examplesJava17/records/UnusedRecordMethodTest.java
similarity index 66%
rename from src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java
rename to src/test/examplesJava17/records/UnusedRecordMethodTest.java
index e78d25b..9f8bd26 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordMethodTest.java
+++ b/src/test/examplesJava17/records/UnusedRecordMethodTest.java
@@ -2,8 +2,9 @@
 // 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.desugar.records;
+package records;
 
+import com.android.tools.r8.JdkClassFileProvider;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -19,9 +20,6 @@
 @RunWith(Parameterized.class)
 public class UnusedRecordMethodTest extends TestBase {
 
-  private static final String RECORD_NAME = "UnusedRecordMethod";
-  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
-  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
   private static final String EXPECTED_RESULT = StringUtils.lines("Hello!");
 
   @Parameter(0)
@@ -40,18 +38,18 @@
   public void testJvm() throws Exception {
     parameters.assumeJvmTestParameters();
     testForJvm(parameters)
-        .addProgramClassFileData(PROGRAM_DATA)
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .run(parameters.getRuntime(), UnusedRecordMethod.class)
         .assertSuccessWithOutput(EXPECTED_RESULT);
-    }
+  }
 
   @Test
   public void testD8() throws Exception {
     testForD8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
+        .addInnerClassesAndStrippedOuter(getClass())
         .setMinApi(parameters)
         .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
+        .run(parameters.getRuntime(), UnusedRecordMethod.class)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
 
@@ -60,19 +58,36 @@
     parameters.assumeR8TestParameters();
     R8FullTestBuilder builder =
         testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
+            .addInnerClassesAndStrippedOuter(getClass())
             .setMinApi(parameters)
-            .addKeepRules("-keep class records.UnusedRecordMethod { *; }")
-            .addKeepMainRule(MAIN_TYPE);
+            .addKeepRules("-keep class records.UnusedRecordMethodTest$UnusedRecordMethod { *; }")
+            .addKeepMainRule(UnusedRecordMethod.class);
     if (parameters.isCfRuntime()) {
       builder
-          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+          .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
           .compile()
           .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .run(parameters.getRuntime(), MAIN_TYPE)
+          .run(parameters.getRuntime(), UnusedRecordMethod.class)
           .assertSuccessWithOutput(EXPECTED_RESULT);
       return;
     }
-    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
+    builder
+        .run(parameters.getRuntime(), UnusedRecordMethod.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  public static class UnusedRecordMethod {
+
+    Record unusedInstanceMethod(Record unused) {
+      return null;
+    }
+
+    void printHello() {
+      System.out.println("Hello!");
+    }
+
+    public static void main(String[] args) {
+      new UnusedRecordMethod().printHello();
+    }
   }
 }
diff --git a/src/test/examplesJava17/records/UnusedRecordReflection.java b/src/test/examplesJava17/records/UnusedRecordReflection.java
deleted file mode 100644
index 1a4891b..0000000
--- a/src/test/examplesJava17/records/UnusedRecordReflection.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2021, 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 records;
-
-import java.lang.reflect.Method;
-
-public class UnusedRecordReflection {
-
-  Record instanceField;
-
-  Record method(int i, Record unused, int j) {
-    return null;
-  }
-
-  Object reflectiveGetField() {
-    try {
-      return this.getClass().getDeclaredField("instanceField").get(this);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  Object reflectiveCallMethod() {
-    try {
-      for (Method declaredMethod : this.getClass().getDeclaredMethods()) {
-        if (declaredMethod.getName().equals("method")) {
-          return declaredMethod.invoke(this, 0, null, 1);
-        }
-      }
-      throw new RuntimeException("Unreachable");
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public static void main(String[] args) {
-    System.out.println(new UnusedRecordReflection().reflectiveGetField());
-    System.out.println(new UnusedRecordReflection().reflectiveCallMethod());
-  }
-}
diff --git a/src/test/examplesJava17/records/UnusedRecordReflectionTest.java b/src/test/examplesJava17/records/UnusedRecordReflectionTest.java
new file mode 100644
index 0000000..2caa702
--- /dev/null
+++ b/src/test/examplesJava17/records/UnusedRecordReflectionTest.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2021, 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 records;
+
+import com.android.tools.r8.JdkClassFileProvider;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.reflect.Method;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UnusedRecordReflectionTest extends TestBase {
+
+  private static final String EXPECTED_RESULT = StringUtils.lines("null", "null");
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withCfRuntimesStartingFromIncluding(CfVm.JDK17)
+        .withDexRuntimes()
+        .withAllApiLevelsAlsoForCf()
+        .build();
+  }
+
+  @Test
+  public void testD8AndJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .run(parameters.getRuntime(), UnusedRecordReflection.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8(parameters.getBackend())
+        .addInnerClassesAndStrippedOuter(getClass())
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), UnusedRecordReflection.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    R8FullTestBuilder builder =
+        testForR8(parameters.getBackend())
+            .addInnerClassesAndStrippedOuter(getClass())
+            .setMinApi(parameters)
+            .addKeepRules(
+                "-keep class records.UnusedRecordReflectionTest$UnusedRecordReflection { *; }")
+            .addKeepMainRule(UnusedRecordReflection.class);
+    if (parameters.isCfRuntime()) {
+      builder
+          .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
+          .compile()
+          .inspect(RecordTestUtils::assertRecordsAreRecords)
+          .run(parameters.getRuntime(), UnusedRecordReflection.class)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    builder
+        .run(parameters.getRuntime(), UnusedRecordReflection.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  public static class UnusedRecordReflection {
+
+    Record instanceField;
+
+    Record method(int i, Record unused, int j) {
+      return null;
+    }
+
+    Object reflectiveGetField() {
+      try {
+        return this.getClass().getDeclaredField("instanceField").get(this);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    Object reflectiveCallMethod() {
+      try {
+        for (Method declaredMethod : this.getClass().getDeclaredMethods()) {
+          if (declaredMethod.getName().equals("method")) {
+            return declaredMethod.invoke(this, 0, null, 1);
+          }
+        }
+        throw new RuntimeException("Unreachable");
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public static void main(String[] args) {
+      System.out.println(new UnusedRecordReflection().reflectiveGetField());
+      System.out.println(new UnusedRecordReflection().reflectiveCallMethod());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
index 5ed2e09..1028b38 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
@@ -6,10 +6,13 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
 import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
@@ -47,6 +50,7 @@
   @Test
   public void testR8() throws Exception {
     StringBuilder log = new StringBuilder();
+    BooleanBox finished = new BooleanBox(false);
     testForR8(parameters.getBackend())
         .setMinApi(parameters)
         .addProgramClasses(FooBar.class)
@@ -59,7 +63,17 @@
                             configurationBuilder.enableOptimizedShrinkingWithR8();
                           }
                           configurationBuilder.setDebugConsumer(
-                              (string, handler) -> log.append(string + "\n"));
+                              new StringConsumer() {
+                                @Override
+                                public void accept(String string, DiagnosticsHandler handler) {
+                                  log.append(string);
+                                }
+
+                                @Override
+                                public void finished(DiagnosticsHandler handler) {
+                                  finished.set(true);
+                                }
+                              });
                           return configurationBuilder.build();
                         }))
         .addAndroidResources(getTestResources(temp))
@@ -79,6 +93,7 @@
         .assertSuccess();
     // TODO(b/360284664): Add (non compatible) logging for optimized shrinking
     if (!optimized) {
+      assertTrue(finished.get());
       // Consistent with the old AGP embedded shrinker
       List<String> strings = StringUtils.splitLines(log.toString());
       // string:bar reachable from code
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkCollection.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkCollection.java
index 4e2a4c2..e3612a0 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkCollection.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkCollection.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.benchmarks.appdumps.NowInAndroidBenchmarks;
 import com.android.tools.r8.benchmarks.appdumps.TiviBenchmarks;
 import com.android.tools.r8.benchmarks.desugaredlib.L8Benchmark;
-import com.android.tools.r8.benchmarks.desugaredlib.LegacyDesugaredLibraryBenchmark;
 import com.android.tools.r8.benchmarks.helloworld.HelloWorldBenchmark;
 import com.android.tools.r8.benchmarks.retrace.RetraceStackTraceBenchmark;
 import java.io.IOException;
@@ -64,7 +63,6 @@
     // Every benchmark that should be active on golem must be setup in this method.
     return new BenchmarkCollection(
         HelloWorldBenchmark.configs(),
-        LegacyDesugaredLibraryBenchmark.configs(),
         L8Benchmark.configs(),
         NowInAndroidBenchmarks.configs(),
         TiviBenchmarks.configs(),
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 7553ea8..6a6140a 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
@@ -242,6 +242,10 @@
     printer.accept(size);
   }
 
+  public int size() {
+    return runtimeResults.size();
+  }
+
   @Override
   public void writeResults(PrintStream out) {
     Gson gson =
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 86cfe62..90a257c 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
@@ -20,7 +20,7 @@
   public JsonElement serialize(
       BenchmarkResultsSingle result, Type type, JsonSerializationContext jsonSerializationContext) {
     JsonArray resultsArray = new JsonArray();
-    for (int iteration = 0; iteration < result.getCodeSizeResults().size(); iteration++) {
+    for (int iteration = 0; iteration < result.size(); iteration++) {
       JsonObject resultObject = new JsonObject();
       addPropertyIfValueDifferentFromRepresentative(
           resultObject,
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkTarget.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkTarget.java
index 5d52187..ad6d841 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkTarget.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkTarget.java
@@ -8,9 +8,8 @@
   // Possible dashboard targets on golem.
   // WARNING: make sure the id-name is 1:1 with tools/run_benchmark.py!
   D8("d8", "D8"),
-  R8_COMPAT("r8-compat", "R8"),
-  R8_NON_COMPAT("r8-full", "R8-full"),
-  R8_FORCE_OPT("r8-force", "R8-full-minify-optimize-shrink");
+  R8("r8-full", "R8-full"),
+  RETRACE("retrace", "retrace");
 
   private final String idName;
   private final String golemName;
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 1ba1fd0..3f7d657 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
@@ -57,6 +57,7 @@
   private String name;
   private BenchmarkDependency dumpDependency;
   private int fromRevision = -1;
+  private boolean enableLibraryDesugaring = true;
   private final List<String> programPackages = new ArrayList<>();
 
   public void verify() {
@@ -71,6 +72,11 @@
     }
   }
 
+  public AppDumpBenchmarkBuilder setEnableLibraryDesugaring(boolean enableLibraryDesugaring) {
+    this.enableLibraryDesugaring = enableLibraryDesugaring;
+    return this;
+  }
+
   public AppDumpBenchmarkBuilder setName(String name) {
     this.name = name;
     return this;
@@ -111,7 +117,7 @@
     verify();
     return BenchmarkConfig.builder()
         .setName(name)
-        .setTarget(BenchmarkTarget.R8_NON_COMPAT)
+        .setTarget(BenchmarkTarget.R8)
         .setSuite(BenchmarkSuite.OPENSOURCE_BENCHMARKS)
         .setMethod(runR8(this, configuration))
         .setFromRevision(fromRevision)
@@ -130,7 +136,7 @@
     verify();
     return BenchmarkConfig.builder()
         .setName(name)
-        .setTarget(BenchmarkTarget.R8_NON_COMPAT)
+        .setTarget(BenchmarkTarget.R8)
         .setSuite(BenchmarkSuite.OPENSOURCE_BENCHMARKS)
         .setMethod(runR8WithResourceShrinking(this, getDefaultR8Configuration()))
         .setFromRevision(fromRevision)
@@ -327,7 +333,7 @@
                       .addProgramFiles(dump.getProgramArchive())
                       .addLibraryFiles(dump.getLibraryArchive())
                       .setMinApi(dumpProperties.getMinApi())
-                      .apply(b -> addDesugaredLibrary(b, dump))
+                      .applyIf(builder.enableLibraryDesugaring, b -> addDesugaredLibrary(b, dump))
                       .benchmarkCompile(results)
                       .benchmarkCodeSize(results);
                 });
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/NowInAndroidBenchmarks.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/NowInAndroidBenchmarks.java
index c401bad..e635966 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/NowInAndroidBenchmarks.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/NowInAndroidBenchmarks.java
@@ -38,6 +38,12 @@
             .setFromRevision(16017)
             .buildBatchD8(),
         AppDumpBenchmarkBuilder.builder()
+            .setName("NowInAndroidAppNoJ$")
+            .setDumpDependencyPath(dump)
+            .setEnableLibraryDesugaring(false)
+            .setFromRevision(16017)
+            .buildBatchD8(),
+        AppDumpBenchmarkBuilder.builder()
             .setName("NowInAndroidApp")
             .setDumpDependencyPath(dump)
             .setFromRevision(16017)
diff --git a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
deleted file mode 100644
index 2d50a3d..0000000
--- a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (c) 2022, 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.desugaredlib;
-
-import com.android.tools.r8.StringResource;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.benchmarks.BenchmarkBase;
-import com.android.tools.r8.benchmarks.BenchmarkConfig;
-import com.android.tools.r8.benchmarks.BenchmarkDependency;
-import com.android.tools.r8.benchmarks.BenchmarkEnvironment;
-import com.android.tools.r8.benchmarks.BenchmarkTarget;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.google.common.collect.ImmutableList;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class LegacyDesugaredLibraryBenchmark extends BenchmarkBase {
-
-  private static final BenchmarkDependency ANDROID_JAR = BenchmarkDependency.getAndroidJar30();
-  private static final BenchmarkDependency LEGACY_CONF =
-      new BenchmarkDependency(
-          "legacyConf",
-          "1.1.5",
-          Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "desugar_jdk_libs_releases"));
-
-  public LegacyDesugaredLibraryBenchmark(BenchmarkConfig config, TestParameters parameters) {
-    super(config, parameters);
-  }
-
-  @Parameters(name = "{0}")
-  public static List<Object[]> data() {
-    return parametersFromConfigs(configs());
-  }
-
-  public static List<BenchmarkConfig> configs() {
-    return ImmutableList.of(
-        BenchmarkConfig.builder()
-            .setName("LegacyDesugaredLibraryConf")
-            .setTarget(BenchmarkTarget.D8)
-            .setFromRevision(12150)
-            .setMethod(LegacyDesugaredLibraryBenchmark::run)
-            .addDependency(ANDROID_JAR)
-            .addDependency(LEGACY_CONF)
-            .measureRunTime()
-            .build());
-  }
-
-  public static void run(BenchmarkEnvironment environment) throws Exception {
-    runner(environment)
-        .setWarmupIterations(1)
-        .setBenchmarkIterations(10)
-        .reportResultSum()
-        .run(
-            results ->
-                testForD8(environment.getTemp(), Backend.DEX)
-                    .setMinApi(AndroidApiLevel.B)
-                    .addLibraryFiles(ANDROID_JAR.getRoot(environment).resolve("android.jar"))
-                    .apply(
-                        b ->
-                            b.getBuilder()
-                                .addDesugaredLibraryConfiguration(
-                                    StringResource.fromFile(
-                                        LEGACY_CONF.getRoot(environment).resolve("desugar.json"))))
-                    .benchmarkCompile(results));
-  }
-
-  static class TestClass {
-
-    public static void main(String[] args) {
-      System.out.println(Stream.of("Hello", "world!").collect(Collectors.joining(" ")));
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/benchmarks/helloworld/HelloWorldBenchmark.java b/src/test/java/com/android/tools/r8/benchmarks/helloworld/HelloWorldBenchmark.java
index 4c8c31c..77b66a9 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/helloworld/HelloWorldBenchmark.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/helloworld/HelloWorldBenchmark.java
@@ -40,7 +40,7 @@
   public static List<BenchmarkConfig> configs() {
     Builder<BenchmarkConfig> benchmarks = ImmutableList.builder();
     makeBenchmark(BenchmarkTarget.D8, HelloWorldBenchmark::benchmarkD8, benchmarks);
-    makeBenchmark(BenchmarkTarget.R8_NON_COMPAT, HelloWorldBenchmark::benchmarkR8, benchmarks);
+    makeBenchmark(BenchmarkTarget.R8, HelloWorldBenchmark::benchmarkR8, benchmarks);
     return benchmarks.build();
   }
 
@@ -60,7 +60,7 @@
 
     public String getName() {
       // The name include each non-target option for the variants to ensure unique benchmarks.
-      String backendString = backend.isCf() ? "Cf" : "Dex";
+      String backendString = backend.isCf() ? "Cf" : "";
       String libraryString = library != null ? "" : "NoLib";
       return "HelloWorld" + backendString + libraryString;
     }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/retrace/RetraceStackTraceBenchmark.java b/src/test/java/com/android/tools/r8/benchmarks/retrace/RetraceStackTraceBenchmark.java
index c3cd9d0..73e7345 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/retrace/RetraceStackTraceBenchmark.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/retrace/RetraceStackTraceBenchmark.java
@@ -46,8 +46,8 @@
     return ImmutableList.<BenchmarkConfig>builder()
         .add(
             BenchmarkConfig.builder()
-                .setName("RetraceStackTraceWithProguardMap")
-                .setTarget(BenchmarkTarget.R8_NON_COMPAT)
+                .setName("R8")
+                .setTarget(BenchmarkTarget.RETRACE)
                 .measureRunTime()
                 .setMethod(benchmarkRetrace())
                 .setFromRevision(12266)
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeStandaloneDayTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeStandaloneDayTest.java
index 1cab68d..629217e 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeStandaloneDayTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeStandaloneDayTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.desugar.desugaredlibrary;
 
 import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8Jdk11;
 
 import com.android.tools.r8.TestParameters;
@@ -24,8 +25,8 @@
 @RunWith(Parameterized.class)
 public class DateTimeStandaloneDayTest extends DesugaredLibraryTestBase {
 
-  // TODO(b/362277530): Replace expected output when desugared library is updated.
-  private static final String EXPECTED_OUTPUT_TO_FIX =
+  // Standalone weekday names is only supported inn JDK-11 desugared library, see b/362277530.
+  private static final String EXPECTED_OUTPUT_TO_JDK8 =
       StringUtils.lines("1", "2", "3", "4", "5", "6", "7");
   private static final String EXPECTED_OUTPUT =
       StringUtils.lines(
@@ -58,10 +59,11 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
         .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutputIf(
-            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O), EXPECTED_OUTPUT)
-        .assertSuccessWithOutputIf(
-            parameters.getApiLevel().isLessThan(AndroidApiLevel.O), EXPECTED_OUTPUT_TO_FIX);
+        .applyIf(
+            libraryDesugaringSpecification == JDK11
+                || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT_TO_JDK8));
   }
 
   public static void main(String[] args) {
diff --git a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java b/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java
deleted file mode 100644
index 9dba59e..0000000
--- a/src/test/java/com/android/tools/r8/desugar/records/UnusedRecordReflectionTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (c) 2021, 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.desugar.records;
-
-import com.android.tools.r8.R8FullTestBuilder;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRuntime.CfVm;
-import com.android.tools.r8.utils.StringUtils;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class UnusedRecordReflectionTest extends TestBase {
-
-  private static final String RECORD_NAME = "UnusedRecordReflection";
-  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
-  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
-  private static final String EXPECTED_RESULT = StringUtils.lines("null", "null");
-
-  @Parameter(0)
-  public TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters()
-        .withCfRuntimesStartingFromIncluding(CfVm.JDK17)
-        .withDexRuntimes()
-        .withAllApiLevelsAlsoForCf()
-        .build();
-  }
-
-  @Test
-  public void testD8AndJvm() throws Exception {
-    parameters.assumeJvmTestParameters();
-    testForJvm(parameters)
-        .addProgramClassFileData(PROGRAM_DATA)
-        .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
-    }
-
-  @Test
-  public void testD8() throws Exception {
-    testForD8(parameters.getBackend())
-        .addProgramClassFileData(PROGRAM_DATA)
-        .setMinApi(parameters)
-        .compile()
-        .run(parameters.getRuntime(), MAIN_TYPE)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    parameters.assumeR8TestParameters();
-    R8FullTestBuilder builder =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
-            .setMinApi(parameters)
-            .addKeepRules("-keep class records.UnusedRecordReflection { *; }")
-            .addKeepMainRule(MAIN_TYPE);
-    if (parameters.isCfRuntime()) {
-      builder
-          .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-          .compile()
-          .inspect(RecordTestUtils::assertRecordsAreRecords)
-          .run(parameters.getRuntime(), MAIN_TYPE)
-          .assertSuccessWithOutput(EXPECTED_RESULT);
-      return;
-    }
-    builder.run(parameters.getRuntime(), MAIN_TYPE).assertSuccessWithOutput(EXPECTED_RESULT);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java
index 0f90b82..cec355d 100644
--- a/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java
@@ -3,12 +3,29 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.metadata;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.Version;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.startup.profile.ExternalStartupClass;
+import com.android.tools.r8.startup.profile.ExternalStartupItem;
+import com.android.tools.r8.startup.utils.StartupTestingUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -28,19 +45,63 @@
 
   @Test
   public void test() throws Exception {
+    ClassReference mainReference = Reference.classFromClass(Main.class);
+    List<ExternalStartupItem> startupProfile =
+        ImmutableList.of(ExternalStartupClass.builder().setClassReference(mainReference).build());
     R8BuildMetadata buildMetadata =
         testForR8(parameters.getBackend())
             .addInnerClasses(getClass())
             .addKeepMainRule(Main.class)
+            .addArtProfileForRewriting(
+                ExternalArtProfile.builder().addClassRule(mainReference).build())
+            .apply(StartupTestingUtils.addStartupProfile(startupProfile))
+            .applyIf(
+                parameters.isDexRuntime(),
+                testBuilder ->
+                    testBuilder.addAndroidResources(getTestResources()).enableOptimizedShrinking())
+            .allowDiagnosticInfoMessages(parameters.canUseNativeMultidex())
             .collectBuildMetadata()
             .setMinApi(parameters)
-            .compile()
+            .compileWithExpectedDiagnostics(
+                diagnostics -> {
+                  if (parameters.canUseNativeMultidex()) {
+                    diagnostics.assertInfosMatch(
+                        diagnosticMessage(containsString("Startup DEX files contains")));
+                  } else {
+                    diagnostics.assertNoMessages();
+                  }
+                })
             .getBuildMetadata();
     String json = buildMetadata.toJson();
     // Inspecting the exact contents is not important here, but it *is* important to test that the
     // property names are unobfuscated when testing with R8lib (!).
-    assertEquals("{\"version\":\"" + Version.LABEL + "\"}", json);
+    assertThat(json, containsString("\"version\":\"" + Version.LABEL + "\""));
     buildMetadata = R8BuildMetadata.fromJson(json);
+    inspectDeserializedBuildMetadata(buildMetadata);
+  }
+
+  private AndroidTestResource getTestResources() throws IOException {
+    return new AndroidTestResourceBuilder().withSimpleManifestAndAppNameString().build(temp);
+  }
+
+  private void inspectDeserializedBuildMetadata(R8BuildMetadata buildMetadata) {
+    assertNotNull(buildMetadata.getBaselineProfileRewritingOptions());
+    assertNotNull(buildMetadata.getOptions());
+    assertNotNull(buildMetadata.getOptions().getKeepAttributesOptions());
+    if (parameters.isDexRuntime()) {
+      R8ResourceOptimizationOptions resourceOptimizationOptions =
+          buildMetadata.getResourceOptimizationOptions();
+      assertNotNull(resourceOptimizationOptions);
+      assertTrue(resourceOptimizationOptions.isOptimizedShrinkingEnabled());
+    } else {
+      assertNull(buildMetadata.getResourceOptimizationOptions());
+    }
+    R8StartupOptimizationOptions startupOptimizationOptions =
+        buildMetadata.getStartupOptizationOptions();
+    assertNotNull(startupOptimizationOptions);
+    assertEquals(
+        parameters.isDexRuntime() && parameters.canUseNativeMultidex() ? 1 : 0,
+        startupOptimizationOptions.getNumberOfStartupDexFiles());
     assertEquals(Version.LABEL, buildMetadata.getVersion());
   }
 
diff --git a/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1 b/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
index ac5fd5a..9f5883e 100644
--- a/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
+++ b/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
@@ -1 +1 @@
-f4f486f37cf801466634ffbf584adb80dc839f4d
\ No newline at end of file
+f3213584e94bf6951c3765c6b8d23405c332ca1b
\ No newline at end of file
diff --git a/tools/perf.py b/tools/perf.py
index 5d3cb2b..fec3449 100755
--- a/tools/perf.py
+++ b/tools/perf.py
@@ -15,11 +15,59 @@
 if utils.is_bot():
     import upload_benchmark_data_to_google_storage
 
-APPS = [
-    'ChromeApp', 'CraneApp', 'JetLaggedApp', 'JetNewsApp', 'JetCasterApp',
-    'JetChatApp', 'JetSnackApp', 'NowInAndroidApp', 'OwlApp', 'ReplyApp',
-    'TiviApp'
-]
+BENCHMARKS = {
+    'ChromeApp': {
+        'targets': ['r8-full']
+    },
+    'CraneApp': {
+        'targets': ['r8-full']
+    },
+    'HelloWorld': {
+        'targets': ['d8']
+    },
+    'HelloWorldNoLib': {
+        'targets': ['d8']
+    },
+    'HelloWorldCf': {
+        'targets': ['d8']
+    },
+    'HelloWorldCfNoLib': {
+        'targets': ['d8']
+    },
+    'JetLaggedApp': {
+        'targets': ['r8-full']
+    },
+    'JetNewsApp': {
+        'targets': ['r8-full']
+    },
+    'JetCasterApp': {
+        'targets': ['r8-full']
+    },
+    'JetChatApp': {
+        'targets': ['r8-full']
+    },
+    'JetSnackApp': {
+        'targets': ['r8-full']
+    },
+    'NowInAndroidApp': {
+        'targets': ['d8', 'r8-full']
+    },
+    'NowInAndroidAppNoJ$': {
+        'targets': ['d8']
+    },
+    'OwlApp': {
+        'targets': ['r8-full']
+    },
+    'R8': {
+        'targets': ['retrace']
+    },
+    'ReplyApp': {
+        'targets': ['r8-full']
+    },
+    'TiviApp': {
+        'targets': ['r8-full']
+    },
+}
 BUCKET = "r8-perf-results"
 SAMPLE_BENCHMARK_RESULT_JSON = {
     'benchmark_name': '<benchmark_name>',
@@ -37,8 +85,8 @@
 # meta contains information about the execution (machine)
 def ParseOptions():
     result = argparse.ArgumentParser()
-    result.add_argument('--app',
-                        help='Specific app(s) to measure.',
+    result.add_argument('--benchmark',
+                        help='Specific benchmark(s) to measure.',
                         action='append')
     result.add_argument('--iterations',
                         help='How many times run_benchmark is run.',
@@ -54,10 +102,10 @@
                         help='Skip if output exists.',
                         action='store_true',
                         default=False)
-    result.add_argument('--target',
-                        help='Specific target to run on.',
-                        default='r8-full',
-                        choices=['d8', 'r8-full', 'r8-force', 'r8-compat'])
+    result.add_argument(
+        '--target',
+        help='Specific target to run on.',
+        choices=['d8', 'r8-full', 'r8-force', 'r8-compat', 'retrace'])
     result.add_argument('--verbose',
                         help='To enable verbose logging.',
                         action='store_true',
@@ -67,21 +115,22 @@
                         help='Use R8 hash for the run (default local build)',
                         default=None)
     options, args = result.parse_known_args()
-    options.apps = options.app or APPS
+    options.benchmarks = options.benchmark or BENCHMARKS.keys()
     options.quiet = not options.verbose
-    del options.app
+    del options.benchmark
     return options, args
 
 
 def Build(options):
     utils.Print('Building', quiet=options.quiet)
-    build_cmd = GetRunCmd('N/A', options, ['--iterations', '0'])
+    target = options.target or 'r8-full'
+    build_cmd = GetRunCmd('N/A', target, options, ['--iterations', '0'])
     subprocess.check_call(build_cmd)
 
 
-def GetRunCmd(app, options, args):
+def GetRunCmd(benchmark, target, options, args):
     base_cmd = [
-        'tools/run_benchmark.py', '--benchmark', app, '--target', options.target
+        'tools/run_benchmark.py', '--benchmark', benchmark, '--target', target
     ]
     if options.verbose:
         base_cmd.append('--verbose')
@@ -119,9 +168,9 @@
         return json.loads(''.join(lines))
 
 
-def GetArtifactLocation(app, target, version, filename):
+def GetArtifactLocation(benchmark, target, version, filename):
     version_or_head = version or utils.get_HEAD_sha1()
-    return f'{app}/{target}/{version_or_head}/{filename}'
+    return f'{benchmark}/{target}/{version_or_head}/{filename}'
 
 
 def GetGSLocation(filename, bucket=BUCKET):
@@ -155,62 +204,70 @@
             download_options = argparse.Namespace(no_build=True, nolib=True)
             r8jar = compiledump.download_distribution(options.version,
                                                       download_options, temp)
-        for app in options.apps:
-            if options.skip_if_output_exists:
-                if options.outdir:
-                    raise NotImplementedError
-                output = GetGSLocation(
-                    GetArtifactLocation(app, options.target, options.version,
-                                        'result.json'))
-                if utils.cloud_storage_exists(output):
-                    print(f'Skipping run, {output} already exists.')
+        for benchmark in options.benchmarks:
+            targets = [options.target
+                      ] if options.target else BENCHMARKS[benchmark]['targets']
+            for target in targets:
+                if options.skip_if_output_exists:
+                    if options.outdir:
+                        raise NotImplementedError
+                    output = GetGSLocation(
+                        GetArtifactLocation(benchmark, target, options.version,
+                                            'result.json'))
+                    if utils.cloud_storage_exists(output):
+                        print(f'Skipping run, {output} already exists.')
+                        continue
+
+                # Run benchmark.
+                benchmark_result_json_files = []
+                failed = False
+                for i in range(options.iterations):
+                    utils.Print(
+                        f'Benchmarking {benchmark} ({i+1}/{options.iterations})',
+                        quiet=options.quiet)
+                    benchhmark_result_file = os.path.join(
+                        temp, f'result_file_{i}')
+                    iteration_cmd = GetRunCmd(benchmark, target, options, [
+                        '--iterations',
+                        str(options.iterations_inner), '--output',
+                        benchhmark_result_file, '--no-build'
+                    ])
+                    try:
+                        subprocess.check_call(iteration_cmd)
+                        benchmark_result_json_files.append(
+                            benchhmark_result_file)
+                    except subprocess.CalledProcessError as e:
+                        failed = True
+                        any_failed = True
+                        break
+
+                if failed:
                     continue
 
-            # Run benchmark.
-            benchmark_result_json_files = []
-            failed = False
-            for i in range(options.iterations):
-                utils.Print(f'Benchmarking {app} ({i+1}/{options.iterations})',
-                            quiet=options.quiet)
-                benchhmark_result_file = os.path.join(temp, f'result_file_{i}')
-                iteration_cmd = GetRunCmd(app, options, [
-                    '--iterations',
-                    str(options.iterations_inner), '--output',
-                    benchhmark_result_file, '--no-build'
-                ])
-                try:
-                    subprocess.check_call(iteration_cmd)
-                    benchmark_result_json_files.append(benchhmark_result_file)
-                except subprocess.CalledProcessError as e:
-                    failed = True
-                    any_failed = True
-                    break
-
-            if failed:
-                continue
-
-            # Merge results and write output.
-            result_file = os.path.join(temp, 'result_file')
-            with open(result_file, 'w') as f:
-                json.dump(
-                    MergeBenchmarkResultJsonFiles(benchmark_result_json_files),
-                    f)
-            ArchiveOutputFile(result_file,
-                              GetArtifactLocation(app, options.target,
-                                                  options.version,
-                                                  'result.json'),
-                              outdir=options.outdir)
-
-            # Write metadata.
-            if utils.is_bot():
-                meta_file = os.path.join(temp, "meta")
-                with open(meta_file, 'w') as f:
-                    f.write("Produced by: " + os.environ.get('SWARMING_BOT_ID'))
-                ArchiveOutputFile(meta_file,
-                                  GetArtifactLocation(app, options.target,
-                                                      options.version, 'meta'),
+                # Merge results and write output.
+                result_file = os.path.join(temp, 'result_file')
+                with open(result_file, 'w') as f:
+                    json.dump(
+                        MergeBenchmarkResultJsonFiles(
+                            benchmark_result_json_files), f)
+                ArchiveOutputFile(result_file,
+                                  GetArtifactLocation(benchmark, target,
+                                                      options.version,
+                                                      'result.json'),
                                   outdir=options.outdir)
 
+                # Write metadata.
+                if utils.is_bot():
+                    meta_file = os.path.join(temp, "meta")
+                    with open(meta_file, 'w') as f:
+                        f.write("Produced by: " +
+                                os.environ.get('SWARMING_BOT_ID'))
+                    ArchiveOutputFile(meta_file,
+                                      GetArtifactLocation(
+                                          benchmark, target, options.version,
+                                          'meta'),
+                                      outdir=options.outdir)
+
     if utils.is_bot():
         upload_benchmark_data_to_google_storage.run()
 
diff --git a/tools/perf/chart.js b/tools/perf/chart.js
new file mode 100644
index 0000000..96c7f07
--- /dev/null
+++ b/tools/perf/chart.js
@@ -0,0 +1,117 @@
+// Copyright (c) 2024, 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.
+import dom from "./dom.js";
+import scales from "./scales.js";
+import state from "./state.js";
+import url from "./url.js";
+
+var chart = null;
+var dataProvider = null;
+
+function getBenchmarkColors(theChart) {
+  const benchmarkColors = {};
+  for (var datasetIndex = 0;
+      datasetIndex < theChart.data.datasets.length;
+      datasetIndex++) {
+    if (theChart.getDatasetMeta(datasetIndex).hidden) {
+      continue;
+    }
+    const dataset = theChart.data.datasets[datasetIndex];
+    const benchmark = dataset.benchmark;
+    const benchmarkColor = dataset.borderColor;
+    if (!(benchmark in benchmarkColors)) {
+      benchmarkColors[benchmark] = benchmarkColor;
+    }
+  }
+  return benchmarkColors;
+}
+
+function get() {
+  return chart;
+}
+
+function getData() {
+  const filteredCommits = state.commits(state.zoom);
+  return dataProvider(filteredCommits);
+}
+
+function getDataLabelFormatter(value, context) {
+  var percentageChange = getDataPercentageChange(context);
+  var percentageChangeTwoDecimals = Math.round(percentageChange * 100) / 100;
+  var glyph = percentageChange < 0 ? 'â–¼' : 'â–²';
+  return glyph + ' ' + percentageChangeTwoDecimals + '%';
+}
+
+function getDataPercentageChange(context) {
+  var i = context.dataIndex;
+  var value = context.dataset.data[i];
+  var j = i;
+  var previousValue;
+  do {
+    if (j == 0) {
+      return null;
+    }
+    previousValue = context.dataset.data[--j];
+  } while (previousValue === undefined || isNaN(previousValue));
+  return (value - previousValue) / previousValue * 100;
+}
+
+function initializeChart(options) {
+  chart = new Chart(dom.canvas, {
+    data: getData(),
+    options: options,
+    plugins: [ChartDataLabels]
+  });
+  // Hide disabled legends.
+  if (state.selectedLegends.size < state.legends.size) {
+    update(false, true);
+  } else {
+    update(false, false);
+  }
+}
+
+function setDataProvider(theDataProvider) {
+ dataProvider = theDataProvider;
+}
+
+function update(dataChanged, legendsChanged) {
+  console.assert(state.zoom.left <= state.zoom.right);
+
+  // Update datasets.
+  if (dataChanged) {
+    const newData = getData();
+    Object.assign(chart.data, newData);
+    // Update chart.
+    chart.update();
+  }
+
+  // Update legends.
+  if (legendsChanged || (dataChanged && state.selectedLegends.size < state.legends.size)) {
+    for (var datasetIndex = 0;
+        datasetIndex < chart.data.datasets.length;
+        datasetIndex++) {
+      const datasetMeta = chart.getDatasetMeta(datasetIndex);
+      datasetMeta.hidden = !state.isLegendSelected(datasetMeta.label);
+    }
+
+    // Update scales.
+    scales.update(chart.options.scales);
+
+    // Update chart.
+    chart.update();
+  }
+
+  dom.updateBenchmarkColors(getBenchmarkColors(chart));
+  dom.updateChartNavigation();
+  url.updateHash(state);
+}
+
+export default {
+  get: get,
+  getDataLabelFormatter: getDataLabelFormatter,
+  getDataPercentageChange: getDataPercentageChange,
+  initializeChart: initializeChart,
+  setDataProvider: setDataProvider,
+  update: update
+};
diff --git a/tools/perf/d8.html b/tools/perf/d8.html
new file mode 100644
index 0000000..bd9c867
--- /dev/null
+++ b/tools/perf/d8.html
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>D8 perf</title>
+  <link rel="stylesheet" href="stylesheet.css">
+</head>
+<body>
+  <div id="benchmark-selectors"></div>
+  <div>
+      <canvas id="myChart"></canvas>
+  </div>
+  <div>
+    <div style="float: left; width: 50%">
+      <button type="button" id="show-more-left" disabled>⇐</button>
+      <button type="button" id="show-less-left">⇒</button>
+    </div>
+    <div style="float: left; text-align: right; width: 50%">
+      <button type="button" id="show-less-right">⇐</button>
+      <button type="button" id="show-more-right" disabled>⇒</button>
+    </div>
+  </div>
+  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
+  <script src="extensions.js"></script>
+  <script src="utils.js"></script>
+  <script type="module">
+    import chart from "./chart.js";
+    import dom from "./dom.js";
+    import scales from "./scales.js";
+    import state from "./state.js";
+
+    const commits = await state.importCommits("./d8_benchmark_data.json");
+    state.initializeBenchmarks();
+    state.initializeLegends({
+      'Dex size': { default: true },
+      'Nondeterminism': { default: true },
+      'Runtime': { default: true },
+      'Runtime variance': { default: false }
+    });
+    state.initializeZoom();
+    dom.initializeBenchmarkSelectors();
+    dom.initializeChartNavigation();
+
+    // Chart data provider.
+    function getData(filteredCommits) {
+      const labels = filteredCommits.map((c, i) => c.index);
+      const datasets = getDatasets(filteredCommits);
+      return {
+        labels: labels,
+        datasets: datasets
+      };
+    }
+
+    function getDatasets(filteredCommits) {
+      const datasets = [];
+      state.forEachSelectedBenchmark(
+        selectedBenchmark => {
+          const codeSizeData =
+              filteredCommits.map(
+                  (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "code_size"));
+          const codeSizeScatterData = [];
+          for (const commit of filteredCommits.values()) {
+            if (!(selectedBenchmark in commit.benchmarks)) {
+              continue;
+            }
+            const seen = new Set();
+            seen.add(getSingleResult(selectedBenchmark, commit, "code_size"));
+            const codeSizes = getAllResults(selectedBenchmark, commit, "code_size")
+            for (const codeSize of codeSizes.values()) {
+              if (!seen.has(codeSize)) {
+                codeSizeScatterData.push({ x: commit.index, y: codeSize });
+                seen.add(codeSize);
+              }
+            }
+          }
+          const runtimeData =
+              filteredCommits.map(
+                  (c, i) =>
+                      selectedBenchmark in filteredCommits[i].benchmarks
+                          ? getAllResults(selectedBenchmark, filteredCommits[i], "runtime")
+                              .min()
+                              .ns_to_s()
+                          : NaN);
+          const runtimeScatterData = [];
+          for (const commit of filteredCommits.values()) {
+            if (!(selectedBenchmark in commit.benchmarks)) {
+              continue;
+            }
+            const runtimes = getAllResults(selectedBenchmark, commit, "runtime")
+            for (const runtime of runtimes.values()) {
+              runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() });
+            }
+          }
+
+          const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;
+          datasets.push(...[
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Dex size',
+              data: codeSizeData,
+              datalabels: {
+                align: 'end',
+                anchor: 'end'
+              },
+              tension: 0.1,
+              yAxisID: 'y',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'scatter',
+              label: 'Nondeterminism',
+              data: codeSizeScatterData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              radius: 6,
+              pointBackgroundColor: 'red'
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Runtime',
+              data: runtimeData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              tension: 0.1,
+              yAxisID: 'y_runtime',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'scatter',
+              label: 'Runtime variance',
+              data: runtimeScatterData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              yAxisID: 'y_runtime'
+            }
+          ]);
+        });
+      return datasets;
+    }
+
+    // Chart options.
+    const options = {
+      onHover: (event, chartElement) =>
+          event.native.target.style.cursor =
+              chartElement[0] ? 'pointer' : 'default',
+      plugins: {
+        datalabels: {
+          backgroundColor: 'rgba(255, 255, 255, 0.7)',
+          borderColor: 'rgba(128, 128, 128, 0.7)',
+          borderRadius: 4,
+          borderWidth: 1,
+          color: context => chart.getDataPercentageChange(context) < 0 ? 'green' : 'red',
+          display: context => {
+            var percentageChange = chart.getDataPercentageChange(context);
+            return percentageChange !== null && Math.abs(percentageChange) >= 0.1;
+          },
+          font: {
+            size: 20,
+            weight: 'bold'
+          },
+          offset: 8,
+          formatter: chart.getDataLabelFormatter,
+          padding: 6
+        },
+        legend: {
+          labels: {
+            filter: (legendItem, data) => {
+              // Only retain the legends for the first selected benchmark. If
+              // multiple benchmarks are selected, then use the legends of the
+              // first selected benchmark to control all selected benchmarks.
+              const numUniqueLegends =
+                  data.datasets.length / state.selectedBenchmarks.size;
+              return legendItem.datasetIndex < numUniqueLegends;
+            },
+          },
+          onClick: (e, legendItem, legend) => {
+            const clickedLegend = legendItem.text;
+            if (state.selectedLegends.has(clickedLegend)) {
+              state.selectedLegends.delete(clickedLegend);
+            } else {
+              state.selectedLegends.add(clickedLegend);
+            }
+            chart.update(false, true);
+          },
+        },
+        tooltip: {
+          callbacks: {
+            title: context => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[state.zoom.left + elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              return commit.title;
+            },
+            footer: context => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[state.zoom.left + elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              const dataset = chart.get().data.datasets[elementInfo.datasetIndex];
+              return `App: ${dataset.benchmark}\n`
+                  + `Author: ${commit.author}\n`
+                  + `Submitted: ${new Date(commit.submitted * 1000).toLocaleString()}\n`
+                  + `Hash: ${commit.hash}\n`
+                  + `Index: ${commit.index}`;
+            }
+          }
+        }
+      },
+      responsive: true,
+      scales: scales.get()
+    };
+
+    chart.setDataProvider(getData);
+    chart.initializeChart(options);
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/tools/perf/dom.js b/tools/perf/dom.js
new file mode 100644
index 0000000..101c588
--- /dev/null
+++ b/tools/perf/dom.js
@@ -0,0 +1,125 @@
+// Copyright (c) 2024, 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.
+import chart from "./chart.js";
+import state from "./state.js";
+
+// DOM references.
+const benchmarkSelectors = document.getElementById('benchmark-selectors');
+const canvas = document.getElementById('myChart');
+const showMoreLeft = document.getElementById('show-more-left');
+const showLessLeft = document.getElementById('show-less-left');
+const showLessRight = document.getElementById('show-less-right');
+const showMoreRight = document.getElementById('show-more-right');
+
+function initializeBenchmarkSelectors() {
+  state.forEachBenchmark(
+    (benchmark, selected) => {
+      const input = document.createElement('input');
+      input.type = 'checkbox';
+      input.name = 'benchmark';
+      input.id = benchmark;
+      input.value = benchmark;
+      input.checked = selected;
+      input.onchange = function (e) {
+        if (e.target.checked) {
+          state.selectedBenchmarks.add(e.target.value);
+        } else {
+          state.selectedBenchmarks.delete(e.target.value);
+        }
+        chart.update(true, false);
+      };
+
+      const label = document.createElement('label');
+      label.id = benchmark + 'Label';
+      label.htmlFor = benchmark;
+      label.innerHTML = benchmark;
+
+      benchmarkSelectors.appendChild(input);
+      benchmarkSelectors.appendChild(label);
+    });
+}
+
+function initializeChartNavigation() {
+  const zoom = state.zoom;
+
+  canvas.onclick = event => {
+    const points =
+        chart.get().getElementsAtEventForMode(
+            event, 'nearest', { intersect: true }, true);
+    if (points.length > 0) {
+      const point = points[0];
+      const commit = state.commits[point.index];
+      window.open('https://r8.googlesource.com/r8/+/' + commit.hash, '_blank');
+    }
+  };
+
+  showMoreLeft.onclick = event => {
+    if (zoom.left == 0) {
+      return;
+    }
+    const currentSize = zoom.right - zoom.left;
+    zoom.left = zoom.left - currentSize;
+    if (zoom.left < 0) {
+      zoom.left = 0;
+    }
+    chart.update(true, false);
+  };
+
+  showLessLeft.onclick = event => {
+    const currentSize = zoom.right - zoom.left;
+    zoom.left = zoom.left + Math.floor(currentSize / 2);
+    if (zoom.left >= zoom.right) {
+      zoom.left = zoom.right - 1;
+    }
+    chart.update(true, false);
+  };
+
+  showLessRight.onclick = event => {
+    if (zoom.right == 0) {
+      return;
+    }
+    const currentSize = zoom.right - zoom.left;
+    zoom.right = zoom.right - Math.floor(currentSize / 2);
+    if (zoom.right < zoom.left) {
+      zoom.right = zoom.left;
+    }
+    chart.update(true, false);
+  };
+
+  showMoreRight.onclick = event => {
+    const currentSize = zoom.right - zoom.left;
+    zoom.right = zoom.right + currentSize;
+    if (zoom.right > state.commits().length) {
+      zoom.right = state.commits().length;
+    }
+    chart.update(true, false);
+  };
+}
+
+function updateBenchmarkColors(benchmarkColors) {
+  state.forEachBenchmark(
+    benchmark => {
+      const benchmarkLabel = document.getElementById(benchmark + 'Label');
+      const benchmarkColor = benchmarkColors[benchmark] || '#000000';
+      const benchmarkFontWeight = benchmark in benchmarkColors ? 'bold' : 'normal';
+      benchmarkLabel.style.color = benchmarkColor;
+      benchmarkLabel.style.fontWeight = benchmarkFontWeight;
+    });
+}
+
+function updateChartNavigation() {
+  const zoom = state.zoom;
+  showMoreLeft.disabled = zoom.left == 0;
+  showLessLeft.disabled = zoom.left == zoom.right - 1;
+  showLessRight.disabled = zoom.left == zoom.right - 1;
+  showMoreRight.disabled = zoom.right == state.commits().length;
+}
+
+export default {
+  canvas: canvas,
+  initializeBenchmarkSelectors: initializeBenchmarkSelectors,
+  initializeChartNavigation: initializeChartNavigation,
+  updateBenchmarkColors: updateBenchmarkColors,
+  updateChartNavigation: updateChartNavigation
+};
\ No newline at end of file
diff --git a/tools/perf/extensions.js b/tools/perf/extensions.js
new file mode 100644
index 0000000..1aa20e0
--- /dev/null
+++ b/tools/perf/extensions.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2024, 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.
+Array.prototype.any = function(predicate) {
+  for (const element of this.values()) {
+    if (predicate(element)) {
+      return true;
+    }
+  }
+  return false;
+};
+Array.prototype.first = function() {
+  return this[0];
+};
+Array.prototype.avg = function() {
+  return this.reduce(function(x, y) { return x + y; }, 0) / this.length;
+};
+Array.prototype.min = function() {
+  return this.reduce(function(x, y) { return x === null ? y : Math.min(x, y); }, null);
+};
+Array.prototype.reverseInPlace = function() {
+  for (var i = 0; i < Math.floor(this.length / 2); i++) {
+    var temp = this[i];
+    this[i] = this[this.length - i - 1];
+    this[this.length - i - 1] = temp;
+  }
+};
+Number.prototype.ns_to_s = function() {
+  const seconds = this/10E8;
+  const seconds_with_one_decimal = Math.round(seconds*10)/10;
+  return seconds;
+};
\ No newline at end of file
diff --git a/tools/perf/index.html b/tools/perf/index.html
deleted file mode 100644
index 8d4a6ef..0000000
--- a/tools/perf/index.html
+++ /dev/null
@@ -1,649 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>R8 perf</title>
-  <link rel="stylesheet" href="stylesheet.css">
-</head>
-<body>
-  <div id="benchmark-selectors"></div>
-  <div>
-      <canvas id="myChart"></canvas>
-  </div>
-  <div>
-    <div style="float: left; width: 50%">
-      <button type="button" id="show-more-left" disabled>⇐</button>
-      <button type="button" id="show-less-left">⇒</button>
-    </div>
-    <div style="float: left; text-align: right; width: 50%">
-      <button type="button" id="show-less-right">⇐</button>
-      <button type="button" id="show-more-right" disabled>⇒</button>
-    </div>
-  </div>
-  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
-  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
-  <script type="module">
-    // Utility methods.
-    Array.prototype.any = function(predicate) {
-      for (const element of this.values()) {
-        if (predicate(element)) {
-          return true;
-        }
-      }
-      return false;
-    };
-    Array.prototype.first = function() {
-      return this[0];
-    };
-    Array.prototype.avg = function() {
-      return this.reduce(function(x, y) { return x + y; }, 0) / this.length;
-    };
-    Array.prototype.min = function() {
-      return this.reduce(function(x, y) { return x === null ? y : Math.min(x, y); }, null);
-    };
-    Array.prototype.reverseInPlace = function() {
-      for (var i = 0; i < Math.floor(this.length / 2); i++) {
-        var temp = this[i];
-        this[i] = this[this.length - i - 1];
-        this[this.length - i - 1] = temp;
-      }
-    };
-    Number.prototype.ns_to_s = function() {
-      const seconds = this/10E8;
-      const seconds_with_one_decimal = Math.round(seconds*10)/10;
-      return seconds;
-    };
-
-    // Import and reverse commits so that newest are last.
-    import commits from "./benchmark_data.json" with { type: "json" };
-    commits.reverseInPlace();
-
-    // Amend the commits with their unique index.
-    for (var i = 0; i < commits.length; i++) {
-      commits[i].index = i;
-    }
-
-    // DOM references.
-    const benchmarkSelectors = document.getElementById('benchmark-selectors');
-    const canvas = document.getElementById('myChart');
-    const showMoreLeft = document.getElementById('show-more-left');
-    const showLessLeft = document.getElementById('show-less-left');
-    const showLessRight = document.getElementById('show-less-right');
-    const showMoreRight = document.getElementById('show-more-right');
-
-    // Initialize benchmark selectors.
-    const benchmarks = new Set();
-    for (const commit of commits.values()) {
-      for (const benchmark in commit.benchmarks) {
-          benchmarks.add(benchmark);
-      }
-    }
-    const selectedBenchmarks = new Set();
-    const urlOptions = unescape(window.location.hash.substring(1)).split(',');
-    for (const benchmark of benchmarks.values()) {
-      for (const filter of urlOptions.values()) {
-        if (filter) {
-          const match = benchmark.match(new RegExp(filter.replace("*", ".*")));
-          if (match) {
-            selectedBenchmarks.add(benchmark);
-            break;
-          }
-        }
-      }
-    }
-    if (selectedBenchmarks.size == 0) {
-      const randomBenchmarkIndex = Math.floor(Math.random() * benchmarks.size);
-      const randomBenchmark = Array.from(benchmarks)[randomBenchmarkIndex];
-      selectedBenchmarks.add(randomBenchmark);
-    }
-    for (const benchmark of benchmarks.values()) {
-      const input = document.createElement('input');
-      input.type = 'checkbox';
-      input.name = 'benchmark';
-      input.id = benchmark;
-      input.value = benchmark;
-      input.checked = selectedBenchmarks.has(benchmark);
-      input.onchange = function (e) {
-        if (e.target.checked) {
-          selectedBenchmarks.add(e.target.value);
-        } else {
-          selectedBenchmarks.delete(e.target.value);
-        }
-        updateChart(true, false);
-      };
-
-      const label = document.createElement('label');
-      label.id = benchmark + 'Label';
-      label.htmlFor = benchmark;
-      label.innerHTML = benchmark;
-
-      benchmarkSelectors.appendChild(input);
-      benchmarkSelectors.appendChild(label);
-    }
-
-    function getSingleResult(benchmark, commit, resultName, resultIteration = 0) {
-      if (!(benchmark in commit.benchmarks)) {
-        return NaN;
-      }
-      const allResults = commit.benchmarks[benchmark].results;
-      const resultsForIteration = allResults[resultIteration];
-      // If a given iteration does not declare a result, then the result
-      // was the same as the first run.
-      if (resultIteration > 0 && !(resultName in resultsForIteration)) {
-        return allResults.first()[resultName];
-      }
-      return resultsForIteration[resultName];
-    }
-
-    function getAllResults(benchmark, commit, resultName) {
-      const result = [];
-      const allResults = commit.benchmarks[benchmark].results;
-      for (var iteration = 0; iteration < allResults.length; iteration++) {
-        result.push(getSingleResult(benchmark, commit, resultName, iteration));
-      }
-      return result;
-    }
-
-    // Chart data provider.
-    function getData() {
-      const filteredCommits = commits.slice(zoom.left, zoom.right);
-      const labels = filteredCommits.map((c, i) => c.index);
-      const datasets = [];
-      for (const selectedBenchmark of selectedBenchmarks.values()) {
-        const codeSizeData =
-            filteredCommits.map(
-                (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "code_size"));
-        const instructionCodeSizeData =
-            filteredCommits.map(
-                (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "ins_code_size"));
-        const composableInstructionCodeSizeData =
-            filteredCommits.map(
-                (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "composable_code_size"));
-        const oatCodeSizeData =
-            filteredCommits.map(
-                (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "oat_code_size"));
-        const codeSizeScatterData = [];
-        for (const commit of filteredCommits.values()) {
-          if (!(selectedBenchmark in commit.benchmarks)) {
-            continue;
-          }
-          const seen = new Set();
-          seen.add(getSingleResult(selectedBenchmark, commit, "code_size"));
-          const codeSizes = getAllResults(selectedBenchmark, commit, "code_size")
-          for (const codeSize of codeSizes.values()) {
-            if (!seen.has(codeSize)) {
-              codeSizeScatterData.push({ x: commit.index, y: codeSize });
-              seen.add(codeSize);
-            }
-          }
-        }
-        const runtimeData =
-            filteredCommits.map(
-                (c, i) =>
-                    selectedBenchmark in filteredCommits[i].benchmarks
-                        ? getAllResults(selectedBenchmark, filteredCommits[i], "runtime")
-                            .min()
-                            .ns_to_s()
-                        : NaN);
-        const runtimeScatterData = [];
-        for (const commit of filteredCommits.values()) {
-          if (!(selectedBenchmark in commit.benchmarks)) {
-            continue;
-          }
-          const runtimes = getAllResults(selectedBenchmark, commit, "runtime")
-          for (const runtime of runtimes.values()) {
-            runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() });
-          }
-        }
-
-        const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;
-        datasets.push(...[
-          {
-            benchmark: selectedBenchmark,
-            type: 'line',
-            label: 'Dex size',
-            data: codeSizeData,
-            datalabels: {
-              align: 'end',
-              anchor: 'end'
-            },
-            tension: 0.1,
-            yAxisID: 'y',
-            segment: {
-              borderColor: ctx =>
-                  skipped(
-                      ctx,
-                      myChart
-                          ? myChart.data.datasets[ctx.datasetIndex].backgroundColor
-                          : undefined),
-              borderDash: ctx => skipped(ctx, [6, 6]),
-            },
-            spanGaps: true
-          },
-          {
-            benchmark: selectedBenchmark,
-            type: 'line',
-            label: 'Instruction size',
-            data: instructionCodeSizeData,
-            datalabels: {
-              align: 'end',
-              anchor: 'end'
-            },
-            tension: 0.1,
-            yAxisID: 'y_ins_code_size',
-            segment: {
-              borderColor: ctx =>
-                  skipped(
-                      ctx,
-                      myChart
-                          ? myChart.data.datasets[ctx.datasetIndex].backgroundColor
-                          : undefined),
-              borderDash: ctx => skipped(ctx, [6, 6]),
-            },
-            spanGaps: true
-          },
-          {
-            benchmark: selectedBenchmark,
-            type: 'line',
-            label: 'Composable size',
-            data: composableInstructionCodeSizeData,
-            datalabels: {
-              align: 'start',
-              anchor: 'start'
-            },
-            tension: 0.1,
-            yAxisID: 'y_ins_code_size',
-            segment: {
-              borderColor: ctx =>
-                  skipped(
-                      ctx,
-                      myChart
-                          ? myChart.data.datasets[ctx.datasetIndex].backgroundColor
-                          : undefined),
-              borderDash: ctx => skipped(ctx, [6, 6]),
-            },
-            spanGaps: true
-          },
-          {
-            benchmark: selectedBenchmark,
-            type: 'line',
-            label: 'Oat size',
-            data: oatCodeSizeData,
-            datalabels: {
-              align: 'start',
-              anchor: 'start'
-            },
-            tension: 0.1,
-            yAxisID: 'y_oat_code_size',
-            segment: {
-              borderColor: ctx =>
-                  skipped(
-                      ctx,
-                      myChart
-                          ? myChart.data.datasets[ctx.datasetIndex].backgroundColor
-                          : undefined),
-              borderDash: ctx => skipped(ctx, [6, 6]),
-            },
-            spanGaps: true
-          },
-          {
-            benchmark: selectedBenchmark,
-            type: 'scatter',
-            label: 'Nondeterminism',
-            data: codeSizeScatterData,
-            datalabels: {
-              labels: {
-                value: null
-              }
-            },
-            radius: 6,
-            pointBackgroundColor: 'red'
-          },
-          {
-            benchmark: selectedBenchmark,
-            type: 'line',
-            label: 'Runtime',
-            data: runtimeData,
-            datalabels: {
-              labels: {
-                value: null
-              }
-            },
-            tension: 0.1,
-            yAxisID: 'y_runtime',
-            segment: {
-              borderColor: ctx =>
-                  skipped(
-                      ctx,
-                      myChart
-                          ? myChart.data.datasets[ctx.datasetIndex].backgroundColor
-                          : undefined),
-              borderDash: ctx => skipped(ctx, [6, 6]),
-            },
-            spanGaps: true
-          },
-          {
-            benchmark: selectedBenchmark,
-            type: 'scatter',
-            label: 'Runtime variance',
-            data: runtimeScatterData,
-            datalabels: {
-              labels: {
-                value: null
-              }
-            },
-            yAxisID: 'y_runtime'
-          }
-        ]);
-      }
-
-      return {
-        labels: labels,
-        datasets: datasets,
-      };
-    }
-
-    // Legend tracking.
-    const legends =
-        new Set(['Dex size', 'Instruction size', 'Composable size', 'Oat size', 'Nondeterminism', 'Runtime', 'Runtime variance']);
-    const selectedLegends =
-        new Set(
-              unescape(window.location.hash.substring(1))
-                  .split(',')
-                  .filter(l => legends.has(l)));
-    if (selectedLegends.size == 0) {
-      legends.forEach(l => selectedLegends.add(l));
-      selectedLegends.delete('Runtime variance')
-    }
-
-    function getDataPercentageChange(context) {
-      var i = context.dataIndex;
-      var value = context.dataset.data[i];
-      var j = i;
-      var previousValue;
-      do {
-        if (j == 0) {
-          return null;
-        }
-        previousValue = context.dataset.data[--j];
-      } while (previousValue === undefined || isNaN(previousValue));
-      return (value - previousValue) / previousValue * 100;
-    }
-
-    // Chart options.
-    const options = {
-      onHover: (event, chartElement) =>
-          event.native.target.style.cursor =
-              chartElement[0] ? 'pointer' : 'default',
-      plugins: {
-        datalabels: {
-          backgroundColor: 'rgba(255, 255, 255, 0.7)',
-          borderColor: 'rgba(128, 128, 128, 0.7)',
-          borderRadius: 4,
-          borderWidth: 1,
-          color: context => getDataPercentageChange(context) < 0 ? 'green' : 'red',
-          display: context => {
-            var percentageChange = getDataPercentageChange(context);
-            return percentageChange !== null && Math.abs(percentageChange) >= 0.1;
-          },
-          font: {
-            size: 20,
-            weight: 'bold'
-          },
-          offset: 8,
-          formatter: (value, context) => {
-            var percentageChange = getDataPercentageChange(context);
-            var percentageChangeTwoDecimals = Math.round(percentageChange * 100) / 100;
-            var glyph = percentageChange < 0 ? 'â–¼' : 'â–²';
-            return glyph + ' ' + percentageChangeTwoDecimals + '%';
-          },
-          padding: 6
-        },
-        legend: {
-          labels: {
-            filter: (legendItem, data) => {
-              // Only retain the legends for the first selected benchmark. If
-              // multiple benchmarks are selected, then use the legends of the
-              // first selected benchmark to control all selected benchmarks.
-              const numUniqueLegends =
-                  data.datasets.length / selectedBenchmarks.size;
-              return legendItem.datasetIndex < numUniqueLegends;
-            },
-          },
-          onClick: (e, legendItem, legend) => {
-            const clickedLegend = legendItem.text;
-            if (selectedLegends.has(clickedLegend)) {
-              selectedLegends.delete(clickedLegend);
-            } else {
-              selectedLegends.add(clickedLegend);
-            }
-            updateChart(false, true);
-          },
-        },
-        tooltip: {
-          callbacks: {
-            title: context => {
-              const elementInfo = context[0];
-              var commit;
-              if (elementInfo.dataset.type == 'line') {
-                commit = commits[zoom.left + elementInfo.dataIndex];
-              } else {
-                console.assert(elementInfo.dataset.type == 'scatter');
-                commit = commits[elementInfo.raw.x];
-              }
-              return commit.title;
-            },
-            footer: context => {
-              const elementInfo = context[0];
-              var commit;
-              if (elementInfo.dataset.type == 'line') {
-                commit = commits[zoom.left + elementInfo.dataIndex];
-              } else {
-                console.assert(elementInfo.dataset.type == 'scatter');
-                commit = commits[elementInfo.raw.x];
-              }
-              const dataset = myChart.data.datasets[elementInfo.datasetIndex];
-              return `App: ${dataset.benchmark}\n`
-                  + `Author: ${commit.author}\n`
-                  + `Submitted: ${new Date(commit.submitted * 1000).toLocaleString()}\n`
-                  + `Hash: ${commit.hash}\n`
-                  + `Index: ${commit.index}`;
-            }
-          }
-        }
-      },
-      responsive: true,
-      scales: {
-        x: {},
-        y: {
-          position: 'left',
-          title: {
-            display: true,
-            text: 'Dex size (bytes)'
-          }
-        },
-        y_runtime: {
-          position: 'right',
-          title: {
-            display: true,
-            text: 'Runtime (seconds)'
-          }
-        },
-        y_ins_code_size: {
-          position: 'left',
-          title: {
-            display: true,
-            text: 'Instruction size (bytes)'
-          }
-        },
-        y_oat_code_size: {
-          position: 'left',
-          title: {
-            display: true,
-            text: 'Oat size (bytes)'
-          }
-        }
-      }
-    };
-
-    // Setup click handler.
-    canvas.onclick = event => {
-      const points =
-          myChart.getElementsAtEventForMode(
-              event, 'nearest', { intersect: true }, true);
-      if (points.length > 0) {
-        const point = points[0];
-        const commit = commits[point.index];
-        window.open('https://r8.googlesource.com/r8/+/' + commit.hash, '_blank');
-      }
-    };
-
-    // Setup chart navigation.
-    var zoom = { left: Math.max(0, commits.length - 75), right: commits.length };
-    for (const urlOption of urlOptions.values()) {
-      if (urlOption.startsWith('L')) {
-        var left = parseInt(urlOption.substring(1));
-        if (isNaN(left)) {
-          continue;
-        }
-        left = left >= 0 ? left : commits.length + left;
-        if (left < 0) {
-          zoom.left = 0;
-        } else if (left >= commits.length) {
-          zoom.left = commits.length - 1;
-        } else {
-          zoom.left = left;
-        }
-      }
-    }
-
-    showMoreLeft.onclick = event => {
-      if (zoom.left == 0) {
-        return;
-      }
-      const currentSize = zoom.right - zoom.left;
-      zoom.left = zoom.left - currentSize;
-      if (zoom.left < 0) {
-        zoom.left = 0;
-      }
-      updateChart(true, false);
-    };
-
-    showLessLeft.onclick = event => {
-      const currentSize = zoom.right - zoom.left;
-      zoom.left = zoom.left + Math.floor(currentSize / 2);
-      if (zoom.left >= zoom.right) {
-        zoom.left = zoom.right - 1;
-      }
-      updateChart(true, false);
-    };
-
-    showLessRight.onclick = event => {
-      if (zoom.right == 0) {
-        return;
-      }
-      const currentSize = zoom.right - zoom.left;
-      zoom.right = zoom.right - Math.floor(currentSize / 2);
-      if (zoom.right < zoom.left) {
-        zoom.right = zoom.left;
-      }
-      updateChart(true, false);
-    };
-
-    showMoreRight.onclick = event => {
-      const currentSize = zoom.right - zoom.left;
-      zoom.right = zoom.right + currentSize;
-      if (zoom.right > commits.length) {
-        zoom.right = commits.length;
-      }
-      updateChart(true, false);
-    };
-
-    function updateChart(dataChanged, legendsChanged) {
-      console.assert(zoom.left <= zoom.right);
-
-      // Update datasets.
-      if (dataChanged) {
-        const newData = getData();
-        Object.assign(myChart.data, newData);
-        // Update chart.
-        myChart.update();
-      }
-
-      // Update legends.
-      if (legendsChanged || (dataChanged && selectedLegends.size < legends.size)) {
-        for (var datasetIndex = 0;
-            datasetIndex < myChart.data.datasets.length;
-            datasetIndex++) {
-          const datasetMeta = myChart.getDatasetMeta(datasetIndex);
-          datasetMeta.hidden = !selectedLegends.has(datasetMeta.label);
-        }
-
-        // Update scales.
-        options.scales.y.display = selectedLegends.has('Dex size');
-        options.scales.y_ins_code_size.display =
-            selectedLegends.has('Instruction size') || selectedLegends.has('Composable size');
-        options.scales.y_oat_code_size.display = selectedLegends.has('Oat size');
-        options.scales.y_runtime.display =
-            selectedLegends.has('Runtime') || selectedLegends.has('Runtime variance');
-
-        // Update chart.
-        myChart.update();
-      }
-
-
-      // Update checkbox colors.
-      const benchmarkColors = {};
-      for (var datasetIndex = 0;
-          datasetIndex < myChart.data.datasets.length;
-          datasetIndex++) {
-        if (myChart.getDatasetMeta(datasetIndex).hidden) {
-          continue;
-        }
-        const dataset = myChart.data.datasets[datasetIndex];
-        const benchmark = dataset.benchmark;
-        const benchmarkColor = dataset.borderColor;
-        if (!(benchmark in benchmarkColors)) {
-          benchmarkColors[benchmark] = benchmarkColor;
-        }
-      }
-      for (const benchmark of benchmarks.values()) {
-        const benchmarkLabel = document.getElementById(benchmark + 'Label');
-        const benchmarkColor = benchmarkColors[benchmark] || '#000000';
-        const benchmarkFontWeight = benchmark in benchmarkColors ? 'bold' : 'normal';
-        benchmarkLabel.style.color = benchmarkColor;
-        benchmarkLabel.style.fontWeight = benchmarkFontWeight;
-      }
-
-      // Update navigation.
-      showMoreLeft.disabled = zoom.left == 0;
-      showLessLeft.disabled = zoom.left == zoom.right - 1;
-      showLessRight.disabled = zoom.left == zoom.right - 1;
-      showMoreRight.disabled = zoom.right == commits.length;
-
-      // Update hash.
-      window.location.hash =
-          Array.from(selectedBenchmarks)
-              .concat(
-                  selectedLegends.size == legends.size
-                      ? []
-                      : Array.from(selectedLegends))
-              .join(',');
-    }
-
-    // Create chart.
-    const myChart = new Chart(canvas, {
-      data: getData(),
-      options: options,
-      plugins: [ChartDataLabels]
-    });
-
-    // Hide disabled legends.
-    if (selectedLegends.size < legends.size) {
-      updateChart(false, true);
-    } else {
-      updateChart(false, false);
-    }
-  </script>
-</body>
-</html>
\ No newline at end of file
diff --git a/tools/perf/r8.html b/tools/perf/r8.html
new file mode 100644
index 0000000..a7da848
--- /dev/null
+++ b/tools/perf/r8.html
@@ -0,0 +1,336 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>R8 perf</title>
+  <link rel="stylesheet" href="stylesheet.css">
+</head>
+<body>
+  <div id="benchmark-selectors"></div>
+  <div>
+      <canvas id="myChart"></canvas>
+  </div>
+  <div>
+    <div style="float: left; width: 50%">
+      <button type="button" id="show-more-left" disabled>⇐</button>
+      <button type="button" id="show-less-left">⇒</button>
+    </div>
+    <div style="float: left; text-align: right; width: 50%">
+      <button type="button" id="show-less-right">⇐</button>
+      <button type="button" id="show-more-right" disabled>⇒</button>
+    </div>
+  </div>
+  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
+  <script src="extensions.js"></script>
+  <script src="utils.js"></script>
+  <script type="module">
+    import chart from "./chart.js";
+    import dom from "./dom.js";
+    import scales from "./scales.js";
+    import state from "./state.js";
+
+    const commits = await state.importCommits("./r8_benchmark_data.json");
+    state.initializeBenchmarks();
+    state.initializeLegends({
+      'Dex size': { default: true },
+      'Instruction size': { default: true },
+      'Composable size': { default: true },
+      'Oat size': { default: true },
+      'Nondeterminism': { default: true },
+      'Runtime': { default: true },
+      'Runtime variance': { default: false }
+    });
+    state.initializeZoom();
+    dom.initializeBenchmarkSelectors();
+    dom.initializeChartNavigation();
+
+    // Chart data provider.
+    function getData(filteredCommits) {
+      const labels = filteredCommits.map((c, i) => c.index);
+      const datasets = getDatasets(filteredCommits);
+      return {
+        labels: labels,
+        datasets: datasets
+      };
+    }
+
+    function getDatasets(filteredCommits) {
+      const datasets = [];
+      state.forEachSelectedBenchmark(
+        selectedBenchmark => {
+          const codeSizeData =
+              filteredCommits.map(
+                  (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "code_size"));
+          const instructionCodeSizeData =
+              filteredCommits.map(
+                  (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "ins_code_size"));
+          const composableInstructionCodeSizeData =
+              filteredCommits.map(
+                  (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "composable_code_size"));
+          const oatCodeSizeData =
+              filteredCommits.map(
+                  (c, i) => getSingleResult(selectedBenchmark, filteredCommits[i], "oat_code_size"));
+          const codeSizeScatterData = [];
+          for (const commit of filteredCommits.values()) {
+            if (!(selectedBenchmark in commit.benchmarks)) {
+              continue;
+            }
+            const seen = new Set();
+            seen.add(getSingleResult(selectedBenchmark, commit, "code_size"));
+            const codeSizes = getAllResults(selectedBenchmark, commit, "code_size")
+            for (const codeSize of codeSizes.values()) {
+              if (!seen.has(codeSize)) {
+                codeSizeScatterData.push({ x: commit.index, y: codeSize });
+                seen.add(codeSize);
+              }
+            }
+          }
+          const runtimeData =
+              filteredCommits.map(
+                  (c, i) =>
+                      selectedBenchmark in filteredCommits[i].benchmarks
+                          ? getAllResults(selectedBenchmark, filteredCommits[i], "runtime")
+                              .min()
+                              .ns_to_s()
+                          : NaN);
+          const runtimeScatterData = [];
+          for (const commit of filteredCommits.values()) {
+            if (!(selectedBenchmark in commit.benchmarks)) {
+              continue;
+            }
+            const runtimes = getAllResults(selectedBenchmark, commit, "runtime")
+            for (const runtime of runtimes.values()) {
+              runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() });
+            }
+          }
+
+          const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;
+          datasets.push(...[
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Dex size',
+              data: codeSizeData,
+              datalabels: {
+                align: 'end',
+                anchor: 'end'
+              },
+              tension: 0.1,
+              yAxisID: 'y',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Instruction size',
+              data: instructionCodeSizeData,
+              datalabels: {
+                align: 'end',
+                anchor: 'end'
+              },
+              tension: 0.1,
+              yAxisID: 'y_ins_code_size',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Composable size',
+              data: composableInstructionCodeSizeData,
+              datalabels: {
+                align: 'start',
+                anchor: 'start'
+              },
+              tension: 0.1,
+              yAxisID: 'y_ins_code_size',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Oat size',
+              data: oatCodeSizeData,
+              datalabels: {
+                align: 'start',
+                anchor: 'start'
+              },
+              tension: 0.1,
+              yAxisID: 'y_oat_code_size',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'scatter',
+              label: 'Nondeterminism',
+              data: codeSizeScatterData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              radius: 6,
+              pointBackgroundColor: 'red'
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Runtime',
+              data: runtimeData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              tension: 0.1,
+              yAxisID: 'y_runtime',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'scatter',
+              label: 'Runtime variance',
+              data: runtimeScatterData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              yAxisID: 'y_runtime'
+            }
+          ]);
+        });
+      return datasets;
+    }
+
+    // Chart options.
+    const options = {
+      onHover: (event, chartElement) =>
+          event.native.target.style.cursor =
+              chartElement[0] ? 'pointer' : 'default',
+      plugins: {
+        datalabels: {
+          backgroundColor: 'rgba(255, 255, 255, 0.7)',
+          borderColor: 'rgba(128, 128, 128, 0.7)',
+          borderRadius: 4,
+          borderWidth: 1,
+          color: context => chart.getDataPercentageChange(context) < 0 ? 'green' : 'red',
+          display: context => {
+            var percentageChange = chart.getDataPercentageChange(context);
+            return percentageChange !== null && Math.abs(percentageChange) >= 0.1;
+          },
+          font: {
+            size: 20,
+            weight: 'bold'
+          },
+          offset: 8,
+          formatter: chart.getDataLabelFormatter,
+          padding: 6
+        },
+        legend: {
+          labels: {
+            filter: (legendItem, data) => {
+              // Only retain the legends for the first selected benchmark. If
+              // multiple benchmarks are selected, then use the legends of the
+              // first selected benchmark to control all selected benchmarks.
+              const numUniqueLegends =
+                  data.datasets.length / state.selectedBenchmarks.size;
+              return legendItem.datasetIndex < numUniqueLegends;
+            },
+          },
+          onClick: (e, legendItem, legend) => {
+            const clickedLegend = legendItem.text;
+            if (state.selectedLegends.has(clickedLegend)) {
+              state.selectedLegends.delete(clickedLegend);
+            } else {
+              state.selectedLegends.add(clickedLegend);
+            }
+            chart.update(false, true);
+          },
+        },
+        tooltip: {
+          callbacks: {
+            title: context => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[state.zoom.left + elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              return commit.title;
+            },
+            footer: context => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[state.zoom.left + elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              const dataset = chart.get().data.datasets[elementInfo.datasetIndex];
+              return `App: ${dataset.benchmark}\n`
+                  + `Author: ${commit.author}\n`
+                  + `Submitted: ${new Date(commit.submitted * 1000).toLocaleString()}\n`
+                  + `Hash: ${commit.hash}\n`
+                  + `Index: ${commit.index}`;
+            }
+          }
+        }
+      },
+      responsive: true,
+      scales: scales.get()
+    };
+
+    chart.setDataProvider(getData);
+    chart.initializeChart(options);
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/tools/perf/retrace.html b/tools/perf/retrace.html
new file mode 100644
index 0000000..98a6661
--- /dev/null
+++ b/tools/perf/retrace.html
@@ -0,0 +1,203 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Retrace perf</title>
+  <link rel="stylesheet" href="stylesheet.css">
+</head>
+<body>
+  <div id="benchmark-selectors"></div>
+  <div>
+      <canvas id="myChart"></canvas>
+  </div>
+  <div>
+    <div style="float: left; width: 50%">
+      <button type="button" id="show-more-left" disabled>⇐</button>
+      <button type="button" id="show-less-left">⇒</button>
+    </div>
+    <div style="float: left; text-align: right; width: 50%">
+      <button type="button" id="show-less-right">⇐</button>
+      <button type="button" id="show-more-right" disabled>⇒</button>
+    </div>
+  </div>
+  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
+  <script src="extensions.js"></script>
+  <script src="utils.js"></script>
+  <script type="module">
+    import chart from "./chart.js";
+    import dom from "./dom.js";
+    import scales from "./scales.js";
+    import state from "./state.js";
+
+    const commits = await state.importCommits("./retrace_benchmark_data.json");
+    state.initializeBenchmarks();
+    state.initializeLegends({
+      'Runtime': { default: true },
+      'Runtime variance': { default: true }
+    });
+    state.initializeZoom();
+    dom.initializeBenchmarkSelectors();
+    dom.initializeChartNavigation();
+
+    // Chart data provider.
+    function getData(filteredCommits) {
+      const labels = filteredCommits.map((c, i) => c.index);
+      const datasets = getDatasets(filteredCommits);
+      return {
+        labels: labels,
+        datasets: datasets
+      };
+    }
+
+    function getDatasets(filteredCommits) {
+      const datasets = [];
+      state.forEachSelectedBenchmark(
+        selectedBenchmark => {
+          const runtimeData =
+              filteredCommits.map(
+                  (c, i) =>
+                      selectedBenchmark in filteredCommits[i].benchmarks
+                          ? getAllResults(selectedBenchmark, filteredCommits[i], "runtime")
+                              .min()
+                              .ns_to_s()
+                          : NaN);
+          const runtimeScatterData = [];
+          for (const commit of filteredCommits.values()) {
+            if (!(selectedBenchmark in commit.benchmarks)) {
+              continue;
+            }
+            const runtimes = getAllResults(selectedBenchmark, commit, "runtime")
+            for (const runtime of runtimes.values()) {
+              runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() });
+            }
+          }
+
+          const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;
+          datasets.push(...[
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Runtime',
+              data: runtimeData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              tension: 0.1,
+              yAxisID: 'y_runtime',
+              segment: {
+                borderColor: ctx =>
+                    skipped(
+                        ctx,
+                        chart.get()
+                            ? chart.get().data.datasets[ctx.datasetIndex].backgroundColor
+                            : undefined),
+                borderDash: ctx => skipped(ctx, [6, 6]),
+              },
+              spanGaps: true
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'scatter',
+              label: 'Runtime variance',
+              data: runtimeScatterData,
+              datalabels: {
+                labels: {
+                  value: null
+                }
+              },
+              yAxisID: 'y_runtime'
+            }
+          ]);
+        });
+      return datasets;
+    }
+
+    // Chart options.
+    const options = {
+      onHover: (event, chartElement) =>
+          event.native.target.style.cursor =
+              chartElement[0] ? 'pointer' : 'default',
+      plugins: {
+        datalabels: {
+          backgroundColor: 'rgba(255, 255, 255, 0.7)',
+          borderColor: 'rgba(128, 128, 128, 0.7)',
+          borderRadius: 4,
+          borderWidth: 1,
+          color: context => chart.getDataPercentageChange(context) < 0 ? 'green' : 'red',
+          display: context => {
+            var percentageChange = chart.getDataPercentageChange(context);
+            return percentageChange !== null && Math.abs(percentageChange) >= 0.1;
+          },
+          font: {
+            size: 20,
+            weight: 'bold'
+          },
+          offset: 8,
+          formatter: chart.getDataLabelFormatter,
+          padding: 6
+        },
+        legend: {
+          labels: {
+            filter: (legendItem, data) => {
+              // Only retain the legends for the first selected benchmark. If
+              // multiple benchmarks are selected, then use the legends of the
+              // first selected benchmark to control all selected benchmarks.
+              const numUniqueLegends =
+                  data.datasets.length / state.selectedBenchmarks.size;
+              return legendItem.datasetIndex < numUniqueLegends;
+            },
+          },
+          onClick: (e, legendItem, legend) => {
+            const clickedLegend = legendItem.text;
+            if (state.selectedLegends.has(clickedLegend)) {
+              state.selectedLegends.delete(clickedLegend);
+            } else {
+              state.selectedLegends.add(clickedLegend);
+            }
+            chart.update(false, true);
+          },
+        },
+        tooltip: {
+          callbacks: {
+            title: context => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[state.zoom.left + elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              return commit.title;
+            },
+            footer: context => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[state.zoom.left + elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              const dataset = chart.get().data.datasets[elementInfo.datasetIndex];
+              return `App: ${dataset.benchmark}\n`
+                  + `Author: ${commit.author}\n`
+                  + `Submitted: ${new Date(commit.submitted * 1000).toLocaleString()}\n`
+                  + `Hash: ${commit.hash}\n`
+                  + `Index: ${commit.index}`;
+            }
+          }
+        }
+      },
+      responsive: true,
+      scales: scales.get()
+    };
+
+    chart.setDataProvider(getData);
+    chart.initializeChart(options);
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/tools/perf/scales.js b/tools/perf/scales.js
new file mode 100644
index 0000000..8378e2f
--- /dev/null
+++ b/tools/perf/scales.js
@@ -0,0 +1,72 @@
+// Copyright (c) 2024, 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.
+import state from "./state.js";
+
+function get() {
+  const scales = {};
+  scales.x = {};
+  if (state.hasLegend('Dex size')) {
+    scales.y = {
+      position: 'left',
+      title: {
+        display: true,
+        text: 'Dex size (bytes)'
+      }
+    };
+  } else {
+    console.assert(!state.hasLegend('Instruction size'));
+    console.assert(!state.hasLegend('Composable size'));
+    console.assert(!state.hasLegend('Oat size'));
+  }
+  console.assert(state.hasLegend('Runtime'));
+  console.assert(state.hasLegend('Runtime variance'));
+  scales.y_runtime = {
+    position: state.hasLegend('Dex size') ? 'right' : 'left',
+    title: {
+      display: true,
+      text: 'Runtime (seconds)'
+    }
+  };
+  if (state.hasLegend('Instruction size') || state.hasLegend('Composable size')) {
+    scales.y_ins_code_size = {
+      position: 'left',
+      title: {
+        display: true,
+        text: 'Instruction size (bytes)'
+      }
+    };
+  }
+  if (state.hasLegend('Oat size')) {
+    scales.y_oat_code_size = {
+      position: 'left',
+      title: {
+        display: true,
+        text: 'Oat size (bytes)'
+      }
+    };
+  }
+  return scales;
+}
+
+function update(scales) {
+  if (scales.y) {
+    scales.y.display = state.isLegendSelected('Dex size');
+  }
+  if (scales.y_ins_code_size) {
+    scales.y_ins_code_size.display =
+        state.isLegendSelected('Instruction size') || state.isLegendSelected('Composable size');
+  }
+  if (scales.y_oat_code_size) {
+    scales.y_oat_code_size.display = state.isLegendSelected('Oat size');
+  }
+  if (scales.y_runtime) {
+    scales.y_runtime.display =
+        state.isLegendSelected('Runtime') || state.isLegendSelected('Runtime variance');
+  }
+}
+
+export default {
+  get: get,
+  update: update
+};
\ No newline at end of file
diff --git a/tools/perf/state.js b/tools/perf/state.js
new file mode 100644
index 0000000..4d0128f
--- /dev/null
+++ b/tools/perf/state.js
@@ -0,0 +1,121 @@
+// Copyright (c) 2024, 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.
+import url from "./url.js";
+
+var commits = null;
+
+const benchmarks = new Set();
+const selectedBenchmarks = new Set();
+
+const legends = new Set();
+const selectedLegends = new Set();
+
+const zoom = { left: -1, right: -1 };
+
+function forEachBenchmark(callback) {
+  for (const benchmark of benchmarks.values()) {
+    callback(benchmark, selectedBenchmarks.has(benchmark));
+  }
+}
+
+function forEachSelectedBenchmark(callback) {
+  forEachBenchmark((benchmark, selected) => {
+    if (selected) {
+      callback(benchmark);
+    }
+  });
+}
+
+function hasLegend(legend) {
+  return legends.has(legend);
+}
+
+function importCommits(url) {
+  return import(url, { with: { type: "json" }})
+      .then(module => {
+        commits = module.default;
+        commits.reverseInPlace();
+        // Amend the commits with their unique index.
+        for (var i = 0; i < commits.length; i++) {
+          commits[i].index = i;
+        }
+        return commits;
+      });
+}
+
+function initializeBenchmarks() {
+  for (const commit of commits.values()) {
+    for (const benchmark in commit.benchmarks) {
+        benchmarks.add(benchmark);
+    }
+  }
+  for (const benchmark of benchmarks.values()) {
+    if (url.matches(benchmark)) {
+      selectedBenchmarks.add(benchmark);
+    }
+  }
+  if (selectedBenchmarks.size == 0) {
+    const randomBenchmarkIndex = Math.floor(Math.random() * benchmarks.size);
+    const randomBenchmark = Array.from(benchmarks)[randomBenchmarkIndex];
+    selectedBenchmarks.add(randomBenchmark);
+  }
+}
+
+function initializeLegends(legendsInfo) {
+  for (var legend in legendsInfo) {
+    legends.add(legend);
+    if (url.contains(legend)) {
+      selectedLegends.add(legend);
+    }
+  }
+  if (selectedLegends.size == 0) {
+    for (let [legend, legendInfo] of Object.entries(legendsInfo)) {
+      if (legendInfo.default) {
+        selectedLegends.add(legend);
+      }
+    }
+  }
+}
+
+function initializeZoom() {
+  zoom.left = Math.max(0, commits.length - 75);
+  zoom.right = commits.length;
+  for (const urlOption of url.values()) {
+    if (urlOption.startsWith('L')) {
+      var left = parseInt(urlOption.substring(1));
+      if (isNaN(left)) {
+        continue;
+      }
+      left = left >= 0 ? left : commits.length + left;
+      if (left < 0) {
+        zoom.left = 0;
+      } else if (left >= commits.length) {
+        zoom.left = commits.length - 1;
+      } else {
+        zoom.left = left;
+      }
+    }
+  }
+}
+
+function isLegendSelected(legend) {
+  return selectedLegends.has(legend);
+}
+
+export default {
+  benchmarks: benchmarks,
+  commits: zoom => zoom ? commits.slice(zoom.left, zoom.right) : commits,
+  legends: legends,
+  selectedBenchmarks: selectedBenchmarks,
+  selectedLegends: selectedLegends,
+  forEachBenchmark: forEachBenchmark,
+  forEachSelectedBenchmark: forEachSelectedBenchmark,
+  hasLegend: hasLegend,
+  initializeBenchmarks: initializeBenchmarks,
+  initializeLegends: initializeLegends,
+  initializeZoom: initializeZoom,
+  importCommits: importCommits,
+  isLegendSelected: isLegendSelected,
+  zoom: zoom
+};
diff --git a/tools/perf/url.js b/tools/perf/url.js
new file mode 100644
index 0000000..f00a2f77
--- /dev/null
+++ b/tools/perf/url.js
@@ -0,0 +1,41 @@
+// Copyright (c) 2024, 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.
+const options = unescape(window.location.hash.substring(1)).split(',');
+
+function contains(subject) {
+  return options.includes(subject);
+}
+
+function matches(subject) {
+  for (const filter of options.values()) {
+    if (filter) {
+      const match = subject.match(new RegExp(filter.replace("*", ".*")));
+      if (match) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+function updateHash(state) {
+  window.location.hash =
+      Array.from(state.selectedBenchmarks)
+          .concat(
+              state.selectedLegends.size == state.legends.size
+                  ? []
+                  : Array.from(state.selectedLegends))
+          .join(',');
+}
+
+function values() {
+  return options;
+}
+
+export default {
+  contains: contains,
+  matches: matches,
+  updateHash: updateHash,
+  values: values
+};
\ No newline at end of file
diff --git a/tools/perf/utils.js b/tools/perf/utils.js
new file mode 100644
index 0000000..46890d0
--- /dev/null
+++ b/tools/perf/utils.js
@@ -0,0 +1,25 @@
+// Copyright (c) 2024, 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.
+function getSingleResult(benchmark, commit, resultName, resultIteration = 0) {
+  if (!(benchmark in commit.benchmarks)) {
+    return NaN;
+  }
+  const allResults = commit.benchmarks[benchmark].results;
+  const resultsForIteration = allResults[resultIteration];
+  // If a given iteration does not declare a result, then the result
+  // was the same as the first run.
+  if (resultIteration > 0 && !(resultName in resultsForIteration)) {
+    return allResults.first()[resultName];
+  }
+  return resultsForIteration[resultName];
+}
+
+function getAllResults(benchmark, commit, resultName) {
+  const result = [];
+  const allResults = commit.benchmarks[benchmark].results;
+  for (var iteration = 0; iteration < allResults.length; iteration++) {
+    result.push(getSingleResult(benchmark, commit, resultName, iteration));
+  }
+  return result;
+}
diff --git a/tools/run_benchmark.py b/tools/run_benchmark.py
index 783e695..436c046 100755
--- a/tools/run_benchmark.py
+++ b/tools/run_benchmark.py
@@ -47,7 +47,7 @@
         help='The test target to run',
         required=True,
         # These should 1:1 with benchmarks/BenchmarkTarget.java
-        choices=['d8', 'r8-full', 'r8-force', 'r8-compat'])
+        choices=['d8', 'r8-full', 'r8-force', 'r8-compat', 'retrace'])
     result.add_argument(
         '--debug-agent',
         '--debug_agent',
@@ -165,8 +165,11 @@
 def run(options, r8jar, testjars):
     jdkhome = get_jdk_home(options, options.benchmark)
     cmd = [
-        jdk.GetJavaExecutable(jdkhome), '-Xms8g', '-Xmx8g',
-        '-XX:+TieredCompilation', '-XX:TieredStopAtLevel=4',
+        jdk.GetJavaExecutable(jdkhome),
+        '-Xms8g',
+        '-Xmx8g',
+        '-XX:+TieredCompilation',
+        '-XX:TieredStopAtLevel=4',
         '-DBENCHMARK_IGNORE_CODE_SIZE_DIFFERENCES',
         f'-DBUILD_PROP_KEEPANNO_RUNTIME_PATH={utils.REPO_ROOT}/d8_r8/keepanno/build/classes/java/main',
         # Since we change the working directory to a temp folder.
diff --git a/tools/test.py b/tools/test.py
index aa63a0f..16bb545 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -264,6 +264,10 @@
                         help='Pass --no-daemon to the gradle run',
                         default=False,
                         action='store_true')
+    result.add_argument('--low-priority',
+                        help='Run gradle with priority=low (higher niceness)',
+                        default=False,
+                        action='store_true')
     result.add_argument(
         '--kotlin-compiler-dev',
         help='Specify to download a kotlin dev compiler and run '
@@ -384,6 +388,9 @@
         # Bots don't like dangling processes.
         gradle_args.append('--no-daemon')
 
+    if options.low_priority:
+        gradle_args.append('--priority=low')
+
     # Set all necessary Gradle properties and options first.
     if options.shard_count:
         assert options.shard_number
diff --git a/tools/upload_benchmark_data_to_google_storage.py b/tools/upload_benchmark_data_to_google_storage.py
index f293c2c..f979010 100755
--- a/tools/upload_benchmark_data_to_google_storage.py
+++ b/tools/upload_benchmark_data_to_google_storage.py
@@ -12,11 +12,14 @@
 
 import sys
 
-APPS = perf.APPS
+BENCHMARKS = perf.BENCHMARKS
 TARGETS = ['r8-full']
 NUM_COMMITS = 1000
 
-INDEX_HTML = os.path.join(utils.TOOLS_DIR, 'perf/index.html')
+FILES = [
+    'chart.js', 'd8.html', 'dom.js', 'extensions.js', 'r8.html', 'retrace.html',
+    'scales.js', 'state.js', 'stylesheet.css', 'url.js', 'utils.js'
+]
 
 
 def DownloadCloudBucket(dest):
@@ -40,6 +43,51 @@
             return None
 
 
+def RecordBenchmarkResult(commit, benchmark, benchmark_info, local_bucket,
+                          target, benchmarks):
+    if not target in benchmark_info['targets']:
+        return
+    filename = perf.GetArtifactLocation(benchmark, target, commit.hash(),
+                                        'result.json')
+    benchmark_data = ParseJsonFromCloudStorage(filename, local_bucket)
+    if benchmark_data:
+        benchmarks[benchmark] = benchmark_data
+
+
+def RecordBenchmarkResults(commit, benchmarks, benchmark_data):
+    if benchmarks or benchmark_data:
+        benchmark_data.append({
+            'author': commit.author_name(),
+            'hash': commit.hash(),
+            'submitted': commit.committer_timestamp(),
+            'title': commit.title(),
+            'benchmarks': benchmarks
+        })
+
+
+def TrimBenchmarkResults(benchmark_data):
+    new_benchmark_data_len = len(benchmark_data)
+    while new_benchmark_data_len > 0:
+        candidate_len = new_benchmark_data_len - 1
+        if not benchmark_data[candidate_len]['benchmarks']:
+            new_benchmark_data_len = candidate_len
+        else:
+            break
+    return benchmark_data[0:new_benchmark_data_len]
+
+
+def ArchiveBenchmarkResults(benchmark_data, dest, temp):
+    # Serialize JSON to temp file.
+    benchmark_data_file = os.path.join(temp, dest)
+    with open(benchmark_data_file, 'w') as f:
+        json.dump(benchmark_data, f)
+
+    # Write output files to public bucket.
+    perf.ArchiveOutputFile(benchmark_data_file,
+                           dest,
+                           header='Cache-Control:no-store')
+
+
 def run():
     # Get the N most recent commits sorted by newest first.
     top = utils.get_sha1_from_revision('origin/main')
@@ -52,49 +100,45 @@
         local_bucket = os.path.join(temp, perf.BUCKET)
         DownloadCloudBucket(local_bucket)
 
-        # Aggregate all the result.json files into a single benchmark_data.json file
-        # that has the same format as tools/perf/benchmark_data.json.
-        benchmark_data = []
+        # Aggregate all the result.json files into a single file that has the
+        # same format as tools/perf/benchmark_data.json.
+        d8_benchmark_data = []
+        r8_benchmark_data = []
+        retrace_benchmark_data = []
         for commit in commits:
-            benchmarks = {}
-            for app in APPS:
-                for target in TARGETS:
-                    filename = perf.GetArtifactLocation(app, target,
-                                                        commit.hash(),
-                                                        'result.json')
-                    app_benchmark_data = ParseJsonFromCloudStorage(
-                        filename, local_bucket)
-                    if app_benchmark_data:
-                        benchmarks[app] = app_benchmark_data
-            if benchmarks or benchmark_data:
-                benchmark_data.append({
-                    'author': commit.author_name(),
-                    'hash': commit.hash(),
-                    'submitted': commit.committer_timestamp(),
-                    'title': commit.title(),
-                    'benchmarks': benchmarks
-                })
+            d8_benchmarks = {}
+            r8_benchmarks = {}
+            retrace_benchmarks = {}
+            for benchmark, benchmark_info in BENCHMARKS.items():
+                RecordBenchmarkResult(commit, benchmark, benchmark_info,
+                                      local_bucket, 'd8', d8_benchmarks)
+                RecordBenchmarkResult(commit, benchmark, benchmark_info,
+                                      local_bucket, 'r8-full', r8_benchmarks)
+                RecordBenchmarkResult(commit, benchmark, benchmark_info,
+                                      local_bucket, 'retrace',
+                                      retrace_benchmarks)
+            RecordBenchmarkResults(commit, d8_benchmarks, d8_benchmark_data)
+            RecordBenchmarkResults(commit, r8_benchmarks, r8_benchmark_data)
+            RecordBenchmarkResults(commit, retrace_benchmarks,
+                                   retrace_benchmark_data)
 
         # Trim data.
-        new_benchmark_data_len = len(benchmark_data)
-        while new_benchmark_data_len > 0:
-            candidate_len = new_benchmark_data_len - 1
-            if not benchmark_data[candidate_len]['benchmarks']:
-                new_benchmark_data_len = candidate_len
-            else:
-                break
-        benchmark_data = benchmark_data[0:new_benchmark_data_len]
-
-        # Serialize JSON to temp file.
-        benchmark_data_file = os.path.join(temp, 'benchmark_data.json')
-        with open(benchmark_data_file, 'w') as f:
-            json.dump(benchmark_data, f)
+        d8_benchmark_data = TrimBenchmarkResults(d8_benchmark_data)
+        r8_benchmark_data = TrimBenchmarkResults(r8_benchmark_data)
+        retrace_benchmark_data = TrimBenchmarkResults(retrace_benchmark_data)
 
         # Write output files to public bucket.
-        perf.ArchiveOutputFile(benchmark_data_file,
-                               'benchmark_data.json',
-                               header='Cache-Control:no-store')
-        perf.ArchiveOutputFile(INDEX_HTML, 'index.html')
+        ArchiveBenchmarkResults(d8_benchmark_data, 'd8_benchmark_data.json',
+                                temp)
+        ArchiveBenchmarkResults(r8_benchmark_data, 'r8_benchmark_data.json',
+                                temp)
+        ArchiveBenchmarkResults(retrace_benchmark_data,
+                                'retrace_benchmark_data.json', temp)
+
+        # Write remaining files to public bucket.
+        for file in FILES:
+            dest = os.path.join(utils.TOOLS_DIR, 'perf', file)
+            perf.ArchiveOutputFile(dest, file)
 
 
 def main():