Merge commit 'b5a44aa689addd3df744f4ebe7666308fb5c0ea2' into dev-release

Change-Id: I28f97b104b6a7d4fd32490d0d428a1ffeae556ab
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 2393dab..5e116de 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -284,7 +284,8 @@
         // without iterating again the IR. We fall-back to writing one app with rewriting and
         // merging it with the other app in rewriteNonDexInputs.
         timing.begin("Rewrite non-dex inputs");
-        DexApplication app = rewriteNonDexInputs(appView, inputApp, executor, marker, timing);
+        LazyLoadedDexApplication app =
+            rewriteNonDexInputs(appView, inputApp, executor, marker, timing);
         timing.end();
         appView.rebuildAppInfo(app);
         appView.setNamingLens(NamingLens.getIdentityLens());
@@ -404,7 +405,7 @@
         "Api reference stubber", () -> new ApiReferenceStubber(appView).run(executorService));
   }
 
-  private static DexApplication rewriteNonDexInputs(
+  private static LazyLoadedDexApplication rewriteNonDexInputs(
       AppView<AppInfo> appView,
       AndroidApp inputApp,
       ExecutorService executor,
@@ -440,9 +441,9 @@
     builder.getProgramResourceProviders().clear();
     builder.addProgramResourceProvider(convertedCfFiles);
     AndroidApp newAndroidApp = builder.build();
-    DexApplication newApp =
+    LazyLoadedDexApplication newApp =
         new ApplicationReader(newAndroidApp, appView.options(), timing).read(executor);
-    DexApplication.Builder<?> finalDexApp = newApp.builder();
+    LazyLoadedDexApplication.Builder finalDexApp = newApp.builder();
     for (DexProgramClass dexProgramClass : dexProgramClasses) {
       finalDexApp.addProgramClass(dexProgramClass);
     }
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index 7ddbafb..a800696 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -5,9 +5,9 @@
 
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.graph.AssemblyWriter;
-import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexByteCodeWriter;
 import com.android.tools.r8.graph.DexByteCodeWriter.OutputStreamProvider;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.graph.SmaliWriter;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.CommandLineOrigin;
@@ -351,7 +351,7 @@
       throws IOException {
     ExecutorService executor = ThreadUtils.getExecutorService(options);
     try {
-      DexApplication application =
+      LazyLoadedDexApplication application =
           new ApplicationReader(
                   AndroidApp.builder()
                       .addProgramResourceProvider(() -> Collections.singletonList(programResource))
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
index 50ad4cd..a2d7fef 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -12,10 +12,10 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexProgramClass;
 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,7 +48,7 @@
   private void run(AndroidApp app, ExecutorService executor, SortingStringConsumer consumer)
       throws IOException {
     try {
-      DexApplication application =
+      LazyLoadedDexApplication application =
           new ApplicationReader(app, options, Timing.empty()).read(executor);
       traceMainDexForGenerateMainDexList(executor, application)
           .forEach(type -> consumer.accept(type.toBinaryName() + ".class", options.reporter));
@@ -62,12 +62,12 @@
       throws ExecutionException {
     return traceMainDex(
         AppView.createForD8MainDexTracing(
-            appView.app().toDirect(), appView.appInfo().getMainDexInfo()),
+            appView.app().asLazy().toDirect(), appView.appInfo().getMainDexInfo()),
         executor);
   }
 
   public MainDexInfo traceMainDexForGenerateMainDexList(
-      ExecutorService executor, DexApplication application) throws ExecutionException {
+      ExecutorService executor, LazyLoadedDexApplication application) throws ExecutionException {
     return traceMainDex(AppView.createForR8(application.toDirect()), executor);
   }
 
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 91a94b1..fd20ec5 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -296,9 +296,7 @@
             options.partialSubCompilationConfiguration != null
                 ? options.partialSubCompilationConfiguration.asR8().getAndClearKeepDeclarations()
                 : lazyLoaded.getKeepDeclarations();
-        timing.begin("To direct app");
-        DirectMappedDexApplication application = lazyLoaded.toDirect();
-        timing.end();
+        DirectMappedDexApplication application = lazyLoaded.toDirect(timing);
         timing.end();
         options
             .getLibraryDesugaringOptions()
diff --git a/src/main/java/com/android/tools/r8/R8Partial.java b/src/main/java/com/android/tools/r8/R8Partial.java
index afbc968..fec7386 100644
--- a/src/main/java/com/android/tools/r8/R8Partial.java
+++ b/src/main/java/com/android/tools/r8/R8Partial.java
@@ -245,7 +245,10 @@
     r8Options.setKeepSpecificationSources(options.getKeepSpecifications());
     r8Options.getTestingOptions().enableEmbeddedKeepAnnotations =
         options.getTestingOptions().enableEmbeddedKeepAnnotations;
+    r8Options.configurationConsumer = options.configurationConsumer;
     r8Options.mapConsumer = options.mapConsumer;
+    r8Options.proguardSeedsConsumer = options.proguardSeedsConsumer;
+    r8Options.usageInformationConsumer = options.usageInformationConsumer;
     r8Options.sourceFileProvider = options.sourceFileProvider;
     r8Options
         .getLibraryDesugaringOptions()
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
index dc49336..26090d0 100644
--- a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
@@ -52,6 +52,14 @@
     return null;
   }
 
+  public boolean isServiceLoaderLoad() {
+    return false;
+  }
+
+  public ServiceLoaderLoad asServiceLoaderLoad() {
+    return null;
+  }
+
   public boolean isClassGetMember() {
     return false;
   }
@@ -119,7 +127,7 @@
       case ATOMIC_FIELD_UPDATER_NEW_UPDATER:
         return new AtomicFieldUpdaterNewUpdater(eventType, stack, args, factory);
       case SERVICE_LOADER_LOAD:
-        break;
+        return new ServiceLoaderLoad(eventType, stack, args, factory);
       case PROXY_NEW_PROXY_INSTANCE:
         break;
     }
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ServiceLoaderLoad.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ServiceLoaderLoad.java
new file mode 100644
index 0000000..782805d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ServiceLoaderLoad.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2025, 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.assistant.postprocessing.model;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+
+public class ServiceLoaderLoad extends ReflectiveEvent {
+
+  private final DexType type;
+  private final DexType classLoader;
+
+  public ServiceLoaderLoad(
+      ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+    super(eventType, stack);
+    assert args.length == 2;
+    type = toType(args[0], factory);
+    classLoader = toTypeOrTripleStar(args[0], factory);
+  }
+
+  @Override
+  public boolean isServiceLoaderLoad() {
+    return true;
+  }
+
+  @Override
+  public ServiceLoaderLoad asServiceLoaderLoad() {
+    return this;
+  }
+
+  public DexType getType() {
+    return type;
+  }
+
+  @Override
+  public String getContentsString() {
+    return type + ", " + classLoader;
+  }
+
+  @Override
+  public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+    return keepInfoCollectionExported.getKeepClassInfo(type.asTypeReference()) != null;
+  }
+}
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 bd1bc7a..f361d81 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -268,7 +268,7 @@
   }
 
   private void readProguardMap(
-      StringResource map, DexApplication.Builder<?> builder, TaskCollection<?> tasks)
+      StringResource map, DexApplication.Builder<?, ?> builder, TaskCollection<?> tasks)
       throws ExecutionException {
     // Read the Proguard mapping file in parallel with DexCode and DexProgramClass items.
     if (map == null) {
diff --git a/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java b/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java
index d9f6def..9f0fa1f 100644
--- a/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java
+++ b/src/main/java/com/android/tools/r8/dex/DefaultMixedSectionLayoutStrategy.java
@@ -78,13 +78,12 @@
       this.appView = appView;
     }
 
-    public void addCode(DexWritableCode dexWritableCode, ProgramMethod method) {
+    public void addCode(DexWritableCode code, ProgramMethod method) {
       assert appView.options().canUseCanonicalizedCodeObjects();
       if (counts == null) {
         counts = new HashMap<>();
       }
-      DexWritableCacheKey cacheKey =
-          dexWritableCode.getCacheLookupKey(method, appView.dexItemFactory());
+      DexWritableCacheKey cacheKey = code.getCacheLookupKey(method, appView.dexItemFactory());
       if (!counts.containsKey(cacheKey)) {
         counts.put(cacheKey, 1);
       } else {
@@ -98,9 +97,11 @@
             || method.getDefinition().getDexWritableCodeOrNull() == null;
         return 1;
       }
-      DexWritableCode dexWritableCodeOrNull = method.getDefinition().getDexWritableCodeOrNull();
-      DexWritableCacheKey cacheLookupKey =
-          dexWritableCodeOrNull.getCacheLookupKey(method, appView.dexItemFactory());
+      DexWritableCode code = method.getDefinition().getCode().asDexWritableCode();
+      if (!code.canBeCanonicalized(appView.options())) {
+        return 1;
+      }
+      DexWritableCacheKey cacheLookupKey = code.getCacheLookupKey(method, appView.dexItemFactory());
       assert counts.containsKey(cacheLookupKey);
       return counts.get(cacheLookupKey);
     }
@@ -117,7 +118,7 @@
             DexWritableCode code = method.getDefinition().getDexWritableCodeOrNull();
             assert code != null || method.getDefinition().shouldNotHaveCode();
             if (code != null) {
-              if (appView.options().canUseCanonicalizedCodeObjects()) {
+              if (code.canBeCanonicalized(appView.options())) {
                 codeCounts.addCode(code, method);
               }
               codesSorted.add(method);
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 7cdd5cc..5826972 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -251,9 +251,7 @@
       Map<DexWritableCacheKey, Integer> offsetCache = new HashMap<>();
       for (ProgramMethod method : codes) {
         DexWritableCode dexWritableCode = method.getDefinition().getCode().asDexWritableCode();
-        if (!options.canUseCanonicalizedCodeObjects()) {
-          writeCodeItem(method, dexWritableCode);
-        } else {
+        if (dexWritableCode.canBeCanonicalized(options)) {
           DexWritableCacheKey cacheLookupKey =
               dexWritableCode.getCacheLookupKey(method, appView.dexItemFactory());
           Integer offsetOrNull = offsetCache.get(cacheLookupKey);
@@ -262,6 +260,8 @@
           } else {
             offsetCache.put(cacheLookupKey, writeCodeItem(method, dexWritableCode));
           }
+        } else {
+          writeCodeItem(method, dexWritableCode);
         }
       }
       assert sizeAndCountOfCodeItems.getCount()
@@ -479,7 +479,7 @@
     Set<DexWritableCacheKey> cache = new HashSet<>();
     for (ProgramMethod method : methods) {
       DexWritableCode code = method.getDefinition().getCode().asDexWritableCode();
-      if (!options.canUseCanonicalizedCodeObjects()
+      if (!code.canBeCanonicalized(options)
           || cache.add(code.getCacheLookupKey(method, appView.dexItemFactory()))) {
         sizeAndCount.count++;
         sizeAndCount.size = alignSize(4, sizeAndCount.size) + sizeOfCodeItem(code);
diff --git a/src/main/java/com/android/tools/r8/dex/code/DexInstruction.java b/src/main/java/com/android/tools/r8/dex/code/DexInstruction.java
index 70079d8..faa31b9 100644
--- a/src/main/java/com/android/tools/r8/dex/code/DexInstruction.java
+++ b/src/main/java/com/android/tools/r8/dex/code/DexInstruction.java
@@ -229,6 +229,14 @@
     return null;
   }
 
+  public boolean isInvokeSuper() {
+    return false;
+  }
+
+  public boolean isInvokeSuperRange() {
+    return false;
+  }
+
   public boolean isInvokeVirtual() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuper.java b/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuper.java
index 46af3323229..1586793 100644
--- a/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuper.java
+++ b/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuper.java
@@ -49,6 +49,11 @@
   }
 
   @Override
+  public boolean isInvokeSuper() {
+    return true;
+  }
+
+  @Override
   public void registerUse(UseRegistry<?> registry) {
     registry.registerInvokeSuper(getMethod());
   }
diff --git a/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuperRange.java b/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuperRange.java
index 015f6a5..2a8e5a0 100644
--- a/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuperRange.java
+++ b/src/main/java/com/android/tools/r8/dex/code/DexInvokeSuperRange.java
@@ -49,6 +49,11 @@
   }
 
   @Override
+  public boolean isInvokeSuperRange() {
+    return true;
+  }
+
+  @Override
   public void registerUse(UseRegistry<?> registry) {
     registry.registerInvokeSuper(getMethod());
   }
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 40c2ff0..38264ec 100644
--- a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
@@ -43,7 +43,7 @@
   private final RetracerForCodePrinting retracer;
 
   public AssemblyWriter(
-      DexApplication application,
+      LazyLoadedDexApplication application,
       InternalOptions options,
       boolean allInfo,
       boolean writeIR,
@@ -52,7 +52,7 @@
   }
 
   public AssemblyWriter(
-      DexApplication application,
+      LazyLoadedDexApplication application,
       InternalOptions options,
       boolean allInfo,
       boolean writeIR,
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 2c41285..a74e532 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -7,7 +7,6 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.DataResourceProvider;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.timing.Timing;
@@ -47,7 +46,7 @@
     this.timing = timing;
   }
 
-  public abstract Builder<?> builder();
+  public abstract Builder<?, ?> builder();
 
   @Override
   public DexItemFactory dexItemFactory() {
@@ -142,7 +141,7 @@
     return proguardMap;
   }
 
-  public abstract static class Builder<T extends Builder<T>> {
+  public abstract static class Builder<S extends DexApplication, T extends Builder<S, T>> {
 
     private final List<DexProgramClass> programClasses = new ArrayList<>();
 
@@ -163,7 +162,7 @@
     abstract T self();
 
     public Builder(DexApplication application) {
-      flags = application.flags;
+      flags = application.getFlags();
       programClasses.addAll(application.programClasses());
       dataResourceProviders.addAll(application.dataResourceProviders);
       proguardMap = application.getProguardMap();
@@ -217,7 +216,6 @@
             }
           });
       this.flags = builder.build();
-
       return self();
     }
 
@@ -244,7 +242,11 @@
       return programClasses;
     }
 
-    public abstract DexApplication build();
+    public final S build() {
+      return build(Timing.empty());
+    }
+
+    public abstract S build(Timing timing);
   }
 
   public static LazyLoadedDexApplication.Builder builder(InternalOptions options, Timing timing) {
@@ -252,11 +254,14 @@
   }
 
   public DirectMappedDexApplication asDirect() {
-    throw new Unreachable("Cannot use a LazyDexApplication where a DirectDexApplication is"
-        + " expected.");
+    return null;
   }
 
-  public abstract DirectMappedDexApplication toDirect();
+  public boolean isDirect() {
+    return false;
+  }
 
-  public abstract boolean isDirect();
+  public LazyLoadedDexApplication asLazy() {
+    return null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index e64405a..1150791 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -40,6 +40,7 @@
 import com.android.tools.r8.lightir.ByteUtils;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.DexDebugUtils.PositionInfo;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.structural.Equatable;
@@ -159,6 +160,14 @@
     int unused = hashCode(); // Cache the hash code eagerly.
   }
 
+  @Override
+  public boolean canBeCanonicalized(InternalOptions options) {
+    // Do not canonicalize code objects with invoke-super instructions due to ART's thread
+    // interpreter cache. See also b/445349082.
+    return options.canUseCanonicalizedCodeObjects()
+        && ArrayUtils.none(instructions, i -> i.isInvokeSuper() || i.isInvokeSuperRange());
+  }
+
   public DexCode withCodeLens(GraphLens codeLens) {
     return new DexCode(this) {
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index ad567ab..ad19a9f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -53,7 +53,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.WeakHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -2943,9 +2942,15 @@
 
   public class ProxyMethods {
 
+    public final DexMethod getProxyClass;
     public final DexMethod newProxyInstance;
 
     private ProxyMethods() {
+      getProxyClass =
+          createMethod(
+              proxyType,
+              createProto(classType, classLoaderType, classArrayType),
+              createString("getProxyClass"));
       newProxyInstance =
           createMethod(
               proxyType,
@@ -3668,37 +3673,6 @@
     allTypes.forEach(f);
   }
 
-  public void gc() {
-    markers = new WeakHashMap<>(markers);
-    methodHandles = new WeakHashMap<>(methodHandles);
-    strings = new WeakHashMap<>(strings);
-    types = new WeakHashMap<>(types);
-    fields = new WeakHashMap<>(fields);
-    protos = new WeakHashMap<>(protos);
-    methods = new WeakHashMap<>(methods);
-    committedMethodHandles = new WeakHashMap<>(committedMethodHandles);
-    committedStrings = new WeakHashMap<>(committedStrings);
-    committedTypes = new WeakHashMap<>(committedTypes);
-    committedFields = new WeakHashMap<>(committedFields);
-    committedProtos = new WeakHashMap<>(committedProtos);
-    committedMethods = new WeakHashMap<>(committedMethods);
-    System.gc();
-    System.gc();
-    markers = new ConcurrentHashMap<>(markers);
-    methodHandles = new ConcurrentHashMap<>(methodHandles);
-    strings = new ConcurrentHashMap<>(strings);
-    types = new ConcurrentHashMap<>(types);
-    fields = new ConcurrentHashMap<>(fields);
-    protos = new ConcurrentHashMap<>(protos);
-    methods = new ConcurrentHashMap<>(methods);
-    committedMethodHandles = new HashMap<>(committedMethodHandles);
-    committedStrings = new HashMap<>(committedStrings);
-    committedTypes = new HashMap<>(committedTypes);
-    committedFields = new HashMap<>(committedFields);
-    committedProtos = new HashMap<>(committedProtos);
-    committedMethods = new HashMap<>(committedMethods);
-  }
-
   public void commitPendingItems() {
     commitPendingItems(committedMethodHandles, methodHandles);
     commitPendingItems(committedStrings, strings);
diff --git a/src/main/java/com/android/tools/r8/graph/DexWritableCode.java b/src/main/java/com/android/tools/r8/graph/DexWritableCode.java
index e5037e5..7f018ce 100644
--- a/src/main/java/com/android/tools/r8/graph/DexWritableCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexWritableCode.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexCode.TryHandler;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.HashingVisitor;
 import java.nio.ShortBuffer;
@@ -24,6 +25,10 @@
     THROW_EXCEPTION
   }
 
+  default boolean canBeCanonicalized(InternalOptions options) {
+    return options.canUseCanonicalizedCodeObjects();
+  }
+
   boolean isThrowExceptionCode();
 
   ThrowExceptionCode asThrowExceptionCode();
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 3f654fa..702bd04 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -131,11 +131,6 @@
   }
 
   @Override
-  public DirectMappedDexApplication toDirect() {
-    return this;
-  }
-
-  @Override
   public boolean isDirect() {
     return true;
   }
@@ -211,7 +206,7 @@
     return true;
   }
 
-  public static class Builder extends DexApplication.Builder<Builder> {
+  public static class Builder extends DexApplication.Builder<DirectMappedDexApplication, Builder> {
 
     private ImmutableCollection<DexClasspathClass> classpathClasses;
     private Map<DexType, DexLibraryClass> libraryClasses;
@@ -219,10 +214,8 @@
     private final List<DexClasspathClass> pendingClasspathClasses = new ArrayList<>();
     private final Set<DexType> pendingClasspathRemovalIfPresent = Sets.newIdentityHashSet();
 
-    Builder(LazyLoadedDexApplication application) {
+    Builder(LazyLoadedDexApplication application, AllClasses allClasses) {
       super(application);
-      // As a side-effect, this will force-load all classes.
-      AllClasses allClasses = application.loadAllClasses();
       classpathClasses = allClasses.getClasspathClasses().values();
       libraryClasses = allClasses.getLibraryClasses();
       replaceProgramClasses(allClasses.getProgramClasses().values());
@@ -325,36 +318,39 @@
     }
 
     @Override
-    public DirectMappedDexApplication 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.
-      commitPendingClasspathClasses();
-      Map<DexType, ProgramOrClasspathClass> programAndClasspathClasses =
-          new IdentityHashMap<>(getProgramClasses().size() + classpathClasses.size());
-      // Note: writing classes in reverse priority order, so a duplicate will be correctly ordered.
-      // There should not be duplicates between program and classpath and that is asserted in the
-      // addAll subroutine.
-      ImmutableCollection<DexClasspathClass> newClasspathClasses = classpathClasses;
-      if (addAll(programAndClasspathClasses, classpathClasses)) {
-        ImmutableList.Builder<DexClasspathClass> builder = ImmutableList.builder();
-        for (DexClasspathClass classpathClass : classpathClasses) {
-          if (!pendingClasspathRemovalIfPresent.contains(classpathClass.getType())) {
-            builder.add(classpathClass);
+    public DirectMappedDexApplication build(Timing timing) {
+      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.
+        commitPendingClasspathClasses();
+        Map<DexType, ProgramOrClasspathClass> programAndClasspathClasses =
+            new IdentityHashMap<>(getProgramClasses().size() + classpathClasses.size());
+        // Note: writing classes in reverse priority order, so a duplicate will be correctly
+        // ordered.
+        // There should not be duplicates between program and classpath and that is asserted in the
+        // addAll subroutine.
+        ImmutableCollection<DexClasspathClass> newClasspathClasses = classpathClasses;
+        if (addAll(programAndClasspathClasses, classpathClasses)) {
+          ImmutableList.Builder<DexClasspathClass> builder = ImmutableList.builder();
+          for (DexClasspathClass classpathClass : classpathClasses) {
+            if (!pendingClasspathRemovalIfPresent.contains(classpathClass.getType())) {
+              builder.add(classpathClass);
+            }
           }
+          newClasspathClasses = builder.build();
         }
-        newClasspathClasses = builder.build();
+        addAll(programAndClasspathClasses, getProgramClasses());
+        return new DirectMappedDexApplication(
+            proguardMap,
+            flags,
+            ImmutableMap.copyOf(programAndClasspathClasses),
+            getLibraryClassesAsImmutableMap(),
+            ImmutableList.copyOf(getProgramClasses()),
+            newClasspathClasses,
+            ImmutableList.copyOf(dataResourceProviders),
+            options,
+            timing);
       }
-      addAll(programAndClasspathClasses, getProgramClasses());
-      return new DirectMappedDexApplication(
-          proguardMap,
-          flags,
-          ImmutableMap.copyOf(programAndClasspathClasses),
-          getLibraryClassesAsImmutableMap(),
-          ImmutableList.copyOf(getProgramClasses()),
-          newClasspathClasses,
-          ImmutableList.copyOf(dataResourceProviders),
-          options,
-          timing);
     }
 
     private <T extends ProgramOrClasspathClass> boolean addAll(
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 a4cc5a7..447178a 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
@@ -149,74 +149,82 @@
     private final ImmutableMap<DexType, DexClasspathClass> classpathClasses;
     private final ImmutableMap<DexType, DexLibraryClass> libraryClasses;
 
-    @SuppressWarnings("ReferenceEquality")
     AllClasses(
         LibraryClassCollection libraryClassesLoader,
         ClasspathClassCollection classpathClassesLoader,
         Map<DexType, DexClasspathClass> synthesizedClasspathClasses,
         ProgramClassCollection programClassesLoader,
-        InternalOptions options) {
-
+        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 == options.dexItemFactory().varHandleType
-                      || type == options.dexItemFactory().lookupType));
+                  && (type.isIdenticalTo(factory.varHandleType)
+                      || type.isIdenticalTo(factory.lookupType)));
 
       // Force-load library classes.
       ImmutableMap<DexType, DexLibraryClass> allLibraryClasses;
-      if (libraryClassesLoader != null) {
-        libraryClassesLoader.forceLoad(forceLoadPredicate);
-        allLibraryClasses = libraryClassesLoader.getAllClassesInMap();
-      } else {
-        allLibraryClasses = ImmutableMap.of();
+      try (Timing t0 = timing.begin("Force-load library classes")) {
+        if (libraryClassesLoader != null) {
+          libraryClassesLoader.forceLoad(forceLoadPredicate);
+          allLibraryClasses = libraryClassesLoader.getAllClassesInMap();
+        } else {
+          allLibraryClasses = ImmutableMap.of();
+        }
       }
 
       // Program classes should be fully loaded.
-      assert programClassesLoader != null;
-      assert programClassesLoader.isFullyLoaded();
-      ImmutableMap<DexType, DexProgramClass> allProgramClasses =
-          programClassesLoader.forceLoad().getAllClassesInMap();
+      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.Builder<DexType, DexClasspathClass> allClasspathClassesBuilder =
-          ImmutableMap.builder();
-      if (classpathClassesLoader != null) {
-        classpathClassesLoader.forceLoad().forEach(allClasspathClassesBuilder::put);
+      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);
+        }
+        if (synthesizedClasspathClasses != null) {
+          allClasspathClassesBuilder.putAll(synthesizedClasspathClasses);
+        }
+        allClasspathClasses = allClasspathClassesBuilder.build();
       }
-      if (synthesizedClasspathClasses != null) {
-        allClasspathClassesBuilder.putAll(synthesizedClasspathClasses);
-      }
-      ImmutableMap<DexType, DexClasspathClass> allClasspathClasses =
-          allClasspathClassesBuilder.build();
 
       // Collect loaded classes in the precedence order library classes, program classes and
       // class path classes or program classes, classpath classes and library classes depending
       // on the configured lookup order.
-      if (options.loadAllClassDefinitions) {
-        libraryClasses = allLibraryClasses;
-        programClasses = allProgramClasses;
-        classpathClasses =
-            fillPrioritizedClasses(allClasspathClasses, programClasses::get, options);
-      } else {
-        programClasses = fillPrioritizedClasses(allProgramClasses, type -> null, options);
-        classpathClasses =
-            fillPrioritizedClasses(allClasspathClasses, programClasses::get, options);
-        libraryClasses =
-            fillPrioritizedClasses(
-                allLibraryClasses,
-                type -> {
-                  DexProgramClass clazz = programClasses.get(type);
-                  if (clazz != null) {
-                    options.recordLibraryAndProgramDuplicate(
-                        type, clazz, allLibraryClasses.get(type));
-                    return clazz;
-                  }
-                  return classpathClasses.get(type);
-                },
-                options);
+      try (Timing t0 = timing.begin("Fill prioritized classes")) {
+        if (options.loadAllClassDefinitions) {
+          libraryClasses = allLibraryClasses;
+          programClasses = allProgramClasses;
+          classpathClasses =
+              fillPrioritizedClasses(allClasspathClasses, programClasses::get, options);
+        } else {
+          programClasses = fillPrioritizedClasses(allProgramClasses, type -> null, options);
+          classpathClasses =
+              fillPrioritizedClasses(allClasspathClasses, programClasses::get, options);
+          libraryClasses =
+              fillPrioritizedClasses(
+                  allLibraryClasses,
+                  type -> {
+                    DexProgramClass clazz = programClasses.get(type);
+                    if (clazz != null) {
+                      options.recordLibraryAndProgramDuplicate(
+                          type, clazz, allLibraryClasses.get(type));
+                      return clazz;
+                    }
+                    return classpathClasses.get(type);
+                  },
+                  options);
+        }
       }
     }
 
@@ -277,15 +285,18 @@
     options.reporter.warning(message);
   }
 
-  /**
-   * Force load all classes and return type -> class map containing all the classes.
-   */
-  public AllClasses loadAllClasses() {
+  /** 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);
+        libraryClasses,
+        classpathClasses,
+        synthesizedClasspathClasses,
+        programClasses,
+        options,
+        timing);
   }
 
-  public static class Builder extends DexApplication.Builder<Builder> {
+  public static class Builder extends DexApplication.Builder<LazyLoadedDexApplication, Builder> {
 
     private ClasspathClassCollection classpathClasses;
     private Map<DexType, DexClasspathClass> synthesizedClasspathClasses;
@@ -345,7 +356,7 @@
     }
 
     @Override
-    public LazyLoadedDexApplication build() {
+    public LazyLoadedDexApplication build(Timing timing) {
       ProgramClassConflictResolver resolver =
           options.programClassConflictResolver == null
               ? ProgramClassCollection.defaultConflictResolver(options.reporter)
@@ -370,13 +381,22 @@
   }
 
   @Override
-  public DirectMappedDexApplication toDirect() {
-    return new DirectMappedDexApplication.Builder(this).build().asDirect();
+  public LazyLoadedDexApplication asLazy() {
+    return this;
   }
 
-  @Override
-  public boolean isDirect() {
-    return false;
+  public DirectMappedDexApplication toDirect() {
+    return toDirect(Timing.empty());
+  }
+
+  public DirectMappedDexApplication toDirect(Timing timing) {
+    try (Timing t0 = timing.begin("To direct app")) {
+      // As a side-effect, this will force-load all classes.
+      AllClasses allClasses = loadAllClasses(timing);
+      DirectMappedDexApplication.Builder builder =
+          new DirectMappedDexApplication.Builder(this, allClasses);
+      return builder.build(timing);
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
index f431563..20cc228 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
@@ -84,7 +83,7 @@
     processCovariantReturnTypeAnnotations(profileCollectionAdditions, executorService);
 
     // Build a new application with jumbo string info,
-    Builder<?> builder = application.builder();
+    DexApplication.Builder<?, ?> builder = application.builder();
 
     if (appView.options().getLibraryDesugaringOptions().isDesugaredLibraryCompilation()) {
       new EmulatedInterfaceApplicationRewriter(appView).rewriteApplication(builder);
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 35d8ff3..bc3be29 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,6 +28,7 @@
 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;
@@ -503,7 +504,7 @@
     ExecutorService executorService = ThreadUtils.getExecutorService(options);
     assert !options.ignoreJavaLibraryOverride;
     options.ignoreJavaLibraryOverride = true;
-    DexApplication appForMax = applicationReader.read(executorService);
+    LazyLoadedDexApplication appForMax = applicationReader.read(executorService);
     options.ignoreJavaLibraryOverride = false;
     DexClass varHandle =
         appForMax.definitionFor(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceApplicationRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceApplicationRewriter.java
index 27265e4..2db425e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceApplicationRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceApplicationRewriter.java
@@ -38,7 +38,7 @@
         .forEach((ei, descriptor) -> emulatedInterfaces.put(ei, descriptor.getRewrittenType()));
   }
 
-  public void rewriteApplication(DexApplication.Builder<?> builder) {
+  public void rewriteApplication(DexApplication.Builder<?, ?> builder) {
     assert appView.options().getLibraryDesugaringOptions().isDesugaredLibraryCompilation();
     ArrayList<DexProgramClass> newProgramClasses = new ArrayList<>();
     for (DexProgramClass clazz : builder.getProgramClasses()) {
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 5906b9f..61362ac 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
@@ -184,7 +184,7 @@
           desugaredOutputClasses.add(clazz);
         }
       }
-      DirectMappedDexApplication app = appView.app().toDirect();
+      DirectMappedDexApplication app = appView.app().asLazy().toDirect();
       outputClasspathClasses = app.classpathClasses();
       outputLibraryClasses = app.libraryClasses();
       startupProfile = appView.getStartupProfile();
diff --git a/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java b/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
index 0b201a2..7f2238d 100644
--- a/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
+++ b/src/main/java/com/android/tools/r8/shaking/IfRuleEvaluator.java
@@ -228,7 +228,9 @@
     boolean classNameResult = memberRule.getClassNames().matches(source.type);
     assert classNameResult;
     if (memberRule.hasInheritanceClassName()) {
-      boolean inheritanceResult = rootSetBuilder.satisfyInheritanceRule(target, memberRule);
+      boolean inheritanceResult =
+          memberRule.satisfyInheritanceRule(
+              target, appView, rootSetBuilder::handleMatchedAnnotation);
       assert inheritanceResult;
     }
   }
@@ -278,7 +280,7 @@
     }
     if (rule.hasInheritanceClassName()) {
       // Try another live type since the current one doesn't satisfy the inheritance rule.
-      return rootSetBuilder.satisfyInheritanceRule(clazz, rule);
+      return rule.satisfyInheritanceRule(clazz, appView, rootSetBuilder::handleMatchedAnnotation);
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardAssumeValuesRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardAssumeValuesRule.java
index 8685347..a3b9e8c 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardAssumeValuesRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardAssumeValuesRule.java
@@ -79,6 +79,16 @@
   }
 
   @Override
+  public boolean isApplicableToClasspathClasses() {
+    return true;
+  }
+
+  @Override
+  public boolean isApplicableToLibraryClasses() {
+    return true;
+  }
+
+  @Override
   String typeString() {
     return "assumevalues";
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardClassNameList.java b/src/main/java/com/android/tools/r8/shaking/ProguardClassNameList.java
index 9555058..fd68364 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardClassNameList.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardClassNameList.java
@@ -38,6 +38,8 @@
     return new SingleClassNameList(matcher);
   }
 
+  public abstract boolean isMatchAnyClassPattern();
+
   public abstract int size();
 
   public static class Builder {
@@ -154,6 +156,11 @@
     }
 
     @Override
+    public boolean isMatchAnyClassPattern() {
+      return false;
+    }
+
+    @Override
     public int size() {
       return 0;
     }
@@ -207,6 +214,11 @@
     }
 
     @Override
+    public boolean isMatchAnyClassPattern() {
+      return className.isMatchAnyClassPattern();
+    }
+
+    @Override
     public int size() {
       return 1;
     }
@@ -300,6 +312,11 @@
     }
 
     @Override
+    public boolean isMatchAnyClassPattern() {
+      return false;
+    }
+
+    @Override
     public int size() {
       return classNames.size();
     }
@@ -412,6 +429,11 @@
     }
 
     @Override
+    public boolean isMatchAnyClassPattern() {
+      return false;
+    }
+
+    @Override
     public int size() {
       return classNames.size();
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java b/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
index 98f41cd..00e4830 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.position.TextRange;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -264,6 +265,19 @@
     return memberRules;
   }
 
+  public void getFieldAndMethodRules(
+      Collection<ProguardMemberRule> fieldRules, Collection<ProguardMemberRule> methodRules) {
+    for (ProguardMemberRule memberRule : memberRules) {
+      ProguardMemberType ruleType = memberRule.getRuleType();
+      if (ruleType.includesFields()) {
+        fieldRules.add(memberRule);
+      }
+      if (ruleType.includesMethods()) {
+        methodRules.add(memberRule);
+      }
+    }
+  }
+
   public boolean getInheritanceIsExtends() {
     return inheritanceIsExtends;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
index 828ca04..84518dd 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
@@ -4,6 +4,10 @@
 package com.android.tools.r8.shaking;
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.shaking.RootSetUtils.RootSetBuilder.satisfyAccessFlag;
+import static com.android.tools.r8.shaking.RootSetUtils.RootSetBuilder.satisfyAnnotation;
+import static com.android.tools.r8.shaking.RootSetUtils.RootSetBuilder.satisfyClassType;
+import static com.android.tools.r8.shaking.RootSetUtils.RootSetBuilder.satisfyNonSyntheticClass;
 import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -15,8 +19,9 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.shaking.ProguardWildcard.BackReference;
-import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.shaking.RootSetUtils.RootSetBuilder;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Iterables;
 import java.util.List;
@@ -30,6 +35,7 @@
   // TODO(b/164019179): Since we are using the rule language for tracing main dex we can end up in
   //  a situation where the references to types are dead.
   private boolean canReferenceDeadTypes = false;
+  private OptionalBool trivialAllClassMatch = OptionalBool.unknown();
 
   ProguardConfigurationRule(
       Origin origin,
@@ -61,21 +67,27 @@
         memberRules);
   }
 
-  public boolean isTrivalAllClassMatch() {
-    BooleanBox booleanBox = new BooleanBox(true);
-    getClassNames()
-        .forEachTypeMatcher(
-            unused -> booleanBox.set(false),
-            proguardTypeMatcher -> !proguardTypeMatcher.isMatchAnyClassPattern());
-    return booleanBox.get()
+  public boolean isTrivialAllClassMatch() {
+    if (trivialAllClassMatch.isUnknown()) {
+      trivialAllClassMatch = OptionalBool.of(computeIsTrivialAllClassMatch());
+    }
+    assert trivialAllClassMatch.isTrue() == computeIsTrivialAllClassMatch();
+    return trivialAllClassMatch.isTrue();
+  }
+
+  public boolean isTrivialAllClassMatchWithNoMembersRules() {
+    return isTrivialAllClassMatch() && !hasMemberRules();
+  }
+
+  public boolean computeIsTrivialAllClassMatch() {
+    return getClassNames().isMatchAnyClassPattern()
         && getClassAnnotations().isEmpty()
         && getClassAccessFlags().isDefaultFlags()
         && getNegatedClassAccessFlags().isDefaultFlags()
         && !getClassTypeNegated()
         && getClassType() == ProguardClassType.CLASS
         && getInheritanceAnnotations().isEmpty()
-        && getInheritanceClassName() == null
-        && getMemberRules().isEmpty();
+        && !hasInheritanceClassName();
   }
 
   public boolean isUsed() {
@@ -201,6 +213,10 @@
     return false;
   }
 
+  public final boolean isOnlyApplicableToProgramClasses() {
+    return !isApplicableToClasspathClasses() && !isApplicableToLibraryClasses();
+  }
+
   protected boolean hasBackReferences() {
     return !Iterables.isEmpty(getBackReferences());
   }
@@ -263,4 +279,141 @@
     super.append(builder);
     return builder;
   }
+
+  public boolean testClassCondition(
+      DexClass clazz,
+      AppView<?> appView,
+      Consumer<AnnotationMatchResult> annotationMatchResultConsumer) {
+    if (!satisfyNonSyntheticClass(clazz, appView)) {
+      return false;
+    }
+    if (!satisfyClassType(this, clazz)) {
+      return false;
+    }
+    if (!satisfyAccessFlag(this, clazz)) {
+      return false;
+    }
+    AnnotationMatchResult annotationMatchResult = satisfyAnnotation(this, clazz);
+    if (annotationMatchResult == null) {
+      return false;
+    }
+    annotationMatchResultConsumer.accept(annotationMatchResult);
+    // In principle it should make a difference whether the user specified in a class
+    // spec that a class either extends or implements another type. However, proguard
+    // seems not to care, so users have started to use this inconsistently. We are thus
+    // inconsistent, as well, but tell them.
+    // TODO(herhut): One day make this do what it says.
+    if (hasInheritanceClassName()
+        && !satisfyInheritanceRule(clazz, appView, annotationMatchResultConsumer)) {
+      return false;
+    }
+    return getClassNames().matches(clazz.type);
+  }
+
+  public boolean satisfyInheritanceRule(
+      DexClass clazz,
+      AppView<?> appView,
+      Consumer<AnnotationMatchResult> annotationMatchResultConsumer) {
+    return satisfyExtendsRule(clazz, appView, annotationMatchResultConsumer)
+        || satisfyImplementsRule(clazz, appView, annotationMatchResultConsumer);
+  }
+
+  private boolean satisfyExtendsRule(
+      DexClass clazz,
+      AppView<?> appView,
+      Consumer<AnnotationMatchResult> annotationMatchResultConsumer) {
+    if (anySuperTypeMatchesExtendsRule(clazz.superType, appView, annotationMatchResultConsumer)) {
+      return true;
+    }
+    // It is possible that this class used to inherit from another class X, but no longer does it,
+    // because X has been merged into `clazz`.
+    return anySourceMatchesInheritanceRuleDirectly(clazz, false, appView);
+  }
+
+  private boolean anySuperTypeMatchesExtendsRule(
+      DexType type,
+      AppView<?> appView,
+      Consumer<AnnotationMatchResult> annotationMatchResultConsumer) {
+    while (type != null) {
+      DexClass clazz = appView.definitionFor(type);
+      if (clazz == null) {
+        // TODO(herhut): Warn about broken supertype chain?
+        return false;
+      }
+      // TODO(b/110141157): Should the vertical class merger move annotations from the source to
+      //  the target class? If so, it is sufficient only to apply the annotation-matcher to the
+      //  annotations of `class`.
+      if (getInheritanceClassName().matches(clazz.type, appView)) {
+        AnnotationMatchResult annotationMatchResult =
+            RootSetBuilder.containsAllAnnotations(getInheritanceAnnotations(), clazz);
+        if (annotationMatchResult != null) {
+          annotationMatchResultConsumer.accept(annotationMatchResult);
+          return true;
+        }
+      }
+      type = clazz.superType;
+    }
+    return false;
+  }
+
+  private boolean satisfyImplementsRule(
+      DexClass clazz,
+      AppView<?> appView,
+      Consumer<AnnotationMatchResult> annotationMatchResultConsumer) {
+    if (anyImplementedInterfaceMatchesImplementsRule(
+        clazz, appView, annotationMatchResultConsumer)) {
+      return true;
+    }
+    // It is possible that this class used to implement an interface I, but no longer does it,
+    // because I has been merged into `clazz`.
+    return anySourceMatchesInheritanceRuleDirectly(clazz, true, appView);
+  }
+
+  private boolean anyImplementedInterfaceMatchesImplementsRule(
+      DexClass clazz,
+      AppView<?> appView,
+      Consumer<AnnotationMatchResult> annotationMatchResultConsumer) {
+    // TODO(herhut): Maybe it would be better to do this breadth first.
+    for (DexType iface : clazz.getInterfaces()) {
+      DexClass ifaceClass = appView.definitionFor(iface);
+      if (ifaceClass == null) {
+        // TODO(herhut): Warn about broken supertype chain?
+        continue;
+      }
+      // TODO(b/110141157): Should the vertical class merger move annotations from the source to
+      // the target class? If so, it is sufficient only to apply the annotation-matcher to the
+      // annotations of `ifaceClass`.
+      if (getInheritanceClassName().matches(iface, appView)) {
+        AnnotationMatchResult annotationMatchResult =
+            RootSetBuilder.containsAllAnnotations(getInheritanceAnnotations(), ifaceClass);
+        if (annotationMatchResult != null) {
+          annotationMatchResultConsumer.accept(annotationMatchResult);
+          return true;
+        }
+      }
+      if (anyImplementedInterfaceMatchesImplementsRule(
+          ifaceClass, appView, annotationMatchResultConsumer)) {
+        return true;
+      }
+    }
+    if (!clazz.hasSuperType()) {
+      return false;
+    }
+    DexClass superClass = appView.definitionFor(clazz.getSuperType());
+    return superClass != null
+        && anyImplementedInterfaceMatchesImplementsRule(
+            superClass, appView, annotationMatchResultConsumer);
+  }
+
+  private boolean anySourceMatchesInheritanceRuleDirectly(
+      DexClass clazz, boolean isInterface, AppView<?> appView) {
+    // TODO(b/110141157): Figure out what to do with annotations. Should the annotations of
+    // the DexClass corresponding to `sourceType` satisfy the `annotation`-matcher?
+    return appView.getVerticallyMergedClasses() != null
+        && appView.getVerticallyMergedClasses().getSourcesFor(clazz.type).stream()
+            .filter(
+                sourceType ->
+                    appView.definitionFor(sourceType).accessFlags.isInterface() == isInterface)
+            .anyMatch(getInheritanceClassName()::matches);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardIfRulePreconditionMatch.java b/src/main/java/com/android/tools/r8/shaking/ProguardIfRulePreconditionMatch.java
index 81919d3..000cfc3 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardIfRulePreconditionMatch.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardIfRulePreconditionMatch.java
@@ -33,7 +33,7 @@
   public void disallowOptimizationsForReevaluation(
       Enqueuer enqueuer, ConsequentRootSetBuilder rootSetBuilder) {
     if (enqueuer.getMode().isInitialTreeShaking()
-        && !ifRule.isTrivalAllClassMatch()
+        && !ifRule.isTrivialAllClassMatchWithNoMembersRules()
         && classMatch.isProgramClass()) {
       disallowClassOptimizationsForReevaluation(rootSetBuilder);
       disallowMethodOptimizationsForReevaluation(rootSetBuilder);
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 13653e1..2541fcb 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -339,39 +339,29 @@
       return this;
     }
 
+    private void process(
+        Collection<DexClass> classes,
+        ProguardConfigurationRule rule,
+        ProguardIfRulePreconditionMatch ifRulePreconditionMatch,
+        Timing timing) {
+      for (DexClass clazz : classes) {
+        process(clazz, rule, ifRulePreconditionMatch, timing);
+      }
+    }
+
     // Process a class with the keep rule.
     private void process(
         DexClass clazz,
         ProguardConfigurationRule rule,
-        ProguardIfRulePreconditionMatch ifRulePreconditionMatch) {
-      if (!satisfyNonSyntheticClass(clazz, appView)) {
+        ProguardIfRulePreconditionMatch ifRulePreconditionMatch,
+        Timing timing) {
+      if (!rule.testClassCondition(clazz, appView, this::handleMatchedAnnotation)) {
         return;
       }
-      if (!satisfyClassType(rule, clazz)) {
-        return;
-      }
-      if (!satisfyAccessFlag(rule, clazz)) {
-        return;
-      }
-      AnnotationMatchResult annotationMatchResult = satisfyAnnotation(rule, clazz);
-      if (annotationMatchResult == null) {
-        return;
-      }
-      handleMatchedAnnotation(annotationMatchResult);
-      // In principle it should make a difference whether the user specified in a class
-      // spec that a class either extends or implements another type. However, proguard
-      // seems not to care, so users have started to use this inconsistently. We are thus
-      // inconsistent, as well, but tell them.
-      // TODO(herhut): One day make this do what it says.
-      if (rule.hasInheritanceClassName() && !satisfyInheritanceRule(clazz, rule)) {
-        return;
-      }
-
-      if (!rule.getClassNames().matches(clazz.type)) {
-        return;
-      }
-
       Collection<ProguardMemberRule> memberKeepRules = rule.getMemberRules();
+      Collection<ProguardMemberRule> fieldKeepRules = new ArrayList<>();
+      Collection<ProguardMemberRule> methodKeepRules = new ArrayList<>();
+      rule.getFieldAndMethodRules(fieldKeepRules, methodKeepRules);
       Map<Predicate<DexDefinition>, DexClass> preconditionSupplier;
       if (rule instanceof ProguardKeepRule) {
         ProguardKeepRule keepRule = rule.asProguardKeepRule();
@@ -389,7 +379,7 @@
             preconditionSupplier = ImmutableMap.of(definition -> true, clazz);
             markMatchingVisibleMethods(
                 clazz,
-                memberKeepRules,
+                methodKeepRules,
                 rule,
                 preconditionSupplier,
                 keepRule.getIncludeDescriptorClasses(),
@@ -397,7 +387,7 @@
                 ifRulePreconditionMatch);
             markMatchingVisibleFields(
                 clazz,
-                memberKeepRules,
+                fieldKeepRules,
                 rule,
                 preconditionSupplier,
                 keepRule.getIncludeDescriptorClasses(),
@@ -424,7 +414,7 @@
             }
             markMatchingVisibleMethods(
                 clazz,
-                memberKeepRules,
+                methodKeepRules,
                 rule,
                 preconditionSupplier,
                 keepRule.getIncludeDescriptorClasses(),
@@ -432,7 +422,7 @@
                 ifRulePreconditionMatch);
             markMatchingVisibleFields(
                 clazz,
-                memberKeepRules,
+                fieldKeepRules,
                 rule,
                 preconditionSupplier,
                 keepRule.getIncludeDescriptorClasses(),
@@ -458,29 +448,29 @@
           || rule instanceof ProguardWhyAreYouKeepingRule) {
         markClass(clazz, rule, ifRulePreconditionMatch);
         markMatchingVisibleMethods(
-            clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+            clazz, methodKeepRules, rule, null, true, true, ifRulePreconditionMatch);
         markMatchingVisibleFields(
-            clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+            clazz, fieldKeepRules, rule, null, true, true, ifRulePreconditionMatch);
       } else if (rule instanceof ProguardAssumeMayHaveSideEffectsRule) {
         markMatchingVisibleMethods(
-            clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+            clazz, methodKeepRules, rule, null, true, true, ifRulePreconditionMatch);
         markMatchingOverriddenMethods(
-            clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+            clazz, methodKeepRules, rule, null, true, true, ifRulePreconditionMatch);
         markMatchingVisibleFields(
-            clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+            clazz, fieldKeepRules, rule, null, true, true, ifRulePreconditionMatch);
       } else if (rule instanceof ProguardAssumeNoSideEffectRule
           || rule instanceof ProguardAssumeValuesRule) {
         if (assumeInfoCollectionBuilder != null) {
           markMatchingVisibleMethods(
-              clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+              clazz, methodKeepRules, rule, null, true, true, ifRulePreconditionMatch, timing);
           markMatchingOverriddenMethods(
-              clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+              clazz, methodKeepRules, rule, null, true, true, ifRulePreconditionMatch, timing);
           markMatchingVisibleFields(
-              clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+              clazz, fieldKeepRules, rule, null, true, true, ifRulePreconditionMatch, timing);
         }
       } else if (rule instanceof NoFieldTypeStrengtheningRule
           || rule instanceof NoRedundantFieldLoadEliminationRule) {
-        markMatchingFields(clazz, memberKeepRules, rule, null, ifRulePreconditionMatch);
+        markMatchingFields(clazz, fieldKeepRules, rule, ifRulePreconditionMatch);
       } else if (rule instanceof InlineRule
           || rule instanceof KeepConstantArgumentRule
           || rule instanceof KeepUnusedReturnValueRule
@@ -492,7 +482,7 @@
           || rule instanceof ReprocessMethodRule
           || rule instanceof WhyAreYouNotInliningRule
           || rule.isMaximumRemovedAndroidLogLevelRule()) {
-        markMatchingMethods(clazz, memberKeepRules, rule, null, ifRulePreconditionMatch);
+        markMatchingMethods(clazz, methodKeepRules, rule, ifRulePreconditionMatch);
       } else if (rule instanceof ClassInlineRule
           || rule instanceof NoUnusedInterfaceRemovalRule
           || rule instanceof NoVerticalClassMergingRule
@@ -503,15 +493,15 @@
         }
       } else if (rule instanceof NoValuePropagationRule) {
         markMatchingVisibleMethods(
-            clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+            clazz, methodKeepRules, rule, null, true, true, ifRulePreconditionMatch);
         markMatchingVisibleFields(
-            clazz, memberKeepRules, rule, null, true, true, ifRulePreconditionMatch);
+            clazz, fieldKeepRules, rule, null, true, true, ifRulePreconditionMatch);
       } else if (rule instanceof ProguardIdentifierNameStringRule) {
-        markMatchingFields(clazz, memberKeepRules, rule, null, ifRulePreconditionMatch);
-        markMatchingMethods(clazz, memberKeepRules, rule, null, ifRulePreconditionMatch);
+        markMatchingFields(clazz, fieldKeepRules, rule, ifRulePreconditionMatch);
+        markMatchingMethods(clazz, methodKeepRules, rule, ifRulePreconditionMatch);
       } else {
         assert rule instanceof ConvertCheckNotNullRule;
-        markMatchingMethods(clazz, memberKeepRules, rule, null, ifRulePreconditionMatch);
+        markMatchingMethods(clazz, methodKeepRules, rule, ifRulePreconditionMatch);
       }
     }
 
@@ -524,37 +514,101 @@
       if (rule.getClassNames().hasSpecificTypes()) {
         // This keep rule only lists specific type matches.
         // This means there is no need to iterate over all classes.
-        timing.begin("Process rule");
+        timing.begin("Process rule with specific types");
         for (DexType type : rule.getClassNames().getSpecificTypes()) {
           DexClass clazz = application.definitionFor(type);
           // Ignore keep rule iff it does not reference a class in the app.
           if (clazz != null) {
-            process(clazz, rule, ifRulePreconditionMatch);
+            process(clazz, rule, ifRulePreconditionMatch, timing);
           }
         }
         timing.end();
-        return;
+      } else if (rule.isOnlyApplicableToProgramClasses() && !rule.hasMemberRules()) {
+        timing.begin("Fork rule evaluation ST");
+        evaluateRuleConcurrentlySingleTask(tasks, rule, ifRulePreconditionMatch, timing);
+        timing.end();
+      } else {
+        timing.begin("Fork rule evaluation");
+        evaluateRuleConcurrently(tasks, rule, ifRulePreconditionMatch, timing);
+        timing.end();
+      }
+    }
+
+    private void evaluateRuleConcurrently(
+        TaskCollection<?> tasks,
+        ProguardConfigurationRule rule,
+        ProguardIfRulePreconditionMatch ifRulePreconditionMatch,
+        Timing timing)
+        throws ExecutionException {
+      int batchSize =
+          (int)
+              Math.ceil(
+                  (double) application.classes().size() / tasks.getNumberOfThreadsOrDefault(1));
+      List<DexClass> classes = new ArrayList<>(batchSize);
+      rule.forEachRelevantCandidate(
+          appView,
+          subtypingInfo,
+          application.classes(),
+          alwaysTrue(),
+          clazz -> {
+            classes.add(clazz);
+            if (classes.size() == batchSize) {
+              List<DexClass> job = new ArrayList<>(classes);
+              tasks.submitUnchecked(
+                  () -> {
+                    Timing threadTiming =
+                        timing.createThreadTiming("Process rule: " + rule, options);
+                    process(job, rule, ifRulePreconditionMatch, threadTiming);
+                    threadTiming.end().notifyThreadTimingFinished();
+                  });
+              classes.clear();
+            }
+          });
+
+      // Process remaining classes.
+      if (!classes.isEmpty()) {
+        tasks.submitUnchecked(
+            () -> {
+              Timing threadTiming = timing.createThreadTiming("Process rule", options);
+              process(classes, rule, ifRulePreconditionMatch, threadTiming);
+              threadTiming.end().notifyThreadTimingFinished();
+            });
       }
 
-      tasks.submit(
+      if (!rule.isOnlyApplicableToProgramClasses()) {
+        tasks.submit(
+            () -> {
+              Timing threadTiming = timing.createThreadTiming("Evaluate non-program", options);
+              if (rule.isApplicableToClasspathClasses()) {
+                for (DexClasspathClass clazz : application.classpathClasses()) {
+                  process(clazz, rule, ifRulePreconditionMatch, threadTiming);
+                }
+              }
+              if (rule.isApplicableToLibraryClasses()) {
+                for (DexLibraryClass clazz : application.libraryClasses()) {
+                  process(clazz, rule, ifRulePreconditionMatch, threadTiming);
+                }
+              }
+              threadTiming.end().notifyThreadTimingFinished();
+            });
+      }
+    }
+
+    private void evaluateRuleConcurrentlySingleTask(
+        TaskCollection<?> tasks,
+        ProguardConfigurationRule rule,
+        ProguardIfRulePreconditionMatch ifRulePreconditionMatch,
+        Timing timing) {
+      assert rule.isOnlyApplicableToProgramClasses();
+      tasks.submitUnchecked(
           () -> {
-            Timing threadTiming = timing.createThreadTiming("Process rule", options);
+            Timing threadTiming = timing.createThreadTiming("Process rule ST", options);
             rule.forEachRelevantCandidate(
                 appView,
                 subtypingInfo,
                 application.classes(),
                 alwaysTrue(),
-                clazz -> process(clazz, rule, ifRulePreconditionMatch));
-            if (rule.isApplicableToClasspathClasses()) {
-              for (DexClasspathClass clazz : application.classpathClasses()) {
-                process(clazz, rule, ifRulePreconditionMatch);
-              }
-            }
-            if (rule.isApplicableToLibraryClasses()) {
-              for (DexLibraryClass clazz : application.libraryClasses()) {
-                process(clazz, rule, ifRulePreconditionMatch);
-              }
-            }
+                clazz -> process(clazz, rule, ifRulePreconditionMatch, threadTiming));
             threadTiming.end().notifyThreadTimingFinished();
           });
     }
@@ -716,13 +770,37 @@
         Map<Predicate<DexDefinition>, DexClass> preconditionSupplier,
         boolean includeClasspathClasses,
         boolean includeLibraryClasses,
+        ProguardIfRulePreconditionMatch ifRulePreconditionMatch,
+        Timing timing) {
+      timing.begin("Mark visible methods");
+      markMatchingVisibleMethods(
+          clazz,
+          memberKeepRules,
+          rule,
+          preconditionSupplier,
+          includeClasspathClasses,
+          includeLibraryClasses,
+          ifRulePreconditionMatch);
+      timing.end();
+    }
+
+    private void markMatchingVisibleMethods(
+        DexClass clazz,
+        Collection<ProguardMemberRule> memberKeepRules,
+        ProguardConfigurationRule rule,
+        Map<Predicate<DexDefinition>, DexClass> preconditionSupplier,
+        boolean includeClasspathClasses,
+        boolean includeLibraryClasses,
         ProguardIfRulePreconditionMatch ifRulePreconditionMatch) {
+      if (memberKeepRules.isEmpty()) {
+        return;
+      }
+      // TODO(b/422947619): Evaluate the impact of matching the rule against the superclass.
+      boolean includeSuperclasses = !rule.isTrivialAllClassMatch();
       Set<Wrapper<DexMethod>> methodsMarked =
           options.forceProguardCompatibility ? null : new HashSet<>();
-      Deque<DexClass> worklist = new ArrayDeque<>();
-      worklist.add(clazz);
-      while (!worklist.isEmpty()) {
-        DexClass currentClass = worklist.pop();
+      DexClass currentClass = clazz;
+      do {
         if (!includeClasspathClasses && currentClass.isClasspathClass()) {
           break;
         }
@@ -733,7 +811,7 @@
         currentClass.forEachClassMethodMatching(
             method ->
                 method.belongsToVirtualPool()
-                    || currentClass == clazz
+                    || method.getHolderType().isIdenticalTo(clazz.getType())
                     || (method.isStatic() && !method.isPrivate() && !method.isInitializer())
                     || options.forceProguardCompatibility,
             method -> {
@@ -747,13 +825,12 @@
                   precondition,
                   ifRulePreconditionMatch);
             });
-        if (currentClass.superType != null) {
-          DexClass dexClass = application.definitionFor(currentClass.superType);
-          if (dexClass != null) {
-            worklist.add(dexClass);
-          }
+        if (currentClass.hasSuperType() && includeSuperclasses) {
+          currentClass = application.definitionFor(currentClass.getSuperType());
+        } else {
+          break;
         }
-      }
+      } while (currentClass != null);
       // TODO(b/143643942): Generalize the below approach to also work for subtyping hierarchies in
       //  fullmode.
       if (clazz.isProgramClass()
@@ -906,13 +983,35 @@
         Map<Predicate<DexDefinition>, DexClass> preconditionSupplier,
         boolean includeClasspathClasses,
         boolean includeLibraryClasses,
+        ProguardIfRulePreconditionMatch ifRulePreconditionMatch,
+        Timing timing) {
+      timing.begin("Mark overridden methods");
+      markMatchingOverriddenMethods(
+          clazz,
+          memberKeepRules,
+          rule,
+          preconditionSupplier,
+          includeClasspathClasses,
+          includeLibraryClasses,
+          ifRulePreconditionMatch);
+      timing.end();
+    }
+
+    private void markMatchingOverriddenMethods(
+        DexClass clazz,
+        Collection<ProguardMemberRule> memberKeepRules,
+        ProguardConfigurationRule rule,
+        Map<Predicate<DexDefinition>, DexClass> preconditionSupplier,
+        boolean includeClasspathClasses,
+        boolean includeLibraryClasses,
         ProguardIfRulePreconditionMatch ifRulePreconditionMatch) {
+      if (memberKeepRules.isEmpty()) {
+        return;
+      }
       Set<DexClass> visited = Sets.newIdentityHashSet();
-      Deque<DexClass> worklist = new ArrayDeque<>();
       // Intentionally skip the current `clazz`, assuming it's covered by
       // markMatchingVisibleMethods.
-      worklist.addAll(subtypingInfo.getSubclasses(clazz));
-
+      Deque<DexClass> worklist = new ArrayDeque<>(subtypingInfo.getSubclasses(clazz));
       while (!worklist.isEmpty()) {
         DexClass currentClass = worklist.poll();
         if (!visited.add(currentClass)) {
@@ -940,14 +1039,30 @@
         DexClass clazz,
         Collection<ProguardMemberRule> memberKeepRules,
         ProguardConfigurationRule rule,
-        Map<Predicate<DexDefinition>, DexClass> preconditionSupplier,
         ProguardIfRulePreconditionMatch ifRulePreconditionMatch) {
       clazz.forEachClassMethod(
-          method -> {
-            DexClass precondition =
-                testAndGetPrecondition(method.getDefinition(), preconditionSupplier);
-            markMethod(method, memberKeepRules, null, rule, precondition, ifRulePreconditionMatch);
-          });
+          method -> markMethod(method, memberKeepRules, null, rule, null, ifRulePreconditionMatch));
+    }
+
+    private void markMatchingVisibleFields(
+        DexClass clazz,
+        Collection<ProguardMemberRule> memberKeepRules,
+        ProguardConfigurationRule rule,
+        Map<Predicate<DexDefinition>, DexClass> preconditionSupplier,
+        boolean includeClasspathClasses,
+        boolean includeLibraryClasses,
+        ProguardIfRulePreconditionMatch ifRulePreconditionMatch,
+        Timing timing) {
+      timing.begin("Mark visible fields");
+      markMatchingVisibleFields(
+          clazz,
+          memberKeepRules,
+          rule,
+          preconditionSupplier,
+          includeClasspathClasses,
+          includeLibraryClasses,
+          ifRulePreconditionMatch);
+      timing.end();
     }
 
     private void markMatchingVisibleFields(
@@ -958,6 +1073,11 @@
         boolean includeClasspathClasses,
         boolean includeLibraryClasses,
         ProguardIfRulePreconditionMatch ifRulePreconditionMatch) {
+      if (memberKeepRules.isEmpty()) {
+        return;
+      }
+      // TODO(b/422947619): Evaluate the impact of matching the rule against the superclass.
+      boolean includeSuperclasses = !rule.isTrivialAllClassMatch();
       while (clazz != null) {
         if (!includeClasspathClasses && clazz.isClasspathClass()) {
           return;
@@ -971,7 +1091,11 @@
                   testAndGetPrecondition(field.getDefinition(), preconditionSupplier);
               markField(field, memberKeepRules, rule, precondition, ifRulePreconditionMatch);
             });
-        clazz = clazz.superType == null ? null : application.definitionFor(clazz.superType);
+        if (clazz.hasSuperType() && includeSuperclasses) {
+          clazz = application.definitionFor(clazz.superType);
+        } else {
+          break;
+        }
       }
     }
 
@@ -979,14 +1103,9 @@
         DexClass clazz,
         Collection<ProguardMemberRule> memberKeepRules,
         ProguardConfigurationRule rule,
-        Map<Predicate<DexDefinition>, DexClass> preconditionSupplier,
         ProguardIfRulePreconditionMatch ifRulePreconditionMatch) {
       clazz.forEachClassField(
-          field -> {
-            DexClass precondition =
-                testAndGetPrecondition(field.getDefinition(), preconditionSupplier);
-            markField(field, memberKeepRules, rule, precondition, ifRulePreconditionMatch);
-          });
+          field -> markField(field, memberKeepRules, rule, null, ifRulePreconditionMatch));
     }
 
     // TODO(b/67934426): Test this code.
@@ -1075,105 +1194,6 @@
       return containsAllAnnotations(rule.getClassAnnotations(), clazz);
     }
 
-    boolean satisfyInheritanceRule(DexClass clazz, ProguardConfigurationRule rule) {
-      if (satisfyExtendsRule(clazz, rule)) {
-        return true;
-      }
-
-      return satisfyImplementsRule(clazz, rule);
-    }
-
-    boolean satisfyExtendsRule(DexClass clazz, ProguardConfigurationRule rule) {
-      if (anySuperTypeMatchesExtendsRule(clazz.superType, rule)) {
-        return true;
-      }
-      // It is possible that this class used to inherit from another class X, but no longer does it,
-      // because X has been merged into `clazz`.
-      return anySourceMatchesInheritanceRuleDirectly(clazz, rule, false);
-    }
-
-    boolean anySuperTypeMatchesExtendsRule(DexType type, ProguardConfigurationRule rule) {
-      while (type != null) {
-        DexClass clazz = application.definitionFor(type);
-        if (clazz == null) {
-          // TODO(herhut): Warn about broken supertype chain?
-          return false;
-        }
-        // TODO(b/110141157): Should the vertical class merger move annotations from the source to
-        // the target class? If so, it is sufficient only to apply the annotation-matcher to the
-        // annotations of `class`.
-        if (rule.getInheritanceClassName().matches(clazz.type, appView)) {
-          AnnotationMatchResult annotationMatchResult =
-              containsAllAnnotations(rule.getInheritanceAnnotations(), clazz);
-          if (annotationMatchResult != null) {
-            handleMatchedAnnotation(annotationMatchResult);
-            return true;
-          }
-        }
-        type = clazz.superType;
-      }
-      return false;
-    }
-
-    boolean satisfyImplementsRule(DexClass clazz, ProguardConfigurationRule rule) {
-      if (anyImplementedInterfaceMatchesImplementsRule(clazz, rule)) {
-        return true;
-      }
-      // It is possible that this class used to implement an interface I, but no longer does it,
-      // because I has been merged into `clazz`.
-      return anySourceMatchesInheritanceRuleDirectly(clazz, rule, true);
-    }
-
-    private boolean anyImplementedInterfaceMatchesImplementsRule(
-        DexClass clazz, ProguardConfigurationRule rule) {
-      // TODO(herhut): Maybe it would be better to do this breadth first.
-      if (clazz == null) {
-        return false;
-      }
-      for (DexType iface : clazz.interfaces.values) {
-        DexClass ifaceClass = application.definitionFor(iface);
-        if (ifaceClass == null) {
-          // TODO(herhut): Warn about broken supertype chain?
-          return false;
-        }
-        // TODO(b/110141157): Should the vertical class merger move annotations from the source to
-        // the target class? If so, it is sufficient only to apply the annotation-matcher to the
-        // annotations of `ifaceClass`.
-        if (rule.getInheritanceClassName().matches(iface, appView)) {
-          AnnotationMatchResult annotationMatchResult =
-              containsAllAnnotations(rule.getInheritanceAnnotations(), ifaceClass);
-          if (annotationMatchResult != null) {
-            handleMatchedAnnotation(annotationMatchResult);
-            return true;
-          }
-        }
-        if (anyImplementedInterfaceMatchesImplementsRule(ifaceClass, rule)) {
-          return true;
-        }
-      }
-      if (clazz.superType == null) {
-        return false;
-      }
-      DexClass superClass = application.definitionFor(clazz.superType);
-      if (superClass == null) {
-        // TODO(herhut): Warn about broken supertype chain?
-        return false;
-      }
-      return anyImplementedInterfaceMatchesImplementsRule(superClass, rule);
-    }
-
-    private boolean anySourceMatchesInheritanceRuleDirectly(
-        DexClass clazz, ProguardConfigurationRule rule, boolean isInterface) {
-      // TODO(b/110141157): Figure out what to do with annotations. Should the annotations of
-      // the DexClass corresponding to `sourceType` satisfy the `annotation`-matcher?
-      return appView.getVerticallyMergedClasses() != null
-          && appView.getVerticallyMergedClasses().getSourcesFor(clazz.type).stream()
-              .filter(
-                  sourceType ->
-                      appView.definitionFor(sourceType).accessFlags.isInterface() == isInterface)
-              .anyMatch(rule.getInheritanceClassName()::matches);
-    }
-
     private boolean allRulesSatisfied(
         Collection<ProguardMemberRule> memberKeepRules, DexClass clazz) {
       for (ProguardMemberRule rule : memberKeepRules) {
@@ -1673,8 +1693,8 @@
       if (rule.getMemberRules().isEmpty()) {
         evaluateCheckDiscardClassAndAllMembersRule(clazz, rule);
       } else if (clazz.hasFields() || clazz.hasMethods()) {
-        markMatchingFields(clazz, rule.getMemberRules(), rule, null, null);
-        markMatchingMethods(clazz, rule.getMemberRules(), rule, null, null);
+        markMatchingFields(clazz, rule.getMemberRules(), rule, null);
+        markMatchingMethods(clazz, rule.getMemberRules(), rule, null);
         classesWithCheckDiscardedMembers.add(clazz);
       }
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index b17e643..5c629c0 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -96,7 +96,6 @@
     appView.pruneItems(prunedItems, executorService, timing);
     appView.notifyOptimizationFinishedForTesting();
     timing.end();
-    appView.dexItemFactory().gc();
   }
 
   private DirectMappedDexApplication.Builder removeUnused(DirectMappedDexApplication application) {
diff --git a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java
index 0941d2a..5042fc5 100644
--- a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/EnqueuerReflectiveIdentificationEventConsumer.java
@@ -107,7 +107,7 @@
   }
 
   @Override
-  public void onJavaLangReflectProxyNewProxyInstance(
+  public void onJavaLangReflectProxyGetProxyClassOrNewProxyInstance(
       Set<DexProgramClass> classes, ProgramMethod context) {
     KeepReason reason = KeepReason.reflectiveUseIn(context);
     for (DexProgramClass clazz : classes) {
diff --git a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/KeepAllReflectiveIdentificationEventConsumer.java b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/KeepAllReflectiveIdentificationEventConsumer.java
index ea733a4..50c601f 100644
--- a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/KeepAllReflectiveIdentificationEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/KeepAllReflectiveIdentificationEventConsumer.java
@@ -59,7 +59,7 @@
   }
 
   @Override
-  public void onJavaLangReflectProxyNewProxyInstance(
+  public void onJavaLangReflectProxyGetProxyClassOrNewProxyInstance(
       Set<DexProgramClass> classes, ProgramMethod context) {
     for (DexProgramClass clazz : classes) {
       keep(clazz, context);
diff --git a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentification.java b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentification.java
index 5d49cb7..8aef51a 100644
--- a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentification.java
+++ b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentification.java
@@ -97,7 +97,8 @@
       }
     } else if (holder.isIdenticalTo(factory.proxyType)) {
       // java.lang.reflect.Proxy
-      if (invokedMethod.isIdenticalTo(factory.proxyMethods.newProxyInstance)) {
+      if (invokedMethod.isIdenticalTo(factory.proxyMethods.getProxyClass)
+          || invokedMethod.isIdenticalTo(factory.proxyMethods.newProxyInstance)) {
         enqueue(method);
       }
     } else if (holder.isIdenticalTo(factory.serviceLoaderType)) {
@@ -187,8 +188,9 @@
       }
     } else if (holder.isIdenticalTo(factory.proxyType)) {
       // java.lang.reflect.Proxy
-      if (invokedMethod.isIdenticalTo(factory.proxyMethods.newProxyInstance)) {
-        handleJavaLangReflectProxyNewProxyInstance(method, invoke);
+      if (invokedMethod.isIdenticalTo(factory.proxyMethods.getProxyClass)
+          || invokedMethod.isIdenticalTo(factory.proxyMethods.newProxyInstance)) {
+        handleJavaLangReflectProxyGetProxyClassOrNewProxyInstance(method, invoke);
         return true;
       }
     } else if (holder.isIdenticalTo(factory.serviceLoaderType)) {
@@ -423,7 +425,7 @@
    * Handles reflective uses of {@link java.lang.reflect.Proxy#newProxyInstance(ClassLoader,
    * Class[], InvocationHandler)}.
    */
-  private void handleJavaLangReflectProxyNewProxyInstance(
+  private void handleJavaLangReflectProxyGetProxyClassOrNewProxyInstance(
       ProgramMethod method, InvokeMethod invoke) {
     if (!invoke.isInvokeStatic()) {
       assert false;
@@ -466,7 +468,7 @@
       }
     }
     if (!classes.isEmpty()) {
-      eventConsumer.onJavaLangReflectProxyNewProxyInstance(classes, method);
+      eventConsumer.onJavaLangReflectProxyGetProxyClassOrNewProxyInstance(classes, method);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentificationEventConsumer.java b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentificationEventConsumer.java
index d6255c3..3c3163b 100644
--- a/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentificationEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/shaking/reflectiveidentification/ReflectiveIdentificationEventConsumer.java
@@ -24,7 +24,8 @@
 
   void onJavaLangReflectConstructorNewInstance(ProgramMethod initializer, ProgramMethod context);
 
-  void onJavaLangReflectProxyNewProxyInstance(Set<DexProgramClass> classes, ProgramMethod context);
+  void onJavaLangReflectProxyGetProxyClassOrNewProxyInstance(
+      Set<DexProgramClass> classes, ProgramMethod context);
 
   void onJavaUtilConcurrentAtomicAtomicIntegerFieldUpdaterNewUpdater(
       ProgramField field, ProgramMethod context);
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index f2eea5b..e0a0ee9 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -463,7 +463,7 @@
       assert verifyNonRepresentativesRemovedFromApplication(application, syntheticMethodGroups);
 
       timing.begin("Tree fixing");
-      DexApplication.Builder<?> builder = application.builder();
+      DexApplication.Builder<?, ?> builder = application.builder();
       treeFixer.fixupClasses(deduplicatedClasses);
       builder.replaceProgramClasses(treeFixer.fixupClasses(application.classes()));
       application = builder.build();
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index d29c13f..c1e8b7f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -1287,7 +1287,7 @@
       committedProgramTypes = ImmutableList.of();
       amendedApplication = application;
     } else {
-      DexApplication.Builder<?> appBuilder = application.builder();
+      DexApplication.Builder<?, ?> appBuilder = application.builder();
       ImmutableList.Builder<DexType> committedProgramTypesBuilder = ImmutableList.builder();
       for (SyntheticDefinition<?, ?, ?> definition : pending.definitions.values()) {
         if (!removedClasses.contains(definition.getHolder().getType())) {
diff --git a/src/main/java/com/android/tools/r8/threading/TaskCollection.java b/src/main/java/com/android/tools/r8/threading/TaskCollection.java
index 6030de8..97c1508 100644
--- a/src/main/java/com/android/tools/r8/threading/TaskCollection.java
+++ b/src/main/java/com/android/tools/r8/threading/TaskCollection.java
@@ -7,6 +7,7 @@
 import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.ThrowingAction;
 import com.android.tools.r8.utils.UncheckedExecutionException;
 import com.google.common.util.concurrent.Futures;
@@ -47,6 +48,10 @@
     this(options.getThreadingModule(), executorService, initialCapacity);
   }
 
+  public int getNumberOfThreadsOrDefault(int defaultValue) {
+    return ThreadUtils.getNumberOfThreadsOrDefault(executorService, defaultValue);
+  }
+
   public <S> void stream(Collection<S> items, Function<S, T> fn, Consumer<T> consumer)
       throws ExecutionException {
     if (threadingModule.isSingleThreaded()) {
diff --git a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
index b532a2e..89f259b 100644
--- a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
@@ -320,9 +320,13 @@
   }
 
   public static int getNumberOfThreads(ExecutorService service) {
+    return getNumberOfThreadsOrDefault(service, -1);
+  }
+
+  public static int getNumberOfThreadsOrDefault(ExecutorService service, int defaultValue) {
     if (service instanceof ForkJoinPool) {
       return ((ForkJoinPool) service).getParallelism();
     }
-    return -1;
+    return defaultValue;
   }
 }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index c34780a..a5be337 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -82,12 +82,6 @@
 
     public Builder addResourceTable(String path, byte[] bytes, FeatureSplit featureSplit) {
       resourceTables.put(new PathAndBytes(bytes, path), featureSplit);
-      try {
-        ResourceTable resourceTable = ResourceTable.parseFrom(bytes);
-        System.currentTimeMillis();
-      } catch (InvalidProtocolBufferException e) {
-        throw new RuntimeException(e);
-      }
       return this;
     }
 
diff --git a/src/test/java/com/android/tools/r8/assistant/ServiceLoaderJsonTest.java b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderJsonTest.java
new file mode 100644
index 0000000..7163502
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderJsonTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2025, 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.assistant;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.postprocessing.ReflectiveOperationJsonParser;
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.postprocessing.model.ServiceLoaderLoad;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+import com.android.tools.r8.utils.Box;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderJsonTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testInstrumentationWithCustomOracle() throws Exception {
+    Path path = Paths.get(temp.newFile().getAbsolutePath());
+    Box<DexItemFactory> factoryBox = new Box<>();
+    testForAssistant()
+        .addProgramClassesAndInnerClasses(ServiceLoaderTestClass.class)
+        .addInstrumentationClasses(Instrumentation.class)
+        .setCustomReflectiveOperationReceiver(Instrumentation.class)
+        .setMinApi(parameters)
+        .addOptionsModification(opt -> factoryBox.set(opt.itemFactory))
+        .compile()
+        .addVmArguments("-Dcom.android.tools.r8.reflectiveJsonLogger=" + path)
+        .run(parameters.getRuntime(), ServiceLoaderTestClass.class)
+        .assertSuccess();
+    List<ReflectiveEvent> reflectiveEvents =
+        new ReflectiveOperationJsonParser(factoryBox.get()).parse(path);
+    Assert.assertEquals(3, reflectiveEvents.size());
+
+    assertTrue(reflectiveEvents.get(0).isServiceLoaderLoad());
+    ServiceLoaderLoad updater0 = reflectiveEvents.get(0).asServiceLoaderLoad();
+    assertEquals(
+        ServiceLoaderTestClass.NameService.class.getName(), updater0.getType().toSourceString());
+
+    assertTrue(reflectiveEvents.get(1).isServiceLoaderLoad());
+    ServiceLoaderLoad updater1 = reflectiveEvents.get(1).asServiceLoaderLoad();
+    assertEquals(
+        ServiceLoaderTestClass.NameService.class.getName(), updater1.getType().toSourceString());
+
+    assertTrue(reflectiveEvents.get(2).isServiceLoaderLoad());
+    ServiceLoaderLoad updater2 = reflectiveEvents.get(2).asServiceLoaderLoad();
+    assertEquals(
+        ServiceLoaderTestClass.NameService.class.getName(), updater2.getType().toSourceString());
+
+    Box<KeepInfoCollectionExported> keepInfoBox = new Box<>();
+    testForR8(parameters)
+        .addProgramClassesAndInnerClasses(ServiceLoaderTestClass.class)
+        .addOptionsModification(
+            opt -> opt.testing.finalKeepInfoCollectionConsumer = keepInfoBox::set)
+        .setMinApi(parameters)
+        .addKeepMainRule(ServiceLoaderTestClass.class)
+        .addKeepRules("-keep class " + ServiceLoaderTestClass.NameService.class.getName())
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .run(parameters.getRuntime(), ServiceLoaderTestClass.class)
+        .assertSuccessWithOutputLines();
+    KeepInfoCollectionExported keepInfoCollectionExported = keepInfoBox.get();
+
+    assertTrue(updater0.isKeptBy(keepInfoCollectionExported));
+    assertTrue(updater1.isKeptBy(keepInfoCollectionExported));
+    assertTrue(updater2.isKeptBy(keepInfoCollectionExported));
+  }
+
+  public static class Instrumentation extends ReflectiveOperationJsonLogger {
+
+    public Instrumentation() throws IOException {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java
index e1c3e30..1babaac 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.benchmarks.BenchmarkBase;
 import com.android.tools.r8.benchmarks.BenchmarkConfig;
 import com.android.tools.r8.utils.LibraryProvidedProguardRulesTestUtils;
+import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -49,7 +50,13 @@
             .setDumpDependencyPath(dir)
             .setFromRevision(16457)
             .buildR8WithPartialShrinking(
-                ChromeBenchmarks::configurePartial, ChromeBenchmarks::inspectPartial));
+                ChromeBenchmarks::configurePartial, ChromeBenchmarks::inspectPartial),
+        AppDumpBenchmarkBuilder.builder()
+            .setName("ChromeAppTreeShaking")
+            .setDumpDependencyPath(dir)
+            .setFromRevision(16457)
+            .setRuntimeOnly()
+            .buildR8(ChromeBenchmarks::configureTreeShaking));
   }
 
   private static void configure(R8FullTestBuilder testBuilder) {
@@ -72,6 +79,24 @@
         .allowUnnecessaryDontWarnWildcards();
   }
 
+  private static void configureTreeShaking(R8FullTestBuilder testBuilder) {
+    configure(testBuilder);
+    testBuilder.addOptionsModification(
+        options ->
+            options.getTestingOptions().enqueuerInspector =
+                (appInfo, enqueuerMode) -> {
+                  if (appInfo.options().printTimes) {
+                    Timing timing = appInfo.app().timing;
+                    timing.end(); // End "Create result"
+                    timing.end(); // End "Trace application"
+                    timing.end(); // End "Enqueuer"
+                    timing.end(); // End "Strip unused code"
+                    timing.report(); // Report "R8 main"
+                  }
+                  throw new AbortBenchmarkException();
+                });
+  }
+
   private static void inspectPartial(R8PartialTestCompileResult compileResult) {
     compileResult.inspectDiagnosticMessages(
         diagnostics ->
@@ -101,4 +126,9 @@
   public void testChromeAppPartial() throws Exception {
     testBenchmarkWithName("ChromeAppPartial");
   }
+
+  @Test
+  public void testChromeTreeShaking() throws Exception {
+    testBenchmarkWithName("ChromeTreeShaking");
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithGetProxyClassTest.java
similarity index 64%
copy from src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java
copy to src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithGetProxyClassTest.java
index ecdf0ba..93e146c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithGetProxyClassTest.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2025, 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.
 
@@ -10,31 +10,31 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.StringUtils;
+import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Proxy;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class InterfaceWithProxyTest extends TestBase {
+public class InterfaceWithGetProxyClassTest extends TestBase {
 
   private static final String EXPECTED = StringUtils.lines("Hello world!");
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public InterfaceWithProxyTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void testReference() throws Exception {
     testForRuntime(parameters)
-        .addInnerClasses(InterfaceWithProxyTest.class)
+        .addInnerClasses(InterfaceWithGetProxyClassTest.class)
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED);
   }
@@ -42,7 +42,7 @@
   @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
-        .addInnerClasses(InterfaceWithProxyTest.class)
+        .addInnerClasses(InterfaceWithGetProxyClassTest.class)
         .addKeepMainRule(TestClass.class)
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
@@ -53,17 +53,15 @@
 
   static class TestClass {
 
-    public static void main(String[] args) {
-      I obj =
-          (I)
-              Proxy.newProxyInstance(
-                  TestClass.class.getClassLoader(),
-                  new Class<?>[] {I.class},
-                  (proxy, method, args1) -> {
-                    System.out.print("Hello");
-                    return null;
-                  });
-      obj.greet();
+    public static void main(String[] args) throws Exception {
+      Class<?> proxyClass = Proxy.getProxyClass(TestClass.class.getClassLoader(), I.class);
+      InvocationHandler ih =
+          (proxy, method, args1) -> {
+            System.out.print("Hello");
+            return null;
+          };
+      I i = (I) proxyClass.getDeclaredConstructor(InvocationHandler.class).newInstance(ih);
+      i.greet();
       new A().greet();
     }
   }
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithNewProxyInstanceTest.java
similarity index 84%
rename from src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithNewProxyInstanceTest.java
index ecdf0ba..a512441 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithNewProxyInstanceTest.java
@@ -14,27 +14,26 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class InterfaceWithProxyTest extends TestBase {
+public class InterfaceWithNewProxyInstanceTest extends TestBase {
 
   private static final String EXPECTED = StringUtils.lines("Hello world!");
 
-  private final TestParameters parameters;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
-  public InterfaceWithProxyTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void testReference() throws Exception {
     testForRuntime(parameters)
-        .addInnerClasses(InterfaceWithProxyTest.class)
+        .addInnerClasses(InterfaceWithNewProxyInstanceTest.class)
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED);
   }
@@ -42,7 +41,7 @@
   @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
-        .addInnerClasses(InterfaceWithProxyTest.class)
+        .addInnerClasses(InterfaceWithNewProxyInstanceTest.class)
         .addKeepMainRule(TestClass.class)
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
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 b690ee9..4fc37c5 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
@@ -119,6 +119,7 @@
     DirectMappedDexApplication finalApp =
         inspector
             .getApplication()
+            .asLazy()
             .toDirect()
             .builder()
             .replaceLibraryClasses(libHolder.libraryClasses())
diff --git a/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java b/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java
index 55f23f9..90322ea 100644
--- a/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java
+++ b/src/test/java/com/android/tools/r8/dex/DexCodeDeduppingTest.java
@@ -29,11 +29,11 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class DexCodeDeduppingTest extends TestBase {
-  private final TestParameters parameters;
   private static final List<String> EXPECTED = ImmutableList.of("foo", "bar", "foo", "bar");
 
   private static final int ONE_CLASS_COUNT = 4;
@@ -42,15 +42,14 @@
   private static final int TWO_CLASS_COUNT = 6;
   private static final int TWO_CLASS_DEDUPLICATED_COUNT = 3;
 
+  @Parameter(0)
+  public TestParameters parameters;
+
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withDexRuntimes().withAllApiLevels().build();
   }
 
-  public DexCodeDeduppingTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void testR8SingleClass() throws Exception {
     R8TestCompileResult compile =
diff --git a/src/test/java/com/android/tools/r8/dex/DexCodeInvokeSuperDeduppingTest.java b/src/test/java/com/android/tools/r8/dex/DexCodeInvokeSuperDeduppingTest.java
new file mode 100644
index 0000000..b5dd40c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dex/DexCodeInvokeSuperDeduppingTest.java
@@ -0,0 +1,149 @@
+// Copyright (c) 2025, 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.dex;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.DexSegments;
+import com.android.tools.r8.DexSegments.SegmentInfo;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class DexCodeInvokeSuperDeduppingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean enableMappingOutput;
+
+  @Parameters(name = "{0}, map output: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimesAndAllApiLevels().build(), BooleanUtils.values());
+  }
+
+  @Test
+  public void testD8MappingOutput() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClasses(Base.class, FooBase.class, BarBase.class, Main.class)
+        .addProgramClassFileData(getProgramClassFileData())
+        .setMinApi(parameters)
+        .release()
+        .applyIf(enableMappingOutput, D8TestBuilder::internalEnableMappingOutput)
+        .compile()
+        .apply(this::inspectCodeSegment)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("FooBase", "BarBase");
+  }
+
+  private List<byte[]> getProgramClassFileData() throws IOException {
+    return ImmutableList.of(
+        transformer(Foo.class)
+            .transformMethodInsnInMethod(
+                "foo",
+                (opcode, owner, name, descriptor, isInterface, visitor) -> {
+                  if (opcode == Opcodes.INVOKESPECIAL) {
+                    assertEquals(
+                        "com/android/tools/r8/dex/DexCodeInvokeSuperDeduppingTest$FooBase", owner);
+                    owner = binaryName(Base.class);
+                  }
+                  visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+                })
+            .transform(),
+        transformer(Bar.class)
+            .transformMethodInsnInMethod(
+                "bar",
+                (opcode, owner, name, descriptor, isInterface, visitor) -> {
+                  if (opcode == Opcodes.INVOKESPECIAL) {
+                    assertEquals(
+                        "com/android/tools/r8/dex/DexCodeInvokeSuperDeduppingTest$BarBase", owner);
+                    owner = binaryName(Base.class);
+                  }
+                  visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+                })
+            .transform());
+  }
+
+  private void inspectCodeSegment(D8TestCompileResult compileResult) throws Exception {
+    SegmentInfo codeSegmentInfo = getCodeSegmentInfo(compileResult.writeToZip());
+    int expectedCodeItems = 11;
+    if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.S) && enableMappingOutput) {
+      // Main.<init> and Base.<init> are canonicalized, and so is FooBase.<init> and BarBase.<init>.
+      assertEquals(expectedCodeItems - 2, codeSegmentInfo.getItemCount());
+    } else {
+      assertEquals(expectedCodeItems, codeSegmentInfo.getItemCount());
+    }
+  }
+
+  public SegmentInfo getCodeSegmentInfo(Path path)
+      throws CompilationFailedException, ResourceException, IOException {
+    DexSegments.Command command = DexSegments.Command.builder().addProgramFiles(path).build();
+    Map<Integer, SegmentInfo> segmentInfoMap = DexSegments.runForTesting(command);
+    return segmentInfoMap.get(Constants.TYPE_CODE_ITEM);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new Foo().foo();
+      new Bar().bar();
+    }
+  }
+
+  abstract static class Base {
+
+    abstract void m();
+  }
+
+  static class FooBase extends Base {
+
+    @Override
+    void m() {
+      System.out.println("FooBase");
+    }
+  }
+
+  static class Foo extends FooBase {
+
+    void foo() {
+      // Symbolic holder transformed to Base class.
+      super.m();
+    }
+  }
+
+  static class BarBase extends Base {
+
+    @Override
+    void m() {
+      System.out.println("BarBase");
+    }
+  }
+
+  static class Bar extends BarBase {
+
+    void bar() {
+      // Symbolic holder transformed to Base class.
+      super.m();
+    }
+  }
+}
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 efbccf0..5058af1 100644
--- a/src/test/java/com/android/tools/r8/ir/InlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/InlineTest.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexItemFactory;
 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;
@@ -463,7 +464,8 @@
         application, options, methodSubject, ImmutableList.of(codeA, codeB));
   }
 
-  protected DexApplication buildApplication(SmaliBuilder builder, InternalOptions options) {
+  protected LazyLoadedDexApplication buildApplication(
+      SmaliBuilder builder, InternalOptions options) {
     try {
       AndroidApp app =
           AndroidApp.builder()
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationPrintConfigurationTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationPrintConfigurationTest.java
new file mode 100644
index 0000000..45276b3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationPrintConfigurationTest.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2025, 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.partial;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.R8PartialTestCompileResult;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PartialCompilationPrintConfigurationTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testBuilder() throws Exception {
+    Path out = temp.newFile("config.txt").toPath();
+    testForR8Partial(parameters)
+        .addR8ExcludedClasses(ExcludedClass.class)
+        .addR8IncludedClasses(IncludedClass.class)
+        .addKeepClassRules(IncludedClass.class)
+        .apply(
+            testBuilder ->
+                testBuilder
+                    .getBuilder()
+                    .setProguardConfigurationConsumer(new StringConsumer.FileConsumer(out)))
+        .compile();
+    inspectFile(out);
+  }
+
+  @Test
+  public void testKeepRule() throws Exception {
+    R8PartialTestCompileResult compileResult =
+        testForR8Partial(parameters)
+            .addR8ExcludedClasses(ExcludedClass.class)
+            .addR8IncludedClasses(IncludedClass.class)
+            .addKeepClassRules(IncludedClass.class)
+            .addKeepRules("-printconfiguration")
+            .collectStdout()
+            .compile();
+    inspectStdout(compileResult.getStdout());
+  }
+
+  @Test
+  public void testBoth() throws Exception {
+    Path out = temp.newFile("config.txt").toPath();
+    R8PartialTestCompileResult compileResult =
+        testForR8Partial(parameters)
+            .addR8ExcludedClasses(ExcludedClass.class)
+            .addR8IncludedClasses(IncludedClass.class)
+            .addKeepClassRules(IncludedClass.class)
+            .addKeepRules("-printconfiguration")
+            .collectStdout()
+            .apply(
+                testBuilder ->
+                    testBuilder
+                        .getBuilder()
+                        .setProguardConfigurationConsumer(new StringConsumer.FileConsumer(out)))
+            .compile();
+    inspectFile(out);
+    inspectStdout(compileResult.getStdout());
+  }
+
+  private void inspectFile(Path out) throws IOException {
+    assertTrue(Files.exists(out));
+    inspectLines(Files.readAllLines(out));
+  }
+
+  private void inspectStdout(String stdout) {
+    inspectLines(StringUtils.splitLines(stdout));
+  }
+
+  private void inspectLines(List<String> lines) {
+    assertEquals(
+        1,
+        lines.stream()
+            .filter(line -> line.equals("-keep class " + IncludedClass.class.getTypeName()))
+            .count());
+  }
+
+  static class ExcludedClass {}
+
+  static class IncludedClass {}
+}
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 17ba25b..4fe4ad3 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBase.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBase.java
@@ -34,7 +34,6 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -45,6 +44,7 @@
 import com.android.tools.r8.graph.GenericSignature;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.graph.ImmediateAppSubtypingInfo;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.SmaliWriter;
 import com.android.tools.r8.jasmin.JasminBuilder;
@@ -898,8 +898,8 @@
     return newJar;
   }
 
-  private static DexApplication readApplicationForDexOutput(AndroidApp app, InternalOptions options)
-      throws Exception {
+  private static LazyLoadedDexApplication readApplicationForDexOutput(
+      AndroidApp app, InternalOptions options) throws Exception {
     assert options.programConsumer == null;
     options.programConsumer = DexIndexedConsumer.emptyConsumer();
     return new ApplicationReader(app, options, Timing.empty()).read();
@@ -959,7 +959,7 @@
     if (optionsConsumer != null) {
       optionsConsumer.accept(options);
     }
-    DexApplication dexApplication = readApplicationForDexOutput(app, options);
+    LazyLoadedDexApplication dexApplication = readApplicationForDexOutput(app, options);
     AppView<AppInfoWithClassHierarchy> appView = AppView.createForR8(dexApplication.toDirect());
     appView.setAppServices(AppServices.builder(appView).build());
     return appView;
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 7ecd693..e929946 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.shaking.FilteredClassPath;
@@ -2704,8 +2705,8 @@
   }
 
   public static void disassemble(AndroidApp app, PrintStream ps) throws IOException {
-    DexApplication application =
-        new ApplicationReader(app, new InternalOptions(), Timing.empty()).read().toDirect();
+    LazyLoadedDexApplication application =
+        new ApplicationReader(app, new InternalOptions(), Timing.empty()).read();
     new AssemblyWriter(application, new InternalOptions(), true, false, true).write(ps);
   }
 
diff --git a/tools/perf.py b/tools/perf.py
index 40333ee..0d4d8c0 100755
--- a/tools/perf.py
+++ b/tools/perf.py
@@ -153,6 +153,9 @@
     'AGSA': {
         'targets': ['r8-full']
     },
+    'ChromeAppTreeShaking': {
+        'targets': ['r8-full']
+    },
     'SystemUIAppTreeShaking': {
         'targets': ['r8-full']
     },