Merge commit 'fb3d2d71009af3eb1fc32c5c9a5826ac1fbb1e98' into dev-release

Change-Id: I1f5de7705e86c4596d2d5986316ec49d6381a06d
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 8fccc8d..ae6a649 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -328,7 +328,8 @@
       reportSyntheticInformation(appView);
 
       if (options.isGeneratingClassFiles()) {
-        new CfApplicationWriter(appView, marker).write(options.getClassFileConsumer(), inputApp);
+        new CfApplicationWriter(appView, marker)
+            .write(options.getClassFileConsumer(), executor, inputApp);
       } else {
         ApplicationWriter.create(appView, marker).write(executor, inputApp);
       }
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
index 4b2ab5a..f0445dd 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
@@ -135,7 +135,7 @@
               } else {
                 assert options.isGeneratingClassFiles();
                 new CfApplicationWriter(appView, options.getMarker())
-                    .write(options.getClassFileConsumer(), app);
+                    .write(options.getClassFileConsumer(), executorService, app);
               }
             } catch (ExecutionException e) {
               throw unwrapExecutionException(e);
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index 1c1e64c..f744a67 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -149,7 +149,8 @@
               appView));
       new GenericSignatureRewriter(appView).run(appView.appInfo().classes(), executor);
 
-      new CfApplicationWriter(appView, options.getMarker()).write(options.getClassFileConsumer());
+      new CfApplicationWriter(appView, options.getMarker())
+          .write(options.getClassFileConsumer(), executor);
       options.printWarnings();
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 93926dd..46f11fd 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -222,7 +222,8 @@
       Marker marker = options.getMarker();
       assert marker != null;
       if (options.isGeneratingClassFiles()) {
-        new CfApplicationWriter(appView, marker).write(options.getClassFileConsumer(), inputApp);
+        new CfApplicationWriter(appView, marker)
+            .write(options.getClassFileConsumer(), executorService, inputApp);
       } else {
         ApplicationWriter.create(appView, marker).write(executorService, inputApp);
       }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index b6c9ae4..52ae388 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -739,6 +739,7 @@
     }
 
     private R8Command makeR8Command() {
+      long created = System.nanoTime();
       Reporter reporter = getReporter();
       DexItemFactory factory = new DexItemFactory();
       List<ProguardConfigurationRule> mainDexKeepRules =
@@ -839,7 +840,8 @@
               resourceShrinkerConfiguration,
               keepSpecifications,
               buildMetadataConsumer,
-              partialCompilationConfiguration);
+              partialCompilationConfiguration,
+              created);
 
       if (inputDependencyGraphConsumer != null) {
         inputDependencyGraphConsumer.finished();
@@ -1062,6 +1064,7 @@
   private final ResourceShrinkerConfiguration resourceShrinkerConfiguration;
   private final Consumer<? super R8BuildMetadata> buildMetadataConsumer;
   private final R8PartialCompilationConfiguration partialCompilationConfiguration;
+  private final long created;
 
   /** Get a new {@link R8Command.Builder}. */
   public static Builder builder() {
@@ -1162,7 +1165,8 @@
       ResourceShrinkerConfiguration resourceShrinkerConfiguration,
       List<KeepSpecificationSource> keepSpecifications,
       Consumer<? super R8BuildMetadata> buildMetadataConsumer,
-      R8PartialCompilationConfiguration partialCompilationConfiguration) {
+      R8PartialCompilationConfiguration partialCompilationConfiguration,
+      long created) {
     super(
         inputApp,
         mode,
@@ -1213,6 +1217,7 @@
     this.resourceShrinkerConfiguration = resourceShrinkerConfiguration;
     this.buildMetadataConsumer = buildMetadataConsumer;
     this.partialCompilationConfiguration = partialCompilationConfiguration;
+    this.created = created;
   }
 
   private R8Command(boolean printHelp, boolean printVersion) {
@@ -1243,6 +1248,7 @@
     resourceShrinkerConfiguration = null;
     buildMetadataConsumer = null;
     partialCompilationConfiguration = null;
+    created = -1;
   }
 
   public DexItemFactory getDexItemFactory() {
@@ -1267,6 +1273,7 @@
   @Override
   InternalOptions getInternalOptions() {
     InternalOptions internal = new InternalOptions(getMode(), proguardConfiguration, getReporter());
+    internal.created = created;
     assert !internal.testing.allowOutlinerInterfaceArrayArguments;  // Only allow in tests.
     internal.programConsumer = getProgramConsumer();
     internal.setMinApiLevel(AndroidApiLevel.getAndroidApiLevel(getMinApiLevel()));
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 9554f4e..55274e8 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -213,8 +213,7 @@
     return desugaredLibraryCodeToKeep;
   }
 
-  private List<VirtualFile> distribute(ExecutorService executorService)
-      throws ExecutionException, IOException {
+  private List<VirtualFile> distribute(ExecutorService executorService) {
     Collection<DexProgramClass> classes = appView.appInfo().classes();
     Collection<DexProgramClass> globalSynthetics = new ArrayList<>();
     if (appView.options().intermediate && appView.options().hasGlobalSyntheticsConsumer()) {
@@ -452,7 +451,7 @@
       options.reporter.failIfPendingErrors();
       // Supply info to all additional resource consumers.
       if (!(programConsumer instanceof ConvertedCfFiles)) {
-        supplyAdditionalConsumers(appView, virtualFiles);
+        supplyAdditionalConsumers(appView, executorService, virtualFiles);
       }
     } finally {
       timing.end();
@@ -655,7 +654,8 @@
   }
 
   @SuppressWarnings("DefaultCharset")
-  public static void supplyAdditionalConsumers(AppView<?> appView, List<VirtualFile> virtualFiles) {
+  public static void supplyAdditionalConsumers(
+      AppView<?> appView, ExecutorService executorService, List<VirtualFile> virtualFiles) {
     InternalOptions options = appView.options();
     Reporter reporter = options.reporter;
     appView.getArtProfileCollection().supplyConsumers(appView);
@@ -711,7 +711,7 @@
     if (options.r8BuildMetadataConsumer != null) {
       assert appView.hasClassHierarchy();
       options.r8BuildMetadataConsumer.accept(
-          BuildMetadataFactory.create(appView.withClassHierarchy(), virtualFiles));
+          BuildMetadataFactory.create(appView.withClassHierarchy(), executorService, 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 2d96710..c8f16fc 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -161,6 +161,10 @@
     return featureSplit;
   }
 
+  public FeatureSplit getFeatureSplitOrBase() {
+    return featureSplit != null ? featureSplit : FeatureSplit.BASE;
+  }
+
   public StartupProfile getStartupProfile() {
     return startupProfile;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/analysis/TransferFunction.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/analysis/TransferFunction.java
index 6e1643e..71b7edb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/analysis/TransferFunction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/analysis/TransferFunction.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.classinliner.analysis;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
 import static com.android.tools.r8.ir.code.Opcodes.CHECK_CAST;
 import static com.android.tools.r8.ir.code.Opcodes.IF;
@@ -394,21 +395,21 @@
         : isMaybeEligibleForClassInlining(clazz.asClasspathOrLibraryClass());
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private boolean isMaybeEligibleForClassInlining(DexProgramClass clazz) {
     // We can only class inline parameters that does not inherit from other classpath or library
     // classes than java.lang.Object.
-    DexType superType = clazz.getSuperType();
-    do {
-      DexClass superClass = appView.definitionFor(superType);
+    while (clazz.hasSuperType()) {
+      DexType superType = clazz.getSuperType();
+      if (superType.isIdenticalTo(dexItemFactory.objectType)) {
+        break;
+      }
+      DexProgramClass superClass = asProgramClassOrNull(appView.definitionFor(superType));
       if (superClass == null) {
         return false;
       }
-      if (!superClass.isProgramClass()) {
-        return superClass.getType() == dexItemFactory.objectType;
-      }
-      superType = superClass.getSuperType();
-    } while (true);
+      clazz = superClass;
+    }
+    return true;
   }
 
   @SuppressWarnings("ReferenceEquality")
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 20de02a..6ccbf71 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -66,6 +66,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import org.objectweb.asm.AnnotationVisitor;
@@ -113,15 +114,16 @@
     return appView.getNamingLens();
   }
 
-  public void write(ClassFileConsumer consumer) {
+  public void write(ClassFileConsumer consumer, ExecutorService executorService) {
     assert !options.hasMappingFileSupport();
-    write(consumer, null);
+    write(consumer, executorService, null);
   }
 
-  public void write(ClassFileConsumer consumer, AndroidApp inputApp) {
+  public void write(
+      ClassFileConsumer consumer, ExecutorService executorService, AndroidApp inputApp) {
     application.timing.begin("CfApplicationWriter.write");
     try {
-      writeApplication(inputApp, consumer);
+      writeApplication(inputApp, consumer, executorService);
     } finally {
       application.timing.end();
     }
@@ -138,7 +140,8 @@
     return true;
   }
 
-  private void writeApplication(AndroidApp inputApp, ClassFileConsumer consumer) {
+  private void writeApplication(
+      AndroidApp inputApp, ClassFileConsumer consumer, ExecutorService executorService) {
     ProguardMapId proguardMapId = null;
     if (options.hasMappingFileSupport()) {
       assert marker.isPresent();
@@ -187,7 +190,7 @@
       }
       globalsConsumer.finished(appView);
     }
-    ApplicationWriter.supplyAdditionalConsumers(appView, Collections.emptyList());
+    ApplicationWriter.supplyAdditionalConsumers(appView, executorService, Collections.emptyList());
   }
 
   private void writeClassCatchingErrors(
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java b/src/main/java/com/android/tools/r8/metadata/D8ApiModelingMetadata.java
similarity index 88%
rename from src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
rename to src/main/java/com/android/tools/r8/metadata/D8ApiModelingMetadata.java
index 8d175f3..cc73c5b 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/D8ApiModelingMetadata.java
@@ -6,4 +6,4 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8ApiModelingOptions {}
+public interface D8ApiModelingMetadata {}
diff --git a/src/main/java/com/android/tools/r8/metadata/D8ApiModelingOptions.java b/src/main/java/com/android/tools/r8/metadata/D8ApiModelingOptions.java
deleted file mode 100644
index 4531502..0000000
--- a/src/main/java/com/android/tools/r8/metadata/D8ApiModelingOptions.java
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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 D8ApiModelingOptions {}
diff --git a/src/main/java/com/android/tools/r8/metadata/D8BuildMetadata.java b/src/main/java/com/android/tools/r8/metadata/D8BuildMetadata.java
index 765c05c..2008658 100644
--- a/src/main/java/com/android/tools/r8/metadata/D8BuildMetadata.java
+++ b/src/main/java/com/android/tools/r8/metadata/D8BuildMetadata.java
@@ -4,10 +4,10 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.metadata.impl.D8ApiModelingOptionsImpl;
+import com.android.tools.r8.metadata.impl.D8ApiModelingMetadataImpl;
 import com.android.tools.r8.metadata.impl.D8BuildMetadataImpl;
-import com.android.tools.r8.metadata.impl.D8LibraryDesugaringOptionsImpl;
-import com.android.tools.r8.metadata.impl.D8OptionsImpl;
+import com.android.tools.r8.metadata.impl.D8LibraryDesugaringMetadataImpl;
+import com.android.tools.r8.metadata.impl.D8OptionsMetadataImpl;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonDeserializer;
 
@@ -17,11 +17,11 @@
   static D8BuildMetadata fromJson(String json) {
     return new GsonBuilder()
         .excludeFieldsWithoutExposeAnnotation()
-        .registerTypeAdapter(D8Options.class, deserializeTo(D8OptionsImpl.class))
+        .registerTypeAdapter(D8OptionsMetadata.class, deserializeTo(D8OptionsMetadataImpl.class))
         .registerTypeAdapter(
-            D8ApiModelingOptions.class, deserializeTo(D8ApiModelingOptionsImpl.class))
+            D8ApiModelingMetadata.class, deserializeTo(D8ApiModelingMetadataImpl.class))
         .registerTypeAdapter(
-            D8LibraryDesugaringOptions.class, deserializeTo(D8LibraryDesugaringOptionsImpl.class))
+            D8LibraryDesugaringMetadata.class, deserializeTo(D8LibraryDesugaringMetadataImpl.class))
         .create()
         .fromJson(json, D8BuildMetadataImpl.class);
   }
@@ -30,7 +30,7 @@
     return (element, type, context) -> context.deserialize(element, implClass);
   }
 
-  D8Options getOptions();
+  D8OptionsMetadata getOptions();
 
   String getVersion();
 
diff --git a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java b/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringMetadata.java
similarity index 67%
rename from src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
rename to src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringMetadata.java
index 30adb4a..8cc342c 100644
--- a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringMetadata.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.metadata.impl.D8R8LibraryDesugaringOptions;
+import com.android.tools.r8.metadata.impl.D8R8LibraryDesugaringMetadata;
 
 @KeepForApi
-public interface D8LibraryDesugaringOptions extends D8R8LibraryDesugaringOptions {}
+public interface D8LibraryDesugaringMetadata extends D8R8LibraryDesugaringMetadata {}
diff --git a/src/main/java/com/android/tools/r8/metadata/D8Options.java b/src/main/java/com/android/tools/r8/metadata/D8Options.java
deleted file mode 100644
index b09f4b4..0000000
--- a/src/main/java/com/android/tools/r8/metadata/D8Options.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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;
-import com.android.tools.r8.metadata.impl.D8R8Options;
-
-@KeepForApi
-public interface D8Options extends D8R8Options<D8ApiModelingOptions, D8LibraryDesugaringOptions> {}
diff --git a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java b/src/main/java/com/android/tools/r8/metadata/D8OptionsMetadata.java
similarity index 63%
copy from src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
copy to src/main/java/com/android/tools/r8/metadata/D8OptionsMetadata.java
index 30adb4a..0b9c4f9 100644
--- a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/D8OptionsMetadata.java
@@ -4,7 +4,8 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.metadata.impl.D8R8LibraryDesugaringOptions;
+import com.android.tools.r8.metadata.impl.D8R8OptionsMetadata;
 
 @KeepForApi
-public interface D8LibraryDesugaringOptions extends D8R8LibraryDesugaringOptions {}
+public interface D8OptionsMetadata
+    extends D8R8OptionsMetadata<D8ApiModelingMetadata, D8LibraryDesugaringMetadata> {}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java b/src/main/java/com/android/tools/r8/metadata/R8ApiModelingMetadata.java
similarity index 88%
copy from src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
copy to src/main/java/com/android/tools/r8/metadata/R8ApiModelingMetadata.java
index 8d175f3..19c62b1 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8ApiModelingMetadata.java
@@ -6,4 +6,4 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8ApiModelingOptions {}
+public interface R8ApiModelingMetadata {}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java b/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingMetadata.java
similarity index 85%
copy from src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
copy to src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingMetadata.java
index 8d175f3..0b24945 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingMetadata.java
@@ -6,4 +6,4 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8ApiModelingOptions {}
+public interface R8BaselineProfileRewritingMetadata {}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptions.java b/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptions.java
deleted file mode 100644
index 7598aee..0000000
--- a/src/main/java/com/android/tools/r8/metadata/R8BaselineProfileRewritingOptions.java
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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/R8BuildMetadata.java b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
index c3f13b0..153dd1e 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
@@ -4,14 +4,19 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.metadata.impl.R8ApiModelingOptionsImpl;
-import com.android.tools.r8.metadata.impl.R8BaselineProfileRewritingOptionsImpl;
+import com.android.tools.r8.metadata.impl.R8ApiModelingMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8BaselineProfileRewritingMetadataImpl;
 import com.android.tools.r8.metadata.impl.R8BuildMetadataImpl;
-import com.android.tools.r8.metadata.impl.R8KeepAttributesOptionsImpl;
-import com.android.tools.r8.metadata.impl.R8LibraryDesugaringOptionsImpl;
-import com.android.tools.r8.metadata.impl.R8OptionsImpl;
-import com.android.tools.r8.metadata.impl.R8ResourceOptimizationOptionsImpl;
-import com.android.tools.r8.metadata.impl.R8StartupOptimizationOptionsImpl;
+import com.android.tools.r8.metadata.impl.R8CompilationMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8DexFileMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8FeatureSplitMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8FeatureSplitsMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8KeepAttributesMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8LibraryDesugaringMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8OptionsMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8ResourceOptimizationMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8StartupOptimizationMetadataImpl;
+import com.android.tools.r8.metadata.impl.R8StatsMetadataImpl;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonDeserializer;
 import java.util.List;
@@ -22,22 +27,30 @@
   static R8BuildMetadata fromJson(String json) {
     return new GsonBuilder()
         .excludeFieldsWithoutExposeAnnotation()
-        .registerTypeAdapter(R8Options.class, deserializeTo(R8OptionsImpl.class))
+        .registerTypeAdapter(R8OptionsMetadata.class, deserializeTo(R8OptionsMetadataImpl.class))
         .registerTypeAdapter(
-            R8ApiModelingOptions.class, deserializeTo(R8ApiModelingOptionsImpl.class))
+            R8ApiModelingMetadata.class, deserializeTo(R8ApiModelingMetadataImpl.class))
         .registerTypeAdapter(
-            R8BaselineProfileRewritingOptions.class,
-            deserializeTo(R8BaselineProfileRewritingOptionsImpl.class))
+            R8BaselineProfileRewritingMetadata.class,
+            deserializeTo(R8BaselineProfileRewritingMetadataImpl.class))
         .registerTypeAdapter(
-            R8KeepAttributesOptions.class, deserializeTo(R8KeepAttributesOptionsImpl.class))
+            R8CompilationMetadata.class, deserializeTo(R8CompilationMetadataImpl.class))
+        .registerTypeAdapter(R8DexFileMetadata.class, deserializeTo(R8DexFileMetadataImpl.class))
+        .registerTypeAdapter(R8StatsMetadata.class, deserializeTo(R8StatsMetadataImpl.class))
         .registerTypeAdapter(
-            R8LibraryDesugaringOptions.class, deserializeTo(R8LibraryDesugaringOptionsImpl.class))
+            R8FeatureSplitMetadata.class, deserializeTo(R8FeatureSplitMetadataImpl.class))
         .registerTypeAdapter(
-            R8ResourceOptimizationOptions.class,
-            deserializeTo(R8ResourceOptimizationOptionsImpl.class))
+            R8FeatureSplitsMetadata.class, deserializeTo(R8FeatureSplitsMetadataImpl.class))
         .registerTypeAdapter(
-            R8StartupOptimizationOptions.class,
-            deserializeTo(R8StartupOptimizationOptionsImpl.class))
+            R8KeepAttributesMetadata.class, deserializeTo(R8KeepAttributesMetadataImpl.class))
+        .registerTypeAdapter(
+            R8LibraryDesugaringMetadata.class, deserializeTo(R8LibraryDesugaringMetadataImpl.class))
+        .registerTypeAdapter(
+            R8ResourceOptimizationMetadata.class,
+            deserializeTo(R8ResourceOptimizationMetadataImpl.class))
+        .registerTypeAdapter(
+            R8StartupOptimizationMetadata.class,
+            deserializeTo(R8StartupOptimizationMetadataImpl.class))
         .create()
         .fromJson(json, R8BuildMetadataImpl.class);
   }
@@ -46,27 +59,36 @@
     return (element, type, context) -> context.deserialize(element, implClass);
   }
 
-  R8Options getOptions();
+  R8OptionsMetadata getOptionsMetadata();
 
   /**
    * @return null if baseline profile rewriting is disabled.
    */
-  R8BaselineProfileRewritingOptions getBaselineProfileRewritingOptions();
+  R8BaselineProfileRewritingMetadata getBaselineProfileRewritingMetadata();
+
+  R8CompilationMetadata getCompilationMetadata();
 
   /**
    * @return null if not compiling to dex.
    */
-  List<String> getDexChecksums();
+  List<R8DexFileMetadata> getDexFilesMetadata();
+
+  /**
+   * @return null if not using feature splits.
+   */
+  R8FeatureSplitsMetadata getFeatureSplitsMetadata();
 
   /**
    * @return null if resource optimization is disabled.
    */
-  R8ResourceOptimizationOptions getResourceOptimizationOptions();
+  R8ResourceOptimizationMetadata getResourceOptimizationMetadata();
 
   /**
    * @return null if startup optimization is disabled.
    */
-  R8StartupOptimizationOptions getStartupOptizationOptions();
+  R8StartupOptimizationMetadata getStartupOptizationOptions();
+
+  R8StatsMetadata getStatsMetadata();
 
   String getVersion();
 
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java b/src/main/java/com/android/tools/r8/metadata/R8CompilationMetadata.java
similarity index 77%
copy from src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
copy to src/main/java/com/android/tools/r8/metadata/R8CompilationMetadata.java
index 693286e..a4ce3aa 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8CompilationMetadata.java
@@ -6,7 +6,9 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8ResourceOptimizationOptions {
+public interface R8CompilationMetadata {
 
-  boolean isOptimizedShrinkingEnabled();
+  long getBuildTime();
+
+  long getNumberOfThreads();
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/R8DexFileMetadata.java b/src/main/java/com/android/tools/r8/metadata/R8DexFileMetadata.java
new file mode 100644
index 0000000..fc841d0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8DexFileMetadata.java
@@ -0,0 +1,21 @@
+// 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 R8DexFileMetadata {
+
+  /**
+   * Returns the SHA-256 checksum of the entire dex file.
+   *
+   * <p>This can be used to check if the given dex file has been tampered with after compilation.
+   *
+   * <p>Note: This differs from the checksum in the dex format, as the checksum embedded in the dex
+   * is the adler32 checksum of the dex file excluding the magic value and the checksum itself. See
+   * also https://source.android.com/docs/core/runtime/dex-format.
+   */
+  String getChecksum();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java b/src/main/java/com/android/tools/r8/metadata/R8FeatureSplitMetadata.java
similarity index 74%
copy from src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
copy to src/main/java/com/android/tools/r8/metadata/R8FeatureSplitMetadata.java
index 693286e..f9c5d47 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8FeatureSplitMetadata.java
@@ -4,9 +4,10 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.util.List;
 
 @KeepForApi
-public interface R8ResourceOptimizationOptions {
+public interface R8FeatureSplitMetadata {
 
-  boolean isOptimizedShrinkingEnabled();
+  List<R8DexFileMetadata> getDexFilesMetadata();
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java b/src/main/java/com/android/tools/r8/metadata/R8FeatureSplitsMetadata.java
similarity index 67%
copy from src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
copy to src/main/java/com/android/tools/r8/metadata/R8FeatureSplitsMetadata.java
index 30adb4a..66fd1e2 100644
--- a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8FeatureSplitsMetadata.java
@@ -4,7 +4,12 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.metadata.impl.D8R8LibraryDesugaringOptions;
+import java.util.List;
 
 @KeepForApi
-public interface D8LibraryDesugaringOptions extends D8R8LibraryDesugaringOptions {}
+public interface R8FeatureSplitsMetadata {
+
+  List<R8FeatureSplitMetadata> getFeatureSplits();
+
+  boolean isIsolatedSplitsEnabled();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptions.java b/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesMetadata.java
similarity index 95%
rename from src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptions.java
rename to src/main/java/com/android/tools/r8/metadata/R8KeepAttributesMetadata.java
index b590c7e..fa58986 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8KeepAttributesMetadata.java
@@ -6,7 +6,7 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8KeepAttributesOptions {
+public interface R8KeepAttributesMetadata {
 
   boolean isAnnotationDefaultKept();
 
diff --git a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java b/src/main/java/com/android/tools/r8/metadata/R8LibraryDesugaringMetadata.java
similarity index 67%
copy from src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
copy to src/main/java/com/android/tools/r8/metadata/R8LibraryDesugaringMetadata.java
index 30adb4a..c577d7d 100644
--- a/src/main/java/com/android/tools/r8/metadata/D8LibraryDesugaringOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8LibraryDesugaringMetadata.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.metadata;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.metadata.impl.D8R8LibraryDesugaringOptions;
+import com.android.tools.r8.metadata.impl.D8R8LibraryDesugaringMetadata;
 
 @KeepForApi
-public interface D8LibraryDesugaringOptions extends D8R8LibraryDesugaringOptions {}
+public interface R8LibraryDesugaringMetadata extends D8R8LibraryDesugaringMetadata {}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8LibraryDesugaringOptions.java b/src/main/java/com/android/tools/r8/metadata/R8LibraryDesugaringOptions.java
deleted file mode 100644
index 6a9df33..0000000
--- a/src/main/java/com/android/tools/r8/metadata/R8LibraryDesugaringOptions.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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;
-import com.android.tools.r8.metadata.impl.D8R8LibraryDesugaringOptions;
-
-@KeepForApi
-public interface R8LibraryDesugaringOptions extends D8R8LibraryDesugaringOptions {}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8Options.java b/src/main/java/com/android/tools/r8/metadata/R8OptionsMetadata.java
similarity index 79%
rename from src/main/java/com/android/tools/r8/metadata/R8Options.java
rename to src/main/java/com/android/tools/r8/metadata/R8OptionsMetadata.java
index 697af39..c9279c2 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8Options.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8OptionsMetadata.java
@@ -6,22 +6,22 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8Options {
+public interface R8OptionsMetadata {
 
   /**
    * @return null if API modeling is disabled.
    */
-  R8ApiModelingOptions getApiModelingOptions();
+  R8ApiModelingMetadata getApiModelingMetadata();
 
   /**
    * @return null if no ProGuard configuration is provided.
    */
-  R8KeepAttributesOptions getKeepAttributesOptions();
+  R8KeepAttributesMetadata getKeepAttributesMetadata();
 
   /**
    * @return null if library desugaring is disabled.
    */
-  R8LibraryDesugaringOptions getLibraryDesugaringOptions();
+  R8LibraryDesugaringMetadata getLibraryDesugaringMetadata();
 
   int getMinApiLevel();
 
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java b/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationMetadata.java
similarity index 87%
rename from src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
rename to src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationMetadata.java
index 693286e..1dd29f1 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8ResourceOptimizationMetadata.java
@@ -6,7 +6,7 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8ResourceOptimizationOptions {
+public interface R8ResourceOptimizationMetadata {
 
   boolean isOptimizedShrinkingEnabled();
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptions.java b/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationMetadata.java
similarity index 87%
rename from src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptions.java
rename to src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationMetadata.java
index a57b01e..608d05f 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8StartupOptimizationMetadata.java
@@ -6,7 +6,7 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8StartupOptimizationOptions {
+public interface R8StartupOptimizationMetadata {
 
   int getNumberOfStartupDexFiles();
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java b/src/main/java/com/android/tools/r8/metadata/R8StatsMetadata.java
similarity index 68%
copy from src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
copy to src/main/java/com/android/tools/r8/metadata/R8StatsMetadata.java
index 8d175f3..6b446ba 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8ApiModelingOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8StatsMetadata.java
@@ -6,4 +6,11 @@
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 
 @KeepForApi
-public interface R8ApiModelingOptions {}
+public interface R8StatsMetadata {
+
+  float getNoObfuscationPercentage();
+
+  float getNoOptimizationPercentage();
+
+  float getNoShrinkingPercentage();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java b/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java
index 69e906c..e40691b 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.metadata.impl;
 
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.Version;
 import com.android.tools.r8.dex.VirtualFile;
 import com.android.tools.r8.graph.AppInfo;
@@ -11,27 +14,51 @@
 import com.android.tools.r8.metadata.D8BuildMetadata;
 import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.utils.InternalOptions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
 
 public class BuildMetadataFactory {
 
   public static D8BuildMetadata create(AppView<AppInfo> appView) {
     return D8BuildMetadataImpl.builder()
-        .setOptions(new D8OptionsImpl(appView.options()))
+        .setOptions(new D8OptionsMetadataImpl(appView.options()))
         .setVersion(Version.LABEL)
         .build();
   }
 
   public static R8BuildMetadata create(
-      AppView<? extends AppInfoWithClassHierarchy> appView, List<VirtualFile> virtualFiles) {
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      ExecutorService executorService,
+      List<VirtualFile> virtualFiles) {
+    Map<FeatureSplit, List<VirtualFile>> virtualFilesForFeatureSplit = new IdentityHashMap<>();
+    for (VirtualFile virtualFile : virtualFiles) {
+      FeatureSplit featureSplit = virtualFile.getFeatureSplitOrBase();
+      virtualFilesForFeatureSplit
+          .computeIfAbsent(featureSplit, ignoreKey(ArrayList::new))
+          .add(virtualFile);
+    }
+    List<VirtualFile> baseVirtualFiles =
+        virtualFilesForFeatureSplit.getOrDefault(FeatureSplit.BASE, Collections.emptyList());
     InternalOptions options = appView.options();
     return R8BuildMetadataImpl.builder()
-        .setOptions(new R8OptionsImpl(options))
-        .setBaselineProfileRewritingOptions(R8BaselineProfileRewritingOptionsImpl.create(options))
-        .applyIf(options.isGeneratingDex(), builder -> builder.setDexChecksums(virtualFiles))
-        .setResourceOptimizationOptions(R8ResourceOptimizationOptionsImpl.create(options))
+        .setOptions(new R8OptionsMetadataImpl(options))
+        .setBaselineProfileRewritingOptions(R8BaselineProfileRewritingMetadataImpl.create(options))
+        .setCompilationInfo(R8CompilationMetadataImpl.create(appView, executorService))
+        .applyIf(
+            options.isGeneratingDex(), builder -> builder.setDexFilesMetadata(baseVirtualFiles))
+        .applyIf(
+            options.hasFeatureSplitConfiguration(),
+            builder ->
+                builder.setFeatureSplitsMetadata(
+                    R8FeatureSplitsMetadataImpl.create(appView, virtualFilesForFeatureSplit)))
+        .setResourceOptimizationOptions(R8ResourceOptimizationMetadataImpl.create(options))
         .setStartupOptimizationOptions(
-            R8StartupOptimizationOptionsImpl.create(options, virtualFiles))
+            R8StartupOptimizationMetadataImpl.create(options, baseVirtualFiles))
+        .setStatsMetadata(R8StatsMetadataImpl.create(appView))
         .setVersion(Version.LABEL)
         .build();
   }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8ApiModelingMetadataImpl.java
similarity index 79%
copy from src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingOptionsImpl.java
copy to src/main/java/com/android/tools/r8/metadata/impl/D8ApiModelingMetadataImpl.java
index c1ed920..4f252c3 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8ApiModelingMetadataImpl.java
@@ -8,7 +8,7 @@
 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.metadata.R8ApiModelingOptions;
+import com.android.tools.r8.metadata.D8ApiModelingMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.SerializedName;
 
@@ -19,13 +19,13 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8ApiModelingOptionsImpl implements R8ApiModelingOptions {
+public class D8ApiModelingMetadataImpl implements D8ApiModelingMetadata {
 
-  private R8ApiModelingOptionsImpl() {}
+  private D8ApiModelingMetadataImpl() {}
 
-  public static R8ApiModelingOptionsImpl create(InternalOptions options) {
+  public static D8ApiModelingMetadataImpl create(InternalOptions options) {
     return options.apiModelingOptions().enableLibraryApiModeling
-        ? new R8ApiModelingOptionsImpl()
+        ? new D8ApiModelingMetadataImpl()
         : null;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8ApiModelingOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8ApiModelingOptionsImpl.java
deleted file mode 100644
index 64a6302..0000000
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8ApiModelingOptionsImpl.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.impl;
-
-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.metadata.D8ApiModelingOptions;
-import com.android.tools.r8.utils.InternalOptions;
-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 D8ApiModelingOptionsImpl implements D8ApiModelingOptions {
-
-  private D8ApiModelingOptionsImpl() {}
-
-  public static D8ApiModelingOptionsImpl create(InternalOptions options) {
-    return options.apiModelingOptions().enableLibraryApiModeling
-        ? new D8ApiModelingOptionsImpl()
-        : null;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8BuildMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8BuildMetadataImpl.java
index 9f4bd36..22b91ed 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8BuildMetadataImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8BuildMetadataImpl.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.metadata.D8BuildMetadata;
-import com.android.tools.r8.metadata.D8Options;
+import com.android.tools.r8.metadata.D8OptionsMetadata;
 import com.google.gson.Gson;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
@@ -25,13 +25,13 @@
 
   @Expose
   @SerializedName("options")
-  private final D8Options options;
+  private final D8OptionsMetadata options;
 
   @Expose
   @SerializedName("version")
   private final String version;
 
-  public D8BuildMetadataImpl(D8Options options, String version) {
+  public D8BuildMetadataImpl(D8OptionsMetadata options, String version) {
     this.options = options;
     this.version = version;
   }
@@ -41,7 +41,7 @@
   }
 
   @Override
-  public D8Options getOptions() {
+  public D8OptionsMetadata getOptions() {
     return options;
   }
 
@@ -57,10 +57,10 @@
 
   public static class Builder {
 
-    private D8Options options;
+    private D8OptionsMetadata options;
     private String version;
 
-    public Builder setOptions(D8Options options) {
+    public Builder setOptions(D8OptionsMetadata options) {
       this.options = options;
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8LibraryDesugaringMetadataImpl.java
similarity index 74%
copy from src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringOptionsImpl.java
copy to src/main/java/com/android/tools/r8/metadata/impl/D8LibraryDesugaringMetadataImpl.java
index cad11e6..4b61718 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8LibraryDesugaringMetadataImpl.java
@@ -8,7 +8,7 @@
 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.metadata.R8LibraryDesugaringOptions;
+import com.android.tools.r8.metadata.D8LibraryDesugaringMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.SerializedName;
 
@@ -19,16 +19,16 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8LibraryDesugaringOptionsImpl extends D8R8LibraryDesugaringOptionsImpl
-    implements R8LibraryDesugaringOptions {
+public class D8LibraryDesugaringMetadataImpl extends D8R8LibraryDesugaringMetadataImpl
+    implements D8LibraryDesugaringMetadata {
 
-  private R8LibraryDesugaringOptionsImpl(InternalOptions options) {
+  private D8LibraryDesugaringMetadataImpl(InternalOptions options) {
     super(options);
   }
 
-  public static R8LibraryDesugaringOptionsImpl create(InternalOptions options) {
+  public static D8LibraryDesugaringMetadataImpl create(InternalOptions options) {
     return !options.machineDesugaredLibrarySpecification.isEmpty()
-        ? new R8LibraryDesugaringOptionsImpl(options)
+        ? new D8LibraryDesugaringMetadataImpl(options)
         : null;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8LibraryDesugaringOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8LibraryDesugaringOptionsImpl.java
deleted file mode 100644
index 0571af6..0000000
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8LibraryDesugaringOptionsImpl.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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.impl;
-
-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.metadata.D8LibraryDesugaringOptions;
-import com.android.tools.r8.utils.InternalOptions;
-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 D8LibraryDesugaringOptionsImpl extends D8R8LibraryDesugaringOptionsImpl
-    implements D8LibraryDesugaringOptions {
-
-  private D8LibraryDesugaringOptionsImpl(InternalOptions options) {
-    super(options);
-  }
-
-  public static D8LibraryDesugaringOptionsImpl create(InternalOptions options) {
-    return !options.machineDesugaredLibrarySpecification.isEmpty()
-        ? new D8LibraryDesugaringOptionsImpl(options)
-        : null;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8OptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8OptionsMetadataImpl.java
similarity index 68%
rename from src/main/java/com/android/tools/r8/metadata/impl/D8OptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/D8OptionsMetadataImpl.java
index 39210bd..76640a5 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8OptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8OptionsMetadataImpl.java
@@ -8,9 +8,9 @@
 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.metadata.D8ApiModelingOptions;
-import com.android.tools.r8.metadata.D8LibraryDesugaringOptions;
-import com.android.tools.r8.metadata.D8Options;
+import com.android.tools.r8.metadata.D8ApiModelingMetadata;
+import com.android.tools.r8.metadata.D8LibraryDesugaringMetadata;
+import com.android.tools.r8.metadata.D8OptionsMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.SerializedName;
 
@@ -21,13 +21,14 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class D8OptionsImpl extends D8R8OptionsImpl<D8ApiModelingOptions, D8LibraryDesugaringOptions>
-    implements D8Options {
+public class D8OptionsMetadataImpl
+    extends D8R8OptionsMetadataImpl<D8ApiModelingMetadata, D8LibraryDesugaringMetadata>
+    implements D8OptionsMetadata {
 
-  public D8OptionsImpl(InternalOptions options) {
+  public D8OptionsMetadataImpl(InternalOptions options) {
     super(
-        D8ApiModelingOptionsImpl.create(options),
-        D8LibraryDesugaringOptionsImpl.create(options),
+        D8ApiModelingMetadataImpl.create(options),
+        D8LibraryDesugaringMetadataImpl.create(options),
         options);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptions.java b/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringMetadata.java
similarity index 85%
rename from src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptions.java
rename to src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringMetadata.java
index 8ea55df..4070510 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptions.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringMetadata.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.metadata.impl;
 
-public interface D8R8LibraryDesugaringOptions {
+public interface D8R8LibraryDesugaringMetadata {
 
   String getIdentifier();
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringMetadataImpl.java
similarity index 88%
rename from src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringMetadataImpl.java
index 8cc4c8b..4ec551c 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringMetadataImpl.java
@@ -19,13 +19,13 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-abstract class D8R8LibraryDesugaringOptionsImpl implements D8R8LibraryDesugaringOptions {
+abstract class D8R8LibraryDesugaringMetadataImpl implements D8R8LibraryDesugaringMetadata {
 
   @Expose
   @SerializedName("identifier")
   private final String identifier;
 
-  public D8R8LibraryDesugaringOptionsImpl(InternalOptions options) {
+  public D8R8LibraryDesugaringMetadataImpl(InternalOptions options) {
     this.identifier = options.machineDesugaredLibrarySpecification.getIdentifier();
   }
 
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8R8Options.java b/src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsMetadata.java
similarity index 65%
rename from src/main/java/com/android/tools/r8/metadata/impl/D8R8Options.java
rename to src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsMetadata.java
index 6b7f8fc..0ec0446 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8R8Options.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsMetadata.java
@@ -3,18 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.metadata.impl;
 
-public interface D8R8Options<
-    ApiModelingOptions, LibraryDesugaringOptions extends D8R8LibraryDesugaringOptions> {
+public interface D8R8OptionsMetadata<
+    ApiModelingMetadata, LibraryDesugaringMetadata extends D8R8LibraryDesugaringMetadata> {
 
   /**
    * @return null if API modeling is disabled.
    */
-  ApiModelingOptions getApiModelingOptions();
+  ApiModelingMetadata getApiModelingMetadata();
 
   /**
    * @return null if library desugaring is disabled.
    */
-  LibraryDesugaringOptions getLibraryDesugaringOptions();
+  LibraryDesugaringMetadata getLibraryDesugaringMetadata();
 
   int getMinApiLevel();
 
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsMetadataImpl.java
similarity index 65%
rename from src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsMetadataImpl.java
index adc01e3..00b0527 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/D8R8OptionsMetadataImpl.java
@@ -19,17 +19,17 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-abstract class D8R8OptionsImpl<
-        ApiModelingOptions, LibraryDesugaringOptions extends D8R8LibraryDesugaringOptions>
-    implements D8R8Options<ApiModelingOptions, LibraryDesugaringOptions> {
+abstract class D8R8OptionsMetadataImpl<
+        ApiModelingMetadata, LibraryDesugaringMetadata extends D8R8LibraryDesugaringMetadata>
+    implements D8R8OptionsMetadata<ApiModelingMetadata, LibraryDesugaringMetadata> {
 
   @Expose
-  @SerializedName("apiModelingOptions")
-  private final ApiModelingOptions apiModelingOptions;
+  @SerializedName("apiModeling")
+  private final ApiModelingMetadata apiModelingMetadata;
 
   @Expose
-  @SerializedName("libraryDesugaringOptions")
-  private final LibraryDesugaringOptions libraryDesugaringOptions;
+  @SerializedName("libraryDesugaring")
+  private final LibraryDesugaringMetadata libraryDesugaringMetadata;
 
   @Expose
   @SerializedName("minApiLevel")
@@ -39,24 +39,24 @@
   @SerializedName("isDebugModeEnabled")
   private final boolean isDebugModeEnabled;
 
-  public D8R8OptionsImpl(
-      ApiModelingOptions apiModelingOptions,
-      LibraryDesugaringOptions libraryDesugaringOptions,
+  public D8R8OptionsMetadataImpl(
+      ApiModelingMetadata apiModelingMetadata,
+      LibraryDesugaringMetadata libraryDesugaringMetadata,
       InternalOptions options) {
-    this.apiModelingOptions = apiModelingOptions;
-    this.libraryDesugaringOptions = libraryDesugaringOptions;
+    this.apiModelingMetadata = apiModelingMetadata;
+    this.libraryDesugaringMetadata = libraryDesugaringMetadata;
     this.minApiLevel = options.isGeneratingDex() ? options.getMinApiLevel().getLevel() : -1;
     this.isDebugModeEnabled = options.debug;
   }
 
   @Override
-  public ApiModelingOptions getApiModelingOptions() {
-    return apiModelingOptions;
+  public ApiModelingMetadata getApiModelingMetadata() {
+    return apiModelingMetadata;
   }
 
   @Override
-  public LibraryDesugaringOptions getLibraryDesugaringOptions() {
-    return libraryDesugaringOptions;
+  public LibraryDesugaringMetadata getLibraryDesugaringMetadata() {
+    return libraryDesugaringMetadata;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingMetadataImpl.java
similarity index 79%
rename from src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingOptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingMetadataImpl.java
index c1ed920..28f388a 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8ApiModelingMetadataImpl.java
@@ -8,7 +8,7 @@
 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.metadata.R8ApiModelingOptions;
+import com.android.tools.r8.metadata.R8ApiModelingMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.SerializedName;
 
@@ -19,13 +19,13 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8ApiModelingOptionsImpl implements R8ApiModelingOptions {
+public class R8ApiModelingMetadataImpl implements R8ApiModelingMetadata {
 
-  private R8ApiModelingOptionsImpl() {}
+  private R8ApiModelingMetadataImpl() {}
 
-  public static R8ApiModelingOptionsImpl create(InternalOptions options) {
+  public static R8ApiModelingMetadataImpl create(InternalOptions options) {
     return options.apiModelingOptions().enableLibraryApiModeling
-        ? new R8ApiModelingOptionsImpl()
+        ? new R8ApiModelingMetadataImpl()
         : null;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8BaselineProfileRewritingOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8BaselineProfileRewritingMetadataImpl.java
similarity index 75%
rename from src/main/java/com/android/tools/r8/metadata/impl/R8BaselineProfileRewritingOptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/R8BaselineProfileRewritingMetadataImpl.java
index 36b7683..9b2b20b 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8BaselineProfileRewritingOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8BaselineProfileRewritingMetadataImpl.java
@@ -8,7 +8,7 @@
 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.metadata.R8BaselineProfileRewritingOptions;
+import com.android.tools.r8.metadata.R8BaselineProfileRewritingMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.SerializedName;
 
@@ -19,14 +19,14 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8BaselineProfileRewritingOptionsImpl implements R8BaselineProfileRewritingOptions {
+public class R8BaselineProfileRewritingMetadataImpl implements R8BaselineProfileRewritingMetadata {
 
-  private R8BaselineProfileRewritingOptionsImpl() {}
+  private R8BaselineProfileRewritingMetadataImpl() {}
 
-  public static R8BaselineProfileRewritingOptionsImpl create(InternalOptions options) {
+  public static R8BaselineProfileRewritingMetadataImpl create(InternalOptions options) {
     if (options.getArtProfileOptions().getArtProfilesForRewriting().isEmpty()) {
       return null;
     }
-    return new R8BaselineProfileRewritingOptionsImpl();
+    return new R8BaselineProfileRewritingMetadataImpl();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java
index 47d97b5..38cb87d 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java
@@ -11,11 +11,16 @@
 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.metadata.R8BaselineProfileRewritingOptions;
+import com.android.tools.r8.metadata.R8BaselineProfileRewritingMetadata;
 import com.android.tools.r8.metadata.R8BuildMetadata;
-import com.android.tools.r8.metadata.R8Options;
-import com.android.tools.r8.metadata.R8ResourceOptimizationOptions;
-import com.android.tools.r8.metadata.R8StartupOptimizationOptions;
+import com.android.tools.r8.metadata.R8CompilationMetadata;
+import com.android.tools.r8.metadata.R8DexFileMetadata;
+import com.android.tools.r8.metadata.R8FeatureSplitsMetadata;
+import com.android.tools.r8.metadata.R8OptionsMetadata;
+import com.android.tools.r8.metadata.R8ResourceOptimizationMetadata;
+import com.android.tools.r8.metadata.R8StartupOptimizationMetadata;
+import com.android.tools.r8.metadata.R8StatsMetadata;
+import com.android.tools.r8.utils.ListUtils;
 import com.google.gson.Gson;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
@@ -34,40 +39,58 @@
 
   @Expose
   @SerializedName("options")
-  private final R8Options options;
+  private final R8OptionsMetadata optionsMetadata;
 
   @Expose
-  @SerializedName("baselineProfileRewritingOptions")
-  private final R8BaselineProfileRewritingOptions baselineProfileRewritingOptions;
+  @SerializedName("baselineProfileRewriting")
+  private final R8BaselineProfileRewritingMetadata baselineProfileRewritingMetadata;
 
   @Expose
-  @SerializedName("dexChecksums")
-  private final List<String> dexChecksums;
+  @SerializedName("compilation")
+  private final R8CompilationMetadata compilationMetadata;
 
   @Expose
-  @SerializedName("resourceOptimizationOptions")
-  private final R8ResourceOptimizationOptions resourceOptimizationOptions;
+  @SerializedName("dexFiles")
+  private final List<R8DexFileMetadata> dexFilesMetadata;
 
   @Expose
-  @SerializedName("startupOptimizationOptions")
-  private final R8StartupOptimizationOptions startupOptimizationOptions;
+  @SerializedName("stats")
+  private final R8StatsMetadata statsMetadata;
+
+  @Expose
+  @SerializedName("featureSplits")
+  private final R8FeatureSplitsMetadata featureSplitsMetadata;
+
+  @Expose
+  @SerializedName("resourceOptimization")
+  private final R8ResourceOptimizationMetadata resourceOptimizationMetadata;
+
+  @Expose
+  @SerializedName("startupOptimization")
+  private final R8StartupOptimizationMetadata startupOptimizationMetadata;
 
   @Expose
   @SerializedName("version")
   private final String version;
 
   public R8BuildMetadataImpl(
-      R8Options options,
-      R8BaselineProfileRewritingOptions baselineProfileRewritingOptions,
-      List<String> dexChecksums,
-      R8ResourceOptimizationOptions resourceOptimizationOptions,
-      R8StartupOptimizationOptions startupOptimizationOptions,
+      R8OptionsMetadata options,
+      R8BaselineProfileRewritingMetadata baselineProfileRewritingOptions,
+      R8CompilationMetadata compilationMetadata,
+      List<R8DexFileMetadata> dexFilesMetadata,
+      R8StatsMetadata statsMetadata,
+      R8FeatureSplitsMetadata featureSplitsMetadata,
+      R8ResourceOptimizationMetadata resourceOptimizationMetadata,
+      R8StartupOptimizationMetadata startupOptimizationMetadata,
       String version) {
-    this.options = options;
-    this.baselineProfileRewritingOptions = baselineProfileRewritingOptions;
-    this.dexChecksums = dexChecksums;
-    this.resourceOptimizationOptions = resourceOptimizationOptions;
-    this.startupOptimizationOptions = startupOptimizationOptions;
+    this.optionsMetadata = options;
+    this.baselineProfileRewritingMetadata = baselineProfileRewritingOptions;
+    this.compilationMetadata = compilationMetadata;
+    this.dexFilesMetadata = dexFilesMetadata;
+    this.statsMetadata = statsMetadata;
+    this.featureSplitsMetadata = featureSplitsMetadata;
+    this.resourceOptimizationMetadata = resourceOptimizationMetadata;
+    this.startupOptimizationMetadata = startupOptimizationMetadata;
     this.version = version;
   }
 
@@ -76,28 +99,43 @@
   }
 
   @Override
-  public R8Options getOptions() {
-    return options;
+  public R8OptionsMetadata getOptionsMetadata() {
+    return optionsMetadata;
   }
 
   @Override
-  public R8BaselineProfileRewritingOptions getBaselineProfileRewritingOptions() {
-    return baselineProfileRewritingOptions;
+  public R8BaselineProfileRewritingMetadata getBaselineProfileRewritingMetadata() {
+    return baselineProfileRewritingMetadata;
   }
 
   @Override
-  public List<String> getDexChecksums() {
-    return dexChecksums;
+  public R8CompilationMetadata getCompilationMetadata() {
+    return compilationMetadata;
   }
 
   @Override
-  public R8ResourceOptimizationOptions getResourceOptimizationOptions() {
-    return resourceOptimizationOptions;
+  public List<R8DexFileMetadata> getDexFilesMetadata() {
+    return dexFilesMetadata;
   }
 
   @Override
-  public R8StartupOptimizationOptions getStartupOptizationOptions() {
-    return startupOptimizationOptions;
+  public R8FeatureSplitsMetadata getFeatureSplitsMetadata() {
+    return featureSplitsMetadata;
+  }
+
+  @Override
+  public R8ResourceOptimizationMetadata getResourceOptimizationMetadata() {
+    return resourceOptimizationMetadata;
+  }
+
+  @Override
+  public R8StartupOptimizationMetadata getStartupOptizationOptions() {
+    return startupOptimizationMetadata;
+  }
+
+  @Override
+  public R8StatsMetadata getStatsMetadata() {
+    return statsMetadata;
   }
 
   @Override
@@ -112,11 +150,14 @@
 
   public static class Builder {
 
-    private R8Options options;
-    private R8BaselineProfileRewritingOptions baselineProfileRewritingOptions;
-    private List<String> dexChecksums;
-    private R8ResourceOptimizationOptions resourceOptimizationOptions;
-    private R8StartupOptimizationOptions startupOptimizationOptions;
+    private R8OptionsMetadata options;
+    private R8BaselineProfileRewritingMetadata baselineProfileRewritingOptions;
+    private R8CompilationMetadata compilationInfo;
+    private List<R8DexFileMetadata> dexFilesMetadata;
+    private R8StatsMetadata statsMetadata;
+    private R8FeatureSplitsMetadata featureSplitsMetadata;
+    private R8ResourceOptimizationMetadata resourceOptimizationOptions;
+    private R8StartupOptimizationMetadata startupOptimizationOptions;
     private String version;
 
     public Builder applyIf(boolean condition, Consumer<Builder> thenConsumer) {
@@ -126,34 +167,50 @@
       return this;
     }
 
-    public Builder setOptions(R8Options options) {
+    public Builder setOptions(R8OptionsMetadata options) {
       this.options = options;
       return this;
     }
 
     public Builder setBaselineProfileRewritingOptions(
-        R8BaselineProfileRewritingOptions baselineProfileRewritingOptions) {
+        R8BaselineProfileRewritingMetadata baselineProfileRewritingOptions) {
       this.baselineProfileRewritingOptions = baselineProfileRewritingOptions;
       return this;
     }
 
-    public Builder setDexChecksums(List<VirtualFile> virtualFiles) {
-      this.dexChecksums =
+    public Builder setCompilationInfo(R8CompilationMetadata compilationInfo) {
+      this.compilationInfo = compilationInfo;
+      return this;
+    }
+
+    public Builder setDexFilesMetadata(List<VirtualFile> virtualFiles) {
+      List<String> checksums =
           virtualFiles.stream()
               .filter(not(VirtualFile::isEmpty))
               .map(virtualFile -> virtualFile.getChecksumForBuildMetadata().toString())
               .collect(Collectors.toList());
+      this.dexFilesMetadata = ListUtils.map(checksums, R8DexFileMetadataImpl::new);
+      return this;
+    }
+
+    public Builder setStatsMetadata(R8StatsMetadata statsMetadata) {
+      this.statsMetadata = statsMetadata;
+      return this;
+    }
+
+    public Builder setFeatureSplitsMetadata(R8FeatureSplitsMetadata featureSplitsMetadata) {
+      this.featureSplitsMetadata = featureSplitsMetadata;
       return this;
     }
 
     public Builder setResourceOptimizationOptions(
-        R8ResourceOptimizationOptions resourceOptimizationOptions) {
+        R8ResourceOptimizationMetadata resourceOptimizationOptions) {
       this.resourceOptimizationOptions = resourceOptimizationOptions;
       return this;
     }
 
     public Builder setStartupOptimizationOptions(
-        R8StartupOptimizationOptions startupOptimizationOptions) {
+        R8StartupOptimizationMetadata startupOptimizationOptions) {
       this.startupOptimizationOptions = startupOptimizationOptions;
       return this;
     }
@@ -167,7 +224,10 @@
       return new R8BuildMetadataImpl(
           options,
           baselineProfileRewritingOptions,
-          dexChecksums,
+          compilationInfo,
+          dexFilesMetadata,
+          statsMetadata,
+          featureSplitsMetadata,
           resourceOptimizationOptions,
           startupOptimizationOptions,
           version);
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8CompilationMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8CompilationMetadataImpl.java
new file mode 100644
index 0000000..4be7d15
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8CompilationMetadataImpl.java
@@ -0,0 +1,60 @@
+// 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.impl;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+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.metadata.R8CompilationMetadata;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import java.util.concurrent.ExecutorService;
+
+@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 R8CompilationMetadataImpl implements R8CompilationMetadata {
+
+  @Expose
+  @SerializedName("buildTime")
+  private final long buildTime;
+
+  @Expose
+  @SerializedName("numberOfThreads")
+  private final long numberOfThreads;
+
+  private R8CompilationMetadataImpl(long buildTime, int numberOfThreads) {
+    this.buildTime = buildTime;
+    this.numberOfThreads = numberOfThreads;
+  }
+
+  public static R8CompilationMetadataImpl create(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ExecutorService executorService) {
+    InternalOptions options = appView.options();
+    assert options.created > 0;
+    long buildTime = System.nanoTime() - options.created;
+    return new R8CompilationMetadataImpl(
+        buildTime, ThreadUtils.getNumberOfThreads(executorService));
+  }
+
+  @Override
+  public long getBuildTime() {
+    return buildTime;
+  }
+
+  @Override
+  public long getNumberOfThreads() {
+    return numberOfThreads;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8DexFileMetadataImpl.java
similarity index 72%
copy from src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java
copy to src/main/java/com/android/tools/r8/metadata/impl/R8DexFileMetadataImpl.java
index 8cc4c8b..e0680dc 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8DexFileMetadataImpl.java
@@ -8,7 +8,7 @@
 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.android.tools.r8.metadata.R8DexFileMetadata;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
 
@@ -19,18 +19,18 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-abstract class D8R8LibraryDesugaringOptionsImpl implements D8R8LibraryDesugaringOptions {
+public class R8DexFileMetadataImpl implements R8DexFileMetadata {
 
   @Expose
-  @SerializedName("identifier")
-  private final String identifier;
+  @SerializedName("checksum")
+  private final String checksum;
 
-  public D8R8LibraryDesugaringOptionsImpl(InternalOptions options) {
-    this.identifier = options.machineDesugaredLibrarySpecification.getIdentifier();
+  public R8DexFileMetadataImpl(String checksum) {
+    this.checksum = checksum;
   }
 
   @Override
-  public String getIdentifier() {
-    return identifier;
+  public String getChecksum() {
+    return checksum;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8FeatureSplitMetadataImpl.java
similarity index 67%
copy from src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java
copy to src/main/java/com/android/tools/r8/metadata/impl/R8FeatureSplitMetadataImpl.java
index 8cc4c8b..521642e 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/D8R8LibraryDesugaringOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8FeatureSplitMetadataImpl.java
@@ -8,9 +8,11 @@
 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.android.tools.r8.metadata.R8DexFileMetadata;
+import com.android.tools.r8.metadata.R8FeatureSplitMetadata;
 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",
@@ -19,18 +21,18 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-abstract class D8R8LibraryDesugaringOptionsImpl implements D8R8LibraryDesugaringOptions {
+public class R8FeatureSplitMetadataImpl implements R8FeatureSplitMetadata {
 
   @Expose
-  @SerializedName("identifier")
-  private final String identifier;
+  @SerializedName("dexFiles")
+  private final List<R8DexFileMetadata> dexFilesMetadata;
 
-  public D8R8LibraryDesugaringOptionsImpl(InternalOptions options) {
-    this.identifier = options.machineDesugaredLibrarySpecification.getIdentifier();
+  public R8FeatureSplitMetadataImpl(List<R8DexFileMetadata> dexFilesMetadata) {
+    this.dexFilesMetadata = dexFilesMetadata;
   }
 
   @Override
-  public String getIdentifier() {
-    return identifier;
+  public List<R8DexFileMetadata> getDexFilesMetadata() {
+    return dexFilesMetadata;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8FeatureSplitsMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8FeatureSplitsMetadataImpl.java
new file mode 100644
index 0000000..d3b6b7a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8FeatureSplitsMetadataImpl.java
@@ -0,0 +1,82 @@
+// 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.impl;
+
+import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.dex.VirtualFile;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+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.metadata.R8DexFileMetadata;
+import com.android.tools.r8.metadata.R8FeatureSplitMetadata;
+import com.android.tools.r8.metadata.R8FeatureSplitsMetadata;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@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 R8FeatureSplitsMetadataImpl implements R8FeatureSplitsMetadata {
+
+  @Expose
+  @SerializedName("featureSplits")
+  private final List<R8FeatureSplitMetadata> featureSplitsMetadata;
+
+  @Expose
+  @SerializedName("isolatedSplits")
+  private final boolean isolatedSplits;
+
+  public R8FeatureSplitsMetadataImpl(
+      FeatureSplitConfiguration configuration, List<R8FeatureSplitMetadata> featureSplitsMetadata) {
+    this.featureSplitsMetadata = featureSplitsMetadata;
+    this.isolatedSplits = configuration.isIsolatedSplitsEnabled();
+  }
+
+  public static R8FeatureSplitsMetadataImpl create(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Map<FeatureSplit, List<VirtualFile>> virtualFilesForFeatureSplit) {
+    InternalOptions options = appView.options();
+    assert options.hasFeatureSplitConfiguration();
+    FeatureSplitConfiguration configuration = options.getFeatureSplitConfiguration();
+    List<FeatureSplit> featureSplits = configuration.getFeatureSplits();
+    List<R8FeatureSplitMetadata> featureSplitsMetadata = new ArrayList<>();
+    for (FeatureSplit featureSplit : featureSplits) {
+      List<VirtualFile> featureSplitVirtualFiles =
+          virtualFilesForFeatureSplit.getOrDefault(featureSplit, Collections.emptyList());
+      List<R8DexFileMetadata> dexFilesMetadata =
+          ListUtils.map(
+              featureSplitVirtualFiles,
+              featureSplitVirtualFile ->
+                  new R8DexFileMetadataImpl(
+                      featureSplitVirtualFile.getChecksumForBuildMetadata().toString()));
+      featureSplitsMetadata.add(new R8FeatureSplitMetadataImpl(dexFilesMetadata));
+    }
+    return new R8FeatureSplitsMetadataImpl(configuration, featureSplitsMetadata);
+  }
+
+  @Override
+  public List<R8FeatureSplitMetadata> getFeatureSplits() {
+    return featureSplitsMetadata;
+  }
+
+  @Override
+  public boolean isIsolatedSplitsEnabled() {
+    return isolatedSplits;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8KeepAttributesOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8KeepAttributesMetadataImpl.java
similarity index 96%
rename from src/main/java/com/android/tools/r8/metadata/impl/R8KeepAttributesOptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/R8KeepAttributesMetadataImpl.java
index ccec35f..ee9f918 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8KeepAttributesOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8KeepAttributesMetadataImpl.java
@@ -8,7 +8,7 @@
 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.metadata.R8KeepAttributesOptions;
+import com.android.tools.r8.metadata.R8KeepAttributesMetadata;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
@@ -20,7 +20,7 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8KeepAttributesOptionsImpl implements R8KeepAttributesOptions {
+public class R8KeepAttributesMetadataImpl implements R8KeepAttributesMetadata {
 
   @Expose
   @SerializedName("isAnnotationDefaultKept")
@@ -98,7 +98,7 @@
   @SerializedName("isStackMapTableKept")
   private final boolean isStackMapTableKept;
 
-  public R8KeepAttributesOptionsImpl(ProguardKeepAttributes keepAttributes) {
+  public R8KeepAttributesMetadataImpl(ProguardKeepAttributes keepAttributes) {
     this.isAnnotationDefaultKept = keepAttributes.annotationDefault;
     this.isEnclosingMethodKept = keepAttributes.enclosingMethod;
     this.isExceptionsKept = keepAttributes.exceptions;
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringMetadataImpl.java
similarity index 74%
rename from src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringOptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringMetadataImpl.java
index cad11e6..84059cd 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8LibraryDesugaringMetadataImpl.java
@@ -8,7 +8,7 @@
 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.metadata.R8LibraryDesugaringOptions;
+import com.android.tools.r8.metadata.R8LibraryDesugaringMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.SerializedName;
 
@@ -19,16 +19,16 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8LibraryDesugaringOptionsImpl extends D8R8LibraryDesugaringOptionsImpl
-    implements R8LibraryDesugaringOptions {
+public class R8LibraryDesugaringMetadataImpl extends D8R8LibraryDesugaringMetadataImpl
+    implements R8LibraryDesugaringMetadata {
 
-  private R8LibraryDesugaringOptionsImpl(InternalOptions options) {
+  private R8LibraryDesugaringMetadataImpl(InternalOptions options) {
     super(options);
   }
 
-  public static R8LibraryDesugaringOptionsImpl create(InternalOptions options) {
+  public static R8LibraryDesugaringMetadataImpl create(InternalOptions options) {
     return !options.machineDesugaredLibrarySpecification.isEmpty()
-        ? new R8LibraryDesugaringOptionsImpl(options)
+        ? new R8LibraryDesugaringMetadataImpl(options)
         : null;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8OptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8OptionsMetadataImpl.java
similarity index 75%
rename from src/main/java/com/android/tools/r8/metadata/impl/R8OptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/R8OptionsMetadataImpl.java
index 28e5e92..70cec6f 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8OptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8OptionsMetadataImpl.java
@@ -8,10 +8,10 @@
 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.metadata.R8ApiModelingOptions;
-import com.android.tools.r8.metadata.R8KeepAttributesOptions;
-import com.android.tools.r8.metadata.R8LibraryDesugaringOptions;
-import com.android.tools.r8.metadata.R8Options;
+import com.android.tools.r8.metadata.R8ApiModelingMetadata;
+import com.android.tools.r8.metadata.R8KeepAttributesMetadata;
+import com.android.tools.r8.metadata.R8LibraryDesugaringMetadata;
+import com.android.tools.r8.metadata.R8OptionsMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
@@ -23,12 +23,13 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8OptionsImpl extends D8R8OptionsImpl<R8ApiModelingOptions, R8LibraryDesugaringOptions>
-    implements R8Options {
+public class R8OptionsMetadataImpl
+    extends D8R8OptionsMetadataImpl<R8ApiModelingMetadata, R8LibraryDesugaringMetadata>
+    implements R8OptionsMetadata {
 
   @Expose
-  @SerializedName("keepAttributesOptions")
-  private final R8KeepAttributesOptions keepAttributesOptions;
+  @SerializedName("keepAttributes")
+  private final R8KeepAttributesMetadata keepAttributesMetadata;
 
   @Expose
   @SerializedName("isAccessModificationEnabled")
@@ -50,14 +51,14 @@
   @SerializedName("isShrinkingEnabled")
   private final boolean isShrinkingEnabled;
 
-  public R8OptionsImpl(InternalOptions options) {
+  public R8OptionsMetadataImpl(InternalOptions options) {
     super(
-        R8ApiModelingOptionsImpl.create(options),
-        R8LibraryDesugaringOptionsImpl.create(options),
+        R8ApiModelingMetadataImpl.create(options),
+        R8LibraryDesugaringMetadataImpl.create(options),
         options);
-    this.keepAttributesOptions =
+    this.keepAttributesMetadata =
         options.hasProguardConfiguration()
-            ? new R8KeepAttributesOptionsImpl(
+            ? new R8KeepAttributesMetadataImpl(
                 options.getProguardConfiguration().getKeepAttributes())
             : null;
     this.isAccessModificationEnabled = options.isAccessModificationEnabled();
@@ -68,8 +69,8 @@
   }
 
   @Override
-  public R8KeepAttributesOptions getKeepAttributesOptions() {
-    return keepAttributesOptions;
+  public R8KeepAttributesMetadata getKeepAttributesMetadata() {
+    return keepAttributesMetadata;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8ResourceOptimizationOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8ResourceOptimizationMetadataImpl.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/metadata/impl/R8ResourceOptimizationOptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/R8ResourceOptimizationMetadataImpl.java
index 3507271..790557e 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8ResourceOptimizationOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8ResourceOptimizationMetadataImpl.java
@@ -9,7 +9,7 @@
 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.metadata.R8ResourceOptimizationOptions;
+import com.android.tools.r8.metadata.R8ResourceOptimizationMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
@@ -21,22 +21,22 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8ResourceOptimizationOptionsImpl implements R8ResourceOptimizationOptions {
+public class R8ResourceOptimizationMetadataImpl implements R8ResourceOptimizationMetadata {
 
   @Expose
   @SerializedName("isOptimizedShrinkingEnabled")
   private final boolean isOptimizedShrinkingEnabled;
 
-  private R8ResourceOptimizationOptionsImpl(
+  private R8ResourceOptimizationMetadataImpl(
       ResourceShrinkerConfiguration resourceShrinkerConfiguration) {
     this.isOptimizedShrinkingEnabled = resourceShrinkerConfiguration.isOptimizedShrinking();
   }
 
-  public static R8ResourceOptimizationOptionsImpl create(InternalOptions options) {
+  public static R8ResourceOptimizationMetadataImpl create(InternalOptions options) {
     if (options.androidResourceProvider == null) {
       return null;
     }
-    return new R8ResourceOptimizationOptionsImpl(options.resourceShrinkerConfiguration);
+    return new R8ResourceOptimizationMetadataImpl(options.resourceShrinkerConfiguration);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8StartupOptimizationOptionsImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8StartupOptimizationMetadataImpl.java
similarity index 82%
rename from src/main/java/com/android/tools/r8/metadata/impl/R8StartupOptimizationOptionsImpl.java
rename to src/main/java/com/android/tools/r8/metadata/impl/R8StartupOptimizationMetadataImpl.java
index 0406f26..f18efd4 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8StartupOptimizationOptionsImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8StartupOptimizationMetadataImpl.java
@@ -9,7 +9,7 @@
 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.metadata.R8StartupOptimizationOptions;
+import com.android.tools.r8.metadata.R8StartupOptimizationMetadata;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
@@ -22,23 +22,23 @@
     kind = KeepItemKind.CLASS_AND_FIELDS,
     fieldAccess = {FieldAccessFlags.PRIVATE},
     fieldAnnotatedByClassConstant = SerializedName.class)
-public class R8StartupOptimizationOptionsImpl implements R8StartupOptimizationOptions {
+public class R8StartupOptimizationMetadataImpl implements R8StartupOptimizationMetadata {
 
   @Expose
   @SerializedName("numberOfStartupDexFiles")
   private final int numberOfStartupDexFiles;
 
-  public R8StartupOptimizationOptionsImpl(List<VirtualFile> virtualFiles) {
+  public R8StartupOptimizationMetadataImpl(List<VirtualFile> virtualFiles) {
     this.numberOfStartupDexFiles =
         (int) virtualFiles.stream().filter(VirtualFile::isStartup).count();
   }
 
-  public static R8StartupOptimizationOptionsImpl create(
+  public static R8StartupOptimizationMetadataImpl create(
       InternalOptions options, List<VirtualFile> virtualFiles) {
     if (options.getStartupOptions().getStartupProfileProviders().isEmpty()) {
       return null;
     }
-    return new R8StartupOptimizationOptionsImpl(virtualFiles);
+    return new R8StartupOptimizationMetadataImpl(virtualFiles);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
new file mode 100644
index 0000000..05b8e2f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
@@ -0,0 +1,119 @@
+// 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.impl;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramDefinition;
+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.metadata.R8StatsMetadata;
+import com.android.tools.r8.shaking.KeepInfo;
+import com.android.tools.r8.utils.BooleanUtils;
+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 R8StatsMetadataImpl implements R8StatsMetadata {
+
+  @Expose
+  @SerializedName("noObfuscationPercentage")
+  private final float noObfuscationPercentage;
+
+  @Expose
+  @SerializedName("noOptimizationPercentage")
+  private final float noOptimizationPercentage;
+
+  @Expose
+  @SerializedName("noShrinkingPercentage")
+  private final float noShrinkingPercentage;
+
+  private R8StatsMetadataImpl(
+      float noObfuscationPercentage, float noOptimizationPercentage, float noShrinkingPercentage) {
+    this.noObfuscationPercentage = noObfuscationPercentage;
+    this.noOptimizationPercentage = noOptimizationPercentage;
+    this.noShrinkingPercentage = noShrinkingPercentage;
+  }
+
+  public static R8StatsMetadataImpl create(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    Counters counters = Counters.create(appView);
+    return new R8StatsMetadataImpl(
+        counters.getNoObfuscationPercentage(),
+        counters.getNoOptimizationPercentage(),
+        counters.getNoShrinkingPercentage());
+  }
+
+  @Override
+  public float getNoObfuscationPercentage() {
+    return noObfuscationPercentage;
+  }
+
+  @Override
+  public float getNoOptimizationPercentage() {
+    return noOptimizationPercentage;
+  }
+
+  @Override
+  public float getNoShrinkingPercentage() {
+    return noShrinkingPercentage;
+  }
+
+  private static class Counters {
+
+    private int itemsCount = 0;
+    private int noObfuscationCount = 0;
+    private int noOptimizationCount = 0;
+    private int noShrinkingCount = 0;
+
+    private Counters() {}
+
+    static Counters create(AppView<? extends AppInfoWithClassHierarchy> appView) {
+      Counters counters = new Counters();
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
+        counters.add(appView, clazz);
+        clazz.forEachProgramMember(member -> counters.add(appView, member));
+      }
+      return counters;
+    }
+
+    private void add(
+        AppView<? extends AppInfoWithClassHierarchy> appView, ProgramDefinition definition) {
+      KeepInfo<?, ?> keepInfo = appView.getKeepInfo(definition);
+      InternalOptions options = appView.options();
+      itemsCount++;
+      noObfuscationCount += BooleanUtils.intValue(!keepInfo.isMinificationAllowed(options));
+      noOptimizationCount += BooleanUtils.intValue(!keepInfo.isOptimizationAllowed(options));
+      noShrinkingCount += BooleanUtils.intValue(!keepInfo.isShrinkingAllowed(options));
+    }
+
+    float getNoObfuscationPercentage() {
+      return toPercentageWithTwoDecimals(noObfuscationCount);
+    }
+
+    float getNoOptimizationPercentage() {
+      return toPercentageWithTwoDecimals(noOptimizationCount);
+    }
+
+    float getNoShrinkingPercentage() {
+      return toPercentageWithTwoDecimals(noShrinkingCount);
+    }
+
+    float toPercentageWithTwoDecimals(int count) {
+      // Multiply by 100 twice to get percentage with two decimals.
+      float number = (float) (count * 100 * 100) / itemsCount;
+      return (float) Math.round(number) / 100;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/relocator/Relocator.java b/src/main/java/com/android/tools/r8/relocator/Relocator.java
index a0824ca..d74a6e9 100644
--- a/src/main/java/com/android/tools/r8/relocator/Relocator.java
+++ b/src/main/java/com/android/tools/r8/relocator/Relocator.java
@@ -81,7 +81,8 @@
       appView.setAppServices(AppServices.builder(appView).build());
       appView.setNamingLens(command.getMapping().compute(appView));
       new GenericSignatureRewriter(appView).run(appInfo.classes(), executor);
-      new CfApplicationWriter(appView, new Marker(Tool.Relocator)).write(command.getConsumer());
+      new CfApplicationWriter(appView, new Marker(Tool.Relocator))
+          .write(command.getConsumer(), executor);
       options.printWarnings();
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
index 07bea60..02373b6 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
@@ -11,6 +11,7 @@
 public class ProguardConfigurationParserOptions {
 
   private final OptionalBool enableEmptyMemberRulesToDefaultInitRuleConversion;
+  private final boolean enableEmptyMemberRulesToDefaultInitRuleConversionWarnings;
   private final boolean enableExperimentalCheckEnumUnboxed;
   private final boolean enableExperimentalConvertCheckNotNull;
   private final boolean enableExperimentalWhyAreYouNotInlining;
@@ -18,6 +19,7 @@
 
   ProguardConfigurationParserOptions(
       OptionalBool enableEmptyMemberRulesToDefaultInitRuleConversion,
+      boolean enableEmptyMemberRulesToDefaultInitRuleConversionWarnings,
       boolean enableExperimentalCheckEnumUnboxed,
       boolean enableExperimentalConvertCheckNotNull,
       boolean enableExperimentalWhyAreYouNotInlining,
@@ -28,6 +30,8 @@
     this.enableTestingOptions = enableTestingOptions;
     this.enableEmptyMemberRulesToDefaultInitRuleConversion =
         enableEmptyMemberRulesToDefaultInitRuleConversion;
+    this.enableEmptyMemberRulesToDefaultInitRuleConversionWarnings =
+        enableEmptyMemberRulesToDefaultInitRuleConversionWarnings;
   }
 
   public static Builder builder() {
@@ -45,7 +49,7 @@
       ProguardConfiguration.Builder configurationBuilder) {
     assert isEmptyMemberRulesToDefaultInitRuleConversionEnabled(configurationBuilder);
     return !configurationBuilder.isForceProguardCompatibility()
-        && enableEmptyMemberRulesToDefaultInitRuleConversion.isUnknown();
+        && enableEmptyMemberRulesToDefaultInitRuleConversionWarnings;
   }
 
   public boolean isExperimentalCheckEnumUnboxedEnabled() {
@@ -67,6 +71,7 @@
   public static class Builder {
 
     private OptionalBool enableEmptyMemberRulesToDefaultInitRuleConversion = OptionalBool.UNKNOWN;
+    private boolean enableEmptyMemberRulesToDefaultInitRuleConversionWarnings = false;
     private boolean enableExperimentalCheckEnumUnboxed;
     private boolean enableExperimentalConvertCheckNotNull;
     private boolean enableExperimentalWhyAreYouNotInlining;
@@ -77,6 +82,10 @@
           parseSystemPropertyOrDefault(
               "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion",
               OptionalBool.UNKNOWN);
+      enableEmptyMemberRulesToDefaultInitRuleConversionWarnings =
+          parseSystemPropertyOrDefault(
+              "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversionWarnings",
+              false);
       enableExperimentalCheckEnumUnboxed =
           parseSystemPropertyOrDefault(
               "com.android.tools.r8.experimental.enablecheckenumunboxed", false);
@@ -124,6 +133,7 @@
     public ProguardConfigurationParserOptions build() {
       return new ProguardConfigurationParserOptions(
           enableEmptyMemberRulesToDefaultInitRuleConversion,
+          enableEmptyMemberRulesToDefaultInitRuleConversionWarnings,
           enableExperimentalCheckEnumUnboxed,
           enableExperimentalConvertCheckNotNull,
           enableExperimentalWhyAreYouNotInlining,
diff --git a/src/main/java/com/android/tools/r8/startup/diagnostic/MissingStartupProfileItemsDiagnostic.java b/src/main/java/com/android/tools/r8/startup/diagnostic/MissingStartupProfileItemsDiagnostic.java
index c5ab7a9..1087442 100644
--- a/src/main/java/com/android/tools/r8/startup/diagnostic/MissingStartupProfileItemsDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/startup/diagnostic/MissingStartupProfileItemsDiagnostic.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.profile.startup.profile.StartupProfileClassRule;
 import com.android.tools.r8.profile.startup.profile.StartupProfileMethodRule;
+import com.android.tools.r8.utils.SystemPropertyUtils;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -22,6 +23,10 @@
 @KeepForApi
 public class MissingStartupProfileItemsDiagnostic implements Diagnostic {
 
+  private static final int THRESHOLD =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.startup.diagnostic.limit", 100);
+
   private final List<DexReference> missingStartupItems;
   private final Origin origin;
 
@@ -43,6 +48,10 @@
 
   @Override
   public String getDiagnosticMessage() {
+    if (THRESHOLD == 0) {
+      return "Found " + missingStartupItems.size() + " missing startup classes and methods";
+    }
+
     StringBuilder builder = new StringBuilder();
     Iterator<DexReference> missingStartupItemIterator = missingStartupItems.iterator();
 
@@ -50,9 +59,21 @@
     writeMissingStartupItem(builder, missingStartupItemIterator.next());
 
     // Write remaining missing startup items with line separator before.
-    while (missingStartupItemIterator.hasNext()) {
+    int itemsToReport = (THRESHOLD > 0 ? THRESHOLD : Integer.MAX_VALUE) - 1;
+    while (missingStartupItemIterator.hasNext() && itemsToReport > 0) {
       writeMissingStartupItem(
           builder.append(System.lineSeparator()), missingStartupItemIterator.next());
+      itemsToReport--;
+    }
+
+    if (missingStartupItemIterator.hasNext()) {
+      assert THRESHOLD > 0;
+      int omittedItems = missingStartupItems.size() - THRESHOLD;
+      builder
+          .append(System.lineSeparator())
+          .append("Found ")
+          .append(omittedItems)
+          .append(" other missing startup classes and methods");
     }
 
     return builder.toString();
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 763198e..ad6845e 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -241,6 +241,7 @@
     return proguardConfiguration;
   }
 
+  public long created = -1;
   private final ProguardConfiguration proguardConfiguration;
   public final Reporter reporter;
 
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsAdapterBase.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsAdapterBase.java
new file mode 100644
index 0000000..ddc7b2d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsAdapterBase.java
@@ -0,0 +1,30 @@
+// 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.benchmarks;
+
+import com.google.gson.JsonObject;
+import java.util.Collection;
+import java.util.function.IntToLongFunction;
+
+public abstract class BenchmarkResultsAdapterBase {
+
+  void addPropertyIfValueDifferentFromRepresentative(
+      JsonObject resultObject,
+      String propertyName,
+      int iteration,
+      Collection<?> results,
+      IntToLongFunction getter) {
+    if (results.isEmpty()) {
+      return;
+    }
+    long result = getter.applyAsLong(iteration);
+    if (iteration != 0) {
+      long representativeResult = getter.applyAsLong(0);
+      if (result == representativeResult) {
+        return;
+      }
+    }
+    resultObject.addProperty(propertyName, result);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
index 7d006fc..9ca4f81 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
@@ -86,11 +86,13 @@
   }
 
   @Override
-  public void writeResults(Path path) throws IOException {
+  public void writeResults(Path path, BenchmarkResults warmupResults) throws IOException {
     for (Entry<String, BenchmarkResultsSingle> entry : results.entrySet()) {
       String name = entry.getKey();
       BenchmarkResultsSingle result = entry.getValue();
-      result.writeResults(path.resolve(name));
+      BenchmarkResults warmupSubResults =
+          warmupResults != null ? warmupResults.getSubResults(name) : null;
+      result.writeResults(path.resolve(name), warmupSubResults);
     }
   }
 }
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 e029bcd..22fbf4a 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.dex.DexSection;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongList;
@@ -268,14 +269,20 @@
   }
 
   @Override
-  public void writeResults(Path path) throws IOException {
+  public void writeResults(Path path, BenchmarkResults warmupResults) throws IOException {
     try (PrintStream out = new PrintStream(Files.newOutputStream(path))) {
       Gson gson =
           new GsonBuilder()
               .registerTypeAdapter(
                   BenchmarkResultsSingle.class, new BenchmarkResultsSingleAdapter())
+              .registerTypeAdapter(
+                  BenchmarkResultsWarmup.class, new BenchmarkResultsWarmupAdapter())
               .create();
-      out.print(gson.toJson(this));
+      JsonObject json = (JsonObject) gson.toJsonTree(this);
+      if (warmupResults != null) {
+        json.add("warmup", gson.toJsonTree(warmupResults));
+      }
+      out.print(json);
     }
   }
 }
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 90a257c..7e62cfc 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
@@ -11,10 +11,9 @@
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
 import java.lang.reflect.Type;
-import java.util.Collection;
-import java.util.function.IntToLongFunction;
 
-public class BenchmarkResultsSingleAdapter implements JsonSerializer<BenchmarkResultsSingle> {
+public class BenchmarkResultsSingleAdapter extends BenchmarkResultsAdapterBase
+    implements JsonSerializer<BenchmarkResultsSingle> {
 
   @Override
   public JsonElement serialize(
@@ -43,7 +42,7 @@
       for (int section : DexSection.getConstants()) {
         String sectionName = DexSection.typeName(section);
         String sectionNameUnderscore =
-            CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, sectionName);
+            CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, sectionName);
         addPropertyIfValueDifferentFromRepresentative(
             resultObject,
             "dex_" + sectionNameUnderscore + "_size",
@@ -71,23 +70,4 @@
     benchmarkObject.add("results", resultsArray);
     return benchmarkObject;
   }
-
-  private void addPropertyIfValueDifferentFromRepresentative(
-      JsonObject resultObject,
-      String propertyName,
-      int iteration,
-      Collection<?> results,
-      IntToLongFunction getter) {
-    if (results.isEmpty()) {
-      return;
-    }
-    long result = getter.applyAsLong(iteration);
-    if (iteration != 0) {
-      long representativeResult = getter.applyAsLong(0);
-      if (result == representativeResult) {
-        return;
-      }
-    }
-    resultObject.addProperty(propertyName, result);
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
index 16084c1..12e4b9a 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
@@ -12,6 +12,9 @@
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongList;
 import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
 
 public class BenchmarkResultsWarmup implements BenchmarkResults {
 
@@ -20,10 +23,19 @@
   private long codeSizeResult = -1;
   private long resourceSizeResult = -1;
 
+  private final Map<String, BenchmarkResultsWarmup> results = new HashMap<>();
+
   public BenchmarkResultsWarmup(String name) {
     this.name = name;
   }
 
+  public BenchmarkResultsWarmup(String name, Map<String, Set<BenchmarkMetric>> benchmarks) {
+    this.name = name;
+    benchmarks.forEach(
+        (benchmarkName, metrics) ->
+            results.put(benchmarkName, new BenchmarkResultsWarmup(benchmarkName)));
+  }
+
   @Override
   public void addRuntimeResult(long result) {
     runtimeResults.add(result);
@@ -84,10 +96,13 @@
     addRuntimeResult(averageRuntimeResult);
   }
 
+  public LongList getRuntimeResults() {
+    return runtimeResults;
+  }
+
   @Override
   public BenchmarkResults getSubResults(String name) {
-    // When running warmups all results are amended to the single warmup result.
-    return this;
+    return results.get(name);
   }
 
   @Override
@@ -95,6 +110,10 @@
     return false;
   }
 
+  public int size() {
+    return runtimeResults.size();
+  }
+
   @Override
   public void printResults(ResultMode mode, boolean failOnCodeSizeDifferences) {
     if (runtimeResults.isEmpty()) {
@@ -108,7 +127,7 @@
   }
 
   @Override
-  public void writeResults(Path path) {
+  public void writeResults(Path path, BenchmarkResults warmupResults) {
     throw new Unimplemented();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmupAdapter.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmupAdapter.java
new file mode 100644
index 0000000..19cffef
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmupAdapter.java
@@ -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.
+package com.android.tools.r8.benchmarks;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+
+public class BenchmarkResultsWarmupAdapter extends BenchmarkResultsAdapterBase
+    implements JsonSerializer<BenchmarkResultsWarmup> {
+
+  @Override
+  public JsonElement serialize(
+      BenchmarkResultsWarmup result, Type type, JsonSerializationContext jsonSerializationContext) {
+    JsonArray resultsArray = new JsonArray();
+    for (int iteration = 0; iteration < result.size(); iteration++) {
+      JsonObject resultObject = new JsonObject();
+      addPropertyIfValueDifferentFromRepresentative(
+          resultObject,
+          "runtime",
+          iteration,
+          result.getRuntimeResults(),
+          i -> result.getRuntimeResults().getLong(i));
+      resultsArray.add(resultObject);
+    }
+    return resultsArray;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java
index 3f1db19..85ae724 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java
@@ -53,8 +53,12 @@
   public void run(BenchmarkRunnerFunction fn) throws Exception {
     long warmupTotalTime = 0;
     BenchmarkConfig config = environment.getConfig();
-    BenchmarkResults warmupResults = new BenchmarkResultsWarmup(config.getName());
+    BenchmarkResultsWarmup warmupResults = null;
     if (warmups > 0) {
+      warmupResults =
+          config.isSingleBenchmark()
+              ? new BenchmarkResultsWarmup(config.getName())
+              : new BenchmarkResultsWarmup(config.getName(), config.getSubBenchmarks());
       long start = System.nanoTime();
       for (int i = 0; i < warmups; i++) {
         fn.run(warmupResults);
@@ -84,7 +88,7 @@
     printMetaInfo("benchmark", getBenchmarkIterations(), benchmarkTotalTime);
     results.printResults(resultMode, environment.failOnCodeSizeDifferences());
     if (environment.hasOutputPath()) {
-      results.writeResults(environment.getOutputPath());
+      results.writeResults(environment.getOutputPath(), warmupResults);
     }
     System.out.println();
   }
diff --git a/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
index 04b401c..24927fd 100644
--- a/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
@@ -51,15 +51,20 @@
         ImmutableList.of(ExternalStartupClass.builder().setClassReference(mainReference).build());
     R8BuildMetadata buildMetadata =
         testForR8(parameters.getBackend())
-            .addInnerClasses(getClass())
+            .addProgramClasses(Main.class)
             .addKeepMainRule(Main.class)
             .addArtProfileForRewriting(
                 ExternalArtProfile.builder().addClassRule(mainReference).build())
-            .apply(StartupTestingUtils.addStartupProfile(startupProfile))
             .applyIf(
                 parameters.isDexRuntime(),
                 testBuilder ->
-                    testBuilder.addAndroidResources(getTestResources()).enableOptimizedShrinking())
+                    testBuilder
+                        .addAndroidResources(getTestResources())
+                        .addFeatureSplit(FeatureSplitMain.class)
+                        .addKeepMainRule(FeatureSplitMain.class)
+                        .apply(StartupTestingUtils.addStartupProfile(startupProfile))
+                        .enableIsolatedSplits(true)
+                        .enableOptimizedShrinking())
             .allowDiagnosticInfoMessages(parameters.canUseNativeMultidex())
             .collectBuildMetadata()
             .setMinApi(parameters)
@@ -86,27 +91,47 @@
   }
 
   private void inspectDeserializedBuildMetadata(R8BuildMetadata buildMetadata) {
-    assertNotNull(buildMetadata.getBaselineProfileRewritingOptions());
-    assertNotNull(buildMetadata.getOptions());
-    assertNotNull(buildMetadata.getOptions().getKeepAttributesOptions());
+    assertNotNull(buildMetadata.getBaselineProfileRewritingMetadata());
+    assertNotNull(buildMetadata.getCompilationMetadata());
+    R8FeatureSplitsMetadata featureSplitsMetadata = buildMetadata.getFeatureSplitsMetadata();
+    if (parameters.isDexRuntime()) {
+      assertNotNull(featureSplitsMetadata);
+      assertTrue(featureSplitsMetadata.isIsolatedSplitsEnabled());
+      assertEquals(1, featureSplitsMetadata.getFeatureSplits().size());
+      R8FeatureSplitMetadata featureSplitMetadata = featureSplitsMetadata.getFeatureSplits().get(0);
+      assertNotNull(featureSplitMetadata);
+      assertEquals(1, featureSplitMetadata.getDexFilesMetadata().size());
+      R8DexFileMetadata featureSplitDexFile = featureSplitMetadata.getDexFilesMetadata().get(0);
+      assertNotNull(featureSplitDexFile);
+      assertNotNull(featureSplitDexFile.getChecksum());
+    } else {
+      assertNull(featureSplitsMetadata);
+    }
+    assertNotNull(buildMetadata.getOptionsMetadata());
+    assertNotNull(buildMetadata.getOptionsMetadata().getKeepAttributesMetadata());
     assertEquals(
         parameters.isCfRuntime() ? -1 : parameters.getApiLevel().getLevel(),
-        buildMetadata.getOptions().getMinApiLevel());
-    assertFalse(buildMetadata.getOptions().isDebugModeEnabled());
+        buildMetadata.getOptionsMetadata().getMinApiLevel());
+    assertFalse(buildMetadata.getOptionsMetadata().isDebugModeEnabled());
     if (parameters.isDexRuntime()) {
-      R8ResourceOptimizationOptions resourceOptimizationOptions =
-          buildMetadata.getResourceOptimizationOptions();
+      R8ResourceOptimizationMetadata resourceOptimizationOptions =
+          buildMetadata.getResourceOptimizationMetadata();
       assertNotNull(resourceOptimizationOptions);
       assertTrue(resourceOptimizationOptions.isOptimizedShrinkingEnabled());
     } else {
-      assertNull(buildMetadata.getResourceOptimizationOptions());
+      assertNull(buildMetadata.getResourceOptimizationMetadata());
     }
-    R8StartupOptimizationOptions startupOptimizationOptions =
+    R8StartupOptimizationMetadata startupOptimizationOptions =
         buildMetadata.getStartupOptizationOptions();
-    assertNotNull(startupOptimizationOptions);
-    assertEquals(
-        parameters.isDexRuntime() && parameters.canUseNativeMultidex() ? 1 : 0,
-        startupOptimizationOptions.getNumberOfStartupDexFiles());
+    if (parameters.isDexRuntime()) {
+      assertNotNull(startupOptimizationOptions);
+      assertEquals(
+          parameters.isDexRuntime() && parameters.canUseNativeMultidex() ? 1 : 0,
+          startupOptimizationOptions.getNumberOfStartupDexFiles());
+    } else {
+      assertNull(startupOptimizationOptions);
+    }
+    assertNotNull(buildMetadata.getStatsMetadata());
     assertEquals(Version.LABEL, buildMetadata.getVersion());
   }
 
@@ -114,4 +139,9 @@
 
     public static void main(String[] args) {}
   }
+
+  static class FeatureSplitMain {
+
+    public static void main(String[] args) {}
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/regress/Regress371247958.java b/src/test/java/com/android/tools/r8/regress/Regress371247958.java
new file mode 100644
index 0000000..96fc07d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/Regress371247958.java
@@ -0,0 +1,61 @@
+// 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.regress;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class Regress371247958 extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withAllApiLevels().build();
+  }
+
+  public Regress371247958(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        // TODO(b/371247958): Should throw a RuntimeException
+        .assertSuccess();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .applyIf(parameters.isDexRuntime(), b -> b.setMinApi(parameters))
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(RuntimeException.class);
+  }
+
+  public static class TestClass {
+    static int throwException(RuntimeException runtimeException, boolean shouldReturnValue) {
+      if (shouldReturnValue) {
+        return 1;
+      }
+      throw runtimeException;
+    }
+
+    public static void main(String[] p) throws Exception {
+      throwException(new RuntimeException("always thrown"), false);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
index 564eac9..22a5d8a 100644
--- a/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
@@ -3,12 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking;
 
-import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
-import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
-import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
 
@@ -17,9 +13,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ThrowableConsumer;
-import com.android.tools.r8.errors.EmptyMemberRulesToDefaultInitRuleConversionDiagnostic;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.StringUtils;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -74,28 +68,9 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepClassRules(Main.class)
-        .allowDiagnosticWarningMessages()
         .clearEnableEmptyMemberRulesToDefaultInitRuleConversion()
         .setMinApi(parameters)
-        .compileWithExpectedDiagnostics(
-            diagnostics ->
-                diagnostics
-                    .assertOnlyWarnings()
-                    .assertWarningsMatch(
-                        allOf(
-                            diagnosticType(
-                                EmptyMemberRulesToDefaultInitRuleConversionDiagnostic.class),
-                            diagnosticMessage(
-                                equalTo(
-                                    StringUtils.joinLines(
-                                        "The current version of R8 implicitly keeps the default"
-                                            + " constructor for Proguard configuration rules that"
-                                            + " have no member pattern. If the following rule"
-                                            + " should continue to keep the default constructor in"
-                                            + " the next major version of R8, then it must be"
-                                            + " augmented with the member pattern `{ void <init>();"
-                                            + " }` to explicitly keep the default constructor:",
-                                        "-keep class " + Main.class.getTypeName()))))))
+        .compile()
         .inspect(inspector -> assertThat(inspector.clazz(Main.class).init(), isPresent()));
   }
 
diff --git a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
index 42fb589..92cfd22 100644
--- a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
+++ b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
@@ -40,7 +40,7 @@
 
   void printResults(ResultMode resultMode, boolean failOnCodeSizeDifferences);
 
-  void writeResults(Path path) throws IOException;
+  void writeResults(Path path, BenchmarkResults warmupResults) throws IOException;
 
   static String prettyTime(long nanoTime) {
     return "" + (nanoTime / 1000000) + " ms";
diff --git a/tools/perf/d8.html b/tools/perf/d8.html
index d495324..848ceea 100644
--- a/tools/perf/d8.html
+++ b/tools/perf/d8.html
@@ -48,7 +48,8 @@
       'Dex size': { default: true },
       'Nondeterminism': { default: true },
       'Runtime': { default: true },
-      'Runtime variance': { default: false }
+      'Runtime variance': { default: false },
+      'Warmup': { default: false }
     });
     state.initializeZoom();
     dom.initializeBenchmarkSelectors();
@@ -89,11 +90,9 @@
           const runtimeData =
               filteredCommits.map(
                   (c, i) =>
-                      selectedBenchmark in filteredCommits[i].benchmarks
-                          ? getAllResults(selectedBenchmark, filteredCommits[i], "runtime",
-                                results => dom.transformRuntimeData(results))
-                              .ns_to_s()
-                          : NaN);
+                      getAllResults(
+                          selectedBenchmark, filteredCommits[i], "runtime",
+                          results => dom.transformRuntimeData(results).ns_to_s()));
           const runtimeScatterData = [];
           for (const commit of filteredCommits.values()) {
             if (!(selectedBenchmark in commit.benchmarks)) {
@@ -104,6 +103,12 @@
               runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() });
             }
           }
+          const warmupData =
+              filteredCommits.map(
+                  (c, i) =>
+                      getAllWarmupResults(
+                          selectedBenchmark, filteredCommits[i], "runtime",
+                          results => dom.transformRuntimeData(results).ns_to_s()));
 
           const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;
           datasets.push(...[
@@ -176,6 +181,29 @@
                 }
               },
               yAxisID: 'y_runtime'
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Warmup',
+              data: warmupData,
+              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
             }
           ]);
         });
diff --git a/tools/perf/r8.html b/tools/perf/r8.html
index 23b6c7b..a992005 100644
--- a/tools/perf/r8.html
+++ b/tools/perf/r8.html
@@ -51,7 +51,8 @@
       'Oat size': { default: true },
       'Nondeterminism': { default: true },
       'Runtime': { default: true },
-      'Runtime variance': { default: false }
+      'Runtime variance': { default: false },
+      'Warmup': { default: false }
     });
     state.initializeZoom();
     dom.initializeBenchmarkSelectors();
@@ -101,11 +102,9 @@
           const runtimeData =
               filteredCommits.map(
                   (c, i) =>
-                      selectedBenchmark in filteredCommits[i].benchmarks
-                          ? getAllResults(selectedBenchmark, filteredCommits[i], "runtime",
-                                results => dom.transformRuntimeData(results))
-                              .ns_to_s()
-                          : NaN);
+                      getAllResults(
+                          selectedBenchmark, filteredCommits[i], "runtime",
+                          results => dom.transformRuntimeData(results).ns_to_s()));
           const runtimeScatterData = [];
           for (const commit of filteredCommits.values()) {
             if (!(selectedBenchmark in commit.benchmarks)) {
@@ -116,6 +115,12 @@
               runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() });
             }
           }
+          const warmupData =
+              filteredCommits.map(
+                  (c, i) =>
+                      getAllWarmupResults(
+                          selectedBenchmark, filteredCommits[i], "runtime",
+                          results => dom.transformRuntimeData(results).ns_to_s()));
 
           const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;
           datasets.push(...[
@@ -254,6 +259,29 @@
                 }
               },
               yAxisID: 'y_runtime'
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Warmup',
+              data: warmupData,
+              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
             }
           ]);
         });
diff --git a/tools/perf/retrace.html b/tools/perf/retrace.html
index 7437025..b6331c6 100644
--- a/tools/perf/retrace.html
+++ b/tools/perf/retrace.html
@@ -46,7 +46,8 @@
     state.initializeBenchmarks();
     state.initializeLegends({
       'Runtime': { default: true },
-      'Runtime variance': { default: true }
+      'Runtime variance': { default: true },
+      'Warmup': { default: false }
     });
     state.initializeZoom();
     dom.initializeBenchmarkSelectors();
@@ -69,11 +70,9 @@
           const runtimeData =
               filteredCommits.map(
                   (c, i) =>
-                      selectedBenchmark in filteredCommits[i].benchmarks
-                          ? getAllResults(selectedBenchmark, filteredCommits[i], "runtime",
-                                results => dom.transformRuntimeData(results))
-                              .ns_to_s()
-                          : NaN);
+                      getAllResults(
+                          selectedBenchmark, filteredCommits[i], "runtime",
+                          results => dom.transformRuntimeData(results).ns_to_s()));
           const runtimeScatterData = [];
           for (const commit of filteredCommits.values()) {
             if (!(selectedBenchmark in commit.benchmarks)) {
@@ -84,6 +83,12 @@
               runtimeScatterData.push({ x: commit.index, y: runtime.ns_to_s() });
             }
           }
+          const warmupData =
+              filteredCommits.map(
+                  (c, i) =>
+                      getAllWarmupResults(
+                          selectedBenchmark, filteredCommits[i], "runtime",
+                          results => dom.transformRuntimeData(results).ns_to_s()));
 
           const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined;
           datasets.push(...[
@@ -121,6 +126,29 @@
                 }
               },
               yAxisID: 'y_runtime'
+            },
+            {
+              benchmark: selectedBenchmark,
+              type: 'line',
+              label: 'Warmup',
+              data: warmupData,
+              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
             }
           ]);
         });
diff --git a/tools/perf/scales.js b/tools/perf/scales.js
index 8378e2f..bd3226a 100644
--- a/tools/perf/scales.js
+++ b/tools/perf/scales.js
@@ -21,6 +21,7 @@
   }
   console.assert(state.hasLegend('Runtime'));
   console.assert(state.hasLegend('Runtime variance'));
+  console.assert(state.hasLegend('Warmup'));
   scales.y_runtime = {
     position: state.hasLegend('Dex size') ? 'right' : 'left',
     title: {
@@ -62,7 +63,9 @@
   }
   if (scales.y_runtime) {
     scales.y_runtime.display =
-        state.isLegendSelected('Runtime') || state.isLegendSelected('Runtime variance');
+        state.isLegendSelected('Runtime')
+            || state.isLegendSelected('Runtime variance')
+            || state.isLegendSelected('Warmup');
   }
 }
 
diff --git a/tools/perf/utils.js b/tools/perf/utils.js
index 13cd09c..e699f32 100644
--- a/tools/perf/utils.js
+++ b/tools/perf/utils.js
@@ -1,11 +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.
-function getSingleResult(benchmark, commit, resultName, resultIteration = 0) {
+function getSingleResult(benchmark, commit, resultName, resultIteration = 0, warmup = false) {
   if (!(benchmark in commit.benchmarks)) {
     return NaN;
   }
-  const allResults = commit.benchmarks[benchmark].results;
+  const benchmarkData = commit.benchmarks[benchmark];
+  const allResults = warmup ? benchmarkData.warmup : benchmarkData.results;
   const resultsForIteration = allResults[resultIteration];
   // If a given iteration does not declare a result, then the result
   // was the same as the first run.
@@ -15,14 +16,25 @@
   return resultsForIteration[resultName];
 }
 
-function getAllResults(benchmark, commit, resultName, transformation) {
+function getAllResults(benchmark, commit, resultName, transformation, warmup = false) {
+  if (!(benchmark in commit.benchmarks)) {
+    return NaN;
+  }
+  const benchmarkData = commit.benchmarks[benchmark];
+  if (warmup && !('warmup' in benchmarkData)) {
+    return NaN;
+  }
   const result = [];
-  const allResults = commit.benchmarks[benchmark].results;
+  const allResults = warmup ? benchmarkData.warmup : benchmarkData.results;
   for (var iteration = 0; iteration < allResults.length; iteration++) {
-    result.push(getSingleResult(benchmark, commit, resultName, iteration));
+    result.push(getSingleResult(benchmark, commit, resultName, iteration, warmup));
   }
   if (transformation) {
     return transformation(result);
   }
   return result;
 }
+
+function getAllWarmupResults(benchmark, commit, resultName, transformation) {
+  return getAllResults(benchmark, commit, resultName, transformation, true);
+}