Begin parsing class files eagerly

Change-Id: Ib34270e86968d1cd9ae66901387d92a094a5b463
diff --git a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
index 392a4bb..8a89dde 100644
--- a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OneShotByteResource;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import java.io.Closeable;
@@ -28,6 +29,7 @@
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -95,6 +97,33 @@
   }
 
   @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) {
+    ZipFile zipFile = ensureZipFile();
+    try {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        try (InputStream stream = zipFile.getInputStream(entry)) {
+          String name = entry.getName();
+          Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
+          if (ZipUtils.isClassFile(name) && include.test(name)) {
+            String descriptor = DescriptorUtils.guessTypeDescriptor(name);
+            ProgramResource resource =
+                OneShotByteResource.create(
+                    Kind.CF,
+                    entryOrigin,
+                    ByteStreams.toByteArray(stream),
+                    Collections.singleton(descriptor));
+            consumer.accept(resource);
+          }
+        }
+      }
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  @Override
   public void finished(DiagnosticsHandler handler) throws IOException {
     close();
   }
@@ -136,7 +165,7 @@
       try {
         reopenZipFile();
       } catch (IOException e) {
-        throw new RuntimeException(e);
+        throw new UncheckedIOException(e);
       }
     }
     return lazyZipFile;
diff --git a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
index 1d98610..e541923 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.origin.ArchiveEntryOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.ZipUtils;
@@ -22,6 +23,7 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
@@ -138,4 +140,41 @@
       throw new ResourceException(origin, e);
     }
   }
+
+  @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) throws ResourceException {
+    try {
+      BooleanBox seenCf = new BooleanBox();
+      BooleanBox seenDex = new BooleanBox();
+      readArchive(
+          (entry, stream) -> {
+            String name = entry.getEntryName();
+            if (include.test(name)) {
+              if (ZipUtils.isDexFile(name)) {
+                consumer.accept(
+                    ProgramResource.fromBytes(
+                        entry, Kind.DEX, ByteStreams.toByteArray(stream), null));
+                seenDex.set();
+              } else if (ZipUtils.isClassFile(name)) {
+                String descriptor = DescriptorUtils.guessTypeDescriptor(name);
+                consumer.accept(
+                    ProgramResource.fromBytes(
+                        entry,
+                        Kind.CF,
+                        ByteStreams.toByteArray(stream),
+                        Collections.singleton(descriptor)));
+                seenCf.set();
+              }
+            }
+          });
+      if (seenCf.isTrue() && seenDex.isTrue()) {
+        throw new CompilationError(
+            "Cannot create android app from an archive containing both DEX and Java-bytecode "
+                + "content.",
+            origin);
+      }
+    } catch (IOException e) {
+      throw new ResourceException(origin, e);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
index 29b468f..2177c57 100644
--- a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import java.io.IOException;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * Program resource provider for program resources of class-file kind.
@@ -36,6 +38,10 @@
    */
   ProgramResource getProgramResource(String descriptor);
 
+  default void getProgramResources(Consumer<ProgramResource> consumer) {
+    throw new Unimplemented();
+  }
+
   /**
    * Callback signifying that a given compilation unit is done using the resource provider.
    *
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 5e116de..464fa02 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -58,6 +58,7 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 
 /**
  * The D8 dex compiler.
@@ -172,7 +173,11 @@
       throws IOException {
     timing.begin("Application read");
     ApplicationReader applicationReader = new ApplicationReader(inputApp, options, timing);
-    LazyLoadedDexApplication app = applicationReader.read(executor);
+    boolean readDirect =
+        options.partialSubCompilationConfiguration != null
+            || (options.isGeneratingDex() && !options.mainDexKeepRules.isEmpty());
+    DexApplication app =
+        readDirect ? applicationReader.readDirect(executor) : applicationReader.read(executor);
     timing.end();
     timing.begin("Load desugared lib");
     options.getLibraryDesugaringOptions().loadMachineDesugaredLibrarySpecification(timing, app);
@@ -469,6 +474,11 @@
     }
 
     @Override
+    public void getProgramResources(Consumer<ProgramResource> consumer) {
+      resources.forEach(consumer);
+    }
+
+    @Override
     public void finished(DiagnosticsHandler handler) {}
   }
 }
diff --git a/src/main/java/com/android/tools/r8/FeatureSplitProgramResourceProvider.java b/src/main/java/com/android/tools/r8/FeatureSplitProgramResourceProvider.java
index b8eab10..00482e0 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplitProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplitProgramResourceProvider.java
@@ -8,6 +8,7 @@
 import com.google.common.collect.Sets;
 import java.util.Collection;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * A wrapper around the ProgramResourceProvider of a feature split, which intentionally returns an
@@ -42,6 +43,24 @@
   }
 
   @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) throws ResourceException {
+    assert factory != null;
+    // If the types in this provider has been unset, then the ClassToFeatureSplitMap has already
+    // been created and we no longer need tracking.
+    if (types == null) {
+      programResourceProvider.getProgramResources(consumer);
+    } else {
+      programResourceProvider.getProgramResources(
+          programResource -> {
+            for (String classDescriptor : programResource.getClassDescriptors()) {
+              types.add(factory.createType(classDescriptor));
+            }
+            consumer.accept(programResource);
+          });
+    }
+  }
+
+  @Override
   public DataResourceProvider getDataResourceProvider() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
index a2d7fef..41bc7e2 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -14,8 +14,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.ImmediateAppSubtypingInfo;
-import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
 import com.android.tools.r8.shaking.Enqueuer;
@@ -48,8 +48,8 @@
   private void run(AndroidApp app, ExecutorService executor, SortingStringConsumer consumer)
       throws IOException {
     try {
-      LazyLoadedDexApplication application =
-          new ApplicationReader(app, options, Timing.empty()).read(executor);
+      DirectMappedDexApplication application =
+          new ApplicationReader(app, options, Timing.empty()).readDirect(executor);
       traceMainDexForGenerateMainDexList(executor, application)
           .forEach(type -> consumer.accept(type.toBinaryName() + ".class", options.reporter));
       consumer.finished(options.reporter);
@@ -62,13 +62,13 @@
       throws ExecutionException {
     return traceMainDex(
         AppView.createForD8MainDexTracing(
-            appView.app().asLazy().toDirect(), appView.appInfo().getMainDexInfo()),
+            appView.app().asDirect(), appView.appInfo().getMainDexInfo()),
         executor);
   }
 
   public MainDexInfo traceMainDexForGenerateMainDexList(
-      ExecutorService executor, LazyLoadedDexApplication application) throws ExecutionException {
-    return traceMainDex(AppView.createForR8(application.toDirect()), executor);
+      ExecutorService executor, DirectMappedDexApplication application) throws ExecutionException {
+    return traceMainDex(AppView.createForR8(application), executor);
   }
 
   private MainDexInfo traceMainDex(
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
index cb5ac75..aaf316f 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsGenerator.java
@@ -161,7 +161,7 @@
       throws IOException {
     timing.begin("Application read");
     ApplicationReader applicationReader = new ApplicationReader(inputApp, options, timing);
-    DirectMappedDexApplication app = applicationReader.read(executor).toDirect();
+    DirectMappedDexApplication app = applicationReader.readDirect(executor);
     timing.end();
     AppInfo appInfo =
         timing.time(
diff --git a/src/main/java/com/android/tools/r8/JdkClassFileProvider.java b/src/main/java/com/android/tools/r8/JdkClassFileProvider.java
index a8b8b1d..22e2a15 100644
--- a/src/main/java/com/android/tools/r8/JdkClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/JdkClassFileProvider.java
@@ -24,6 +24,7 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * Lazy Java class file resource provider loading class files from a JDK.
@@ -37,7 +38,7 @@
  */
 @KeepForApi
 public class JdkClassFileProvider implements ClassFileResourceProvider, Closeable {
-  private Origin origin;
+  private final Origin origin;
   private final Set<String> descriptors = new HashSet<>();
   private final Map<String, String> descriptorToModule = new HashMap<>();
   private URLClassLoader jrtFsJarLoader;
@@ -162,6 +163,13 @@
   }
 
   @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) {
+    for (String descriptor : descriptors) {
+      consumer.accept(getProgramResource(descriptor));
+    }
+  }
+
+  @Override
   @SuppressWarnings("Finalize")
   protected void finalize() throws Throwable {
     close();
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 023f3dd..48edf4d 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -496,6 +496,11 @@
     }
 
     @Override
+    public void getProgramResources(Consumer<ProgramResource> consumer) {
+      resources.forEach(consumer);
+    }
+
+    @Override
     public void finished(DiagnosticsHandler handler) {}
   }
 
diff --git a/src/main/java/com/android/tools/r8/ProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ProgramResourceProvider.java
index f75e570..9fac5a4 100644
--- a/src/main/java/com/android/tools/r8/ProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ProgramResourceProvider.java
@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.function.Consumer;
 
 /** Program resource provider. */
 @KeepForApi
@@ -13,6 +15,10 @@
 
   Collection<ProgramResource> getProgramResources() throws ResourceException;
 
+  default void getProgramResources(Consumer<ProgramResource> consumer) throws ResourceException {
+    throw new Unimplemented();
+  }
+
   default DataResourceProvider getDataResourceProvider() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index fd20ec5..8089d42 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -31,7 +31,6 @@
 import com.android.tools.r8.graph.GenericSignatureContextBuilder;
 import com.android.tools.r8.graph.GenericSignatureCorrectnessHelper;
 import com.android.tools.r8.graph.ImmediateAppSubtypingInfo;
-import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
@@ -291,12 +290,11 @@
       {
         timing.begin("Read app");
         ApplicationReader applicationReader = new ApplicationReader(inputApp, options, timing);
-        LazyLoadedDexApplication lazyLoaded = applicationReader.read(executorService);
+        DirectMappedDexApplication application = applicationReader.readDirect(executorService);
         keepDeclarations =
             options.partialSubCompilationConfiguration != null
                 ? options.partialSubCompilationConfiguration.asR8().getAndClearKeepDeclarations()
-                : lazyLoaded.getKeepDeclarations();
-        DirectMappedDexApplication application = lazyLoaded.toDirect(timing);
+                : application.getKeepDeclarations();
         timing.end();
         options
             .getLibraryDesugaringOptions()
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 7eb7c38..f640e5c 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -48,6 +48,7 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ArchiveResourceProvider;
 import com.android.tools.r8.utils.AssertionConfigurationWithDefault;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.EmbeddedRulesExtractor;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
@@ -1098,7 +1099,7 @@
   }
 
   // Wrapper class to ensure that R8 does not allow DEX as program inputs.
