Reland "Update global synthetics API to support per-file global output."

This reverts commit e00706123c1e8e8144135588a837cb680e207032.
Bug: b/230445931

Change-Id: Ibae969a14f2bcfbd82997fd82cba039fc727121c
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 61aea99..7cb5133 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -185,9 +185,9 @@
             () ->
                 AppInfo.createInitialAppInfo(
                     app,
-                    options.isGeneratingDexFilePerClassFile()
-                        ? GlobalSyntheticsStrategy.forPerFileMode()
-                        : GlobalSyntheticsStrategy.forSingleOutputMode(),
+                    options.isGeneratingDexIndexed()
+                        ? GlobalSyntheticsStrategy.forSingleOutputMode()
+                        : GlobalSyntheticsStrategy.forPerFileMode(),
                     applicationReader.readMainDexClasses(app)));
     return timing.time("Create app-view", () -> AppView.createForD8(appInfo, typeRewriter, timing));
   }
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsConsumer.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsConsumer.java
index ea8ad11..1f982ce 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsConsumer.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsConsumer.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import com.android.tools.r8.references.ClassReference;
+
 /**
  * Consumer receiving the data representing global synthetics for the program.
  *
@@ -25,7 +27,23 @@
    * <p>The encoding of the global synthetics is compiler internal and may vary between compiler
    * versions. The data received here is thus only valid as inputs to the same compiler version.
    *
-   * @param bytes Opaque encoding of the global synthetics for the program.
+   * <p>The context class is the class for which the global synthetic data is needed. If compiling
+   * in DexIndexed mode, the context class will be null.
+   *
+   * <p>The accept method will be called at most once for a given context class (any only once at
+   * all for a DexIndexed mode compilation). The global data for that class may be the same as for
+   * other context classes, but it will be provided for each context.
+   *
+   * @param data Opaque encoding of the global synthetics for the program.
+   * @param context The class giving rise to the global synthetics. Null in DexIndexed mode.
+   * @param handler Diagnostics handler for reporting.
    */
-  void accept(byte[] bytes);
+  void accept(ByteDataView data, ClassReference context, DiagnosticsHandler handler);
+
+  /**
+   * Callback indicating that no more global synthetics will be reported for the compilation unit.
+   *
+   * @param handler Diagnostics handler for reporting.
+   */
+  default void finished(DiagnosticsHandler handler) {}
 }
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 1e62a10..cc04d01 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -52,7 +52,9 @@
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
-import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramConsumer.InternalGlobalSyntheticsDexConsumer;
+import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramConsumer;
+import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramConsumer.InternalGlobalSyntheticsDexIndexedConsumer;
+import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramConsumer.InternalGlobalSyntheticsDexPerFileConsumer;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OriginalSourceFiles;
 import com.android.tools.r8.utils.PredicateUtils;
@@ -90,7 +92,7 @@
   public Set<VirtualFile> globalSyntheticFiles;
 
   public DexIndexedConsumer programConsumer;
-  public InternalGlobalSyntheticsDexConsumer globalsSyntheticsConsumer;
+  public InternalGlobalSyntheticsProgramConsumer globalsSyntheticsConsumer;
 
   private static class SortAnnotations extends MixedSectionCollection {
 
@@ -221,7 +223,11 @@
       globalSyntheticFiles = new HashSet<>(files);
       virtualFiles.addAll(globalSyntheticFiles);
       globalsSyntheticsConsumer =
-          new InternalGlobalSyntheticsDexConsumer(options.getGlobalSyntheticsConsumer());
+          options.isGeneratingDexFilePerClassFile()
+              ? new InternalGlobalSyntheticsDexPerFileConsumer(
+                  options.getGlobalSyntheticsConsumer(), appView)
+              : new InternalGlobalSyntheticsDexIndexedConsumer(
+                  options.getGlobalSyntheticsConsumer());
     }
     return virtualFiles;
   }