-  private static class EnsureNonDexProgramResourceProvider implements ProgramResourceProvider {
+  public static class EnsureNonDexProgramResourceProvider implements ProgramResourceProvider {
 
     final ProgramResourceProvider provider;
 
@@ -1107,12 +1108,30 @@
     }
 
     @Override
+    public void getProgramResources(Consumer<ProgramResource> consumer) throws ResourceException {
+      Box<ProgramResource> dexResource = new Box<>();
+      provider.getProgramResources(
+          resource -> {
+            if (resource.getKind() == Kind.CF) {
+              consumer.accept(resource);
+            } else {
+              assert resource.getKind() == Kind.DEX;
+              dexResource.set(resource);
+            }
+          });
+      if (dexResource.isSet()) {
+        throw new ResourceException(
+            dexResource.get().getOrigin(), "R8 does not support compiling DEX inputs");
+      }
+    }
+
+    @Override
     public Collection<ProgramResource> getProgramResources() throws ResourceException {
       Collection<ProgramResource> resources = provider.getProgramResources();
       for (ProgramResource resource : resources) {
         if (resource.getKind() == Kind.DEX) {
-          throw new ResourceException(resource.getOrigin(),
-              "R8 does not support compiling DEX inputs");
+          throw new ResourceException(
+              resource.getOrigin(), "R8 does not support compiling DEX inputs");
         }
       }
       return resources;
@@ -1122,6 +1141,15 @@
     public DataResourceProvider getDataResourceProvider() {
       return provider.getDataResourceProvider();
     }
+
+    public static ProgramResourceProvider unwrap(ProgramResourceProvider provider) {
+      if (provider instanceof EnsureNonDexProgramResourceProvider) {
+        EnsureNonDexProgramResourceProvider wrapper =
+            (EnsureNonDexProgramResourceProvider) provider;
+        return wrapper.provider;
+      }
+      return provider;
+    }
   }
 
   static String getUsageMessage() {
diff --git a/src/main/java/com/android/tools/r8/R8Partial.java b/src/main/java/com/android/tools/r8/R8Partial.java
index fec7386..535a1d5 100644
--- a/src/main/java/com/android/tools/r8/R8Partial.java
+++ b/src/main/java/com/android/tools/r8/R8Partial.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
-import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.metadata.impl.R8PartialCompilationStatsMetadataBuilder;
 import com.android.tools.r8.partial.R8PartialD8Input;
@@ -36,6 +35,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 
 class R8Partial {
 
@@ -93,10 +93,9 @@
 
   private R8PartialD8Input runProcessInputStep(AndroidApp androidApp, ExecutorService executor)
       throws IOException {
-    LazyLoadedDexApplication lazyApp =
-        new ApplicationReader(androidApp, options, timing).read(executor);
-    List<KeepDeclaration> keepDeclarations = lazyApp.getKeepDeclarations();
-    DirectMappedDexApplication app = lazyApp.toDirect();
+    DirectMappedDexApplication app =
+        new ApplicationReader(androidApp, options, timing).readDirect(executor);
+    List<KeepDeclaration> keepDeclarations = app.getKeepDeclarations();
     R8PartialProgramPartitioning partitioning = R8PartialProgramPartitioning.create(app);
     partitioning.printForTesting(options);
     options.getLibraryDesugaringOptions().loadMachineDesugaredLibrarySpecification(timing, app);
@@ -210,6 +209,11 @@
               }
 
               @Override
+              public void getProgramResources(Consumer<ProgramResource> consumer) {
+                // Intentionally empty.
+              }
+
+              @Override
               public DataResourceProvider getDataResourceProvider() {
                 return programResourceProvider.getDataResourceProvider();
               }
@@ -307,6 +311,11 @@
                     }
 
                     @Override
+                    public void getProgramResources(Consumer<ProgramResource> consumer) {
+                      throw new Unreachable();
+                    }
+
+                    @Override
                     public DataResourceProvider getDataResourceProvider() {
                       return programResourceProvider.getDataResourceProvider();
                     }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index f361d81..d9e3a01 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -9,11 +9,9 @@
 import static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
 
 import com.android.tools.r8.ClassFileResourceProvider;
-import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.ProgramResource;
-import com.android.tools.r8.ProgramResource.Kind;
-import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.R8Command.EnsureNonDexProgramResourceProvider;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.StringResource;
 import com.android.tools.r8.dump.DumpOptions;
@@ -30,9 +28,12 @@
 import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.graph.JarClassFileReader;
 import com.android.tools.r8.graph.LazyLoadedDexApplication;
+import com.android.tools.r8.graph.LazyLoadedDexApplication.AllClasses;
+import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.MainDexInfo;
@@ -46,19 +47,21 @@
 import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LibraryClassCollection;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MainDexListParser;
+import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.Iterables;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 public class ApplicationReader {
@@ -123,7 +126,7 @@
     }
 
     timing.begin("DexApplication.read");
-    final LazyLoadedDexApplication.Builder builder = DexApplication.builder(options, timing);
+    LazyLoadedDexApplication.Builder builder = DexApplication.builder(options, timing);
     TaskCollection<?> tasks = new TaskCollection<>(options, executorService);
     try {
       // Still preload some of the classes, primarily for two reasons:
@@ -135,17 +138,16 @@
       // TODO: try and preload less classes.
       readProguardMap(proguardMap, builder, tasks);
       ClassReader classReader = new ClassReader(tasks);
-      classReader.readSources();
-      tasks.await();
-      flags = classReader.getDexApplicationReadFlags();
-      builder.setFlags(flags);
       classReader.initializeLazyClassCollection(builder);
-      for (ProgramResourceProvider provider : inputApp.getProgramResourceProviders()) {
-        DataResourceProvider dataResourceProvider = provider.getDataResourceProvider();
-        if (dataResourceProvider != null) {
-          builder.addDataResourceProvider(dataResourceProvider);
-        }
-      }
+      classReader.readSources();
+      timing.time("Await read", () -> tasks.await());
+      flags = classReader.getDexApplicationReadFlags();
+      return builder
+          .addDataResourceProviders(inputApp.getProgramResourceProviders())
+          .addProgramClasses(classReader.programClasses)
+          .setFlags(flags)
+          .setKeepDeclarations(classReader.getKeepDeclarations())
+          .build();
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
     } catch (ResourceException e) {
@@ -153,7 +155,58 @@
     } finally {
       timing.end();
     }
-    return builder.build();
+  }
+
+  @Deprecated
+  public DirectMappedDexApplication readDirectSingleThreaded() throws IOException {
+    ExecutorService executor = options.getThreadingModule().createSingleThreadedExecutorService();
+    try {
+      return readDirect(executor);
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  public DirectMappedDexApplication readDirect(ExecutorService executorService) throws IOException {
+    assert verifyMainDexOptionsCompatible(inputApp, options);
+    dumpApplication(options.getDumpInputFlags());
+
+    if (options.testing.verifyInputs) {
+      inputApp.validateInputs();
+    }
+
+    timing.begin("DexApplication.readDirect");
+    DirectMappedDexApplication.Builder builder =
+        DirectMappedDexApplication.directBuilder(options, timing);
+    TaskCollection<?> tasks = new TaskCollection<>(options, executorService);
+    try {
+      readProguardMap(inputApp.getProguardMapInputData(), builder, tasks);
+      ClassReader classReader = new ClassReader(tasks);
+      AllClasses.Builder allClassesBuilder = AllClasses.builder();
+      classReader.acceptClasspathAndLibraryClassCollections(
+          allClassesBuilder::setClasspathClasses, allClassesBuilder::setLibraryClasses);
+      allClassesBuilder.forceLoadNonProgramClassCollections(options, tasks, timing);
+      classReader.readSources();
+      timing.time("Await read", () -> tasks.await());
+      allClassesBuilder.setProgramClasses(
+          ProgramClassCollection.resolveConflicts(classReader.programClasses, options));
+      AllClasses allClasses = allClassesBuilder.build(options, timing);
+      flags = classReader.getDexApplicationReadFlags();
+      return builder
+          .addDataResourceProviders(inputApp.getProgramResourceProviders())
+          .addProgramClasses(allClasses.getProgramClasses())
+          .replaceClasspathClasses(allClasses.getClasspathClasses())
+          .replaceLibraryClasses(allClasses.getLibraryClasses())
+          .setFlags(flags)
+          .setKeepDeclarations(classReader.getKeepDeclarations())
+          .build();
+    } catch (ExecutionException e) {
+      throw unwrapExecutionException(e);
+    } catch (ResourceException e) {
+      throw options.reporter.fatalError(new StringDiagnostic(e.getMessage(), e.getOrigin()));
+    } finally {
+      timing.end();
+    }
   }
 
   public final void dump(DumpInputFlags dumpInputFlags) {
@@ -293,11 +346,8 @@
   private final class ClassReader {
     private final TaskCollection<?> tasks;
 
-    // We use concurrent queues to collect classes
-    // since the classes can be collected concurrently.
+    // We use concurrent queues to collect classes since the classes can be collected concurrently.
     private final Queue<DexProgramClass> programClasses = new ConcurrentLinkedQueue<>();
-    private final Queue<DexClasspathClass> classpathClasses = new ConcurrentLinkedQueue<>();
-    private final Queue<DexLibraryClass> libraryClasses = new ConcurrentLinkedQueue<>();
     // Jar application reader to share across all class readers.
     private final DexApplicationReadFlags.Builder readFlagsBuilder =
         DexApplicationReadFlags.builder();
@@ -325,12 +375,15 @@
           .build();
     }
 
-    private void readDexSources(List<ProgramResource> dexSources, Queue<DexProgramClass> classes)
+    public List<KeepDeclaration> getKeepDeclarations() {
+      return application.getKeepDeclarations();
+    }
+
+    private void readDexSources(List<ProgramResource> dexSources)
         throws IOException, ResourceException, ExecutionException {
       if (dexSources.isEmpty()) {
         return;
       }
-      hasReadProgramResourceFromDex = true;
       List<DexParser<DexProgramClass>> dexParsers = new ArrayList<>(dexSources.size());
       List<DexParser<DexProgramClass>> dexContainerParsers = new ArrayList<>(4);
       AndroidApiLevel computedMinApiLevel = options.getMinApiLevel();
@@ -360,16 +413,13 @@
         ApplicationReaderMap applicationReaderMap = ApplicationReaderMap.getInstance(options);
         // Read the DexCode items and DexProgramClass items in parallel.
         for (DexParser<DexProgramClass> dexParser : dexParsers) {
-          tasks.submit(
-              () -> {
-                dexParser.addClassDefsTo(
-                    classes::add, applicationReaderMap); // Depends on Methods, Code items etc.
-              });
+          // Depends on Methods, Code items etc.
+          tasks.submit(() -> dexParser.addClassDefsTo(programClasses, applicationReaderMap));
         }
         // All DEX parsers for container sections use the same DEX reader,
         // so don't process in parallel.
         for (DexParser<DexProgramClass> dexParser : dexContainerParsers) {
-          dexParser.addClassDefsTo(classes::add, applicationReaderMap);
+          dexParser.addClassDefsTo(programClasses, applicationReaderMap);
         }
       }
     }
@@ -394,94 +444,80 @@
       return retentionAnnotation.annotation.toString().contains("RUNTIME");
     }
 
-    private void readClassSources(
-        List<ProgramResource> classSources, Queue<DexProgramClass> classes)
-        throws ExecutionException {
-      if (classSources.isEmpty()) {
-        return;
-      }
-      hasReadProgramResourceFromCf = true;
-      JarClassFileReader<DexProgramClass> reader =
+    void readSources() throws IOException, ResourceException, ExecutionException {
+      timing.begin("Compute all program resources");
+      List<ProgramResource> dexResources = new ArrayList<>();
+      JarClassFileReader<DexProgramClass> cfReader =
           new JarClassFileReader<>(
               application,
               clazz -> {
                 if (clazz.isAnnotation() && !includeAnnotationClass(clazz)) {
                   return;
                 }
-                classes.add(clazz);
+                programClasses.add(clazz);
               },
               PROGRAM);
-      // Read classes in parallel.
-      for (ProgramResource input : classSources) {
-        tasks.submit(() -> reader.read(input));
-      }
-    }
-
-    void readSources() throws IOException, ResourceException, ExecutionException {
-      Collection<ProgramResource> resources =
-          inputApp.computeAllProgramResources(
-              internalProvider -> programClasses.addAll(internalProvider.getClasses()));
-      List<ProgramResource> dexResources = new ArrayList<>(resources.size());
-      List<ProgramResource> cfResources = new ArrayList<>(resources.size());
-      for (ProgramResource resource : resources) {
-        if (resource.getKind() == Kind.DEX) {
-          dexResources.add(resource);
-        } else {
-          assert resource.getKind() == Kind.CF;
-          cfResources.add(resource);
-        }
-      }
-      readDexSources(dexResources, programClasses);
-      readClassSources(cfResources, programClasses);
+      inputApp.computeAllProgramResources(
+          resource -> {
+            if (resource.getKind() == ProgramResource.Kind.CF) {
+              hasReadProgramResourceFromCf = true;
+              tasks.submitUnchecked(
+                  () -> {
+                    Timing threadTiming =
+                        timing.createThreadTiming("Read program resource", options);
+                    cfReader.read(resource);
+                    threadTiming.end().notifyThreadTimingFinished();
+                  });
+            } else {
+              assert resource.getKind() == ProgramResource.Kind.DEX;
+              dexResources.add(resource);
+            }
+          },
+          internalProvider -> programClasses.addAll(internalProvider.getClasses()),
+          legacyProgramResourceProvider ->
+              options.reporter.warning(
+                  "Program resource provider does not support async parsing: "
+                      + EnsureNonDexProgramResourceProvider.unwrap(legacyProgramResourceProvider)
+                          .getClass()
+                          .getTypeName()),
+          timing);
+      hasReadProgramResourceFromDex = !dexResources.isEmpty();
+      timing.end();
+      readDexSources(dexResources);
     }
 
     private <T extends DexClass> ClassProvider<T> buildClassProvider(
-        ClassKind<T> classKind,
-        Queue<T> preloadedClasses,
-        List<ClassFileResourceProvider> resourceProviders,
-        JarApplicationReader reader) {
-      List<ClassProvider<T>> providers = new ArrayList<>();
-
-      // Preloaded classes.
-      if (!preloadedClasses.isEmpty()) {
-        providers.add(ClassProvider.forPreloadedClasses(classKind, preloadedClasses));
-      }
-
-      // Class file resource providers.
-      for (ClassFileResourceProvider provider : resourceProviders) {
-        providers.add(ClassProvider.forClassFileResources(classKind, provider, reader));
-      }
-
-      // Combine if needed.
-      if (providers.isEmpty()) {
-        return null;
-      }
-      return providers.size() == 1 ? providers.get(0)
-          : ClassProvider.combine(classKind, providers);
+        ClassKind<T> classKind, List<ClassFileResourceProvider> resourceProviders) {
+      return ClassProvider.combine(
+          classKind,
+          ListUtils.map(
+              resourceProviders,
+              resourceProvider ->
+                  ClassProvider.forClassFileResources(classKind, resourceProvider, application)));
     }
 
-    void initializeLazyClassCollection(LazyLoadedDexApplication.Builder builder) {
-      // Add all program classes to the builder.
-      for (DexProgramClass clazz : programClasses) {
-        builder.addProgramClass(clazz.asProgramClass());
-      }
-
+    void acceptClasspathAndLibraryClassCollections(
+        Consumer<ClasspathClassCollection> classpathClassCollectionConsumer,
+        Consumer<LibraryClassCollection> libraryClassCollectionConsumer) {
       // Create classpath class collection if needed.
-      ClassProvider<DexClasspathClass> classpathClassProvider = buildClassProvider(CLASSPATH,
-          classpathClasses, inputApp.getClasspathResourceProviders(), application);
+      ClassProvider<DexClasspathClass> classpathClassProvider =
+          buildClassProvider(CLASSPATH, inputApp.getClasspathResourceProviders());
       if (classpathClassProvider != null) {
-        builder.setClasspathClassCollection(new ClasspathClassCollection(classpathClassProvider));
+        classpathClassCollectionConsumer.accept(
+            new ClasspathClassCollection(classpathClassProvider));
       }
 
       // Create library class collection if needed.
-      ClassProvider<DexLibraryClass> libraryClassProvider = buildClassProvider(LIBRARY,
-          libraryClasses, inputApp.getLibraryResourceProviders(), application);
+      ClassProvider<DexLibraryClass> libraryClassProvider =
+          buildClassProvider(LIBRARY, inputApp.getLibraryResourceProviders());
       if (libraryClassProvider != null) {
-        builder.setLibraryClassCollection(new LibraryClassCollection(libraryClassProvider));
+        libraryClassCollectionConsumer.accept(new LibraryClassCollection(libraryClassProvider));
       }
+    }
 
-      // Transfer the keep declarations found during reading.
-      builder.setKeepDeclarations(application.getKeepDeclarations());
+    void initializeLazyClassCollection(LazyLoadedDexApplication.Builder builder) {
+      acceptClasspathAndLibraryClassCollections(
+          builder::setClasspathClassCollection, builder::setLibraryClassCollection);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index fcb52ce..6405725 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -91,9 +91,9 @@
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -848,7 +848,7 @@
     return methods;
   }
 
-  void addClassDefsTo(Consumer<T> classCollection, ApplicationReaderMap applicationReaderMap) {
+  void addClassDefsTo(Collection<T> classCollection, ApplicationReaderMap applicationReaderMap) {
     final DexSection dexSection = lookupSection(Constants.TYPE_CLASS_DEF_ITEM);
     final int length = dexSection.length;
     indexedItems.initializeClasses(length);
@@ -969,7 +969,7 @@
               // Interpreting reachability sensitivity from DEX inputs is not supported.
               // The compiler does not support building IR from DEX with debug information.
               ReachabilitySensitiveValue.DISABLED);
-      classCollection.accept(clazz); // Update the application object.
+      classCollection.add(clazz); // Update the application object.
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index a8665ac..5a4d355 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -280,12 +280,13 @@
         timing);
   }
 
-  public static AppView<AppInfoWithClassHierarchy> createForR8(DexApplication application) {
+  public static AppView<AppInfoWithClassHierarchy> createForR8(
+      DirectMappedDexApplication application) {
     return createForR8(application, MainDexInfo.none());
   }
 
   public static AppView<AppInfoWithClassHierarchy> createForR8(
-      DexApplication application, MainDexInfo mainDexInfo) {
+      DirectMappedDexApplication application, MainDexInfo mainDexInfo) {
     ClassToFeatureSplitMap classToFeatureSplitMap =
         ClassToFeatureSplitMap.createInitialR8ClassToFeatureSplitMap(application.options);
     AppInfoWithClassHierarchy appInfo =
diff --git a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
index 38264ec..f4aa5d0 100644
--- a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
@@ -69,8 +69,7 @@
     this.writeCode = writeCode;
     if (writeIR) {
       this.appInfo =
-          AppInfo.createInitialAppInfo(
-              application.toDirect(), GlobalSyntheticsStrategy.forNonSynthesizing());
+          AppInfo.createInitialAppInfo(application, GlobalSyntheticsStrategy.forNonSynthesizing());
       if (options.programConsumer == null) {
         // Use class-file backend, since the CF frontend for testing does not support desugaring of
         // synchronized methods for the DEX backend (b/109789541).
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index a74e532..de7d2a2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -7,6 +7,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.timing.Timing;
@@ -179,8 +180,9 @@
       return null;
     }
 
-    public void setFlags(DexApplicationReadFlags flags) {
+    public T setFlags(DexApplicationReadFlags flags) {
       this.flags = flags;
+      return self();
     }
 
     public synchronized T setProguardMap(ClassNameMapper proguardMap) {
@@ -219,8 +221,13 @@
       return self();
     }
 
-    public synchronized T addDataResourceProvider(DataResourceProvider provider) {
-      dataResourceProviders.add(provider);
+    public synchronized T addDataResourceProviders(List<ProgramResourceProvider> providers) {
+      for (ProgramResourceProvider provider : providers) {
+        DataResourceProvider dataResourceProvider = provider.getDataResourceProvider();
+        if (dataResourceProvider != null) {
+          dataResourceProviders.add(dataResourceProvider);
+        }
+      }
       return self();
     }
 
@@ -242,11 +249,7 @@
       return programClasses;
     }
 
-    public final S build() {
-      return build(Timing.empty());
-    }
-
-    public abstract S build(Timing timing);
+    public abstract S build();
   }
 
   public static LazyLoadedDexApplication.Builder builder(InternalOptions options, Timing timing) {
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index 702bd04..30c2029 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.graph.LazyLoadedDexApplication.AllClasses;
 import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.timing.Timing;
@@ -20,6 +21,7 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -32,6 +34,7 @@
   // Mapping from code objects to their encoded-method owner. Used for asserting unique ownership
   // and debugging purposes.
   private final Map<Code, DexEncodedMethod> codeOwners = new IdentityHashMap<>();
+  private List<KeepDeclaration> keepDeclarations;
 
   // Unmodifiable mapping of all types to their definitions.
   private final ImmutableMap<DexType, ProgramOrClasspathClass> programOrClasspathClasses;
@@ -49,6 +52,7 @@
       ImmutableCollection<DexProgramClass> programClasses,
       ImmutableCollection<DexClasspathClass> classpathClasses,
       ImmutableList<DataResourceProvider> dataResourceProviders,
+      List<KeepDeclaration> keepDeclarations,
       InternalOptions options,
       Timing timing) {
     super(proguardMap, flags, dataResourceProviders, options, timing);
@@ -56,6 +60,11 @@
     this.libraryClasses = libraryClasses;
     this.programClasses = programClasses;
     this.classpathClasses = classpathClasses;
+    this.keepDeclarations = keepDeclarations;
+  }
+
+  public static Builder directBuilder(InternalOptions options, Timing timing) {
+    return new Builder(options, timing);
   }
 
   public Collection<DexClasspathClass> classpathClasses() {
@@ -77,6 +86,10 @@
     libraryClasses.forEach((type, clazz) -> consumer.accept(type));
   }
 
+  public List<KeepDeclaration> getKeepDeclarations() {
+    return keepDeclarations;
+  }
+
   public Collection<DexLibraryClass> libraryClasses() {
     return libraryClasses.values();
   }
@@ -210,15 +223,16 @@
 
     private ImmutableCollection<DexClasspathClass> classpathClasses;
     private Map<DexType, DexLibraryClass> libraryClasses;
+    private List<KeepDeclaration> keepDeclarations = Collections.emptyList();
 
     private final List<DexClasspathClass> pendingClasspathClasses = new ArrayList<>();
     private final Set<DexType> pendingClasspathRemovalIfPresent = Sets.newIdentityHashSet();
 
     Builder(LazyLoadedDexApplication application, AllClasses allClasses) {
       super(application);
-      classpathClasses = allClasses.getClasspathClasses().values();
+      classpathClasses = ImmutableList.copyOf(allClasses.getClasspathClasses());
       libraryClasses = allClasses.getLibraryClasses();
-      replaceProgramClasses(allClasses.getProgramClasses().values());
+      replaceProgramClasses(allClasses.getProgramClasses());
     }
 
     private Builder(DirectMappedDexApplication application) {
@@ -227,6 +241,10 @@
       libraryClasses = application.libraryClasses;
     }
 
+    private Builder(InternalOptions options, Timing timing) {
+      super(options, timing);
+    }
+
     @Override
     public boolean isDirect() {
       return true;
@@ -312,13 +330,22 @@
 
     public Builder replaceLibraryClasses(Collection<DexLibraryClass> libraryClasses) {
       ImmutableMap.Builder<DexType, DexLibraryClass> builder = ImmutableMap.builder();
-      libraryClasses.forEach(clazz -> builder.put(clazz.type, clazz));
-      this.libraryClasses = builder.build();
+      libraryClasses.forEach(clazz -> builder.put(clazz.getType(), clazz));
+      return replaceLibraryClasses(builder.build());
+    }
+
+    public Builder replaceLibraryClasses(Map<DexType, DexLibraryClass> libraryClasses) {
+      this.libraryClasses = libraryClasses;
       return self();
     }
 
+    public Builder setKeepDeclarations(List<KeepDeclaration> declarations) {
+      this.keepDeclarations = declarations;
+      return this;
+    }
+
     @Override
-    public DirectMappedDexApplication build(Timing timing) {
+    public DirectMappedDexApplication build() {
       try (Timing t0 = timing.begin("Build")) {
         // Rebuild the map. This will fail if keys are not unique.
         // TODO(zerny): Consider not rebuilding the map if no program classes are added.
@@ -348,6 +375,7 @@
             ImmutableList.copyOf(getProgramClasses()),
             newClasspathClasses,
             ImmutableList.copyOf(dataResourceProviders),
+            keepDeclarations,
             options,
             timing);
       }
diff --git a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
index 447178a..ffb88ba 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
@@ -6,10 +6,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
+import static com.google.common.base.Predicates.alwaysTrue;
+
 import com.android.tools.r8.DataResourceProvider;
-import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
+import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.threading.TaskCollection;
 import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LibraryClassCollection;
@@ -18,12 +22,16 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -63,7 +71,8 @@
 
   @Override
   List<DexProgramClass> programClasses() {
-    return programClasses.forceLoad().getAllClasses();
+    assert programClasses.isFullyLoaded();
+    return programClasses.getAllClasses();
   }
 
   @Override
@@ -141,11 +150,11 @@
     return programClasses.get(type);
   }
 
-  static class AllClasses {
+  public static class AllClasses {
 
     // Mapping of all types to their definitions.
     // Collections of the three different types for iteration.
-    private final ImmutableMap<DexType, DexProgramClass> programClasses;
+    private final Map<DexType, DexProgramClass> programClasses;
     private final ImmutableMap<DexType, DexClasspathClass> classpathClasses;
     private final ImmutableMap<DexType, DexLibraryClass> libraryClasses;
 
@@ -153,44 +162,28 @@
         LibraryClassCollection libraryClassesLoader,
         ClasspathClassCollection classpathClassesLoader,
         Map<DexType, DexClasspathClass> synthesizedClasspathClasses,
-        ProgramClassCollection programClassesLoader,
+        Map<DexType, DexProgramClass> allProgramClasses,
         InternalOptions options,
         Timing timing) {
-      // When desugaring VarHandle do not read the VarHandle and MethodHandles$Lookup classes
-      // from the library as they will be synthesized during desugaring.
-      DexItemFactory factory = options.dexItemFactory();
-      Predicate<DexType> forceLoadPredicate =
-          type ->
-              !(options.shouldDesugarVarHandle()
-                  && (type.isIdenticalTo(factory.varHandleType)
-                      || type.isIdenticalTo(factory.lookupType)));
-
       // Force-load library classes.
       ImmutableMap<DexType, DexLibraryClass> allLibraryClasses;
       try (Timing t0 = timing.begin("Force-load library classes")) {
         if (libraryClassesLoader != null) {
-          libraryClassesLoader.forceLoad(forceLoadPredicate);
+          assert libraryClassesLoader.isFullyLoaded();
           allLibraryClasses = libraryClassesLoader.getAllClassesInMap();
         } else {
           allLibraryClasses = ImmutableMap.of();
         }
       }
 
-      // Program classes should be fully loaded.
-      ImmutableMap<DexType, DexProgramClass> allProgramClasses;
-      try (Timing t0 = timing.begin("Force-load program classes")) {
-        assert programClassesLoader != null;
-        assert programClassesLoader.isFullyLoaded();
-        allProgramClasses = programClassesLoader.forceLoad().getAllClassesInMap();
-      }
-
       // Force-load classpath classes.
       ImmutableMap<DexType, DexClasspathClass> allClasspathClasses;
       try (Timing t0 = timing.begin("Force-load classpath classes")) {
         ImmutableMap.Builder<DexType, DexClasspathClass> allClasspathClassesBuilder =
             ImmutableMap.builder();
         if (classpathClassesLoader != null) {
-          classpathClassesLoader.forceLoad().forEach(allClasspathClassesBuilder::put);
+          assert classpathClassesLoader.isFullyLoaded();
+          classpathClassesLoader.forEach(allClasspathClassesBuilder::put);
         }
         if (synthesizedClasspathClasses != null) {
           allClasspathClassesBuilder.putAll(synthesizedClasspathClasses);
@@ -228,17 +221,94 @@
       }
     }
 
-    public ImmutableMap<DexType, DexProgramClass> getProgramClasses() {
-      return programClasses;
+    public static Builder builder() {
+      return new Builder();
     }
 
-    public ImmutableMap<DexType, DexClasspathClass> getClasspathClasses() {
-      return classpathClasses;
+    public Collection<DexProgramClass> getProgramClasses() {
+      return programClasses.values();
+    }
+
+    public Collection<DexClasspathClass> getClasspathClasses() {
+      return classpathClasses.values();
     }
 
     public ImmutableMap<DexType, DexLibraryClass> getLibraryClasses() {
       return libraryClasses;
     }
+
+    public static class Builder {
+
+      private LibraryClassCollection libraryClasses;
+      private ClasspathClassCollection classpathClasses;
+      private Map<DexType, DexClasspathClass> synthesizedClasspathClasses;
+      private Map<DexType, DexProgramClass> programClasses;
+
+      private Builder() {}
+
+      public Builder setClasspathClasses(ClasspathClassCollection classpathClasses) {
+        this.classpathClasses = classpathClasses;
+        return this;
+      }
+
+      public Builder setLibraryClasses(LibraryClassCollection libraryClasses) {
+        this.libraryClasses = libraryClasses;
+        return this;
+      }
+
+      public Builder setProgramClasses(Map<DexType, DexProgramClass> programClasses) {
+        this.programClasses = programClasses;
+        return this;
+      }
+
+      public Builder setSynthesizedClasspathClasses(
+          Map<DexType, DexClasspathClass> synthesizedClasspathClasses) {
+        this.synthesizedClasspathClasses = synthesizedClasspathClasses;
+        return this;
+      }
+
+      public Builder forceLoadNonProgramClassCollections(
+          InternalOptions options, TaskCollection<?> tasks, Timing timing) {
+        // When desugaring VarHandle do not read the VarHandle and MethodHandles$Lookup classes
+        // from the library as they will be synthesized during desugaring.
+        Predicate<ProgramResource> forceLoadPredicate =
+            programResource -> {
+              if (!options.shouldDesugarVarHandle()) {
+                return true;
+              }
+              Set<String> descriptors = programResource.getClassDescriptors();
+              if (descriptors.size() != 1) {
+                return true;
+              }
+              String descriptor = descriptors.iterator().next();
+              return !descriptor.equals(DexItemFactory.varHandleDescriptorString)
+                  && !descriptor.equals(DexItemFactory.methodHandlesLookupDescriptorString);
+            };
+        if (classpathClasses != null) {
+          classpathClasses.forceLoad(options, tasks, timing, alwaysTrue());
+        }
+        if (libraryClasses != null) {
+          libraryClasses.forceLoad(options, tasks, timing, forceLoadPredicate);
+        }
+        return this;
+      }
+
+      public AllClasses build(InternalOptions options, Timing timing) {
+        if (classpathClasses != null) {
+          classpathClasses.setFullyLoaded();
+        }
+        if (libraryClasses != null) {
+          libraryClasses.setFullyLoaded();
+        }
+        return new AllClasses(
+            libraryClasses,
+            classpathClasses,
+            synthesizedClasspathClasses,
+            programClasses,
+            options,
+            timing);
+      }
+    }
   }
 
   private static <T extends DexClass> ImmutableMap<DexType, T> fillPrioritizedClasses(
@@ -286,14 +356,18 @@
   }
 
   /** Force load all classes and return type -> class map containing all the classes. */
-  public AllClasses loadAllClasses(Timing timing) {
-    return new AllClasses(
-        libraryClasses,
-        classpathClasses,
-        synthesizedClasspathClasses,
-        programClasses,
-        options,
-        timing);
+  public AllClasses loadAllClasses(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    TaskCollection<?> tasks = new TaskCollection<>(options, executorService);
+    AllClasses.Builder allClassesBuilder =
+        AllClasses.builder()
+            .setClasspathClasses(classpathClasses)
+            .setLibraryClasses(libraryClasses)
+            .setProgramClasses(programClasses.getAllClassesInMap())
+            .setSynthesizedClasspathClasses(synthesizedClasspathClasses)
+            .forceLoadNonProgramClassCollections(options, tasks, timing);
+    tasks.await();
+    return allClassesBuilder.build(options, timing);
   }
 
   public static class Builder extends DexApplication.Builder<LazyLoadedDexApplication, Builder> {
@@ -356,15 +430,11 @@
     }
 
     @Override
-    public LazyLoadedDexApplication build(Timing timing) {
-      ProgramClassConflictResolver resolver =
-          options.programClassConflictResolver == null
-              ? ProgramClassCollection.defaultConflictResolver(options.reporter)
-              : options.programClassConflictResolver;
+    public LazyLoadedDexApplication build() {
       return new LazyLoadedDexApplication(
           proguardMap,
           flags,
-          ProgramClassCollection.create(getProgramClasses(), resolver),
+          ProgramClassCollection.create(getProgramClasses(), options),
           ImmutableList.copyOf(dataResourceProviders),
           classpathClasses,
           synthesizedClasspathClasses,
@@ -385,17 +455,22 @@
     return this;
   }
 
-  public DirectMappedDexApplication toDirect() {
-    return toDirect(Timing.empty());
+  @Deprecated
+  public DirectMappedDexApplication toDirectSingleThreadedForTesting() {
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    return toDirectForTesting(executor);
   }
 
-  public DirectMappedDexApplication toDirect(Timing timing) {
+  @Deprecated
+  private DirectMappedDexApplication toDirectForTesting(ExecutorService executorService) {
     try (Timing t0 = timing.begin("To direct app")) {
       // As a side-effect, this will force-load all classes.
-      AllClasses allClasses = loadAllClasses(timing);
+      AllClasses allClasses = loadAllClasses(executorService, timing);
       DirectMappedDexApplication.Builder builder =
           new DirectMappedDexApplication.Builder(this, allClasses);
-      return builder.build(timing);
+      return builder.build();
+    } catch (ExecutionException e) {
+      throw unwrapExecutionException(e);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/SupportedClassesGenerator.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/SupportedClassesGenerator.java
index bc3be29..1f79756 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/SupportedClassesGenerator.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/SupportedClassesGenerator.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.FieldResolutionResult;
-import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
@@ -312,7 +311,7 @@
     }
     AndroidApp implementation = appBuilder.build();
     DirectMappedDexApplication implementationApplication =
-        new ApplicationReader(implementation, options, Timing.empty()).read().toDirect();
+        new ApplicationReader(implementation, options, Timing.empty()).readDirectSingleThreaded();
 
     List<DexMethod> backports = new ArrayList<>();
     List<DexField> backportFields = new ArrayList<>();
@@ -504,7 +503,7 @@
     ExecutorService executorService = ThreadUtils.getExecutorService(options);
     assert !options.ignoreJavaLibraryOverride;
     options.ignoreJavaLibraryOverride = true;
-    LazyLoadedDexApplication appForMax = applicationReader.read(executorService);
+    DirectMappedDexApplication appForMax = applicationReader.readDirect(executorService);
     options.ignoreJavaLibraryOverride = false;
     DexClass varHandle =
         appForMax.definitionFor(
@@ -514,7 +513,7 @@
           "SupportedClassesGenerator expects library above or equal to T, it works below, but the"
               + " modifiers are not correct which is fine for lint but not html doc generation.");
     }
-    return appForMax.toDirect();
+    return appForMax;
   }
 
   private Set<DexMethod> getParallelMethods() {
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialApplicationWriter.java b/src/main/java/com/android/tools/r8/partial/R8PartialApplicationWriter.java
index 9deb733..1fae401 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialApplicationWriter.java
@@ -54,7 +54,7 @@
     // during writing, which we bypass. The D8 lenses may arise from synthetic finalization and
     // horizontal class merging of synthetics.
     rewriteCodeWithLens(executorService);
-    subCompilationConfiguration.writeApplication(appView);
+    subCompilationConfiguration.writeApplication(appView, executorService);
   }
 
   private void rewriteCodeWithLens(ExecutorService executorService) throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java b/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
index 61362ac..b776d08 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
@@ -39,6 +39,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
 
 public abstract class R8PartialSubCompilationConfiguration {
 
@@ -171,7 +172,7 @@
       return this;
     }
 
-    public void writeApplication(AppView<AppInfo> appView) {
+    public void writeApplication(AppView<AppInfo> appView, ExecutorService executorService) {
       artProfiles = appView.getArtProfileCollection().transformForR8Partial(appView);
       classToFeatureSplitMap =
           appView.appInfo().getClassToFeatureSplitMap().commitSyntheticsForR8Partial(appView);
@@ -184,7 +185,7 @@
           desugaredOutputClasses.add(clazz);
         }
       }
-      DirectMappedDexApplication app = appView.app().asLazy().toDirect();
+      DirectMappedDexApplication app = appView.app().asDirect();
       outputClasspathClasses = app.classpathClasses();
       outputLibraryClasses = app.libraryClasses();
       startupProfile = appView.getStartupProfile();
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
index 764240d..9d0ef51 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
@@ -83,7 +83,8 @@
     AppView<AppInfoWithClassHierarchy> appView =
         AppView.createForTracer(
             AppInfoWithClassHierarchy.createInitialAppInfoWithClassHierarchy(
-                new ApplicationReader(builder.build(), options, Timing.empty()).read().toDirect(),
+                new ApplicationReader(builder.build(), options, Timing.empty())
+                    .readDirectSingleThreaded(),
                 ClassToFeatureSplitMap.createEmptyClassToFeatureSplitMap(),
                 MainDexInfo.none(),
                 GlobalSyntheticsStrategy.forSingleOutputMode()));
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java
deleted file mode 100644
index 5c0b302..0000000
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java
+++ /dev/null
@@ -1,20 +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.tracereferences;
-
-import com.android.tools.r8.ResourceException;
-import java.io.IOException;
-
-// Provide access to some package private APIs.
-public class TraceReferencesBridge {
-
-  public static TraceReferencesCommand makeCommand(TraceReferencesCommand.Builder builder) {
-    return builder.makeCommand();
-  }
-
-  public static void runInternal(TraceReferencesCommand command)
-      throws IOException, ResourceException {
-    TraceReferences.runInternal(command, command.getInternalOptions());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
index 6b36180..dd297fb 100644
--- a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
@@ -24,6 +24,7 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.function.Consumer;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
 import java.util.zip.ZipFile;
@@ -44,9 +45,9 @@
     this.archive = archive;
   }
 
-  private List<ProgramResource> readClassesJar(ZipInputStream stream) throws IOException {
+  private void readClassesJar(ZipInputStream stream, Consumer<ProgramResource> consumer)
+      throws IOException {
     ZipEntry entry;
-    List<ProgramResource> resources = new ArrayList<>();
     while (null != (entry = stream.getNextEntry())) {
       String name = entry.getName();
       if (ZipUtils.isClassFile(name)) {
@@ -58,14 +59,12 @@
                 entryOrigin,
                 ByteStreams.toByteArray(stream),
                 Collections.singleton(descriptor));
-        resources.add(resource);
+        consumer.accept(resource);
       }
     }
-    return resources;
   }
 
-  private List<ProgramResource> readArchive() throws IOException {
-    List<ProgramResource> classResources = null;
+  private void readArchive(Consumer<ProgramResource> consumer) throws IOException {
     try (ZipFile zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
@@ -74,7 +73,7 @@
           String name = entry.getName();
           if (name.equals("classes.jar")) {
             try (ZipInputStream classesStream = new ZipInputStream(stream)) {
-              classResources = readClassesJar(classesStream);
+              readClassesJar(classesStream, consumer);
             }
             break;
           }
@@ -83,13 +82,23 @@
     } catch (ZipException e) {
       throw new CompilationError("Zip error while reading '" + archive + "': " + e.getMessage(), e);
     }
-    return classResources == null ? Collections.emptyList() : classResources;
   }
 
   @Override
   public Collection<ProgramResource> getProgramResources() throws ResourceException {
     try {
-      return readArchive();
+      List<ProgramResource> classResources = new ArrayList<>();
+      readArchive(classResources::add);
+      return classResources;
+    } catch (IOException e) {
+      throw new ResourceException(origin, e);
+    }
+  }
+
+  @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) throws ResourceException {
+    try {
+      readArchive(consumer);
     } catch (IOException e) {
       throw new ResourceException(origin, e);
     }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 5cfde66..95e30ba 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.isAarFile;
 import static com.android.tools.r8.utils.FileUtils.isArchive;
@@ -29,6 +30,7 @@
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.R8Command.EnsureNonDexProgramResourceProvider;
 import com.android.tools.r8.Resource;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.StringResource;
@@ -36,6 +38,7 @@
 import com.android.tools.r8.dump.DumpOptions;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InternalCompilerError;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.features.FeatureSplitConfiguration;
@@ -50,6 +53,7 @@
 import com.android.tools.r8.shaking.FilteredClassPath;
 import com.android.tools.r8.startup.StartupProfileProvider;
 import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -271,22 +275,35 @@
   }
 
   public Collection<ProgramResource> computeAllProgramResources() throws ResourceException {
-    return computeAllProgramResources(null);
+    List<ProgramResource> programResources = new ArrayList<>();
+    computeAllProgramResources(programResources::add, null, emptyConsumer(), Timing.empty());
+    return programResources;
   }
 
   /** Get full collection of all program resources from all program providers. */
-  public Collection<ProgramResource> computeAllProgramResources(
-      Consumer<InternalProgramClassProvider> internalProviderConsumer) throws ResourceException {
-    List<ProgramResource> resources = new ArrayList<>();
+  public void computeAllProgramResources(
+      Consumer<ProgramResource> consumer,
+      Consumer<InternalProgramClassProvider> internalProviderConsumer,
+      Consumer<ProgramResourceProvider> legacyProgramResourceProviderConsumer,
+      Timing timing)
+      throws ResourceException {
     for (ProgramResourceProvider provider : programResourceProviders) {
+      timing.begin(
+          "Process "
+              + EnsureNonDexProgramResourceProvider.unwrap(provider).getClass().getTypeName());
       if (provider instanceof InternalProgramClassProvider) {
         InternalProgramClassProvider internalProvider = (InternalProgramClassProvider) provider;
         internalProviderConsumer.accept(internalProvider);
       } else {
-        resources.addAll(provider.getProgramResources());
+        try {
+          provider.getProgramResources(consumer);
+        } catch (Unimplemented e) {
+          legacyProgramResourceProviderConsumer.accept(provider);
+          provider.getProgramResources().forEach(consumer);
+        }
       }
+      timing.end();
     }
-    return resources;
   }
 
   // TODO(zerny): Remove this method.
@@ -1299,7 +1316,11 @@
     }
 
     public Builder setProguardMapInputData(Path mapPath) {
-      this.proguardMapInputData = StringResource.fromFile(mapPath);
+      return setProguardMapInputData(StringResource.fromFile(mapPath));
+    }
+
+    public Builder setProguardMapInputData(StringResource proguardMapInputData) {
+      this.proguardMapInputData = proguardMapInputData;
       return this;
     }
 
@@ -1384,6 +1405,11 @@
             }
 
             @Override
+            public void getProgramResources(Consumer<ProgramResource> consumer) {
+              finalProgramResources.forEach(consumer);
+            }
+
+            @Override
             public DataResourceProvider getDataResourceProvider() {
               if (!finalDataResources.isEmpty()) {
                 return new DataResourceProvider() {
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
index 23c4481..5267e8c 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -61,9 +61,9 @@
     return origin;
   }
 
-  private List<ProgramResource> readArchive() throws IOException {
-    List<ProgramResource> dexResources = new ArrayList<>();
-    List<ProgramResource> classResources = new ArrayList<>();
+  private void readArchive(Consumer<ProgramResource> consumer) throws ResourceException {
+    BooleanBox seenCf = new BooleanBox();
+    BooleanBox seenDex = new BooleanBox();
     try (ZipFile zipFile =
         FileUtils.createZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
@@ -78,7 +78,8 @@
                 ProgramResource resource =
                     OneShotByteResource.create(
                         Kind.DEX, entryOrigin, ByteStreams.toByteArray(stream), null);
-                dexResources.add(resource);
+                consumer.accept(resource);
+                seenDex.set();
               }
             } else if (ZipUtils.isClassFile(name)) {
               String descriptor = DescriptorUtils.guessTypeDescriptor(name);
@@ -88,7 +89,8 @@
                       entryOrigin,
                       ByteStreams.toByteArray(stream),
                       Collections.singleton(descriptor));
-              classResources.add(resource);
+              consumer.accept(resource);
+              seenCf.set();
             }
           }
         }
@@ -96,24 +98,28 @@
     } catch (ZipException e) {
       throw new CompilationError(
           "Zip error while reading '" + archive + "': " + e.getMessage(), e);
+    } catch (IOException e) {
+      throw new ResourceException(origin, e);
     }
-    if (!dexResources.isEmpty() && !classResources.isEmpty()) {
+    if (seenCf.isTrue() && seenDex.isTrue()) {
       throw new CompilationError(
           "Cannot create android app from an archive '"
               + archive
               + "' containing both DEX and Java-bytecode content",
           origin);
     }
-    return !dexResources.isEmpty() ? dexResources : classResources;
   }
 
   @Override
   public Collection<ProgramResource> getProgramResources() throws ResourceException {
-    try {
-      return readArchive();
-    } catch (IOException e) {
-      throw new ResourceException(origin, e);
-    }
+    List<ProgramResource> programResources = new ArrayList<>();
+    readArchive(programResources::add);
+    return programResources;
+  }
+
+  @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) throws ResourceException {
+    readArchive(consumer);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java
index 6871538..f9e89bd 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassMap.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -3,22 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
-import static com.google.common.base.Predicates.alwaysTrue;
-
+import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.threading.TaskCollection;
+import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
@@ -40,13 +36,13 @@
   /**
    * For each type which has ever been queried stores one class loaded from resources provided by
    * different resource providers.
-   * <p>
-   * <b>NOTE:</b> mutated concurrently but we require that the value assigned to a keys never
+   *
+   * <p><b>NOTE:</b> mutated concurrently but we require that the value assigned to a keys never
    * changes its meaning, i.e., the supplier object might change but the contained value does not.
    * We also allow the transition from Supplier of a null value to the actual value null and vice
    * versa.
    */
-  private final Map<DexType, Supplier<T>> classes;
+  private final ConcurrentHashMap<DexType, Supplier<T>> classes;
 
   /**
    * Class provider if available.
@@ -59,9 +55,14 @@
    */
   private final AtomicReference<ClassProvider<T>> classProvider = new AtomicReference<>();
 
-  ClassMap(Map<DexType, Supplier<T>> classes, ClassProvider<T> classProvider) {
+  ClassMap(ConcurrentHashMap<DexType, Supplier<T>> classes) {
+    this.classes = classes;
+    this.classProvider.set(null);
+  }
+
+  ClassMap(ClassProvider<T> classProvider) {
     assert classProvider == null || classProvider.getClassKind() == getClassKind();
-    this.classes = classes == null ? new ConcurrentHashMap<>() : classes;
+    this.classes = new ConcurrentHashMap<>();
     this.classProvider.set(classProvider);
   }
 
@@ -80,7 +81,7 @@
 
   @Override
   public String toString() {
-    return classes.size() + " loaded, provider: " + Objects.toString(this.classProvider.get());
+    return classes.size() + " loaded, provider: " + this.classProvider.get();
   }
 
   /**
@@ -88,34 +89,18 @@
    */
   public T get(DexType type) {
     // If this collection is fully loaded, just return the found result.
-    if (classProvider.get() == null) {
+    ClassProvider<T> classProvider = this.classProvider.get();
+    if (classProvider == null) {
       Supplier<T> supplier = classes.get(type);
       return supplier == null ? null : supplier.get();
     }
+    return internalGetNotFullyLoaded(type, classProvider);
+  }
 
-    Supplier<T> supplier = classes.get(type);
-    // If we find a result, we can just return it as it won't change.
-    if (supplier != null) {
-      return supplier.get();
-    }
-
-    // Otherwise, we have to do the full dance with locking to avoid creating two suppliers.
-    // Lock on this to ensure classProvider is not changed concurrently, so we do not create
-    // a concurrent class loader with a null classProvider.
-    synchronized (this) {
-      supplier = classes.computeIfAbsent(type, key -> {
-        // Get class supplier, create it if it does not
-        // exist and the collection is NOT fully loaded.
-        if (classProvider.get() == null) {
-          // There is no supplier, the collection is fully loaded.
-          return null;
-        }
-
-        return new ConcurrentClassLoader<>(this, classProvider.get(), type);
-      });
-    }
-
-    return supplier == null ? null : supplier.get();
+  private T internalGetNotFullyLoaded(DexType type, ClassProvider<T> classProvider) {
+    return classes
+        .computeIfAbsent(type, key -> new ConcurrentClassLoader<>(this, classProvider, type))
+        .get();
   }
 
   /**
@@ -151,6 +136,9 @@
   }
 
   public ImmutableMap<DexType, T> getAllClassesInMap() {
+    if (classProvider.get() != null) {
+      throw new Unreachable("Getting all classes from not fully loaded collection.");
+    }
     ImmutableMap.Builder<DexType, T> builder = ImmutableMap.builder();
     // This is fully loaded, so the class map will no longer change.
     forEach(builder::put);
@@ -177,10 +165,6 @@
         "Cannot access all types since the classProvider is no longer available");
   }
 
-  public ClassMap<T> forceLoad() {
-    return forceLoad(alwaysTrue());
-  }
-
   /**
    * Forces loading of all the classes satisfying the criteria specified.
    *
@@ -188,75 +172,41 @@
    * sealed. This has one side-effect: if we filter out some of the classes with `load` predicate,
    * these classes will never be loaded.
    */
-  @SuppressWarnings("ReferenceEquality")
-  public ClassMap<T> forceLoad(Predicate<DexType> load) {
-    Set<DexType> knownClasses;
-    ClassProvider<T> classProvider;
-
+  public ClassMap<T> forceLoad(
+      InternalOptions options,
+      TaskCollection<?> tasks,
+      Timing timing,
+      Predicate<ProgramResource> load) {
     // Cache value of class provider, as it might change concurrently.
     if (isFullyLoaded()) {
       return this;
     }
-    classProvider = this.classProvider.get();
 
-    // Collects the types which might be represented in fully loaded class map.
-    knownClasses = Sets.newIdentityHashSet();
-    knownClasses.addAll(classes.keySet());
-
-    // Add all types the class provider provides. Note that it may take time for class
-    // provider to collect these types, so we do it outside synchronized context.
-    knownClasses.addAll(classProvider.collectTypes());
-
-    // Make sure all the types in `knownClasses` are loaded.
-    //
-    // We just go and touch every class, thus triggering their loading if they
-    // are not loaded so far. In case the class has already been loaded,
-    // touching the class will be a no-op with minimal overhead.
-    for (DexType type : knownClasses) {
-      if (load.test(type)) {
-        get(type);
-      }
-    }
-
-    // Lock on this to prevent concurrent changes to classProvider state and to ensure that
-    // only one thread proceeds to rewriting the map.
-    synchronized (this) {
-      if (this.classProvider.get() == null) {
-        return this; // Has been force-loaded concurrently.
-      }
-
-      // We avoid calling get() on a class supplier unless we know it was loaded.
-      // At this time `classes` may have more types then `knownClasses`, but for
-      // all extra classes we expect the supplier to return 'null' after loading.
-      Iterator<Map.Entry<DexType, Supplier<T>>> iterator = classes.entrySet().iterator();
-      while (iterator.hasNext()) {
-        Map.Entry<DexType, Supplier<T>> e = iterator.next();
-
-        if (knownClasses.contains(e.getKey())) {
-          // Get the class (it is expected to be loaded by this time).
-          T clazz = e.getValue().get();
-          if (clazz != null) {
-            // Since the class is already loaded, get rid of possible wrapping suppliers.
-            assert clazz.type == e.getKey();
-            e.setValue(getTransparentSupplier(clazz));
-            continue;
-          }
-        }
-
-        // If the type is not in `knownClasses` or resolves to `null`,
-        // just remove the record from the map.
-        iterator.remove();
-      }
-
-      // Mark the class map as fully loaded. This has to be the last operation, as this toggles
-      // the class map into fully loaded state and the get operation will no longer try to load
-      // classes by blocking on 'this' and hence wait for the loading operation to finish.
-      this.classProvider.set(null);
-    }
-
+    ClassProvider<T> classProvider = this.classProvider.get();
+    classProvider.forceLoad(
+        clazz ->
+            classes.compute(
+                clazz.getType(),
+                (type, existing) ->
+                    getTransparentSupplier(
+                        existing == null ? clazz : resolveClassConflict(existing.get(), clazz))),
+        options,
+        load,
+        tasks,
+        timing);
     return this;
   }
 
+  public void setFullyLoaded() {
+    // Remove null entries.
+    MapUtils.removeIf(classes, (type, supplier) -> supplier.get() == null);
+
+    // Mark the class map as fully loaded. This has to be the last operation, as this toggles
+    // the class map into fully loaded state and the get operation will no longer try to load
+    // classes by blocking on 'this' and hence wait for the loading operation to finish.
+    this.classProvider.set(null);
+  }
+
   public boolean isFullyLoaded() {
     return this.classProvider.get() == null;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/ClassProvider.java b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
index 9608497..c1475af 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
@@ -7,20 +7,22 @@
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.graph.JarClassFileReader;
-import com.google.common.collect.ImmutableListMultimap;
+import com.android.tools.r8.threading.TaskCollection;
+import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** Represents a provider for classes loaded from different sources. */
 public abstract class ClassProvider<T extends DexClass> {
@@ -55,22 +57,19 @@
    */
   public abstract Collection<DexType> collectTypes();
 
+  public abstract void forceLoad(
+      Consumer<T> classConsumer,
+      InternalOptions options,
+      Predicate<ProgramResource> predicate,
+      TaskCollection<?> tasks,
+      Timing timing);
+
   /** Create class provider for java class resource provider. */
   public static <T extends DexClass> ClassProvider<T> forClassFileResources(
       ClassKind<T> classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
     return new ClassFileResourceReader<>(classKind, provider, reader);
   }
 
-  /** Create class provider for preloaded classes, classes may have conflicting names. */
-  public static <T extends DexClass> ClassProvider<T> forPreloadedClasses(
-      ClassKind<T> classKind, Collection<T> classes) {
-    ImmutableListMultimap.Builder<DexType, T> builder = ImmutableListMultimap.builder();
-    for (T clazz : classes) {
-      builder.put(clazz.type, clazz);
-    }
-    return new PreloadedClassProvider<>(classKind, builder.build());
-  }
-
   public FilteringClassProvider<T> without(Set<DexType> filteredTypes) {
     return new FilteringClassProvider<>(classKind, this, filteredTypes);
   }
@@ -78,7 +77,13 @@
   /** Create class provider for preloaded classes. */
   public static <T extends DexClass> ClassProvider<T> combine(
       ClassKind<T> classKind, List<ClassProvider<T>> providers) {
-    return new CombinedClassProvider<>(classKind, providers);
+    if (providers.isEmpty()) {
+      return null;
+    } else if (providers.size() == 1) {
+      return providers.get(0);
+    } else {
+      return new CombinedClassProvider<>(classKind, providers);
+    }
   }
 
   private static class ClassFileResourceReader<T extends DexClass> extends ClassProvider<T> {
@@ -133,35 +138,51 @@
       return types;
     }
 
+    @SuppressWarnings("unchecked")
     @Override
-    public String toString() {
-      return "class-resource-provider(" + provider.toString() + ")";
-    }
-  }
-
-  private static class PreloadedClassProvider<T extends DexClass> extends ClassProvider<T> {
-    private final Multimap<DexType, T> classes;
-
-    private PreloadedClassProvider(ClassKind<T> classKind, Multimap<DexType, T> classes) {
-      super(classKind);
-      this.classes = classes;
-    }
-
-    @Override
-    public void collectClass(DexType type, Consumer<T> classConsumer) {
-      for (T clazz : classes.get(type)) {
-        classConsumer.accept(clazz);
+    public void forceLoad(
+        Consumer<T> classConsumer,
+        InternalOptions options,
+        Predicate<ProgramResource> predicate,
+        TaskCollection<?> tasks,
+        Timing timing) {
+      if (provider instanceof InternalClasspathOrLibraryClassProvider) {
+        InternalClasspathOrLibraryClassProvider<T> internalProvider =
+            (InternalClasspathOrLibraryClassProvider<T>) provider;
+        internalProvider.getClasses().forEach(classConsumer);
+      } else {
+        JarClassFileReader<T> classReader =
+            new JarClassFileReader<>(reader, classConsumer, classKind);
+        try {
+          provider.getProgramResources(
+              programResource -> {
+                if (predicate.test(programResource)) {
+                  tasks.submitUnchecked(
+                      () -> {
+                        Timing threadTiming = timing.createThreadTiming("Force load", options);
+                        try {
+                          classReader.read(programResource);
+                        } catch (ResourceException e) {
+                          throw new RuntimeException(e);
+                        }
+                        threadTiming.end().notifyThreadTimingFinished();
+                      });
+                }
+              });
+        } catch (Unimplemented e) {
+          options.reporter.warning(
+              "Class file resource provider does not support async parsing: "
+                  + provider.getClass().getTypeName());
+          for (DexType type : collectTypes()) {
+            collectClass(type, classConsumer);
+          }
+        }
       }
     }
 
     @Override
-    public Collection<DexType> collectTypes() {
-      return classes.keys();
-    }
-
-    @Override
     public String toString() {
-      return "preloaded(" + classes.size() + ")";
+      return "class-resource-provider(" + provider.toString() + ")";
     }
   }
 
@@ -179,6 +200,25 @@
     }
 
     @Override
+    public void forceLoad(
+        Consumer<T> classConsumer,
+        InternalOptions options,
+        Predicate<ProgramResource> predicate,
+        TaskCollection<?> tasks,
+        Timing timing) {
+      provider.forceLoad(
+          clazz -> {
+            if (!filteredOut.contains(clazz.getType())) {
+              classConsumer.accept(clazz);
+            }
+          },
+          options,
+          predicate,
+          tasks,
+          timing);
+    }
+
+    @Override
     public FilteringClassProvider<T> without(Set<DexType> filteredTypes) {
       ImmutableSet<DexType> newSet =
           ImmutableSet.<DexType>builder().addAll(filteredOut).addAll(filteredTypes).build();
@@ -231,6 +271,18 @@
     }
 
     @Override
+    public void forceLoad(
+        Consumer<T> classConsumer,
+        InternalOptions options,
+        Predicate<ProgramResource> predicate,
+        TaskCollection<?> tasks,
+        Timing timing) {
+      for (ClassProvider<T> provider : providers) {
+        provider.forceLoad(classConsumer, options, predicate, tasks, timing);
+      }
+    }
+
+    @Override
     public String toString() {
       StringBuilder builder = new StringBuilder();
       String prefix = "combined(";
diff --git a/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
index e14d8ed..8a425af 100644
--- a/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
@@ -16,7 +16,7 @@
   }
 
   public ClasspathClassCollection(ClassProvider<DexClasspathClass> classProvider) {
-    super(null, classProvider);
+    super(classProvider);
   }
 
   public static ClasspathClassCollection empty() {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
index 7685a95..e3ac36c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
@@ -29,8 +29,10 @@
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
 import java.util.zip.ZipFile;
 
 /**
@@ -103,6 +105,34 @@
     }
   }
 
+  @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) {
+    try (ZipFile zipFile = FileUtils.createZipFile(path.toFile(), StandardCharsets.UTF_8)) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        try (InputStream stream = zipFile.getInputStream(entry)) {
+          String name = entry.getName();
+          Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
+          if (ZipUtils.isClassFile(name)) {
+            String descriptor = DescriptorUtils.guessTypeDescriptor(name);
+            ProgramResource resource =
+                OneShotByteResource.create(
+                    Kind.CF,
+                    entryOrigin,
+                    ByteStreams.toByteArray(stream),
+                    Collections.singleton(descriptor));
+            consumer.accept(resource);
+          }
+        }
+      }
+    } catch (ZipException e) {
+      throw new CompilationError("Zip error while reading '" + path + "': " + e.getMessage(), e);
+    } catch (IOException e) {
+      throw new RuntimeException(new ResourceException(origin, e));
+    }
+  }
+
   private ZipFile getOpenZipFile() throws IOException {
     if (openedZipFile == null) {
       try {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalClasspathOrLibraryClassProvider.java b/src/main/java/com/android/tools/r8/utils/InternalClasspathOrLibraryClassProvider.java
index 5229dd4..aaea34c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalClasspathOrLibraryClassProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalClasspathOrLibraryClassProvider.java
@@ -30,6 +30,10 @@
     return classes.get(type);
   }
 
+  public Collection<T> getClasses() {
+    return classes.values();
+  }
+
   public Collection<DexType> getTypes() {
     return classes.keySet();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramProvider.java b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramProvider.java
index 33edbcb..8f48413 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalGlobalSyntheticsProgramProvider.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
+
 import com.android.tools.r8.GlobalSyntheticsResourceProvider;
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResource.Kind;
@@ -19,6 +21,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
@@ -50,12 +53,22 @@
   @Override
   public Collection<ProgramResource> getProgramResources() throws ResourceException {
     if (resources == null) {
-      ensureResources();
+      ensureResources(emptyConsumer());
     }
     return resources;
   }
 
-  private synchronized void ensureResources() throws ResourceException {
+  @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) throws ResourceException {
+    if (resources == null) {
+      ensureResources(consumer);
+    } else {
+      resources.forEach(consumer);
+    }
+  }
+
+  private synchronized void ensureResources(Consumer<ProgramResource> consumer)
+      throws ResourceException {
     if (resources != null) {
       return;
     }
@@ -101,7 +114,9 @@
             "Invalid global synthetics provider does not specify its content kind.");
       }
       for (Function<Kind, ProgramResource> fn : delayedResouces) {
-        resources.add(fn.apply(providerKind));
+        ProgramResource resource = fn.apply(providerKind);
+        consumer.accept(resource);
+        resources.add(resource);
       }
     }
     this.resources = resources;
diff --git a/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
index d1957a4..6ee5fc1 100644
--- a/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
@@ -10,7 +10,7 @@
 /** Represents a collection of library classes. */
 public class LibraryClassCollection extends ClassMap<DexLibraryClass> {
   public LibraryClassCollection(ClassProvider<DexLibraryClass> classProvider) {
-    super(null, classProvider);
+    super(classProvider);
   }
 
   public static LibraryClassCollection empty() {
diff --git a/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java b/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
index 76be7da..92d164f 100644
--- a/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
+++ b/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
@@ -10,7 +10,7 @@
 import java.io.InputStream;
 import java.util.Set;
 
-class OneShotByteResource implements ProgramResource {
+public class OneShotByteResource implements ProgramResource {
 
   private final Origin origin;
   private final Kind kind;
diff --git a/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
index 82e6a7e..6ac0afa 100644
--- a/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
@@ -12,6 +12,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * Lazy Java class file resource provider based on preloaded/prebuilt context.
@@ -49,6 +50,16 @@
     if (bytes == null) {
       return null;
     }
+    return createProgramResource(descriptor, bytes);
+  }
+
+  @Override
+  public void getProgramResources(Consumer<ProgramResource> consumer) {
+    content.forEach(
+        (descriptor, bytes) -> consumer.accept(createProgramResource(descriptor, bytes)));
+  }
+
+  private static ProgramResource createProgramResource(String descriptor, byte[] bytes) {
     return ProgramResource.fromBytes(
         new ClassDescriptorOrigin(descriptor), Kind.CF, bytes, Collections.singleton(descriptor));
   }
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
index b177056..bdd7532 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.ClassConflictResolver;
 import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
 import com.android.tools.r8.errors.DuplicateTypesDiagnostic;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
@@ -14,31 +15,53 @@
 import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramProvider.GlobalsEntryOrigin;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Supplier;
 
-/** Represents a collection of library classes. */
+/** Represents a collection of program classes. */
 public class ProgramClassCollection extends ClassMap<DexProgramClass> {
 
-  private final ProgramClassConflictResolver conflictResolver;
-
   public static ProgramClassCollection create(
-      List<DexProgramClass> classes, ProgramClassConflictResolver conflictResolver) {
+      List<DexProgramClass> classes, InternalOptions options) {
     // We have all classes preloaded, but not necessarily without conflicts.
     ConcurrentHashMap<DexType, Supplier<DexProgramClass>> map = new ConcurrentHashMap<>();
+    ProgramClassConflictResolver conflictResolver = createConflictResolver(options);
     for (DexProgramClass clazz : classes) {
       map.merge(
-          clazz.type, clazz, (a, b) -> conflictResolver.resolveClassConflict(a.get(), b.get()));
+          clazz.getType(),
+          clazz,
+          (a, b) -> conflictResolver.resolveClassConflict(a.get(), b.get()));
     }
-    return new ProgramClassCollection(map, conflictResolver);
+    return new ProgramClassCollection(map);
   }
 
-  private ProgramClassCollection(
-      ConcurrentHashMap<DexType, Supplier<DexProgramClass>> classes,
-      ProgramClassConflictResolver conflictResolver) {
-    super(classes, null);
-    this.conflictResolver = conflictResolver;
+  public static ProgramClassConflictResolver createConflictResolver(InternalOptions options) {
+    // The default conflict resolver only merges synthetic classes generated by D8 correctly.
+    // All other conflicts are reported as a fatal error.
+    return options.programClassConflictResolver == null
+        ? wrappedConflictResolver(null, options.reporter)
+        : options.programClassConflictResolver;
+  }
+
+  public static Map<DexType, DexProgramClass> resolveConflicts(
+      Collection<DexProgramClass> classes, InternalOptions options) {
+    Map<DexType, DexProgramClass> map = new IdentityHashMap<>();
+    ProgramClassConflictResolver conflictResolver = createConflictResolver(options);
+    for (DexProgramClass clazz : classes) {
+      map.merge(
+          clazz.getType(),
+          clazz,
+          (a, b) -> conflictResolver.resolveClassConflict(a.get(), b.get()));
+    }
+    return map;
+  }
+
+  private ProgramClassCollection(ConcurrentHashMap<DexType, Supplier<DexProgramClass>> classes) {
+    super(classes);
   }
 
   @Override
@@ -48,7 +71,7 @@
 
   @Override
   DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b) {
-    return conflictResolver.resolveClassConflict(a, b);
+    throw new Unreachable();
   }
 
   @Override
@@ -61,12 +84,6 @@
     return ClassKind.PROGRAM;
   }
 
-  public static ProgramClassConflictResolver defaultConflictResolver(Reporter reporter) {
-    // The default conflict resolver only merges synthetic classes generated by D8 correctly.
-    // All other conflicts are reported as a fatal error.
-    return wrappedConflictResolver(null, reporter);
-  }
-
   @SuppressWarnings("ReferenceEquality")
   public static ProgramClassConflictResolver wrappedConflictResolver(
       ClassConflictResolver clientResolver, Reporter reporter) {
diff --git a/src/test/java/com/android/tools/r8/androidresources/AlwaysKeepIDsInLegacyMode.java b/src/test/java/com/android/tools/r8/androidresources/AlwaysKeepIDsInLegacyMode.java
index 61f007c..5adea06 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AlwaysKeepIDsInLegacyMode.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AlwaysKeepIDsInLegacyMode.java
@@ -54,7 +54,8 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
diff --git a/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java b/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java
index c3d5cf9..daa9310 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ColorInliningTest.java
@@ -99,7 +99,8 @@
                       o.enableXmlInlining = true;
                       o.enableColorInlining = true;
                     }))
-        .applyIf(optimize, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimize, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .applyIf(
             addResourcesSubclass,
             builder ->
diff --git a/src/test/java/com/android/tools/r8/androidresources/DuplicatedEntriesEmptyUnusedTest.java b/src/test/java/com/android/tools/r8/androidresources/DuplicatedEntriesEmptyUnusedTest.java
index 07045d6..92e4008 100644
--- a/src/test/java/com/android/tools/r8/androidresources/DuplicatedEntriesEmptyUnusedTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/DuplicatedEntriesEmptyUnusedTest.java
@@ -74,7 +74,8 @@
         .addAndroidResources(getTestResources(temp))
         .addFeatureSplitAndroidResources(
             getFeatureSplitTestResources(featureSplitTemp), FeatureSplit.class.getName())
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .addKeepMainRule(Base.class)
         .addKeepMainRule(FeatureSplitMain.class)
         .compile()
diff --git a/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java b/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java
index 34c870a..c8b0a4e 100644
--- a/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java
@@ -56,7 +56,8 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
diff --git a/src/test/java/com/android/tools/r8/androidresources/NoOptResourceShrinkingTest.java b/src/test/java/com/android/tools/r8/androidresources/NoOptResourceShrinkingTest.java
index 3e19e29..1b3c4a9 100644
--- a/src/test/java/com/android/tools/r8/androidresources/NoOptResourceShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/NoOptResourceShrinkingTest.java
@@ -53,7 +53,8 @@
     AndroidTestResource testResources = getTestResources(temp);
     testForR8(parameters)
         .addProgramClasses(FooBar.class)
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .addAndroidResources(testResources)
         .addKeepMainRule(FooBar.class)
         .addDontOptimize()
diff --git a/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java b/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
index 4d2a0e8..9c16b45 100644
--- a/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
+++ b/src/test/java/com/android/tools/r8/androidresources/RClassResourceGeneration.java
@@ -50,6 +50,7 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(testResource)
         .addKeepMainRule(FooBar.class)
+        .allowStderrMessages()
         .run(parameters.getRuntime(), FooBar.class)
         // The values from the aapt2 generated R class (validated in testResourceRewriting below)
         //    drawable:
diff --git a/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java b/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java
index fdc8a87..b61d5e8 100644
--- a/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java
@@ -56,6 +56,7 @@
             DescriptorUtils.descriptorToJavaType(RClassDescriptor))
         .addAndroidResources(testResources)
         .addKeepMainRule(FooBar.class)
+        .allowStderrMessages()
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceNameWithDotsRegressionTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceNameWithDotsRegressionTest.java
index 1e28ef0..226f3ab 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceNameWithDotsRegressionTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceNameWithDotsRegressionTest.java
@@ -56,6 +56,7 @@
         .addRunClasspathFiles(resourcesClass)
         .addAndroidResources(testResources)
         .addKeepMainRule(FooBar.class)
+        .allowStderrMessages()
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
index 3759c64..317c67c 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
@@ -53,7 +53,8 @@
     Assume.assumeTrue(optimized || parameters.getPartialCompilationTestParameters().isNone());
     testForR8(parameters)
         .addProgramClasses(FooBar.class)
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .addResourceShrinkerLogCapture()
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingMultiApkAsFeaturesplits.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingMultiApkAsFeaturesplits.java
index 1d0655a..cc9893e 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingMultiApkAsFeaturesplits.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingMultiApkAsFeaturesplits.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.androidresources;
 
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.R8TestCompileResultBase;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -70,7 +71,10 @@
                 // For the feature, we don't add the R class (we already have it in the base)
                 // and to test we add one less xml file.
                 getTestResources(featureSplitTemp, false, VIEW), featureSplitName)
-            .applyIf(optimized, b -> b.enableOptimizedShrinking())
+            .applyIf(
+                optimized,
+                R8TestBuilder::enableOptimizedShrinking,
+                R8TestBuilder::allowStderrMessages)
             .addKeepMainRule(Base.class)
             .compile()
             .inspectShrunkenResources(
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
index 818afca..e543df1 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
@@ -74,7 +74,10 @@
     try {
       testForR8(parameters)
           .addProgramClasses(Base.class)
-          .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+          .applyIf(
+              optimized,
+              R8TestBuilder::enableOptimizedShrinking,
+              R8TestBuilder::allowStderrMessages)
           .addFeatureSplit(builder -> builder.build())
           .compileWithExpectedDiagnostics(
               diagnostics -> {
@@ -114,7 +117,10 @@
             .addAndroidResources(getTestResources(temp))
             .addFeatureSplitAndroidResources(
                 getFeatureSplitTestResources(featureSplitTemp), FeatureSplit.class.getName())
-            .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+            .applyIf(
+                optimized,
+                R8TestBuilder::enableOptimizedShrinking,
+                R8TestBuilder::allowStderrMessages)
             .addKeepMainRule(Base.class)
             .addKeepMainRule(FeatureSplitMain.class)
             .compile();
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest.java
index cc75a09..110fa44 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeaturesAndDuplicatedResEntryTest.java
@@ -74,7 +74,8 @@
         .addAndroidResources(getTestResources(temp))
         .addFeatureSplitAndroidResources(
             getFeatureSplitTestResources(featureSplitTemp), FeatureSplit.class.getName())
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .addKeepMainRule(Base.class)
         .addKeepMainRule(FeatureSplit.FeatureSplitMain.class)
         .compile()
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithSeveralFeaturesTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithSeveralFeaturesTest.java
index fc1f76d..5fd1e0c 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithSeveralFeaturesTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithSeveralFeaturesTest.java
@@ -84,7 +84,8 @@
             getFeatureSplitTestResources(featureSplitTemp), FeatureSplit.class.getName())
         .addFeatureSplitAndroidResources(
             getFeatureSplit2TestResources(featureSplit2Temp), FeatureSplit2.class.getName())
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .addResourceShrinkerLogCapture()
         .addKeepMainRule(Base.class)
         .addKeepMainRule(FeatureSplitMain.class)
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourcesInFilledNewArrayDataTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourcesInFilledNewArrayDataTest.java
index faccca3..5a7ae29 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourcesInFilledNewArrayDataTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourcesInFilledNewArrayDataTest.java
@@ -43,6 +43,7 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
+        .allowStderrMessages()
         .compile()
         // Ensure that we have the fillednewarraydata
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/androidresources/SimpleNoCodeReferenceAndroidResourceTest.java b/src/test/java/com/android/tools/r8/androidresources/SimpleNoCodeReferenceAndroidResourceTest.java
index 8e3a5e9..e59f484 100644
--- a/src/test/java/com/android/tools/r8/androidresources/SimpleNoCodeReferenceAndroidResourceTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/SimpleNoCodeReferenceAndroidResourceTest.java
@@ -70,7 +70,8 @@
         .addInnerClasses(getClass())
         .setMinApi(parameters)
         .addAndroidResources(testResource, output)
-        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimized, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .addKeepMainRule(FooBar.class)
         .compile()
         .inspectShrunkenResources(
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java b/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java
index e7d0e79..ae5bb2c 100644
--- a/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java
+++ b/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java
@@ -57,6 +57,7 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(testResources, resourceOutput)
         .addKeepMainRule(FooBar.class)
+        .allowStderrMessages()
         .compile()
         .run(parameters.getRuntime(), FooBar.class)
         .assertSuccess();
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestNameRemovalInResourceTable.java b/src/test/java/com/android/tools/r8/androidresources/TestNameRemovalInResourceTable.java
index bef5ad8..0c44bf6 100644
--- a/src/test/java/com/android/tools/r8/androidresources/TestNameRemovalInResourceTable.java
+++ b/src/test/java/com/android/tools/r8/androidresources/TestNameRemovalInResourceTable.java
@@ -56,7 +56,10 @@
         testForR8(parameters)
             .addProgramClasses(FooBar.class)
             .addAndroidResources(getTestResources(temp))
-            .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
+            .applyIf(
+                optimized,
+                R8TestBuilder::enableOptimizedShrinking,
+                R8TestBuilder::allowStderrMessages)
             .addKeepMainRule(FooBar.class)
             .compile();
     r8TestCompileResult
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
index f9de0e0..b86f549 100644
--- a/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
+++ b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
@@ -93,7 +93,8 @@
             })
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
-        .applyIf(optimize, R8TestBuilder::enableOptimizedShrinking)
+        .applyIf(
+            optimize, R8TestBuilder::enableOptimizedShrinking, R8TestBuilder::allowStderrMessages)
         .addRunClasspathFiles(AndroidResourceTestingUtils.resourcesClassAsDex(temp))
         .compile()
         .inspectShrunkenResources(
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestShrinkingWithCodeReferences.java b/src/test/java/com/android/tools/r8/androidresources/TestShrinkingWithCodeReferences.java
index 5bd64e3..24be04f 100644
--- a/src/test/java/com/android/tools/r8/androidresources/TestShrinkingWithCodeReferences.java
+++ b/src/test/java/com/android/tools/r8/androidresources/TestShrinkingWithCodeReferences.java
@@ -40,6 +40,7 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
+        .allowStderrMessages()
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
diff --git a/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java b/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java
index b55edff..1935590 100644
--- a/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java
+++ b/src/test/java/com/android/tools/r8/androidresources/XmlFilesWithReferences.java
@@ -41,6 +41,7 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
+        .allowStderrMessages()
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
diff --git a/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java b/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java
index 584426b..b23dd1e 100644
--- a/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java
+++ b/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.androidresources.optimizedshrinking;
 
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.ResourceShrinkerConfiguration;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -61,7 +62,8 @@
                         internalOptions.resourceShrinkerConfiguration =
                             ResourceShrinkerConfiguration.builder(null)
                                 .enableOptimizedShrinkingWithR8()
-                                .build()))
+                                .build()),
+            R8TestBuilder::allowStderrMessages)
         .applyIf(debug, builder -> builder.debug())
         .compile()
         .inspectShrunkenResources(
diff --git a/src/test/java/com/android/tools/r8/annotations/DalvikAnnotationOptimizationTest.java b/src/test/java/com/android/tools/r8/annotations/DalvikAnnotationOptimizationTest.java
index c6c128f..f810c1b 100644
--- a/src/test/java/com/android/tools/r8/annotations/DalvikAnnotationOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/annotations/DalvikAnnotationOptimizationTest.java
@@ -27,6 +27,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -165,6 +166,11 @@
               Origin.unknown(), Kind.CF, bytes, Collections.singleton(descriptor));
     }
 
+    @Override
+    public void getProgramResources(Consumer<ProgramResource> consumer) {
+      getClassDescriptors().forEach(descriptor -> consumer.accept(getProgramResource(descriptor)));
+    }
+
     private byte[] transformPackageName(Class<?> clazz) throws IOException {
       return transformer(clazz)
           .setClassDescriptor(
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java
index 4fc37c5..c284d66 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryInvokeAllResolveTest.java
@@ -115,12 +115,12 @@
             .build();
     InternalOptions options = inspector.getApplication().options;
     DirectMappedDexApplication libHolder =
-        new ApplicationReader(build, options, Timing.empty()).read().toDirect();
+        new ApplicationReader(build, options, Timing.empty()).readDirectSingleThreaded();
     DirectMappedDexApplication finalApp =
         inspector
             .getApplication()
             .asLazy()
-            .toDirect()
+            .toDirectSingleThreadedForTesting()
             .builder()
             .replaceLibraryClasses(libHolder.libraryClasses())
             .build();
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DumpCoreLibUsage.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DumpCoreLibUsage.java
index 69d123c..49b768e 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DumpCoreLibUsage.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DumpCoreLibUsage.java
@@ -66,8 +66,7 @@
         AndroidApp.builder().addLibraryFiles(ToolHelper.getAndroidJar(apiLevel)).build();
     DirectMappedDexApplication dexApplication =
         new ApplicationReader(input, new InternalOptions(factory, new Reporter()), Timing.empty())
-            .read()
-            .toDirect();
+            .readDirectSingleThreaded();
 
     Set<DexReference> found = Sets.newIdentityHashSet();
     found.addAll(filter);
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java
index 130f0e5..5bdb446 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/R8FeatureSplitTest.java
@@ -12,6 +12,8 @@
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.TestParameters;
@@ -28,7 +30,9 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import org.junit.Test;
@@ -54,7 +58,19 @@
 
   private static FeatureSplit emptySplitProvider(FeatureSplit.Builder builder) {
     builder
-        .addProgramResourceProvider(ImmutableList::of)
+        .addProgramResourceProvider(
+            new ProgramResourceProvider() {
+
+              @Override
+              public Collection<ProgramResource> getProgramResources() {
+                return Collections.emptyList();
+              }
+
+              @Override
+              public void getProgramResources(Consumer<ProgramResource> consumer) {
+                // Intentionally empty.
+              }
+            })
         .setProgramConsumer(DexIndexedConsumer.emptyConsumer());
     return builder.build();
   }
diff --git a/src/test/java/com/android/tools/r8/files/ArchiveWithDexTest.java b/src/test/java/com/android/tools/r8/files/ArchiveWithDexTest.java
index 85b25f3..621c9da 100644
--- a/src/test/java/com/android/tools/r8/files/ArchiveWithDexTest.java
+++ b/src/test/java/com/android/tools/r8/files/ArchiveWithDexTest.java
@@ -53,12 +53,17 @@
   private static Origin zipWithDexAndClassOrigin;
 
   @BeforeClass
-  public static void createZipWitDexAndClass() throws IOException {
+  public static void createZipWitDexAndClass() throws CompilationFailedException, IOException {
     Path zipContent = getStaticTemp().newFolder().toPath();
     Files.copy(
         ToolHelper.getClassFileForTestClass(TestClass.class),
         zipContent.resolve("TestClass.class"));
-    Files.createFile(zipContent.resolve("other.dex"));
+    testForD8(getStaticTemp())
+        .addProgramClasses(OtherTestClass.class)
+        .setMinApi(AndroidApiLevel.B)
+        .release()
+        .compile()
+        .writeSingleDexOutputToFile(zipContent.resolve("other.dex"));
     zipWithDexAndClass = getStaticTemp().newFolder().toPath().resolve("input.zip");
     ZipUtils.zip(zipWithDexAndClass, zipContent);
     zipWithDexAndClassOrigin = new PathOrigin(zipWithDexAndClass);
@@ -213,4 +218,11 @@
       System.out.println("Hello, world!");
     }
   }
+
+  static class OtherTestClass {
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/graph/DexTypeTest.java b/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
index 4b05030..282faa9 100644
--- a/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
+++ b/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
@@ -35,8 +35,7 @@
                     .build(),
                 options,
                 Timing.empty())
-            .read()
-            .toDirect();
+            .readDirectSingleThreaded();
     factory = options.itemFactory;
     appInfo = AppView.createForR8(application).appInfo();
   }
diff --git a/src/test/java/com/android/tools/r8/ir/InlineTest.java b/src/test/java/com/android/tools/r8/ir/InlineTest.java
index 5058af1..74075e9 100644
--- a/src/test/java/com/android/tools/r8/ir/InlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/InlineTest.java
@@ -17,8 +17,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.ImmediateAppSubtypingInfo;
-import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -157,7 +157,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -239,7 +239,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -316,7 +316,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -449,7 +449,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -464,7 +464,7 @@
         application, options, methodSubject, ImmutableList.of(codeA, codeB));
   }
 
-  protected LazyLoadedDexApplication buildApplication(
+  protected DirectMappedDexApplication buildApplication(
       SmaliBuilder builder, InternalOptions options) {
     try {
       AndroidApp app =
@@ -472,7 +472,7 @@
               .addDexProgramData(builder.compile(), Origin.unknown())
               .addLibraryFile(getMostRecentAndroidJar())
               .build();
-      return new ApplicationReader(app, options, Timing.empty()).read();
+      return new ApplicationReader(app, options, Timing.empty()).readDirectSingleThreaded();
     } catch (IOException | RecognitionException | ExecutionException e) {
       throw new RuntimeException(e);
     }
@@ -576,7 +576,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -687,7 +687,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -800,7 +800,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -956,7 +956,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
@@ -1202,7 +1202,7 @@
     );
 
     InternalOptions options = createOptions();
-    DexApplication application = buildApplication(builder, options).toDirect();
+    DexApplication application = buildApplication(builder, options);
 
     // Return the processed method for inspection.
     MethodSubject methodSubject = getMethodSubject(application, signature);
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java b/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
index 2f0a421..ace2c2c 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
@@ -136,8 +136,7 @@
                     .build(),
                 options,
                 timing)
-            .read()
-            .toDirect();
+            .readDirectSingleThreaded();
     return AppView.createForR8(application);
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
index 6364e7c..fd671df 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
@@ -60,8 +60,7 @@
                     .build(),
                 options,
                 Timing.empty())
-            .read()
-            .toDirect();
+            .readDirectSingleThreaded();
     factory = options.itemFactory;
     appView = AppView.createForR8(application);
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
index ecc4fa8..455f5c0 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
@@ -111,8 +111,7 @@
                     .build(),
                 options,
                 Timing.empty())
-            .read()
-            .toDirect();
+            .readDirectSingleThreaded();
     AppView<AppInfoWithClassHierarchy> appView = AppView.createForR8(application);
 
     // Type references.
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
index 0add3e3..5a06c1d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
@@ -407,17 +407,22 @@
     }
 
     @Override
-    public Collection<ProgramResource> getProgramResources() throws ResourceException {
+    public Collection<ProgramResource> getProgramResources() {
       return programResources;
     }
 
     @Override
+    public void getProgramResources(Consumer<ProgramResource> consumer) {
+      programResources.forEach(consumer);
+    }
+
+    @Override
     public DataResourceProvider getDataResourceProvider() {
       return this;
     }
 
     @Override
-    public void accept(Visitor visitor) throws ResourceException {
+    public void accept(Visitor visitor) {
       dataResources.forEach(visitor::visit);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/resolution/ArrayTargetLookupTest.java b/src/test/java/com/android/tools/r8/resolution/ArrayTargetLookupTest.java
index d44d061..b8c1595 100644
--- a/src/test/java/com/android/tools/r8/resolution/ArrayTargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/ArrayTargetLookupTest.java
@@ -51,7 +51,7 @@
             .addProgramFiles(ToolHelper.getClassFileForTestClass(Foo.class))
             .build();
     DirectMappedDexApplication application =
-        new ApplicationReader(app, options, timing).read().toDirect();
+        new ApplicationReader(app, options, timing).readDirectSingleThreaded();
     AppInfoWithClassHierarchy appInfo = AppView.createForR8(application).appInfo();
     DexItemFactory factory = options.itemFactory;
     DexType fooType =
diff --git a/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableDesugaringClassesPresentAtKitKatTest.java b/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableDesugaringClassesPresentAtKitKatTest.java
index fdd9e74..f2392e4 100644
--- a/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableDesugaringClassesPresentAtKitKatTest.java
+++ b/src/test/java21/com/android/tools/r8/jdk21/autocloseable/AutoCloseableDesugaringClassesPresentAtKitKatTest.java
@@ -47,7 +47,7 @@
     Path androidJarK = ToolHelper.getAndroidJar(AndroidApiLevel.K);
     AndroidApp app = AndroidApp.builder().addProgramFile(androidJarK).build();
     DirectMappedDexApplication libHolder =
-        new ApplicationReader(app, options, Timing.empty()).read().toDirect();
+        new ApplicationReader(app, options, Timing.empty()).readDirectSingleThreaded();
     AppInfo initialAppInfo =
         AppInfo.createInitialAppInfo(libHolder, GlobalSyntheticsStrategy.forNonSynthesizing());
     AppView<AppInfo> appView = AppView.createForD8(initialAppInfo, Timing.empty());
diff --git a/src/test/java21/com/android/tools/r8/jdk21/twr/LookUpCloseResourceTest.java b/src/test/java21/com/android/tools/r8/jdk21/twr/LookUpCloseResourceTest.java
index 7e7a721..737dd23 100644
--- a/src/test/java21/com/android/tools/r8/jdk21/twr/LookUpCloseResourceTest.java
+++ b/src/test/java21/com/android/tools/r8/jdk21/twr/LookUpCloseResourceTest.java
@@ -185,7 +185,7 @@
   private AppView<?> getAppInfo(InternalOptions options, int api) throws IOException {
     AndroidApp app = AndroidApp.builder().addProgramFile(ToolHelper.getAndroidJar(api)).build();
     DirectMappedDexApplication libHolder =
-        new ApplicationReader(app, options, Timing.empty()).read().toDirect();
+        new ApplicationReader(app, options, Timing.empty()).readDirectSingleThreaded();
     AppInfo initialAppInfo =
         AppInfo.createInitialAppInfo(libHolder, GlobalSyntheticsStrategy.forNonSynthesizing());
     return AppView.createForD8(initialAppInfo, Timing.empty());
diff --git a/src/test/testbase/java/com/android/tools/r8/SingleTestRunResult.java b/src/test/testbase/java/com/android/tools/r8/SingleTestRunResult.java
index 7ee3103..68cc6c8 100644
--- a/src/test/testbase/java/com/android/tools/r8/SingleTestRunResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/SingleTestRunResult.java
@@ -149,7 +149,7 @@
     return self();
   }
 
-  public RR disassemble(PrintStream ps) throws IOException {
+  public RR disassemble(PrintStream ps) throws ExecutionException, IOException {
     ToolHelper.disassemble(app, ps);
     return self();
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBase.java b/src/test/testbase/java/com/android/tools/r8/TestBase.java
index 4fe4ad3..6220f5d 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBase.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBase.java
@@ -960,7 +960,8 @@
       optionsConsumer.accept(options);
     }
     LazyLoadedDexApplication dexApplication = readApplicationForDexOutput(app, options);
-    AppView<AppInfoWithClassHierarchy> appView = AppView.createForR8(dexApplication.toDirect());
+    AppView<AppInfoWithClassHierarchy> appView =
+        AppView.createForR8(dexApplication.toDirectSingleThreadedForTesting());
     appView.setAppServices(AppServices.builder(appView).build());
     return appView;
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java
index 55d667b..93d901b 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBaseBuilder.java
@@ -23,6 +23,7 @@
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 
 public abstract class TestBaseBuilder<
         C extends BaseCommand,
@@ -175,6 +176,11 @@
       public ProgramResource getProgramResource(String descriptor) {
         return resources.get(descriptor);
       }
+
+      @Override
+      public void getProgramResources(Consumer<ProgramResource> consumer) {
+        resources.values().forEach(consumer);
+      }
     };
   }
 
diff --git a/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java
index dac774b..86fe067 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java
@@ -651,7 +651,7 @@
     return self();
   }
 
-  public CR disassemble(PrintStream ps) throws IOException {
+  public CR disassemble(PrintStream ps) throws ExecutionException, IOException {
     ToolHelper.disassemble(app, ps);
     return self();
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
index e929946..cc057b1 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -1686,7 +1686,8 @@
             .addProgramFiles(ListUtils.map(fileNames, Paths::get))
             .addLibraryFiles(androidJar)
             .build();
-    return new ApplicationReader(input, new InternalOptions(), Timing.empty()).read().toDirect();
+    return new ApplicationReader(input, new InternalOptions(), Timing.empty())
+        .readDirectSingleThreaded();
   }
 
   public static ProguardConfiguration loadProguardConfiguration(
@@ -2704,7 +2705,8 @@
     R8.writeApplication(appView, null, Executors.newSingleThreadExecutor());
   }
 
-  public static void disassemble(AndroidApp app, PrintStream ps) throws IOException {
+  public static void disassemble(AndroidApp app, PrintStream ps)
+      throws ExecutionException, IOException {
     LazyLoadedDexApplication application =
         new ApplicationReader(app, new InternalOptions(), Timing.empty()).read();
     new AssemblyWriter(application, new InternalOptions(), true, false, true).write(ps);