@@ -362,7 +368,7 @@
         merger.add(timings);
         merger.end();
         if (globalsSyntheticsConsumer != null) {
-          globalsSyntheticsConsumer.finished(options.reporter);
+          globalsSyntheticsConsumer.finished(appView, namingLens);
         }
       }
 
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 7f8d075..3ac9bc61 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -169,12 +169,12 @@
     }
     if (!globalSyntheticClasses.isEmpty()) {
       InternalGlobalSyntheticsCfConsumer globalsConsumer =
-          new InternalGlobalSyntheticsCfConsumer(options.getGlobalSyntheticsConsumer());
+          new InternalGlobalSyntheticsCfConsumer(options.getGlobalSyntheticsConsumer(), appView);
       for (DexProgramClass clazz : globalSyntheticClasses) {
         writeClassCatchingErrors(
             clazz, globalsConsumer, rewriter, markerString, sourceFileEnvironment);
       }
-      globalsConsumer.finished(options.reporter);
+      globalsConsumer.finished(appView, namingLens);
     }
     ApplicationWriter.supplyAdditionalConsumers(application, appView, namingLens, options);
   }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 34b8b82..f23e6cf 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
@@ -32,19 +33,24 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
+import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.synthesis.SyntheticFinalization.Result;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
@@ -223,12 +229,21 @@
     this.globalSyntheticsStrategy = globalSyntheticsStrategy;
   }
 
-  public Set<DexType> getFinalGlobalSyntheticContexts(DexType globalSynthetic) {
+  public Map<DexType, Set<DexType>> getFinalGlobalSyntheticContexts(
+      AppView appView, NamingLens namingLens) {
     assert isFinalized();
-    assert isSynthetic(globalSynthetic);
-    Set<DexType> contexts = committed.getContextsForGlobal(globalSynthetic);
-    assert !contexts.isEmpty();
-    return contexts;
+    DexItemFactory factory = appView.dexItemFactory();
+    ImmutableMap<DexType, Set<DexType>> globalContexts = committed.getGlobalContexts();
+    Map<DexType, Set<DexType>> rewritten = new IdentityHashMap<>(globalContexts.size());
+    globalContexts.forEach(
+        (global, contexts) -> {
+          Set<DexType> old =
+              rewritten.put(
+                  namingLens.lookupType(global, factory),
+                  SetUtils.mapIdentityHashSet(contexts, c -> namingLens.lookupType(c, factory)));
+          assert old == null;
+        });
+    return rewritten;
   }
 
   public static void collectSyntheticInputs(AppView<?> appView) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
index 3945a6e..f818928 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramConsumer.java
@@ -5,88 +5,147 @@
 
 import static com.android.tools.r8.utils.FileUtils.GLOBAL_SYNTHETIC_EXTENSION;
 
+import com.android.tools.r8.ByteBufferProvider;
 import com.android.tools.r8.ByteDataView;
 import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.GlobalSyntheticsConsumer;
+import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.Version;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.references.Reference;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
-public abstract class InternalGlobalSyntheticsProgramConsumer {
+public abstract class InternalGlobalSyntheticsProgramConsumer
+    implements ProgramConsumer, ByteBufferProvider {
 
   public static final String COMPILER_INFO_ENTRY_NAME = "compilerinfo";
   public static final String OUTPUT_KIND_ENTRY_NAME = "kind";
 
-  private final GlobalSyntheticsConsumer consumer;
-  private final List<Pair<String, byte[]>> content = new ArrayList<>();
+  // Builder for constructing a valid "globals" data payload.
+  private static class GlobalsFileBuilder {
 
-  public InternalGlobalSyntheticsProgramConsumer(GlobalSyntheticsConsumer consumer) {
-    this.consumer = consumer;
-  }
+    private final Kind kind;
+    private final List<Pair<String, byte[]>> content = new ArrayList<>();
 
-  public abstract Kind getKind();
-
-  synchronized void addGlobalSynthetic(String descriptor, byte[] data) {
-    add(getGlobalSyntheticFileName(descriptor), data);
-  }
-
-  private void add(String entryName, byte[] data) {
-    content.add(new Pair<>(entryName, data));
-  }
-
-  public void finished(DiagnosticsHandler handler) {
-    // Add meta information.
-    add(COMPILER_INFO_ENTRY_NAME, Version.getVersionString().getBytes(StandardCharsets.UTF_8));
-    add(OUTPUT_KIND_ENTRY_NAME, getKind().toString().getBytes(StandardCharsets.UTF_8));
-
-    // Size estimate to avoid reallocation of the byte output array.
-    final int zipHeaderOverhead = 500;
-    final int zipEntryOverhead = 200;
-    int estimatedZipSize =
-        zipHeaderOverhead
-            + ListUtils.fold(
-                content,
-                0,
-                (acc, pair) ->
-                    acc + pair.getFirst().length() + pair.getSecond().length + zipEntryOverhead);
-    ByteArrayOutputStream baos = new ByteArrayOutputStream(estimatedZipSize);
-    try (ZipOutputStream stream = new ZipOutputStream(baos)) {
-      for (Pair<String, byte[]> pair : content) {
-        ZipUtils.writeToZipStream(stream, pair.getFirst(), pair.getSecond(), ZipEntry.STORED);
-        // Clear out the bytes to avoid three copies when converting the boas.
-        pair.setSecond(null);
-      }
-    } catch (IOException e) {
-      handler.error(new ExceptionDiagnostic(e));
+    public GlobalsFileBuilder(Kind kind) {
+      this.kind = kind;
     }
-    byte[] bytes = baos.toByteArray();
-    consumer.accept(bytes);
+
+    public Kind getKind() {
+      return kind;
+    }
+
+    void addGlobalSynthetic(String descriptor, byte[] data) {
+      add(getGlobalSyntheticFileName(descriptor), data);
+    }
+
+    private void add(String entryName, byte[] data) {
+      content.add(new Pair<>(entryName, data));
+    }
+
+    public byte[] build() throws IOException {
+      // Add meta information.
+      add(COMPILER_INFO_ENTRY_NAME, Version.getVersionString().getBytes(StandardCharsets.UTF_8));
+      add(OUTPUT_KIND_ENTRY_NAME, getKind().toString().getBytes(StandardCharsets.UTF_8));
+
+      // Size estimate to avoid reallocation of the byte output array.
+      final int zipHeaderOverhead = 500;
+      final int zipEntryOverhead = 200;
+      int estimatedZipSize =
+          zipHeaderOverhead
+              + ListUtils.fold(
+                  content,
+                  0,
+                  (acc, pair) ->
+                      acc + pair.getFirst().length() + pair.getSecond().length + zipEntryOverhead);
+      ByteArrayOutputStream baos = new ByteArrayOutputStream(estimatedZipSize);
+      try (ZipOutputStream stream = new ZipOutputStream(baos)) {
+        for (Pair<String, byte[]> pair : content) {
+          ZipUtils.writeToZipStream(stream, pair.getFirst(), pair.getSecond(), ZipEntry.STORED);
+          // Clear out the bytes to avoid three copies when converting the boas.
+          pair.setSecond(null);
+        }
+      }
+      return baos.toByteArray();
+    }
+
+    private static String getGlobalSyntheticFileName(String descriptor) {
+      assert descriptor != null && DescriptorUtils.isClassDescriptor(descriptor);
+      return DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor)
+          + GLOBAL_SYNTHETIC_EXTENSION;
+    }
   }
 
-  private static String getGlobalSyntheticFileName(String descriptor) {
-    assert descriptor != null && DescriptorUtils.isClassDescriptor(descriptor);
-    return DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor)
-        + GLOBAL_SYNTHETIC_EXTENSION;
-  }
-
-  public static class InternalGlobalSyntheticsDexConsumer
+  public static class InternalGlobalSyntheticsDexIndexedConsumer
       extends InternalGlobalSyntheticsProgramConsumer implements DexFilePerClassFileConsumer {
 
-    public InternalGlobalSyntheticsDexConsumer(GlobalSyntheticsConsumer consumer) {
-      super(consumer);
+    private final GlobalSyntheticsConsumer clientConsumer;
+    private final GlobalsFileBuilder builder = new GlobalsFileBuilder(Kind.DEX);
+
+    public InternalGlobalSyntheticsDexIndexedConsumer(GlobalSyntheticsConsumer clientConsumer) {
+      this.clientConsumer = clientConsumer;
     }
 
     @Override
-    public Kind getKind() {
+    public synchronized void accept(
+        String primaryClassDescriptor,
+        ByteDataView data,
+        Set<String> descriptors,
+        DiagnosticsHandler handler) {
+      builder.addGlobalSynthetic(primaryClassDescriptor, data.copyByteData());
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      throw new Unreachable("Unexpected call to non-internal finished.");
+    }
+
+    @Override
+    public void finished(AppView<?> appView, NamingLens namingLens) {
+      byte[] bytes = null;
+      try {
+        bytes = builder.build();
+      } catch (IOException e) {
+        appView.reporter().error(new ExceptionDiagnostic(e));
+      }
+      if (bytes != null) {
+        clientConsumer.accept(ByteDataView.of(bytes), null, appView.reporter());
+      }
+      clientConsumer.finished(appView.reporter());
+    }
+
+    @Override
+    public boolean combineSyntheticClassesWithPrimaryClass() {
+      return false;
+    }
+  }
+
+  public static class InternalGlobalSyntheticsDexPerFileConsumer extends PerFileBase
+      implements DexFilePerClassFileConsumer {
+
+    public InternalGlobalSyntheticsDexPerFileConsumer(
+        GlobalSyntheticsConsumer consumer, AppView appView) {
+      super(consumer, appView);
+    }
+
+    @Override
+    Kind getKind() {
       return Kind.DEX;
     }
 
@@ -96,7 +155,7 @@
         ByteDataView data,
         Set<String> descriptors,
         DiagnosticsHandler handler) {
-      addGlobalSynthetic(primaryClassDescriptor, data.copyByteData());
+      addGlobal(primaryClassDescriptor, data);
     }
 
     @Override
@@ -105,21 +164,89 @@
     }
   }
 
-  public static class InternalGlobalSyntheticsCfConsumer
-      extends InternalGlobalSyntheticsProgramConsumer implements ClassFileConsumer {
+  public static class InternalGlobalSyntheticsCfConsumer extends PerFileBase
+      implements ClassFileConsumer {
 
-    public InternalGlobalSyntheticsCfConsumer(GlobalSyntheticsConsumer consumer) {
-      super(consumer);
+    public InternalGlobalSyntheticsCfConsumer(GlobalSyntheticsConsumer consumer, AppView appView) {
+      super(consumer, appView);
     }
 
     @Override
-    public Kind getKind() {
+    Kind getKind() {
       return Kind.CF;
     }
 
     @Override
     public void accept(ByteDataView data, String descriptor, DiagnosticsHandler handler) {
-      addGlobalSynthetic(descriptor, data.copyByteData());
+      addGlobal(descriptor, data);
     }
   }
+
+  private abstract static class PerFileBase extends InternalGlobalSyntheticsProgramConsumer {
+
+    private final AppView appView;
+    private final GlobalSyntheticsConsumer clientConsumer;
+    private final Map<DexType, byte[]> globalToBytes = new ConcurrentHashMap<>();
+
+    public PerFileBase(GlobalSyntheticsConsumer consumer, AppView appView) {
+      this.appView = appView;
+      this.clientConsumer = consumer;
+    }
+
+    abstract Kind getKind();
+
+    public final void finished(DiagnosticsHandler handler) {
+      throw new Unreachable("Unexpected call to non-internal finished.");
+    }
+
+    @Override
+    public void finished(AppView<?> appView, NamingLens namingLens) {
+      Map<DexType, Set<DexType>> globalsToContexts =
+          appView.getSyntheticItems().getFinalGlobalSyntheticContexts(appView, namingLens);
+      Map<DexType, Set<DexType>> contextToGlobals = new IdentityHashMap<>();
+      for (DexType globalType : globalToBytes.keySet()) {
+        // It would be good to assert that the global is a synthetic type, but the naming-lens
+        // is not applied to SyntheticItems in AppView.
+        Set<DexType> contexts = globalsToContexts.get(globalType);
+        // TODO(b/231598779): Contexts should never be null once fixed for records.
+        assert (contexts == null) == (globalType == appView.dexItemFactory().recordTagType);
+        if (contexts != null) {
+          assert !contexts.isEmpty();
+          for (DexType contextType : contexts) {
+            contextToGlobals
+                .computeIfAbsent(contextType, k -> SetUtils.newIdentityHashSet())
+                .add(globalType);
+          }
+        }
+      }
+      contextToGlobals.forEach(
+          (context, globals) -> {
+            GlobalsFileBuilder builder = new GlobalsFileBuilder(getKind());
+            globals.forEach(
+                global ->
+                    builder.addGlobalSynthetic(
+                        global.toDescriptorString(), globalToBytes.get(global)));
+            byte[] bytes = null;
+            try {
+              bytes = builder.build();
+            } catch (IOException e) {
+              appView.reporter().error(new ExceptionDiagnostic(e));
+            }
+            if (bytes != null) {
+              clientConsumer.accept(
+                  ByteDataView.of(bytes),
+                  Reference.classFromDescriptor(context.toDescriptorString()),
+                  appView.reporter());
+            }
+          });
+      clientConsumer.finished(appView.reporter());
+    }
+
+    void addGlobal(String descriptor, ByteDataView data) {
+      DexType type = appView.dexItemFactory().createType(descriptor);
+      globalToBytes.put(type, data.copyByteData());
+    }
+  }
+
+  public abstract void finished(AppView<?> appView, NamingLens namingLens);
 }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassTest.java
index b999875..184e984 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassTest.java
@@ -7,18 +7,26 @@
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.globals.GlobalSyntheticsTestingConsumer;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -41,18 +49,26 @@
     return parameters.isDexRuntime() && parameters.getApiLevel().isGreaterThanOrEqualTo(mockLevel);
   }
 
-  private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) {
+  private void setupTestCompileBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) {
     testBuilder
         .addProgramClasses(Main.class, TestClass.class)
         .addLibraryClasses(LibraryClass.class)
         .addDefaultRuntimeLibrary(parameters)
         .setMinApi(parameters.getApiLevel())
-        .addAndroidBuildVersion()
         .apply(ApiModelingTestHelper::enableStubbingOfClasses)
         .apply(setMockApiLevelForClass(LibraryClass.class, mockLevel))
         .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, mockLevel));
   }
 
+  private void setupTestRuntimeBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) {
+    testBuilder.setMinApi(parameters.getApiLevel()).addAndroidBuildVersion();
+  }
+
+  private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) {
+    setupTestCompileBuilder(testBuilder);
+    setupTestRuntimeBuilder(testBuilder);
+  }
+
   private boolean addToBootClasspath() {
     return parameters.isDexRuntime()
         && parameters.getRuntime().maxSupportedApiLevel().isGreaterThanOrEqualTo(mockLevel);
@@ -87,6 +103,63 @@
   }
 
   @Test
+  public void testD8MergeIndexed() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testD8Merge(OutputMode.DexIndexed);
+  }
+
+  @Test
+  public void testD8MergeFilePerClass() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testD8Merge(OutputMode.DexFilePerClass);
+  }
+
+  @Test
+  public void testD8MergeFilePerClassFile() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testD8Merge(OutputMode.DexFilePerClassFile);
+  }
+
+  public void testD8Merge(OutputMode outputMode) throws Exception {
+    GlobalSyntheticsTestingConsumer globals = new GlobalSyntheticsTestingConsumer();
+    Path incrementalOut =
+        testForD8()
+            .debug()
+            .setOutputMode(outputMode)
+            .setIntermediate(true)
+            .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals))
+            // TODO(b/213552119): Remove when enabled by default.
+            .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+            .apply(this::setupTestCompileBuilder)
+            .compile()
+            .writeToZip();
+
+    if (isGreaterOrEqualToMockLevel()) {
+      assertFalse(globals.hasGlobals());
+    } else if (outputMode == OutputMode.DexIndexed) {
+      assertTrue(globals.hasGlobals());
+      assertTrue(globals.isSingleGlobal());
+    } else {
+      assertTrue(globals.hasGlobals());
+      // The TestClass does reference the mock and should have globals.
+      assertNotNull(globals.getProvider(Reference.classFromClass(TestClass.class)));
+      // The Main class does not have references to the mock and should have no globals.
+      assertNull(globals.getProvider(Reference.classFromClass(Main.class)));
+    }
+
+    testForD8()
+        .debug()
+        .addProgramFiles(incrementalOut)
+        .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals.getProviders()))
+        .apply(this::setupTestRuntimeBuilder)
+        .compile()
+        .applyIf(addToBootClasspath(), b -> b.addBootClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkOutput)
+        .inspect(this::inspect);
+  }
+
+  @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
         .apply(this::setupTestBuilder)
diff --git a/src/test/java/com/android/tools/r8/compilerapi/globalsynthetics/GlobalSyntheticsTest.java b/src/test/java/com/android/tools/r8/compilerapi/globalsynthetics/GlobalSyntheticsTest.java
index edc42c2..e711b40 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/globalsynthetics/GlobalSyntheticsTest.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/globalsynthetics/GlobalSyntheticsTest.java
@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.compilerapi.globalsynthetics;
 
+import com.android.tools.r8.ByteDataView;
 import com.android.tools.r8.D8;
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.GlobalSyntheticsConsumer;
 import com.android.tools.r8.GlobalSyntheticsResourceProvider;
 import com.android.tools.r8.ResourceException;
@@ -13,6 +15,7 @@
 import com.android.tools.r8.compilerapi.CompilerApiTest;
 import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -71,10 +74,16 @@
               .setGlobalSyntheticsConsumer(
                   new GlobalSyntheticsConsumer() {
                     @Override
-                    public void accept(byte[] bytes) {
+                    public void accept(
+                        ByteDataView data, ClassReference context, DiagnosticsHandler handler) {
                       // Nothing is actually received here as MockClass does not give rise to
                       // globals.
                     }
+
+                    @Override
+                    public void finished(DiagnosticsHandler handler) {
+                      // Nothing to do, just checking we can override finished.
+                    }
                   })
               .build());
     }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
index d3816f8..b77f5a7 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordMergeTest.java
@@ -10,6 +10,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8TestCompileResult;
@@ -18,7 +19,7 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.errors.DuplicateTypesDiagnostic;
 import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
-import com.android.tools.r8.synthesis.globals.GlobalSyntheticsConsumerAndProvider;
+import com.android.tools.r8.synthesis.globals.GlobalSyntheticsTestingConsumer;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
@@ -72,7 +73,10 @@
 
   @Test
   public void testMergeDesugaredInputs() throws Exception {
-    GlobalSyntheticsConsumerAndProvider globals1 = new GlobalSyntheticsConsumerAndProvider();
+    // TODO(b/231598779): Records do not yet have contexts so per-file modes fail.
+    //  This test should be extended or duplicated to also test the pre-file-dex modes.
+    assumeTrue("b/230445931", parameters.isDexRuntime());
+    GlobalSyntheticsTestingConsumer globals1 = new GlobalSyntheticsTestingConsumer();
     Path output1 =
         testForD8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA_1)
@@ -83,7 +87,7 @@
             .inspect(this::assertDoesNotHaveRecordTag)
             .writeToZip();
 
-    GlobalSyntheticsConsumerAndProvider globals2 = new GlobalSyntheticsConsumerAndProvider();
+    GlobalSyntheticsTestingConsumer globals2 = new GlobalSyntheticsTestingConsumer();
     Path output2 =
         testForD8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA_2)
@@ -94,13 +98,17 @@
             .inspect(this::assertDoesNotHaveRecordTag)
             .writeToZip();
 
-    assertTrue(globals1.hasBytes());
-    assertTrue(globals2.hasBytes());
+    assertTrue(globals1.hasGlobals());
+    assertTrue(globals2.hasGlobals());
 
     D8TestCompileResult result =
         testForD8(parameters.getBackend())
             .addProgramFiles(output1, output2)
-            .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals1, globals2))
+            .apply(
+                b ->
+                    b.getBuilder()
+                        .addGlobalSyntheticsResourceProviders(
+                            globals1.getIndexedModeProvider(), globals2.getIndexedModeProvider()))
             .setMinApi(parameters.getApiLevel())
             .compile()
             .inspect(this::assertHasRecordTag);
@@ -111,7 +119,7 @@
 
   @Test
   public void testMergeDesugaredAndNonDesugaredInputs() throws Exception {
-    GlobalSyntheticsConsumerAndProvider globals1 = new GlobalSyntheticsConsumerAndProvider();
+    GlobalSyntheticsTestingConsumer globals1 = new GlobalSyntheticsTestingConsumer();
     Path output1 =
         testForD8(parameters.getBackend())
             .addProgramClassFileData(PROGRAM_DATA_1)
@@ -124,7 +132,8 @@
     D8TestCompileResult result =
         testForD8(parameters.getBackend())
             .addProgramFiles(output1)
-            .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals1))
+            .apply(
+                b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals1.getProviders()))
             .addProgramClassFileData(PROGRAM_DATA_2)
             .setMinApi(parameters.getApiLevel())
             .compile();
diff --git a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
index e94b6b6..9a74bb5 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.CfVm;
-import com.android.tools.r8.synthesis.globals.GlobalSyntheticsConsumerAndProvider;
+import com.android.tools.r8.synthesis.globals.GlobalSyntheticsTestingConsumer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
@@ -75,11 +75,14 @@
   @Test
   public void testD8Intermediate() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-    GlobalSyntheticsConsumerAndProvider globals = new GlobalSyntheticsConsumerAndProvider();
+    GlobalSyntheticsTestingConsumer globals = new GlobalSyntheticsTestingConsumer();
     Path path = compileIntermediate(globals);
     testForD8()
         .addProgramFiles(path)
-        .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals))
+        .apply(
+            b ->
+                b.getBuilder()
+                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
         .setMinApi(parameters.getApiLevel())
         .setIncludeClassesChecksum(true)
         .run(parameters.getRuntime(), MAIN_TYPE)
@@ -89,12 +92,15 @@
   @Test
   public void testD8IntermediateNoDesugaringInStep2() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-    GlobalSyntheticsConsumerAndProvider globals = new GlobalSyntheticsConsumerAndProvider();
+    GlobalSyntheticsTestingConsumer globals = new GlobalSyntheticsTestingConsumer();
     Path path = compileIntermediate(globals);
     // In Android Studio they disable desugaring at this point to improve build speed.
     testForD8()
         .addProgramFiles(path)
-        .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals))
+        .apply(
+            b ->
+                b.getBuilder()
+                    .addGlobalSyntheticsResourceProviders(globals.getIndexedModeProvider()))
         .setMinApi(parameters.getApiLevel())
         .setIncludeClassesChecksum(true)
         .disableDesugaring()
diff --git a/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsConsumerAndProvider.java b/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsConsumerAndProvider.java
deleted file mode 100644
index 57f5622..0000000
--- a/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsConsumerAndProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.synthesis.globals;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import com.android.tools.r8.GlobalSyntheticsConsumer;
-import com.android.tools.r8.GlobalSyntheticsResourceProvider;
-import com.android.tools.r8.ResourceException;
-import com.android.tools.r8.origin.Origin;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-public class GlobalSyntheticsConsumerAndProvider
-    implements GlobalSyntheticsConsumer, GlobalSyntheticsResourceProvider {
-
-  private byte[] bytes;
-
-  @Override
-  public void accept(byte[] bytes) {
-    assertNull(this.bytes);
-    assertNotNull(bytes);
-    this.bytes = bytes;
-  }
-
-  @Override
-  public Origin getOrigin() {
-    return Origin.unknown();
-  }
-
-  @Override
-  public InputStream getByteStream() throws ResourceException {
-    return new ByteArrayInputStream(bytes);
-  }
-
-  public boolean hasBytes() {
-    return true;
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsTestingConsumer.java b/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsTestingConsumer.java
new file mode 100644
index 0000000..51897e6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticsTestingConsumer.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.synthesis.globals;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.GlobalSyntheticsConsumer;
+import com.android.tools.r8.GlobalSyntheticsResourceProvider;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GlobalSyntheticsTestingConsumer implements GlobalSyntheticsConsumer {
+
+  private final Map<ClassReference, GlobalSyntheticsResourceProvider> globals = new HashMap<>();
+  private boolean finished = false;
+
+  @Override
+  public void accept(ByteDataView data, ClassReference context, DiagnosticsHandler handler) {
+    assertFalse(finished);
+    assertNotNull(data);
+    Origin origin =
+        context == null
+            ? Origin.unknown()
+            : new Origin(Origin.root()) {
+              @Override
+              public String part() {
+                return "globals(" + context.getTypeName() + ")";
+              }
+            };
+    TestingProvider provider = new TestingProvider(origin, data.copyByteData());
+    GlobalSyntheticsResourceProvider old = globals.put(context, provider);
+    assertNull(old);
+  }
+
+  @Override
+  public void finished(DiagnosticsHandler handler) {
+    assertFalse(finished);
+    finished = true;
+  }
+
+  public boolean hasGlobals() {
+    return !globals.isEmpty();
+  }
+
+  public boolean isSingleGlobal() {
+    return globals.size() == 1 && globals.get(null) != null;
+  }
+
+  public GlobalSyntheticsResourceProvider getIndexedModeProvider() {
+    assertTrue(isSingleGlobal());
+    return globals.get(null);
+  }
+
+  public GlobalSyntheticsResourceProvider getProvider(ClassReference clazz) {
+    assertNotNull("Use getIndexedModeProvider to get single outputs", clazz);
+    assertFalse(isSingleGlobal());
+    return globals.get(clazz);
+  }
+
+  public Collection<GlobalSyntheticsResourceProvider> getProviders() {
+    return globals.values();
+  }
+
+  private static class TestingProvider implements GlobalSyntheticsResourceProvider {
+
+    private final Origin origin;
+    private final byte[] bytes;
+
+    public TestingProvider(Origin origin, byte[] bytes) {
+      this.origin = origin;
+      this.bytes = bytes;
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return origin;
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      return new ByteArrayInputStream(bytes);
+    }
+  }
+}