Merge commit '2678e9554995f4cafee6fe3cf886e9683014afe7' into dev-release

Change-Id: I0e2091a4e2510b84afa6388e4d2e93db9fdee741
diff --git a/AUTHORS b/AUTHORS
index df9cd8d..5905529 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -6,6 +6,10 @@
 Google Inc.
 Uber Technologies Inc.
 Square Inc.
+Meta Platforms, Inc.
 
 Albert Jin <albert.jin@gmail.com>
 Kevin Sun <snxngxng@gmail.com>
+Søren Gjesse <soren@gjesse.dk>
+Andreas Gampe <agampe@meta.com>
+Tiangong Li <tgli@meta.com>
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 5d310a10..6e873c7 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -179,15 +179,30 @@
       return [output_api.PresubmitError(diff)]
   return []
 
+def IsTestFile(file):
+  localPath = file.LocalPath()
+  return localPath.endswith('.java') and '/test/' in localPath
+
 def CheckForAddedDisassemble(input_api, output_api):
   results = []
   for (file, line_nr, line) in input_api.RightHandSideLines():
-    if file.LocalPath().endswith('.java') and '.disassemble()' in line:
+    if IsTestFile(file) and '.disassemble()' in line:
       results.append(
           output_api.PresubmitError(
               'Test call to disassemble\n%s:%s %s' % (file.LocalPath(), line_nr, line)))
   return results
 
+def CheckForAddedAllowXxxxxxMessages(input_api, output_api):
+  results = []
+  for (file, line_nr, line) in input_api.RightHandSideLines():
+    if (IsTestFile(file)
+        and ('.allowStdoutMessages()' in line or '.allowStderrMessages()' in line)):
+      results.append(
+          output_api.PresubmitError(
+              'Test call to allowStdoutMessages or allowStderrMessages\n%s:%s %s'
+              % (file.LocalPath(), line_nr, line)))
+  return results
+
 def CheckForAddedPartialDebug(input_api, output_api):
   results = []
   for (file, line_nr, line) in input_api.RightHandSideLines():
@@ -242,6 +257,7 @@
   results.extend(
       CheckDeterministicDebuggingChanged(input_api, output_api, branch))
   results.extend(CheckForAddedDisassemble(input_api, output_api))
+  results.extend(CheckForAddedAllowXxxxxxMessages(input_api, output_api))
   results.extend(CheckForAddedPartialDebug(input_api, output_api))
   results.extend(CheckForCopyRight(input_api, output_api, branch))
   return results
diff --git a/README.md b/README.md
index 487df6d..021b3f5 100644
--- a/README.md
+++ b/README.md
@@ -216,22 +216,28 @@
 If your contribution is owned by your employer you need the
 [Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
 
-Once the license agreement is in place, please send an email to
-[r8-dev@googlegroups.com](mailto:r8-dev@googlegroups.com) to be added as a
-contributor.
+Signing the CLA should be enough to be approved as a contributer. However, please send an email to
+[r8-dev@googlegroups.com](mailto:r8-dev@googlegroups.com) to introduce yourself and your plans
+for contributing to the project.
 
-After being added as a contributer you can upload your patches
-using `git cl` which is available in `depot_tools`. Once you have a
+To create an account for the code review tool (required for uploading changes)
+please navigate to https://r8-review.googlesource.com/ and sign in with the
+account used to sign the CLA. First time you do that you will be asked to
+_Create Gerrit Account_. Plase follow that process.
+
+You can now upload your patches. Once you have a
 change that you are happy with you should make sure that it passes
 all tests and then upload the change to our code review tool using:
 
     $ git cl upload
 
 On your first upload you will be asked to acquire credentials. Follow the
-instructions given by `git cl upload`.
+instructions given by `git cl upload` (as of Aug 5 2025 that is running
+`git credential-luci login`, the previous `.gitcookies` based authentication
+scheme has been deprecated.).
 
 On successful uploads a link to the code review is printed in the
-output of the upload command. In the code review tool you can
+output of the `git cl upload` command. In the code review tool you can
 assign reviewers and mark the change ready for review. At that
 point the code review tool will send emails to reviewers.
 
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 3a60dff..72dfcc9 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -779,7 +779,7 @@
       swarming_tags: "vpython:native-python-wrapper"
       dimensions: "cpu:x86-64"
       dimensions: "normal:true"
-      dimensions: "os:Ubuntu-22.04"
+      dimensions: "os:Ubuntu-20.04"
       dimensions: "pool:luci.r8.ci"
       exe {
         cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build"
@@ -817,7 +817,7 @@
       swarming_tags: "vpython:native-python-wrapper"
       dimensions: "cpu:x86-64"
       dimensions: "normal:true"
-      dimensions: "os:Ubuntu-22.04"
+      dimensions: "os:Ubuntu-20.04"
       dimensions: "pool:luci.r8.ci"
       exe {
         cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build"
diff --git a/infra/config/global/generated/project.cfg b/infra/config/global/generated/project.cfg
index 2475c82..b5be1d4 100644
--- a/infra/config/global/generated/project.cfg
+++ b/infra/config/global/generated/project.cfg
@@ -7,7 +7,7 @@
 name: "r8"
 access: "group:all"
 lucicfg {
-  version: "1.45.3"
+  version: "1.45.6"
   package_dir: ".."
   config_dir: "generated"
   entry_point: "main.star"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 862f835..eb4ad17 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -168,7 +168,7 @@
 
 default_timeout = time.hour * 6
 
-def get_dimensions(windows = False, internal = False, archive = False, noble=True):
+def get_dimensions(windows = False, internal = False, archive = False, jammy=True):
     # We use the following setup:
     #   windows -> always windows machine
     #   internal -> always internal, single small, machine
@@ -181,7 +181,7 @@
     if windows:
         dimensions["os"] = "Windows-11"
     else:
-        if noble:
+        if jammy:
             dimensions["os"] = "Ubuntu-22.04"
         else:
             dimensions["os"] = "Ubuntu-20.04"
@@ -404,7 +404,7 @@
 r8_tester_with_default(
     "linux-android-5",
     ["--dex_vm=5.1.1", "--all_tests", "--command_cache_dir=/tmp/ccache"],
-    dimensions = get_dimensions(noble=True),
+    dimensions = get_dimensions(jammy=False),
 )
 
 r8_tester_with_default(
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
index 22f06cb..7b6ad44 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
@@ -28,13 +28,14 @@
 
   @Override
   public void onClassGetDeclaredMethod(
-      Stack stack, Class<?> clazz, String method, Class<?>... parameters) {}
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {}
 
   @Override
   public void onClassGetDeclaredMethods(Stack stack, Class<?> clazz) {}
 
   @Override
-  public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {}
+  public void onClassGetDeclaredField(
+      Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {}
 
   @Override
   public void onClassGetDeclaredFields(Stack stack, Class<?> clazz) {}
@@ -47,13 +48,13 @@
 
   @Override
   public void onClassGetMethod(
-      Stack stack, Class<?> clazz, String method, Class<?>... parameters) {}
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {}
 
   @Override
   public void onClassGetMethods(Stack stack, Class<?> clazz) {}
 
   @Override
-  public void onClassGetField(Stack stack, Class<?> clazz, String fieldName) {}
+  public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {}
 
   @Override
   public void onClassGetFields(Stack stack, Class<?> clazz) {}
@@ -83,14 +84,8 @@
   public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {}
 
   @Override
-  public void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {}
-
-  @Override
-  public void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {}
-
-  @Override
-  public void onAtomicReferenceFieldUpdaterNewUpdater(
-      Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {}
+  public void onAtomicFieldUpdaterNewUpdater(
+      Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {}
 
   @Override
   public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java
new file mode 100644
index 0000000..7b8b3eb
--- /dev/null
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java
@@ -0,0 +1,37 @@
+// 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.runtime;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public enum ReflectiveEventType {
+  CLASS_NEW_INSTANCE,
+  CLASS_GET_DECLARED_METHOD,
+  CLASS_GET_DECLARED_METHODS,
+  CLASS_GET_DECLARED_FIELD,
+  CLASS_GET_DECLARED_FIELDS,
+  CLASS_GET_DECLARED_CONSTRUCTOR,
+  CLASS_GET_DECLARED_CONSTRUCTORS,
+  CLASS_GET_METHOD,
+  CLASS_GET_METHODS,
+  CLASS_GET_FIELD,
+  CLASS_GET_FIELDS,
+  CLASS_GET_CONSTRUCTOR,
+  CLASS_GET_CONSTRUCTORS,
+  CLASS_GET_NAME,
+  CLASS_FOR_NAME,
+  CLASS_GET_COMPONENT_TYPE,
+  CLASS_GET_PACKAGE,
+  CLASS_IS_ASSIGNABLE_FROM,
+  CLASS_GET_SUPERCLASS,
+  CLASS_AS_SUBCLASS,
+  CLASS_IS_INSTANCE,
+  CLASS_CAST,
+  CLASS_FLAG,
+  ATOMIC_FIELD_UPDATER_NEW_UPDATER,
+  SERVICE_LOADER_LOAD,
+  PROXY_NEW_PROXY_INSTANCE;
+}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java
new file mode 100644
index 0000000..57e3cdf
--- /dev/null
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java
@@ -0,0 +1,271 @@
+// 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.runtime;
+
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.ATOMIC_FIELD_UPDATER_NEW_UPDATER;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_AS_SUBCLASS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_CAST;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_FLAG;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_FOR_NAME;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_COMPONENT_TYPE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_CONSTRUCTOR;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_CONSTRUCTORS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTOR;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTORS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_FIELD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_FIELDS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_METHOD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_METHODS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_FIELD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_FIELDS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_METHOD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_METHODS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_NAME;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_PACKAGE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_SUPERCLASS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_IS_ASSIGNABLE_FROM;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_IS_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_NEW_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.PROXY_NEW_PROXY_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.SERVICE_LOADER_LOAD;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+
+// This logs the information in JSON-like format,
+// manually encoded to avoid loading gson on the mobile.
+@KeepForApi
+public class ReflectiveOperationJsonLogger implements ReflectiveOperationReceiver {
+
+  private final FileWriter output;
+
+  public ReflectiveOperationJsonLogger() throws IOException {
+    String property = System.getProperty("com.android.tools.r8.reflectiveJsonLogger", "log.txt");
+    File file = new File(property);
+    file.createNewFile();
+    this.output = new FileWriter(file);
+    output.write("[");
+  }
+
+  public void finished() throws IOException {
+    output.write("{}]");
+    output.close();
+  }
+
+  private String[] methodToString(
+      Class<?> returnType, Class<?> holder, String method, Class<?>... parameters) {
+    String[] methodStrings = new String[parameters.length + 3];
+    methodStrings[0] = printClass(returnType);
+    methodStrings[1] = printClass(holder);
+    methodStrings[2] = method;
+    for (int i = 0; i < parameters.length; i++) {
+      methodStrings[i + 3] = printClass(parameters[i]);
+    }
+    return methodStrings;
+  }
+
+  private String[] constructorToString(Class<?> holder, Class<?>... parameters) {
+    return methodToString(Void.TYPE, holder, "<init>", parameters);
+  }
+
+  private String printClass(Class<?> clazz) {
+    return clazz == null ? "null" : clazz.getName();
+  }
+
+  private String printClassLoader(ClassLoader classLoader) {
+    return classLoader == null ? "null" : printClass(classLoader.getClass());
+  }
+
+  private void output(ReflectiveEventType event, Stack stack, String... args) {
+    try {
+      output.write("{\"event\": \"");
+      output.write(event.name());
+      output.write("\"");
+      if (stack != null) {
+        output.write(", \"stack\": ");
+        printArray(stack.stackTraceElementsAsString());
+      }
+      assert args != null;
+      output.write(", \"args\": ");
+      printArray(args);
+      output.write("},\n");
+      output.flush();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void printArray(String... args) throws IOException {
+    output.write("[");
+    for (int i = 0; i < args.length; i++) {
+      output.write("\"");
+      output.write(args[i]);
+      output.write("\"");
+      if (i != args.length - 1) {
+        output.write(", ");
+      }
+    }
+    output.write("]");
+  }
+
+  @Override
+  public void onClassNewInstance(Stack stack, Class<?> clazz) {
+    output(CLASS_NEW_INSTANCE, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetDeclaredMethod(
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
+    output(CLASS_GET_DECLARED_METHOD, stack, methodToString(returnType, clazz, method, parameters));
+  }
+
+  @Override
+  public void onClassGetDeclaredMethods(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_DECLARED_METHODS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetDeclaredField(
+      Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
+    output(CLASS_GET_DECLARED_FIELD, stack, printClass(fieldType), printClass(clazz), fieldName);
+  }
+
+  @Override
+  public void onClassGetDeclaredFields(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_DECLARED_FIELDS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetDeclaredConstructor(Stack stack, Class<?> clazz, Class<?>... parameters) {
+    output(CLASS_GET_DECLARED_CONSTRUCTOR, stack, constructorToString(clazz, parameters));
+  }
+
+  @Override
+  public void onClassGetDeclaredConstructors(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_DECLARED_CONSTRUCTORS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetMethod(
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
+    output(CLASS_GET_METHOD, stack, methodToString(returnType, clazz, method, parameters));
+  }
+
+  @Override
+  public void onClassGetMethods(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_METHODS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
+    output(CLASS_GET_FIELD, stack, printClass(fieldType), printClass(clazz), fieldName);
+  }
+
+  @Override
+  public void onClassGetFields(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_FIELDS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetConstructor(Stack stack, Class<?> clazz, Class<?>... parameters) {
+    output(CLASS_GET_CONSTRUCTOR, stack, constructorToString(clazz, parameters));
+  }
+
+  @Override
+  public void onClassGetConstructors(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_CONSTRUCTORS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetName(Stack stack, Class<?> clazz, NameLookupType lookupType) {
+    output(CLASS_GET_NAME, stack, printClass(clazz), lookupType.name());
+  }
+
+  @Override
+  public void onClassForName(
+      Stack stack, String className, boolean initialize, ClassLoader classLoader) {
+    output(
+        CLASS_FOR_NAME,
+        stack,
+        className,
+        Boolean.toString(initialize),
+        printClassLoader(classLoader));
+  }
+
+  @Override
+  public void onClassGetComponentType(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_COMPONENT_TYPE, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetPackage(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_PACKAGE, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassIsAssignableFrom(Stack stack, Class<?> clazz, Class<?> sup) {
+    output(CLASS_IS_ASSIGNABLE_FROM, stack, printClass(clazz), printClass(sup));
+  }
+
+  @Override
+  public void onClassGetSuperclass(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_SUPERCLASS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassAsSubclass(Stack stack, Class<?> holder, Class<?> clazz) {
+    output(CLASS_AS_SUBCLASS, stack, printClass(holder), printClass(clazz));
+  }
+
+  @Override
+  public void onClassIsInstance(Stack stack, Class<?> holder, Object object) {
+    output(CLASS_IS_INSTANCE, stack, printClass(holder), printClass(object.getClass()));
+  }
+
+  @Override
+  public void onClassCast(Stack stack, Class<?> holder, Object object) {
+    output(CLASS_CAST, stack, printClass(holder), printClass(object.getClass()));
+  }
+
+  @Override
+  public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {
+    output(CLASS_FLAG, stack, printClass(clazz), classFlag.name());
+  }
+
+  @Override
+  public void onAtomicFieldUpdaterNewUpdater(
+      Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {
+    output(
+        ATOMIC_FIELD_UPDATER_NEW_UPDATER, stack, printClass(fieldClass), printClass(clazz), name);
+  }
+
+  @Override
+  public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {
+    output(SERVICE_LOADER_LOAD, stack, printClass(clazz), printClassLoader(classLoader));
+  }
+
+  @Override
+  public void onProxyNewProxyInstance(
+      Stack stack,
+      ClassLoader classLoader,
+      Class<?>[] interfaces,
+      InvocationHandler invocationHandler) {
+    String[] methodStrings = new String[interfaces.length + 2];
+    methodStrings[0] = printClassLoader(classLoader);
+    methodStrings[1] = invocationHandler.toString();
+    for (int i = 0; i < interfaces.length; i++) {
+      methodStrings[i + 2] = printClass(interfaces[i]);
+    }
+    output(PROXY_NEW_PROXY_INSTANCE, stack, methodStrings);
+  }
+
+  public boolean requiresStackInformation() {
+    return true;
+  }
+}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
index 7d59d7c..d6f1ead 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
@@ -44,7 +44,7 @@
 
   @Override
   public void onClassGetDeclaredMethod(
-      Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
     System.out.println(
         "Reflectively got declared method "
             + printMethod(method, parameters)
@@ -58,7 +58,8 @@
   }
 
   @Override
-  public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {
+  public void onClassGetDeclaredField(
+      Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
     System.out.println("Reflectively got declared field " + fieldName + " on " + clazz.getName());
   }
 
@@ -82,7 +83,8 @@
   }
 
   @Override
-  public void onClassGetMethod(Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+  public void onClassGetMethod(
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
     System.out.println(
         "Reflectively got method " + printMethod(method, parameters) + " on " + clazz.getName());
   }
@@ -93,7 +95,7 @@
   }
 
   @Override
-  public void onClassGetField(Stack stack, Class<?> clazz, String fieldName) {
+  public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
     System.out.println("Reflectively got field " + fieldName + " on " + clazz.getName());
   }
 
@@ -168,20 +170,8 @@
   }
 
   @Override
-  public void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
-    System.out.println(
-        "Reflectively got AtomicIntegerFieldUpdater.newUpdater on " + clazz + "#" + name);
-  }
-
-  @Override
-  public void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
-    System.out.println(
-        "Reflectively got AtomicLongFieldUpdater.newUpdater on " + clazz + "#" + name);
-  }
-
-  @Override
-  public void onAtomicReferenceFieldUpdaterNewUpdater(
-      Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {
+  public void onAtomicFieldUpdaterNewUpdater(
+      Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {
     System.out.println(
         "Reflectively got AtomicReferenceFieldUpdater.newUpdater on "
             + fieldClass
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
index 206a1f4..4c13dda 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
@@ -18,11 +18,12 @@
 
   void onClassNewInstance(Stack stack, Class<?> clazz);
 
-  void onClassGetDeclaredMethod(Stack stack, Class<?> clazz, String method, Class<?>... parameters);
+  void onClassGetDeclaredMethod(
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters);
 
   void onClassGetDeclaredMethods(Stack stack, Class<?> clazz);
 
-  void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName);
+  void onClassGetDeclaredField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName);
 
   void onClassGetDeclaredFields(Stack stack, Class<?> clazz);
 
@@ -30,11 +31,12 @@
 
   void onClassGetDeclaredConstructors(Stack stack, Class<?> clazz);
 
-  void onClassGetMethod(Stack stack, Class<?> clazz, String method, Class<?>... parameters);
+  void onClassGetMethod(
+      Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters);
 
   void onClassGetMethods(Stack stack, Class<?> clazz);
 
-  void onClassGetField(Stack stack, Class<?> clazz, String fieldName);
+  void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName);
 
   void onClassGetFields(Stack stack, Class<?> clazz);
 
@@ -60,12 +62,8 @@
 
   void onClassIsAssignableFrom(Stack stack, Class<?> clazz, Class<?> sup);
 
-  void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name);
-
-  void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name);
-
-  void onAtomicReferenceFieldUpdaterNewUpdater(
-      Stack stack, Class<?> clazz, Class<?> fieldClass, String name);
+  void onAtomicFieldUpdaterNewUpdater(
+      Stack stack, Class<?> fieldClass, Class<?> clazz, String name);
 
   void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader);
 
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
index 32c841c..7a055b4 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
 import java.util.Arrays;
 
 @KeepForApi
@@ -65,6 +66,14 @@
       }
       return sb.toString();
     }
+
+    public String[] stackTraceElementsAsString() {
+      String[] result = new String[stackTraceElements.length];
+      for (int i = 0; i < stackTraceElements.length; i++) {
+        result[i] = stackTraceElements[i].toString();
+      }
+      return result;
+    }
   }
 
   public static void onClassNewInstance(Class<?> clazz) {
@@ -81,7 +90,14 @@
   }
 
   public static void onClassGetDeclaredMethod(Class<?> clazz, String name, Class<?>... parameters) {
-    getInstance().onClassGetDeclaredMethod(Stack.createStack(), clazz, name, parameters);
+    Class<?> returnType = null;
+    try {
+      Method declaredMethod = clazz.getDeclaredMethod(name, parameters);
+      returnType = declaredMethod.getReturnType();
+    } catch (NoSuchMethodException e) {
+    }
+    getInstance()
+        .onClassGetDeclaredMethod(Stack.createStack(), returnType, clazz, name, parameters);
   }
 
   public static void onClassGetDeclaredMethods(Class<?> clazz) {
@@ -89,7 +105,12 @@
   }
 
   public static void onClassGetDeclaredField(Class<?> clazz, String fieldName) {
-    getInstance().onClassGetDeclaredField(Stack.createStack(), clazz, fieldName);
+    Class<?> fieldType = null;
+    try {
+      fieldType = clazz.getDeclaredField(fieldName).getType();
+    } catch (NoSuchFieldException e) {
+    }
+    getInstance().onClassGetDeclaredField(Stack.createStack(), fieldType, clazz, fieldName);
   }
 
   public static void onClassGetDeclaredFields(Class<?> clazz) {
@@ -105,7 +126,12 @@
   }
 
   public static void onClassGetMethod(Class<?> clazz, String name, Class<?>[] parameterTypes) {
-    getInstance().onClassGetMethod(Stack.createStack(), clazz, name, parameterTypes);
+    Class<?> returnType = null;
+    try {
+      returnType = clazz.getMethod(name, parameterTypes).getReturnType();
+    } catch (NoSuchMethodException e) {
+    }
+    getInstance().onClassGetMethod(Stack.createStack(), returnType, clazz, name, parameterTypes);
   }
 
   public static void onClassGetMethods(Class<?> clazz) {
@@ -113,7 +139,12 @@
   }
 
   public static void onClassGetField(Class<?> clazz, String name) {
-    getInstance().onClassGetField(Stack.createStack(), clazz, name);
+    Class<?> fieldType = null;
+    try {
+      fieldType = clazz.getField(name).getType();
+    } catch (NoSuchFieldException e) {
+    }
+    getInstance().onClassGetField(Stack.createStack(), fieldType, clazz, name);
   }
 
   public static void onClassGetFields(Class<?> clazz) {
@@ -221,17 +252,16 @@
   }
 
   public static void onAtomicIntegerFieldUpdaterNewUpdater(Class<?> clazz, String name) {
-    getInstance().onAtomicIntegerFieldUpdaterNewUpdater(Stack.createStack(), clazz, name);
+    getInstance().onAtomicFieldUpdaterNewUpdater(Stack.createStack(), int.class, clazz, name);
   }
 
   public static void onAtomicLongFieldUpdaterNewUpdater(Class<?> clazz, String name) {
-    getInstance().onAtomicLongFieldUpdaterNewUpdater(Stack.createStack(), clazz, name);
+    getInstance().onAtomicFieldUpdaterNewUpdater(Stack.createStack(), long.class, clazz, name);
   }
 
   public static void onAtomicReferenceFieldUpdaterNewUpdater(
       Class<?> clazz, Class<?> fieldClass, String name) {
-    getInstance()
-        .onAtomicReferenceFieldUpdaterNewUpdater(Stack.createStack(), clazz, fieldClass, name);
+    getInstance().onAtomicFieldUpdaterNewUpdater(Stack.createStack(), fieldClass, clazz, name);
   }
 
   public static void onServiceLoaderLoad(Class<?> clazz) {
diff --git a/src/main/java/com/android/tools/r8/R8Assistant.java b/src/main/java/com/android/tools/r8/R8Assistant.java
index bbcba15..d340c28 100644
--- a/src/main/java/com/android/tools/r8/R8Assistant.java
+++ b/src/main/java/com/android/tools/r8/R8Assistant.java
@@ -32,6 +32,11 @@
 
   public static void run(R8AssistantCommand command) throws CompilationFailedException {
     InternalOptions options = command.getInternalOptions();
+    runForTest(command, options);
+  }
+
+  public static void runForTest(R8AssistantCommand command, InternalOptions options)
+      throws CompilationFailedException {
     ExceptionUtils.withCompilationHandler(
         options.reporter,
         () -> runInternal(command, options, ThreadUtils.getExecutorService(options)));
diff --git a/src/main/java/com/android/tools/r8/R8AssistantCommand.java b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
index 55efbd7..c939563 100644
--- a/src/main/java/com/android/tools/r8/R8AssistantCommand.java
+++ b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
@@ -5,6 +5,8 @@
 
 import com.android.tools.r8.assistant.ClassInjectionHelper;
 import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationLogger;
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver;
 import com.android.tools.r8.assistant.runtime.ReflectiveOracle;
@@ -148,6 +150,8 @@
     R8AssistantCommand makeCommand() {
       injectClasses(
           EmptyReflectiveOperationReceiver.class,
+          ReflectiveEventType.class,
+          ReflectiveOperationJsonLogger.class,
           ReflectiveOperationLogger.class,
           ReflectiveOperationReceiver.NameLookupType.class,
           ReflectiveOperationReceiver.ClassFlag.class,
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
new file mode 100644
index 0000000..f1a4a73
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
@@ -0,0 +1,55 @@
+// 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;
+
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReflectiveOperationJsonParser {
+
+  private final DexItemFactory factory;
+
+  public ReflectiveOperationJsonParser(DexItemFactory factory) {
+    this.factory = factory;
+  }
+
+  public List<ReflectiveEvent> parse(Path file) throws IOException {
+    List<ReflectiveEvent> result = new ArrayList<>();
+    String contents = Files.readString(file) + "{}]";
+    JsonArray events = new JsonParser().parse(contents).getAsJsonArray();
+    for (JsonElement eventElement : events) {
+      JsonObject event = eventElement.getAsJsonObject();
+      if (event.isEmpty()) {
+        break;
+      }
+      ReflectiveEventType eventType = ReflectiveEventType.valueOf(event.get("event").getAsString());
+      JsonElement stackElement = event.get("stack");
+      String[] stack = stackElement != null ? toStringArray(stackElement) : null;
+      JsonElement argsElement = event.get("args");
+      String[] args = argsElement != null ? toStringArray(argsElement) : null;
+      result.add(ReflectiveEvent.instantiate(eventType, stack, args, factory));
+    }
+    return result;
+  }
+
+  private String[] toStringArray(JsonElement argsElement) {
+    JsonArray jsonArray = argsElement.getAsJsonArray();
+    String[] strings = new String[jsonArray.size()];
+    for (int i = 0; i < strings.length; i++) {
+      strings[i] = jsonArray.get(i).getAsString();
+    }
+    return strings;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/AtomicFieldUpdaterNewUpdater.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/AtomicFieldUpdaterNewUpdater.java
new file mode 100644
index 0000000..d2d7678
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/AtomicFieldUpdaterNewUpdater.java
@@ -0,0 +1,54 @@
+// 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.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+
+public class AtomicFieldUpdaterNewUpdater extends ReflectiveEvent {
+  private final DexField field;
+
+  protected AtomicFieldUpdaterNewUpdater(
+      ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+    super(eventType, stack);
+    assert args.length == 3;
+    DexType type = toType(args[0], factory);
+    DexType holder = toType(args[1], factory);
+    String fieldName = args[2];
+    field = factory.createField(holder, type, fieldName);
+  }
+
+  public DexField getField() {
+    return field;
+  }
+
+  @Override
+  public boolean isAtomicFieldUpdaterNewUpdater() {
+    return true;
+  }
+
+  @Override
+  public AtomicFieldUpdaterNewUpdater asAtomicFieldUpdaterNewUpdater() {
+    return this;
+  }
+
+  @Override
+  public String getContentsString() {
+    return field.toString();
+  }
+
+  @Override
+  public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+    KeepFieldInfo keepFieldInfo =
+        keepInfoCollectionExported.getKeepFieldInfo(field.asFieldReference());
+    // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+    //  to be preserved.
+    return keepFieldInfo != null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMember.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMember.java
new file mode 100644
index 0000000..a9c2597
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMember.java
@@ -0,0 +1,75 @@
+// 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.DexMember;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+import com.android.tools.r8.shaking.KeepMethodInfo;
+
+public class ClassGetMember extends ReflectiveEvent {
+
+  private final DexMember<?, ?> member;
+
+  protected ClassGetMember(
+      ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+    super(eventType, stack);
+    DexType type = toTypeOrTripleStar(args[0], factory);
+    DexType holder = toType(args[1], factory);
+    String name = args[2];
+    if (eventType == ReflectiveEventType.CLASS_GET_FIELD
+        || eventType == ReflectiveEventType.CLASS_GET_DECLARED_FIELD) {
+      member = factory.createField(holder, type, name);
+      return;
+    }
+    assert eventType == ReflectiveEventType.CLASS_GET_METHOD
+        || eventType == ReflectiveEventType.CLASS_GET_DECLARED_METHOD
+        || eventType == ReflectiveEventType.CLASS_GET_CONSTRUCTOR
+        || eventType == ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTOR;
+    DexType[] typeArgs = new DexType[args.length - 3];
+    for (int i = 3; i < args.length; i++) {
+      typeArgs[i - 3] = toType(args[i], factory);
+    }
+    member = factory.createMethod(holder, factory.createProto(type, typeArgs), name);
+  }
+
+  public DexMember<?, ?> getMember() {
+    return member;
+  }
+
+  @Override
+  public boolean isClassGetMember() {
+    return true;
+  }
+
+  @Override
+  public ClassGetMember asClassGetMember() {
+    return this;
+  }
+
+  @Override
+  public String getContentsString() {
+    return member.toSourceString();
+  }
+
+  @Override
+  public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+    if (member.isDexField()) {
+      KeepFieldInfo keepFieldInfo =
+          keepInfoCollectionExported.getKeepFieldInfo(member.asDexField().asFieldReference());
+      // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+      //  to be preserved.
+      return keepFieldInfo != null;
+    }
+    KeepMethodInfo keepMethodInfo =
+        keepInfoCollectionExported.getKeepMethodInfo(member.asDexMethod().asMethodReference());
+    // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+    //  to be preserved.
+    return keepMethodInfo != null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMembers.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMembers.java
new file mode 100644
index 0000000..dc41cf4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMembers.java
@@ -0,0 +1,46 @@
+// 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 ClassGetMembers extends ReflectiveEvent {
+
+  private final DexType holder;
+
+  protected ClassGetMembers(
+      ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+    super(eventType, stack);
+    holder = toType(args[0], factory);
+  }
+
+  public DexType getHolder() {
+    return holder;
+  }
+
+  @Override
+  public boolean isClassGetMembers() {
+    return true;
+  }
+
+  @Override
+  public ClassGetMembers asClassGetMembers() {
+    return this;
+  }
+
+  @Override
+  public String getContentsString() {
+    return holder.toSourceString();
+  }
+
+  @Override
+  public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+    // TODO(b/428836085): What does this mean? One member has to be kept?
+    return keepInfoCollectionExported.getKeepClassInfo(holder.asTypeReference()) != null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetName.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetName.java
new file mode 100644
index 0000000..d8f599b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetName.java
@@ -0,0 +1,55 @@
+// 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.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+
+public class ClassGetName extends ReflectiveEvent {
+
+  private final DexType type;
+  private final NameLookupType nameLookupType;
+
+  protected ClassGetName(
+      ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+    super(eventType, stack);
+    assert args.length == 2;
+    type = toType(args[0], factory);
+    nameLookupType = NameLookupType.valueOf(args[1]);
+  }
+
+  @Override
+  public boolean isClassGetName() {
+    return true;
+  }
+
+  @Override
+  public ClassGetName asClassGetName() {
+    return this;
+  }
+
+  public DexType getType() {
+    return type;
+  }
+
+  public NameLookupType getNameLookupType() {
+    return nameLookupType;
+  }
+
+  @Override
+  public String getContentsString() {
+    return type + ", " + nameLookupType;
+  }
+
+  @Override
+  public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+    // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+    //  to be preserved.
+    return keepInfoCollectionExported.getKeepClassInfo(type.asTypeReference()) != null;
+  }
+}
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
new file mode 100644
index 0000000..dc49336
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
@@ -0,0 +1,138 @@
+// 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;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.Arrays;
+
+public abstract class ReflectiveEvent {
+
+  private final ReflectiveEventType eventType;
+  private final String[] stack;
+
+  protected static DexType toTypeOrTripleStar(String javaType, DexItemFactory factory) {
+    if (javaType == null) {
+      return factory.createType("***");
+    }
+    return toType(javaType, factory);
+  }
+
+  protected static DexType toType(String javaType, DexItemFactory factory) {
+    return factory.createType(DescriptorUtils.javaTypeToDescriptor(javaType));
+  }
+
+  protected ReflectiveEvent(ReflectiveEventType eventType, String[] stack) {
+    this.eventType = eventType;
+    this.stack = stack;
+  }
+
+  public ReflectiveEventType getEventType() {
+    return eventType;
+  }
+
+  public boolean isAtomicFieldUpdaterNewUpdater() {
+    return false;
+  }
+
+  public AtomicFieldUpdaterNewUpdater asAtomicFieldUpdaterNewUpdater() {
+    return null;
+  }
+
+  public boolean isClassGetName() {
+    return false;
+  }
+
+  public ClassGetName asClassGetName() {
+    return null;
+  }
+
+  public boolean isClassGetMember() {
+    return false;
+  }
+
+  public ClassGetMember asClassGetMember() {
+    return null;
+  }
+
+  public boolean isClassGetMembers() {
+    return false;
+  }
+
+  public ClassGetMembers asClassGetMembers() {
+    return null;
+  }
+
+  public abstract String getContentsString();
+
+  public abstract boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported);
+
+  @Override
+  public String toString() {
+    return eventType + (stack != null ? "[s]" : "") + "(" + getContentsString() + ")";
+  }
+
+  public static ReflectiveEvent instantiate(
+      ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+    switch (eventType) {
+      case CLASS_NEW_INSTANCE:
+        break;
+      case CLASS_GET_DECLARED_METHOD:
+      case CLASS_GET_DECLARED_FIELD:
+      case CLASS_GET_DECLARED_CONSTRUCTOR:
+      case CLASS_GET_METHOD:
+      case CLASS_GET_FIELD:
+      case CLASS_GET_CONSTRUCTOR:
+        return new ClassGetMember(eventType, stack, args, factory);
+      case CLASS_GET_DECLARED_METHODS:
+      case CLASS_GET_DECLARED_FIELDS:
+      case CLASS_GET_DECLARED_CONSTRUCTORS:
+      case CLASS_GET_METHODS:
+      case CLASS_GET_FIELDS:
+      case CLASS_GET_CONSTRUCTORS:
+        return new ClassGetMembers(eventType, stack, args, factory);
+      case CLASS_GET_NAME:
+        return new ClassGetName(eventType, stack, args, factory);
+      case CLASS_FOR_NAME:
+        break;
+      case CLASS_GET_COMPONENT_TYPE:
+        break;
+      case CLASS_GET_PACKAGE:
+        break;
+      case CLASS_IS_ASSIGNABLE_FROM:
+        break;
+      case CLASS_GET_SUPERCLASS:
+        break;
+      case CLASS_AS_SUBCLASS:
+        break;
+      case CLASS_IS_INSTANCE:
+        break;
+      case CLASS_CAST:
+        break;
+      case CLASS_FLAG:
+        break;
+      case ATOMIC_FIELD_UPDATER_NEW_UPDATER:
+        return new AtomicFieldUpdaterNewUpdater(eventType, stack, args, factory);
+      case SERVICE_LOADER_LOAD:
+        break;
+      case PROXY_NEW_PROXY_INSTANCE:
+        break;
+    }
+    return new ReflectiveEvent(eventType, stack) {
+      @Override
+      public String getContentsString() {
+        return Arrays.toString(args);
+      }
+
+      @Override
+      public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+        return false;
+      }
+    };
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index 67b2e25..9571467 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -137,7 +137,7 @@
   public CfPrinter(CfCode code, DexEncodedMethod method, RetracerForCodePrinting retracer) {
     this.retracer = retracer;
     indent = "  ";
-    instructionIndexSpace = ("" + code.getInstructions().size()).length();
+    instructionIndexSpace = ("" + code.getInstructionCount()).length();
     labelToIndex = new Reference2IntOpenHashMap<>();
     sortedLabels = new ArrayList<>();
     for (CfInstruction instruction : code.getInstructions()) {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
index 86ea3c0..afb1260 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
@@ -97,7 +97,7 @@
     // Linear scan over instructions.
     CfFrameState state = initialState.asContinue().getValue();
     int actualInstructionIndexForReporting = 0;
-    for (int i = 0; i < code.getInstructions().size(); i++) {
+    for (int i = 0; i < code.getInstructionCount(); i++) {
       CfInstruction instruction = code.getInstruction(i);
       assert !state.isError();
       if (instruction.isLabel()) {
@@ -125,7 +125,7 @@
       state = instruction.evaluate(state, appView, config);
       if (instruction.isJumpWithNormalTarget()) {
         CfInstruction fallthroughInstruction =
-            (i + 1) < code.getInstructions().size() ? code.getInstruction(i + 1) : null;
+            (i + 1) < code.getInstructionCount() ? code.getInstruction(i + 1) : null;
         TraversalContinuation<CfCodeDiagnostics, CfFrameState> traversalContinuation =
             instruction.traverseNormalTargets(
                 (target, currentState) -> {
@@ -336,10 +336,10 @@
     if (!instruction.isJump()) {
       return TraversalContinuation.doContinue(state);
     }
-    if (instructionIndex == code.getInstructions().size() - 1) {
+    if (instructionIndex == code.getInstructionCount() - 1) {
       return TraversalContinuation.doContinue(CfFrameState.bottom());
     }
-    if (instructionIndex == code.getInstructions().size() - 2
+    if (instructionIndex == code.getInstructionCount() - 2
         && code.getInstruction(instructionIndex + 1).isLabel()) {
       return TraversalContinuation.doContinue(CfFrameState.bottom());
     }
@@ -375,7 +375,7 @@
     if (!isReturnOrThrow) {
       return false;
     }
-    for (int i = code.getInstructions().size() - 1; i >= 0; i--) {
+    for (int i = code.getInstructionCount() - 1; i >= 0; i--) {
       CfInstruction instr = code.getInstruction(i);
       if (instr == instruction) {
         return true;
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 1e5c7d9..da40bbf 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -42,6 +42,7 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
 import com.android.tools.r8.metadata.impl.BuildMetadataFactory;
+import com.android.tools.r8.metadata.impl.R8StatsMetadataImpl;
 import com.android.tools.r8.naming.KotlinModuleSynthesizer;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapId;
@@ -655,6 +656,8 @@
       assert appView.hasClassHierarchy();
       options.r8BuildMetadataConsumer.accept(
           BuildMetadataFactory.create(appView.withClassHierarchy(), virtualFiles));
+    } else if (appView.hasClassHierarchy()) {
+      assert R8StatsMetadataImpl.Counters.create(appView.withClassHierarchy()).validate();
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index b9af38e..0a8556c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -37,6 +37,7 @@
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
 import com.android.tools.r8.ir.optimize.library.LibraryMemberOptimizer;
 import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutliner;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.SeedMapper;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
@@ -138,6 +139,7 @@
   private ArgumentPropagator argumentPropagator;
   private final LibraryMemberOptimizer libraryMemberOptimizer;
   private final ProtoShrinker protoShrinker;
+  private ThrowBlockOutliner throwBlockOutliner;
 
   // Optimization results.
   private boolean allCodeProcessed = false;
@@ -218,6 +220,7 @@
     if (enableWholeProgramOptimizations() && options().isOptimizedResourceShrinking()) {
       resourceShrinkerState = ResourceShrinkerUtils.createResourceShrinkerState(this);
     }
+    this.throwBlockOutliner = ThrowBlockOutliner.create(this);
     timing.end();
     this.libraryMethodSideEffectModelCollection =
         timing.time("Library side-effects", () -> new LibraryMethodSideEffectModelCollection(this));
@@ -586,6 +589,17 @@
     }
   }
 
+  public void unsetThrowBlockOutliner() {
+    throwBlockOutliner = null;
+  }
+
+  public <E extends Throwable> void withThrowBlockOutliner(
+      ThrowingConsumer<ThrowBlockOutliner, E> consumer) throws E {
+    if (throwBlockOutliner != null) {
+      consumer.accept(throwBlockOutliner);
+    }
+  }
+
   public LibraryMemberOptimizer libraryMethodOptimizer() {
     return libraryMemberOptimizer;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 0b3838a..a7a6922 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -40,6 +40,7 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.HashingVisitor;
@@ -293,7 +294,11 @@
   }
 
   public List<CfInstruction> getInstructions() {
-    return Collections.unmodifiableList(instructions);
+    return ListUtils.unmodifiableForTesting(instructions);
+  }
+
+  public int getInstructionCount() {
+    return instructions.size();
   }
 
   public void setInstructions(List<CfInstruction> instructions) {
@@ -301,7 +306,7 @@
   }
 
   public List<LocalVariableInfo> getLocalVariables() {
-    return Collections.unmodifiableList(localVariables);
+    return ListUtils.unmodifiableForTesting(localVariables);
   }
 
   @Override
@@ -668,7 +673,7 @@
       MutableMethodConversionOptions conversionOptions) {
     try {
       return internalBuild(
-          Collections.unmodifiableList(localVariables),
+          ListUtils.unmodifiableForTesting(localVariables),
           context,
           method,
           appView,
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
index 229e8c0..a19de7a 100644
--- a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
@@ -124,7 +124,7 @@
         || !cfCode.getTryCatchRanges().isEmpty()) {
       return false;
     }
-    if (cfCode.getInstructions().size() > 6) {
+    if (cfCode.getInstructionCount() > 6) {
       // Default instance initializers typically have the following instruction sequence:
       // [CfLabel, CfPosition, CfLoad, CfInvoke, CfReturnVoid, CfLabel].
       return false;
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 40e474c..3b71570 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.analysis.type.Nullability;
@@ -85,10 +86,22 @@
     return DynamicType.create(appView, toTypeElement(appView, nullability));
   }
 
+  public ClassTypeElement toNonNullClassTypeElement(AppView<?> appView) {
+    return toClassTypeElement(appView, Nullability.definitelyNotNull());
+  }
+
   public TypeElement toNonNullTypeElement(AppView<?> appView) {
     return toTypeElement(appView, Nullability.definitelyNotNull());
   }
 
+  public ClassTypeElement toClassTypeElement(AppView<?> appView) {
+    return toClassTypeElement(appView, Nullability.maybeNull());
+  }
+
+  public ClassTypeElement toClassTypeElement(AppView<?> appView, Nullability nullability) {
+    return TypeElement.fromDexClassType(this, nullability, appView);
+  }
+
   public TypeElement toTypeElement(AppView<?> appView) {
     return toTypeElement(appView, Nullability.maybeNull());
   }
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
index 5f99150..77a4cd8 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
@@ -174,8 +174,8 @@
 
   @SuppressWarnings("ReferenceEquality")
   private boolean hasJavacClinitAssertionCode(CfCode code) {
-    for (int i = 0; i < code.getInstructions().size(); i++) {
-      CfInstruction instruction = code.getInstructions().get(i);
+    for (int i = 0; i < code.getInstructionCount(); i++) {
+      CfInstruction instruction = code.getInstruction(i);
       if (instruction.isInvoke()) {
         // Check for the generated instruction sequence by looking for the call to
         // desiredAssertionStatus() followed by the expected instruction types and finally checking
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
index 064b2e0..9a07dbb 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
@@ -1,7 +1,6 @@
 // Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-
 package com.android.tools.r8.horizontalclassmerging.policies;
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -159,7 +158,11 @@
 
     DexMethodSignatureSet signatures = DexMethodSignatureSet.createLinked();
     for (DexProgramClass clazz : group) {
-      signatures.addAllMethods(clazz.methods());
+      for (DexEncodedMethod method : clazz.methods()) {
+        if (!method.isInitializer()) {
+          signatures.add(method);
+        }
+      }
     }
 
     Map<DispatchSignature, HorizontalMergeGroup> newGroups = new LinkedHashMap<>();
@@ -182,10 +185,4 @@
     }
     return removeTrivialGroups(newGroups.values());
   }
-
-  @Override
-  public boolean shouldSkipPolicy() {
-    return appView.options().isGeneratingDex()
-        && !appView.options().canUseDefaultAndStaticInterfaceMethods();
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java
index dc0c539..b12e8cb 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java
@@ -41,8 +41,8 @@
 
   public CfInstruction getFallthroughInstruction(CfCode code) {
     int fallthroughInstructionIndex = getLastInstructionIndex() + 1;
-    return fallthroughInstructionIndex < code.getInstructions().size()
-        ? code.getInstructions().get(fallthroughInstructionIndex)
+    return fallthroughInstructionIndex < code.getInstructionCount()
+        ? code.getInstruction(fallthroughInstructionIndex)
         : null;
   }
 
@@ -59,7 +59,7 @@
   }
 
   public CfInstruction getLastInstruction(CfCode code) {
-    return code.getInstructions().get(lastInstructionIndex);
+    return code.getInstruction(lastInstructionIndex);
   }
 
   public int getLastInstructionIndex() {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java
index 919e8d7..2d66d50 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java
@@ -281,7 +281,7 @@
     }
 
     private void removeBlockForTrailingLabel() {
-      CfInstruction lastInstruction = code.getInstruction(code.getInstructions().size() - 1);
+      CfInstruction lastInstruction = code.getInstruction(code.getInstructionCount() - 1);
       if (lastInstruction.isLabel() && isBlockEntry(lastInstruction)) {
         blocks.remove(lastInstruction);
       }
@@ -292,7 +292,7 @@
     }
 
     private boolean isBlockExit(int instructionIndex) {
-      int lastInstructionIndex = code.getInstructions().size() - 1;
+      int lastInstructionIndex = code.getInstructionCount() - 1;
       if (instructionIndex == lastInstructionIndex) {
         return true;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
index 3f1fc3d..2b4ce3b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
@@ -432,6 +432,15 @@
     return fromDexType(appView.dexItemFactory().stringType, nullability, appView).asClassType();
   }
 
+  public static ClassTypeElement fromDexClassType(
+      DexType type, Nullability nullability, AppView<?> appView) {
+    assert type.isClassType();
+    return appView
+        .dexItemFactory()
+        .createReferenceTypeElement(type, nullability, appView)
+        .asClassType();
+  }
+
   public static TypeElement fromDexType(DexType type, Nullability nullability, AppView<?> appView) {
     return fromDexType(type, nullability, appView, false);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 4059618..c21dc95 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -58,6 +58,7 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Stream;
 
 /**
  * Basic block abstraction.
@@ -813,6 +814,10 @@
         };
   }
 
+  public Stream<Instruction> streamInstructions() {
+    return getInstructions().stream();
+  }
+
   public boolean isEmpty() {
     return instructions.isEmpty();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java b/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
index 2e48a35..d23c1d9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
@@ -365,6 +365,11 @@
   }
 
   @Override
+  public T visit(ThrowBlockOutlineMarker instruction) {
+    return null;
+  }
+
+  @Override
   public T visit(UnusedArgument instruction) {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 3f61db3..f6fa052 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -167,6 +168,14 @@
     return method;
   }
 
+  public DexMethod reference() {
+    return context().getReference();
+  }
+
+  public boolean isD8R8Synthesized() {
+    return context().getDefinition().isD8R8Synthesized();
+  }
+
   @Deprecated
   public DexEncodedMethod method() {
     return method.getDefinition();
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index 9941e6d..80de495 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -318,6 +318,10 @@
     return result;
   }
 
+  public boolean mayHaveThrowBlockOutlineMarker() {
+    return get(Opcodes.THROW_BLOCK_OUTLINE_MARKER);
+  }
+
   public boolean mayHaveUshr() {
     return get(Opcodes.USHR);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index e410c59..0733f34 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -99,6 +99,18 @@
     return next;
   }
 
+  @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
+  public <T extends Instruction> T nextUntilInclusive(Predicate<Instruction> predicate) {
+    Instruction current = this;
+    do {
+      if (predicate.test(current)) {
+        return (T) current;
+      }
+      current = current.getNext();
+    } while (current != null);
+    return null;
+  }
+
   @Override
   public final Position getPosition() {
     assert position != null;
@@ -1198,6 +1210,14 @@
     return null;
   }
 
+  public boolean isThrowBlockOutlineMarker() {
+    return false;
+  }
+
+  public ThrowBlockOutlineMarker asThrowBlockOutlineMarker() {
+    return null;
+  }
+
   public boolean isStaticFieldInstruction() {
     return false;
   }
@@ -1434,6 +1454,10 @@
     return null;
   }
 
+  public InvokeDirect asInvokeConstructor(DexItemFactory dexItemFactory) {
+    return null;
+  }
+
   public boolean isInvokeConstructor(DexItemFactory dexItemFactory) {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java b/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
index d54b02f..16f398f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
@@ -148,6 +148,8 @@
 
   T visit(Throw instruction);
 
+  T visit(ThrowBlockOutlineMarker instruction);
+
   T visit(UnusedArgument instruction);
 
   T visit(Ushr instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index c448a2d..7ec0d42 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -124,6 +124,11 @@
   }
 
   @Override
+  public InvokeDirect asInvokeConstructor(DexItemFactory dexItemFactory) {
+    return isInvokeConstructor(dexItemFactory) ? this : null;
+  }
+
+  @Override
   public boolean isInvokeConstructor(DexItemFactory dexItemFactory) {
     return getInvokedMethod().isInstanceInitializer(dexItemFactory);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Opcodes.java b/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
index 64d8f37..a0bc2f2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
@@ -80,4 +80,5 @@
   int RESOURCE_CONST_NUMBER = 71;
   int ORIGINAL_FIELD_WITNESS = 72;
   int STORE_STORE_FENCE = 73;
+  int THROW_BLOCK_OUTLINE_MARKER = 74;
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java b/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java
new file mode 100644
index 0000000..bd119cb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java
@@ -0,0 +1,135 @@
+// 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.ir.code;
+
+import com.android.tools.r8.cf.LoadStoreHelper;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.CfBuilder;
+import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
+import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
+import com.android.tools.r8.lightir.LirBuilder;
+
+public class ThrowBlockOutlineMarker extends Instruction {
+
+  private final ThrowBlockOutline outline;
+
+  public ThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+    super(null);
+    this.outline = outline;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public ThrowBlockOutline getOutline() {
+    return outline;
+  }
+
+  @Override
+  public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
+    return DeadInstructionResult.notDead();
+  }
+
+  @Override
+  public int opcode() {
+    return Opcodes.THROW_BLOCK_OUTLINE_MARKER;
+  }
+
+  @Override
+  public <T> T accept(InstructionVisitor<T> visitor) {
+    return visitor.visit(this);
+  }
+
+  @Override
+  public void buildCf(CfBuilder builder) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    builder.addThrowBlockOutlineMarker(outline);
+  }
+
+  @Override
+  public void buildDex(DexBuilder builder) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean hasInvariantOutType() {
+    return true;
+  }
+
+  @Override
+  public ConstraintWithTarget inliningConstraint(
+      InliningConstraints inliningConstraints, ProgramMethod context) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public void insertLoadAndStores(LoadStoreHelper helper) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, ProgramMethod context) {
+    return false;
+  }
+
+  @Override
+  public boolean isThrowBlockOutlineMarker() {
+    return true;
+  }
+
+  @Override
+  public ThrowBlockOutlineMarker asThrowBlockOutlineMarker() {
+    return this;
+  }
+
+  @Override
+  public int maxInValueRegister() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public int maxOutValueRegister() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public String toString() {
+    return "ThrowBlockOutlineMarker";
+  }
+
+  @Override
+  public boolean identicalNonValueNonPositionParts(Instruction other) {
+    return false;
+  }
+
+  public static class Builder extends BuilderBase<Builder, ThrowBlockOutlineMarker> {
+
+    private ThrowBlockOutline outline;
+
+    public Builder setOutline(ThrowBlockOutline outline) {
+      this.outline = outline;
+      return this;
+    }
+
+    @Override
+    public ThrowBlockOutlineMarker build() {
+      return amend(new ThrowBlockOutlineMarker(outline));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index 54e7d5c..a3bfb70 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -231,8 +231,8 @@
     this.method = method;
     this.appView = appView;
     int cfPositionCount = 0;
-    for (int i = 0; i < code.getInstructions().size(); i++) {
-      CfInstruction instruction = code.getInstructions().get(i);
+    for (int i = 0; i < code.getInstructionCount(); i++) {
+      CfInstruction instruction = code.getInstruction(i);
       if (instruction instanceof CfLabel) {
         labelOffsets.put((CfLabel) instruction, instructionOffset(i));
       }
@@ -274,7 +274,7 @@
 
   @Override
   public int instructionCount() {
-    return code.getInstructions().size();
+    return code.getInstructionCount();
   }
 
   @Override
@@ -300,7 +300,7 @@
 
   @Override
   public int traceInstruction(int instructionIndex, IRBuilder builder) {
-    CfInstruction instruction = code.getInstructions().get(instructionIndex);
+    CfInstruction instruction = code.getInstruction(instructionIndex);
     assert appView.options().isGeneratingClassFiles()
         == internalOutputMode.isGeneratingClassFiles();
     if (instruction.canThrow()) {
@@ -355,7 +355,7 @@
   }
 
   private int[] getTargets(int instructionIndex) {
-    CfInstruction instruction = code.getInstructions().get(instructionIndex);
+    CfInstruction instruction = code.getInstruction(instructionIndex);
     assert isControlFlow(instruction);
     CfLabel target = instruction.getTarget();
     if (instruction.isReturn() || instruction instanceof CfThrow) {
@@ -517,7 +517,7 @@
     // ensure it is marked as such (via an explict 'end' marker) and thus be live in predecessors.
     // In this case we insert an 'end' point on all explicit goto instructions ensuring that any
     // back-edge will explicitly keep locals live at that point.
-    if (!hasExitingInstruction && code.getInstructions().get(predecessorOffset) instanceof CfGoto) {
+    if (!hasExitingInstruction && code.getInstruction(predecessorOffset) instanceof CfGoto) {
       assert !isExceptional;
       for (Entry<DebugLocalInfo> entry : atSource.int2ReferenceEntrySet()) {
         if (atTarget.get(entry.getIntKey()) == entry.getValue()) {
@@ -535,7 +535,7 @@
       buildExceptionalExitForMethodSynchronization(builder, instructionIndex);
       return;
     }
-    CfInstruction instruction = code.getInstructions().get(instructionIndex);
+    CfInstruction instruction = code.getInstruction(instructionIndex);
     currentInstructionIndex = instructionIndex;
     if (firstBlockInstruction) {
       currentBlockInfo = builder.getCFG().get(instructionIndex);
@@ -647,7 +647,7 @@
 
   private boolean isFirstFrameInBlock() {
     for (int i = currentBlockIndex; i < currentInstructionIndex; i++) {
-      CfInstruction cfInstruction = code.getInstructions().get(i);
+      CfInstruction cfInstruction = code.getInstruction(i);
       if (cfInstruction.isPosition() || cfInstruction.isLabel()) {
         continue;
       }
@@ -676,8 +676,8 @@
     if (type.isUninitializedNew()) {
       int labelOffset = getLabelOffset(type.getUninitializedLabel());
       int insnOffset = labelOffset + 1;
-      while (insnOffset < code.getInstructions().size()) {
-        CfInstruction instruction = code.getInstructions().get(insnOffset);
+      while (insnOffset < code.getInstructionCount()) {
+        CfInstruction instruction = code.getInstruction(insnOffset);
         if (!(instruction instanceof CfLabel)
             && !(instruction instanceof CfFrame)
             && !(instruction instanceof CfPosition)) {
@@ -686,7 +686,7 @@
         }
         insnOffset += 1;
       }
-      CfInstruction instruction = code.getInstructions().get(insnOffset);
+      CfInstruction instruction = code.getInstruction(insnOffset);
       assert instruction instanceof CfNew;
       return ((CfNew) instruction).getType();
     }
@@ -791,7 +791,7 @@
     if (inPrelude) {
       return getIncomingLocal(register);
     }
-    assert !isControlFlow(code.getInstructions().get(currentInstructionIndex))
+    assert !isControlFlow(code.getInstruction(currentInstructionIndex))
         : "Outgoing local is undefined for control-flow instructions";
     return outgoingLocals.get(register);
   }
@@ -802,7 +802,7 @@
       outgoingLocals = incomingLocals;
       return;
     }
-    CfInstruction currentInstruction = code.getInstructions().get(currentInstructionIndex);
+    CfInstruction currentInstruction = code.getInstruction(currentInstructionIndex);
     outgoingLocals =
         !isControlFlow(currentInstruction)
             ? getLocalVariables(currentInstructionIndex + 1).locals
@@ -897,7 +897,7 @@
     return isCurrentlyGeneratingMethodSynchronization()
         // In the prelude we may be materializing arguments from call sites in R8.
         || inPrelude
-        || code.getInstructions().get(currentInstructionIndex).canThrow();
+        || code.getInstruction(currentInstructionIndex).canThrow();
   }
 
   @Override
@@ -922,20 +922,20 @@
                   .collect(Collectors.toList()),
           method.getReference());
     }
-    while (offset + 1 < code.getInstructions().size()) {
-      CfInstruction insn = code.getInstructions().get(offset);
+    while (offset + 1 < code.getInstructionCount()) {
+      CfInstruction insn = code.getInstruction(offset);
       if (!(insn instanceof CfLabel) && !(insn instanceof CfFrame)) {
         break;
       }
       offset += 1;
     }
-    while (offset >= 0 && !(code.getInstructions().get(offset) instanceof CfPosition)) {
+    while (offset >= 0 && !(code.getInstruction(offset) instanceof CfPosition)) {
       offset -= 1;
     }
     if (offset < 0) {
       return canonicalPositions.getPreamblePosition();
     }
-    return getCanonicalPosition(((CfPosition) code.getInstructions().get(offset)).getPosition());
+    return getCanonicalPosition(((CfPosition) code.getInstruction(offset)).getPosition());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 8d39236..a83f45f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -816,6 +816,8 @@
     assert code.verifyNoNullabilityBottomTypes();
     assert code.verifyTypes(appView);
 
+    appView.withThrowBlockOutliner(outliner -> outliner.scan(code));
+
     previous = printMethod(code, "Optimized IR (SSA)", previous);
     timing.begin("Finalize IR");
     finalizeIR(code, feedback, bytecodeMetadataProviderBuilder.build(), timing, previous);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
index f514b6e..51ef202 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
@@ -90,7 +90,7 @@
 
   public static class MutableMethodConversionOptions extends MethodConversionOptions {
 
-    private final Target target;
+    private Target target;
     private boolean finalizeAfterLensCodeRewriter;
 
     private MutableMethodConversionOptions(Target target) {
@@ -102,6 +102,12 @@
       return this;
     }
 
+    public MutableMethodConversionOptions setIsGeneratingLir() {
+      assert isGeneratingDex();
+      target = Target.LIR;
+      return this;
+    }
+
     @Override
     public boolean isGeneratingLir() {
       return target == Target.LIR;
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 29925a7..5f63174 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
@@ -281,6 +281,9 @@
         ClassConverter.create(appView, this, methodProcessor, interfaceProcessor)
             .convertClasses(executorService, timing);
 
+    // Process computed throw outlines.
+    appView.withThrowBlockOutliner(outliner -> outliner.tearDownScanner(executorService));
+
     // The synthesis of accessibility bridges in nest based access desugaring will schedule and
     // await the processing of synthesized methods.
     instructionDesugaring.processClasspath(methodProcessor, executorService, timing);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java
new file mode 100644
index 0000000..dc871e1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java
@@ -0,0 +1,101 @@
+// 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.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirConstant;
+import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.google.common.collect.ConcurrentHashMultiset;
+import com.google.common.collect.Multiset;
+
+public class ThrowBlockOutline implements LirConstant {
+
+  @SuppressWarnings("UnusedVariable")
+  private final LirCode<?> lirCode;
+  private final Multiset<DexMethod> users = ConcurrentHashMultiset.create();
+
+  private ProgramMethod materializedOutlineMethod;
+
+  ThrowBlockOutline(LirCode<?> lirCode) {
+    this.lirCode = lirCode;
+  }
+
+  public void addUser(DexMethod user) {
+    users.add(user);
+  }
+
+  @Override
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.THROW_BLOCK_OUTLINE;
+  }
+
+  public ProgramMethod getMaterializedOutlineMethod() {
+    return materializedOutlineMethod;
+  }
+
+  public int getNumberOfUsers() {
+    return users.size();
+  }
+
+  public ProgramMethod getSynthesizingContext(AppView<?> appView) {
+    DexMethod shortestUser = null;
+    for (DexMethod user : users) {
+      if (shortestUser == null) {
+        shortestUser = user;
+      } else {
+        int userLength = user.getHolderType().getDescriptor().length();
+        int shortestUserLength = shortestUser.getHolderType().getDescriptor().length();
+        if (userLength < shortestUserLength) {
+          shortestUser = user;
+        } else if (userLength == shortestUserLength && user.compareTo(shortestUser) < 0) {
+          shortestUser = user;
+        }
+      }
+    }
+    assert shortestUser != null;
+    return appView.definitionFor(shortestUser).asProgramMethod();
+  }
+
+  public Multiset<DexMethod> getUsers() {
+    return users;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    throw new Unreachable();
+  }
+
+  public boolean isMaterialized() {
+    return materializedOutlineMethod != null;
+  }
+
+  public void materialize(AppView<?> appView, MethodProcessingContext methodProcessingContext) {
+    SyntheticItems syntheticItems = appView.getSyntheticItems();
+    DexProto emptyProto = appView.dexItemFactory().objectMembers.constructor.getProto();
+    materializedOutlineMethod =
+        syntheticItems.createMethod(
+            kinds -> kinds.THROW_BLOCK_OUTLINE,
+            methodProcessingContext.createUniqueContext(),
+            appView,
+            builder ->
+                builder
+                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                    .setCode(methodSig -> lirCode)
+                    .setProto(emptyProto));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java
new file mode 100644
index 0000000..e69cdba
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java
@@ -0,0 +1,111 @@
+// 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.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockInstructionListIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRFinalizer;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.timing.Timing;
+
+/** Rewriter that processes {@link ThrowBlockOutlineMarker} instructions. */
+public class ThrowBlockOutlineMarkerRewriter {
+
+  private final AppView<?> appView;
+  private final DeadCodeRemover deadCodeRemover;
+
+  ThrowBlockOutlineMarkerRewriter(AppView<?> appView) {
+    this.appView = appView;
+    this.deadCodeRemover = new DeadCodeRemover(appView);
+  }
+
+  public void processMethod(ProgramMethod method) {
+    assert method.getDefinition().hasCode();
+    assert method.getDefinition().getCode().isLirCode();
+    // Build IR.
+    LirCode<?> lirCode = method.getDefinition().getCode().asLirCode();
+    IRCode code = lirCode.buildIR(method, appView);
+    assert code.getConversionOptions().isGeneratingDex();
+
+    // Process IR.
+    processOutlineMarkers(code);
+
+    // Convert to DEX.
+    IRFinalizer<?> finalizer = code.getConversionOptions().getFinalizer(deadCodeRemover, appView);
+    Code dexCode = finalizer.finalizeCode(code, BytecodeMetadataProvider.empty(), Timing.empty());
+    method.setCode(dexCode, appView);
+  }
+
+  // TODO(b/434769547): This simply removes all outline markers. We should materialize the outlines
+  //  that have enough uses and rewrite the corresponding markers to call the materialized outline
+  //  methods.
+  private void processOutlineMarkers(IRCode code) {
+    for (BasicBlock block : code.getBlocks()) {
+      Throw throwInstruction = block.exit().asThrow();
+      if (throwInstruction != null) {
+        ThrowBlockOutlineMarker outlineMarker =
+            block.entry().nextUntilInclusive(Instruction::isThrowBlockOutlineMarker);
+        if (outlineMarker != null) {
+          ThrowBlockOutline outline = outlineMarker.getOutline();
+          if (outline.isMaterialized()) {
+            // Insert a call to the materialized outline method and load the return value.
+            BasicBlockInstructionListIterator instructionIterator =
+                block.listIterator(outlineMarker);
+            instructionIterator.add(
+                InvokeStatic.builder()
+                    .setIsInterface(false)
+                    .setMethod(outline.getMaterializedOutlineMethod())
+                    .setPosition(throwInstruction)
+                    .build());
+            Value returnValue = addReturnValue(code, instructionIterator);
+
+            // Replace the throw instruction by a normal return.
+            Return returnInstruction =
+                Return.builder().setPosition(Position.none()).setReturnValue(returnValue).build();
+            block.replaceLastInstruction(returnInstruction);
+
+            // Remove all outlined instructions bottom up.
+            instructionIterator = block.listIterator(returnInstruction);
+            while (instructionIterator.previous() != outlineMarker) {
+              instructionIterator.removeOrReplaceByDebugLocalRead();
+            }
+          }
+
+          // Finally delete the outline marker.
+          outlineMarker.removeOrReplaceByDebugLocalRead();
+        }
+      }
+      assert block.streamInstructions().noneMatch(Instruction::isThrowBlockOutlineMarker);
+    }
+  }
+
+  private Value addReturnValue(IRCode code, BasicBlockInstructionListIterator instructionIterator) {
+    InternalOptions options = appView.options();
+    DexType returnType = code.context().getReturnType();
+    if (returnType.isVoidType()) {
+      return null;
+    } else if (returnType.isPrimitiveType()) {
+      return instructionIterator.insertConstNumberInstruction(
+          code, options, 0, returnType.toTypeElement(appView));
+    } else {
+      assert returnType.isReferenceType();
+      return instructionIterator.insertConstNullInstruction(code, options);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java
new file mode 100644
index 0000000..c323bca
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java
@@ -0,0 +1,164 @@
+// 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.ir.optimize.outliner.exceptions;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.lightir.LirConstant;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+public class ThrowBlockOutliner {
+
+  private final AppView<?> appView;
+
+  // Scans IR code during IR conversion. Responsible for computing candidate outlines.
+  private ThrowBlockOutlinerScanner scanner;
+
+  private ThrowBlockOutliner(AppView<?> appView) {
+    this.appView = appView;
+    this.scanner = new ThrowBlockOutlinerScanner(appView);
+  }
+
+  public static ThrowBlockOutliner create(AppView<?> appView) {
+    return appView.options().getThrowBlockOutlinerOptions().isEnabled(appView)
+        ? new ThrowBlockOutliner(appView)
+        : null;
+  }
+
+  public void scan(IRCode code) {
+    // Notify the scanner.
+    if (scanner != null) {
+      scanner.run(code);
+    }
+  }
+
+  public void tearDownScanner(ExecutorService executorService) throws ExecutionException {
+    // Unset the scanner, which is responsible for computing outline candidates.
+    assert scanner != null;
+    Collection<ThrowBlockOutline> outlines = scanner.getOutlines();
+    scanner = null;
+
+    // Create outlines.
+    materializeOutlines(outlines, executorService);
+    assert supplyOutlineConsumerForTesting(outlines);
+
+    // Convert LIR to DEX.
+    processMethods(outlines, executorService);
+
+    // TODO(b/434769547): Instead of unsetting the outliner here, we should compute a specification
+    //  of the outlining that needs to happen and the methods that need to be reprocessed.
+    appView.unsetThrowBlockOutliner();
+  }
+
+  private void materializeOutlines(
+      Collection<ThrowBlockOutline> outlines, ExecutorService executorService)
+      throws ExecutionException {
+    // Find the outlines that we need to synthesize from each method.
+    ProgramMethodMap<List<ThrowBlockOutline>> synthesizingContexts = ProgramMethodMap.create();
+    for (ThrowBlockOutline outline : outlines) {
+      if (outline.getUsers().size() > 1) {
+        ProgramMethod synthesizingContext = outline.getSynthesizingContext(appView);
+        synthesizingContexts
+            .computeIfAbsent(synthesizingContext, ignoreKey(ArrayList::new))
+            .add(outline);
+      }
+    }
+
+    // Sort the outlines per synthesizing context so that the synthesis order is deterministic.
+    // We use the constant pool index of the outline as sorting key.
+    synthesizingContexts.forEach(
+        (synthesizingContext, outlinesFromSynthesizingContext) -> {
+          if (outlinesFromSynthesizingContext.size() == 1) {
+            return;
+          }
+          LirConstant[] constantPool =
+              synthesizingContext.getDefinition().getCode().asLirCode().getConstantPool();
+          Reference2IntMap<ThrowBlockOutline> outlineConstantPoolIndices =
+              new Reference2IntOpenHashMap<>();
+          for (int i = 0; i < constantPool.length; i++) {
+            LirConstant constant = constantPool[i];
+            if (constant instanceof ThrowBlockOutline) {
+              outlineConstantPoolIndices.put((ThrowBlockOutline) constant, i);
+            }
+          }
+          assert outlinesFromSynthesizingContext.stream()
+              .allMatch(outlineConstantPoolIndices::containsKey);
+          ListUtils.destructiveSort(
+              outlinesFromSynthesizingContext,
+              Comparator.comparingInt(outlineConstantPoolIndices::getInt));
+        });
+
+    // Synthesize the outlines concurrently.
+    ProcessorContext processorContext = appView.createProcessorContext();
+    ThreadUtils.processMap(
+        synthesizingContexts,
+        (synthesizingContext, outlinesFromSynthesizingContext) -> {
+          MethodProcessingContext methodProcessingContext =
+              processorContext.createMethodProcessingContext(synthesizingContext);
+          for (ThrowBlockOutline outline : outlinesFromSynthesizingContext) {
+            outline.materialize(appView, methodProcessingContext);
+          }
+        },
+        appView.options().getThreadingModule(),
+        executorService);
+  }
+
+  private void processMethods(
+      Collection<ThrowBlockOutline> outlines, ExecutorService executorService)
+      throws ExecutionException {
+    ProgramMethodSet methodsToProcess = getMethodsToReprocess(outlines);
+    ThrowBlockOutlineMarkerRewriter rewriter = new ThrowBlockOutlineMarkerRewriter(appView);
+    ThreadUtils.processItems(
+        methodsToProcess,
+        rewriter::processMethod,
+        appView.options().getThreadingModule(),
+        executorService);
+  }
+
+  private ProgramMethodSet getMethodsToReprocess(Collection<ThrowBlockOutline> outlines) {
+    ProgramMethodSet methodsToProcess = ProgramMethodSet.create();
+    Set<DexMethod> seenUsers = Sets.newIdentityHashSet();
+    for (ThrowBlockOutline outline : outlines) {
+      for (DexMethod user : outline.getUsers()) {
+        if (seenUsers.add(user)) {
+          ProgramMethod methodToProcess = appView.definitionFor(user).asProgramMethod();
+          methodsToProcess.add(methodToProcess);
+        }
+      }
+      if (outline.getMaterializedOutlineMethod() != null) {
+        methodsToProcess.add(outline.getMaterializedOutlineMethod());
+      }
+    }
+    return methodsToProcess;
+  }
+
+  private boolean supplyOutlineConsumerForTesting(Collection<ThrowBlockOutline> outlines) {
+    Consumer<Collection<ThrowBlockOutline>> consumer =
+        appView.options().getThrowBlockOutlinerOptions().outlineConsumerForTesting;
+    if (consumer != null) {
+      consumer.accept(outlines);
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerLirCodeEquivalence.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerLirCodeEquivalence.java
new file mode 100644
index 0000000..5213ce9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerLirCodeEquivalence.java
@@ -0,0 +1,43 @@
+// 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.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.lightir.LirCode;
+import com.google.common.base.Equivalence;
+import java.util.Arrays;
+
+/** An equivalence for the simple type of LirCode produced by the ThrowBlockOutliner. */
+public class ThrowBlockOutlinerLirCodeEquivalence extends Equivalence<LirCode<?>> {
+
+  private static final ThrowBlockOutlinerLirCodeEquivalence INSTANCE =
+      new ThrowBlockOutlinerLirCodeEquivalence();
+
+  public static ThrowBlockOutlinerLirCodeEquivalence get() {
+    return INSTANCE;
+  }
+
+  @Override
+  protected boolean doEquivalent(LirCode<?> lirCode, LirCode<?> other) {
+    assert verifyLirCode(lirCode);
+    assert verifyLirCode(other);
+    return lirCode.getInstructionCount() == other.getInstructionCount()
+        && Arrays.equals(lirCode.getInstructionBytes(), other.getInstructionBytes())
+        && Arrays.equals(lirCode.getConstantPool(), other.getConstantPool());
+  }
+
+  @Override
+  protected int doHash(LirCode<?> lirCode) {
+    assert verifyLirCode(lirCode);
+    return 31 * (31 + Arrays.hashCode(lirCode.getConstantPool()))
+        + Arrays.hashCode(lirCode.getInstructionBytes());
+  }
+
+  private boolean verifyLirCode(LirCode<?> lirCode) {
+    assert !lirCode.hasArguments();
+    assert !lirCode.hasDebugLocalInfoTable();
+    assert !lirCode.hasPositionTable();
+    assert !lirCode.hasTryCatchTable();
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java
new file mode 100644
index 0000000..ec77462
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java
@@ -0,0 +1,26 @@
+// 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.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.AppView;
+import java.util.Collection;
+import java.util.function.Consumer;
+
+public class ThrowBlockOutlinerOptions {
+
+  public boolean enable = false;
+
+  public Consumer<Collection<ThrowBlockOutline>> outlineConsumerForTesting = null;
+
+  public boolean isEnabled(AppView<?> appView) {
+    if (!appView.options().isGeneratingDex() || !enable) {
+      return false;
+    }
+    if (appView.enableWholeProgramOptimizations()) {
+      throw new Unimplemented();
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java
new file mode 100644
index 0000000..ca72989
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java
@@ -0,0 +1,251 @@
+// 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.ir.optimize.outliner.exceptions;
+
+import static java.util.Collections.emptyList;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.NumberGenerator;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Position.SyntheticPosition;
+import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRToLirFinalizer;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.timing.Timing;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+public class ThrowBlockOutlinerScanner {
+
+  private static final IRMetadata metadata = IRMetadata.unknown();
+
+  private final AppView<?> appView;
+  private final DexItemFactory factory;
+
+  private final Map<Wrapper<LirCode<?>>, ThrowBlockOutline> outlines = new ConcurrentHashMap<>();
+
+  ThrowBlockOutlinerScanner(AppView<?> appView) {
+    this.appView = appView;
+    this.factory = appView.dexItemFactory();
+  }
+
+  public void run(IRCode code) {
+    assert !code.metadata().mayHaveThrowBlockOutlineMarker();
+    for (BasicBlock block : getThrowBlocks(code)) {
+      processThrowBlock(code, block);
+    }
+    if (code.metadata().mayHaveThrowBlockOutlineMarker()) {
+      assert code.getConversionOptions().isGeneratingDex();
+      code.mutateConversionOptions(MutableMethodConversionOptions::setIsGeneratingLir);
+    } else {
+      assert code.streamInstructions().noneMatch(Instruction::isThrowBlockOutlineMarker);
+    }
+  }
+
+  public Collection<ThrowBlockOutline> getOutlines() {
+    return outlines.values();
+  }
+
+  private List<BasicBlock> getThrowBlocks(IRCode code) {
+    boolean seenReturn = false;
+    List<BasicBlock> throwBlocks = new ArrayList<>();
+    for (BasicBlock block : code.getBlocks()) {
+      if (block.exit().isReturn()) {
+        seenReturn = true;
+      } else if (block.exit().isThrow()) {
+        throwBlocks.add(block);
+      }
+    }
+    // Never outline from methods that always throw.
+    return seenReturn ? throwBlocks : emptyList();
+  }
+
+  private void processThrowBlock(IRCode code, BasicBlock block) {
+    // Recursively build up the outline method. On successful outline creation, the resulting
+    // LirCode is passed to the continuation function.
+    processThrowInstruction(
+        block,
+        block.exit().asThrow(),
+        outlineBuilder -> {
+          // On successful outline creation, store the outline for later processing.
+          LirCode<?> lirCode = outlineBuilder.build(appView, code.context());
+          Wrapper<LirCode<?>> lirCodeWrapper =
+              ThrowBlockOutlinerLirCodeEquivalence.get().wrap(lirCode);
+          ThrowBlockOutline outline =
+              outlines.computeIfAbsent(lirCodeWrapper, w -> new ThrowBlockOutline(w.get()));
+          outline.addUser(code.reference());
+
+          // Insert a synthetic marker instruction that references the outline so that we know where
+          // to materialize the outline call.
+          Instruction insertionPoint = outlineBuilder.getFirstOutlinedInstruction();
+          assert insertionPoint.getBlock() == block;
+          ThrowBlockOutlineMarker marker =
+              ThrowBlockOutlineMarker.builder()
+                  .setOutline(outline)
+                  .setPosition(Position.none())
+                  .build();
+          block.listIterator(insertionPoint).add(marker);
+        });
+  }
+
+  private void processThrowInstruction(
+      BasicBlock throwBlock, Throw throwInstruction, Consumer<OutlineBuilder> continuation) {
+    Value exceptionValue = throwInstruction.exception();
+    if (!exceptionValue.isDefinedByInstructionSatisfying(
+        i -> i.isNewInstance() && i.getBlock() == throwBlock)) {
+      // Exception is not created in the throw block.
+      return;
+    }
+    assert throwInstruction.hasPrev();
+    processExceptionConstructorCall(
+        throwBlock,
+        throwInstruction.getPrev(),
+        outlineBuilder -> {
+          Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(exceptionValue);
+          outlineBuilder.add(
+              Throw.builder()
+                  .setExceptionValue(outlinedExceptionValue)
+                  .setPosition(Position.syntheticNone())
+                  .build());
+          continuation.accept(outlineBuilder);
+        });
+  }
+
+  private void processExceptionConstructorCall(
+      BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
+    InvokeDirect invoke = instruction.asInvokeConstructor(factory);
+    if (invoke == null) {
+      return;
+    }
+    DexMethod constructor = invoke.getInvokedMethod();
+    if (!constructor.getParameters().isEmpty()) {
+      // TODO(b/434769547): Handle constructors with arguments.
+      return;
+    }
+    processNewExceptionInstruction(
+        throwBlock,
+        invoke.getPrev(),
+        outlineBuilder -> {
+          Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(invoke.getReceiver());
+          outlineBuilder.add(
+              InvokeDirect.builder()
+                  .setMethod(constructor)
+                  .setPosition(Position.syntheticNone())
+                  .setSingleArgument(outlinedExceptionValue)
+                  .build());
+          continuation.accept(outlineBuilder);
+        });
+  }
+
+  private void processNewExceptionInstruction(
+      BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
+    NewInstance newInstance = instruction.asNewInstance();
+    if (newInstance == null) {
+      return;
+    }
+    // Check that this is the thrown exception.
+    if (newInstance.outValue() != throwBlock.exit().asThrow().exception()) {
+      return;
+    }
+    OutlineBuilder outlineBuilder = new OutlineBuilder(newInstance);
+    NewInstance outlinedNewInstance =
+        NewInstance.builder()
+            .setFreshOutValue(
+                outlineBuilder.valueNumberGenerator,
+                newInstance.getType().toNonNullClassTypeElement(appView))
+            .setType(newInstance.getType())
+            .setPosition(Position.syntheticNone())
+            .build();
+    outlineBuilder.add(outlinedNewInstance);
+    outlineBuilder.map(newInstance.outValue(), outlinedNewInstance.outValue());
+    continuation.accept(outlineBuilder);
+  }
+
+  private static class OutlineBuilder {
+
+    private final Instruction firstOutlinedInstruction;
+
+    private final BasicBlock outlinedBlock = new BasicBlock(metadata);
+
+    // Map from non-outlined values to their corresponding outlined values.
+    private final Map<Value, Value> outlinedValues = new IdentityHashMap<>();
+
+    private final NumberGenerator blockNumberGenerator = new NumberGenerator();
+    private final NumberGenerator valueNumberGenerator = new NumberGenerator();
+
+    OutlineBuilder(Instruction firstOutlinedInstruction) {
+      this.firstOutlinedInstruction = firstOutlinedInstruction;
+      outlinedBlock.setNumber(blockNumberGenerator.next());
+    }
+
+    void add(Instruction instruction) {
+      outlinedBlock.add(instruction, metadata);
+    }
+
+    void map(Value value, Value outlinedValue) {
+      assert !outlinedValues.containsKey(value);
+      outlinedValues.put(value, outlinedValue);
+    }
+
+    Instruction getFirstOutlinedInstruction() {
+      return firstOutlinedInstruction;
+    }
+
+    Value getOutlinedValue(Value value) {
+      Value outlinedValue = outlinedValues.get(value);
+      assert outlinedValue != null;
+      return outlinedValue;
+    }
+
+    LirCode<?> build(AppView<?> appView, ProgramMethod context) {
+      outlinedBlock.setFilled();
+      IRCode outlineCode =
+          new IRCode(
+              appView.options(),
+              null,
+              SyntheticPosition.syntheticNone(),
+              ListUtils.newLinkedList(outlinedBlock),
+              valueNumberGenerator,
+              blockNumberGenerator,
+              metadata,
+              MethodConversionOptions.forLirPhase(appView)) {
+
+            @Override
+            public DexMethod reference() {
+              return context.getReference();
+            }
+
+            @Override
+            public boolean isD8R8Synthesized() {
+              return true;
+            }
+          };
+      LirCode<?> lirCode =
+          new IRToLirFinalizer(appView)
+              .finalizeCode(outlineCode, BytecodeMetadataProvider.empty(), Timing.empty());
+      return lirCode;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
index a207282..8adf2d3 100644
--- a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
@@ -39,11 +39,7 @@
     this.strategy = strategy;
     this.bytecodeMetadataProvider = bytecodeMetadataProvider;
     this.builder =
-        new LirBuilder<>(
-                irCode.context().getReference(),
-                irCode.context().getDefinition().isD8R8Synthesized(),
-                strategy,
-                options)
+        new LirBuilder<>(irCode.reference(), irCode.isD8R8Synthesized(), strategy, options)
             .prepareForBytecodeInstructionMetadata(bytecodeMetadataProvider.size());
   }
 
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 01b5401..6890bd0 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -97,12 +97,14 @@
 import com.android.tools.r8.ir.code.StringSwitch;
 import com.android.tools.r8.ir.code.Sub;
 import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
 import com.android.tools.r8.ir.code.Ushr;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
 import com.android.tools.r8.lightir.LirBuilder.StringSwitchPayload;
 import com.android.tools.r8.lightir.LirCode.PositionEntry;
@@ -856,6 +858,11 @@
     }
 
     @Override
+    public void onThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+      addInstruction(new ThrowBlockOutlineMarker(outline));
+    }
+
+    @Override
     public void onReturnVoid() {
       addInstruction(new Return());
       closeCurrentBlock();
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index 5aedb4f..bdb4c85 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
 import com.android.tools.r8.lightir.LirCode.DebugLocalInfoTable;
 import com.android.tools.r8.lightir.LirCode.LinePositionEntry;
 import com.android.tools.r8.lightir.LirCode.PositionEntry;
@@ -775,6 +776,10 @@
     return addOneValueInstruction(LirOpcodes.ATHROW, exception);
   }
 
+  public LirBuilder<V, EV> addThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+    return addOneItemInstruction(LirOpcodes.THROWBLOCKOUTLINEMARKER, outline);
+  }
+
   public LirBuilder<V, EV> addReturn(V value) {
     return addOneValueInstruction(LirOpcodes.ARETURN, value);
   }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index bf79eea..8bacebb 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -476,6 +476,10 @@
     return strategyInfo;
   }
 
+  public boolean hasArguments() {
+    return argumentCount > 0;
+  }
+
   public int getArgumentCount() {
     return argumentCount;
   }
@@ -500,6 +504,10 @@
     return constants;
   }
 
+  public boolean hasPositionTable() {
+    return positionTable.length > 0;
+  }
+
   public PositionEntry[] getPositionTable() {
     return positionTable;
   }
@@ -512,6 +520,10 @@
     return tryCatchTable;
   }
 
+  public boolean hasDebugLocalInfoTable() {
+    return debugLocalInfoTable != null;
+  }
+
   public DebugLocalInfoTable<EV> getDebugLocalInfoTable() {
     return debugLocalInfoTable;
   }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirConstant.java b/src/main/java/com/android/tools/r8/lightir/LirConstant.java
index c889c00..d0ee9d9 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirConstant.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirConstant.java
@@ -26,7 +26,8 @@
     FILL_ARRAY,
     NAME_COMPUTATION,
     RECORD_FIELD_VALUES,
-    ORIGINAL_FIELD_WITNESS
+    ORIGINAL_FIELD_WITNESS,
+    THROW_BLOCK_OUTLINE
   }
 
   class LirConstantStructuralAcceptor implements StructuralAcceptor<LirConstant> {
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
index 39522bf..cd1fed4 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
@@ -215,6 +215,7 @@
   int RESOURCENUMBER = 228;
   int ORIGINALFIELDWITNESS = 229;
   int STORESTOREFENCE = 230;
+  int THROWBLOCKOUTLINEMARKER = 231;
 
   static String toString(int opcode) {
     switch (opcode) {
@@ -558,6 +559,8 @@
         return "STRINGSWITCH";
       case RESOURCENUMBER:
         return "RESOURCENUMBER";
+      case THROWBLOCKOUTLINEMARKER:
+        return "THROWBLOCKOUTLINEMARKER";
 
       default:
         throw new Unreachable("Unexpected LIR opcode: " + opcode);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index 77a8ed6..5ab0417 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.MemberType;
 import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
 import com.android.tools.r8.lightir.LirBuilder.FillArrayPayload;
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
 import com.android.tools.r8.lightir.LirBuilder.NameComputationPayload;
@@ -470,6 +471,10 @@
     onInstruction();
   }
 
+  public void onThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+    onInstruction();
+  }
+
   public void onReturnVoid() {
     onInstruction();
   }
@@ -1311,6 +1316,13 @@
           onStoreStoreFence(value);
           return;
         }
+      case LirOpcodes.THROWBLOCKOUTLINEMARKER:
+        {
+          ThrowBlockOutline outline =
+              (ThrowBlockOutline) getConstantItem(view.getNextConstantOperand());
+          onThrowBlockOutlineMarker(outline);
+          return;
+        }
       default:
         throw new Unimplemented("No dispatch for opcode " + LirOpcodes.toString(opcode));
     }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
index 05b8e2f..4928feb 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
@@ -70,7 +70,7 @@
     return noShrinkingPercentage;
   }
 
-  private static class Counters {
+  public static class Counters {
 
     private int itemsCount = 0;
     private int noObfuscationCount = 0;
@@ -79,12 +79,13 @@
 
     private Counters() {}
 
-    static Counters create(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    public static Counters create(AppView<? extends AppInfoWithClassHierarchy> appView) {
       Counters counters = new Counters();
       for (DexProgramClass clazz : appView.appInfo().classes()) {
         counters.add(appView, clazz);
         clazz.forEachProgramMember(member -> counters.add(appView, member));
       }
+      assert counters.validate();
       return counters;
     }
 
@@ -111,9 +112,34 @@
     }
 
     float toPercentageWithTwoDecimals(int count) {
-      // Multiply by 100 twice to get percentage with two decimals.
-      float number = (float) (count * 100 * 100) / itemsCount;
-      return (float) Math.round(number) / 100;
+      if (itemsCount == 0) {
+        return 0f;
+      }
+      float fraction = (float) count / itemsCount;
+      assert verifyValidFraction(fraction);
+      float percentage = fraction * 100;
+      assert verifyValidPercentage(percentage);
+      // Multiply and divide by 100 to get percentage with two decimals.
+      return (float) Math.round(percentage * 100) / 100;
+    }
+
+    public boolean validate() {
+      assert verifyValidPercentage(getNoObfuscationPercentage());
+      assert verifyValidPercentage(getNoOptimizationPercentage());
+      assert verifyValidPercentage(getNoShrinkingPercentage());
+      return true;
+    }
+
+    private boolean verifyValidFraction(float f) {
+      assert 0f <= f;
+      assert f <= 1f;
+      return true;
+    }
+
+    private boolean verifyValidPercentage(float f) {
+      assert 0f <= f;
+      assert f <= 100f;
+      return true;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index 7fcedfc..42adbc6 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -323,7 +323,10 @@
         || !packageDescriptor.equals(target.getHolderType().getPackageDescriptor())) {
       DexClass bridgeHolder =
           findHolderForVisibilityBridge(originalClass, target.getHolder(), packageDescriptor);
-      assert bridgeHolder != null;
+      if (bridgeHolder == null) {
+        // The original class does not have access to the target so we cannot insert a valid bridge.
+        return target.getReference().withHolder(originalClass, appView.dexItemFactory());
+      }
       if (bridgeHolder.isClasspathClass()) {
         // Intentionally empty. We do not need to insert a bridge on a classpath class.
       } else if (bridgeHolder.isLibraryClass()) {
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java b/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java
index 486fe2f..961e95f 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java
@@ -19,9 +19,11 @@
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
 import com.google.common.base.Splitter;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Random;
 import java.util.function.Consumer;
 
@@ -96,6 +98,10 @@
     return disabledConfiguration;
   }
 
+  private static List<String> splitPatternList(String patterns) {
+    return ListUtils.map(Splitter.on(",").splitToList(patterns), String::strip);
+  }
+
   public static R8PartialCompilationConfiguration fromIncludeExcludePatterns(
       String includePatterns, String excludePatterns) {
     boolean enabled = includePatterns != null || excludePatterns != null;
@@ -104,10 +110,10 @@
     }
     Builder builder = builder();
     if (includePatterns != null) {
-      Splitter.on(",").splitToList(includePatterns).forEach(builder::addJavaTypeIncludePattern);
+      splitPatternList(includePatterns).forEach(builder::addJavaTypeIncludePattern);
     }
     if (excludePatterns != null) {
-      Splitter.on(",").splitToList(excludePatterns).forEach(builder::addJavaTypeExcludePattern);
+      splitPatternList(excludePatterns).forEach(builder::addJavaTypeExcludePattern);
     }
     return builder.build();
   }
diff --git a/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java b/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java
index 4322fe2..b554cd7 100644
--- a/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java
+++ b/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java
@@ -34,7 +34,10 @@
   }
 
   public boolean test(DexProgramClass clazz) {
-    DexString descriptor = clazz.getType().getDescriptor();
+    return test(clazz.getType().getDescriptor());
+  }
+
+  public boolean test(DexString descriptor) {
     for (R8PartialPredicate predicate : predicates) {
       if (predicate.test(descriptor)) {
         return true;
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java
index f3144f5..d558858 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java
@@ -70,7 +70,9 @@
     return enableCompletenessCheckForTesting
         && !options.getLibraryDesugaringOptions().isDesugaredLibraryCompilation()
         && !options.getStartupOptions().isStartupCompletenessCheckForTestingEnabled()
-        && !options.getInstrumentationOptions().isInstrumentationEnabled();
+        && !options.getInstrumentationOptions().isInstrumentationEnabled()
+        // TODO(b/434769547): Add support for throw block outlines.
+        && !options.getThrowBlockOutlinerOptions().enable;
   }
 
   public boolean isNopCheckForTestingEnabled() {
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index f2bf9a4..afe30c8 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -165,6 +165,7 @@
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
+import java.io.IOException;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -4692,6 +4693,19 @@
     if (options.testing.enqueuerInspector != null) {
       options.testing.enqueuerInspector.accept(appInfoWithLiveness, mode);
     }
+    if (mode.isFinalTreeShaking()) {
+      if (options.testing.exportFinalKeepInfoCollectionToDirectory != null) {
+        try {
+          keepInfo.exportToDirectory(options.testing.exportFinalKeepInfoCollectionToDirectory);
+        } catch (IOException e) {
+          options.reporter.error(
+              "Could not export initial keep info collection: " + e.getMessage());
+        }
+      }
+      if (options.testing.finalKeepInfoCollectionConsumer != null) {
+        options.testing.finalKeepInfoCollectionConsumer.accept(keepInfo.exportToCollection());
+      }
+    }
     return new EnqueuerResult(appInfoWithLiveness);
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
index 792ad59..b42c6db 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
@@ -67,9 +67,10 @@
   }
 
   private boolean isReflectiveMockInvoke(DexMethod invokedMethod) {
-    return invokedMethod.holder.isIdenticalTo(mockitoType)
+    return invokedMethod.getHolderType().isIdenticalTo(mockitoType)
         && (invokedMethod.getName().isIdenticalTo(mockString)
-            || invokedMethod.getName().isIdenticalTo(spyString));
+            || invokedMethod.getName().isIdenticalTo(spyString))
+        && !invokedMethod.getParameters().isEmpty();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
index 92aaad2..62f7ef3 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
@@ -121,6 +121,25 @@
     classInfos = mapBuilder.build();
   }
 
+  public KeepClassInfo getKeepClassInfo(TypeReference typeReference) {
+    ExportedClassInfo info = classInfos.get(typeReference);
+    return info == null ? null : info.keepClassInfo;
+  }
+
+  public KeepMethodInfo getKeepMethodInfo(MethodReference methodReference) {
+    if (!classInfos.containsKey(methodReference.getHolderClass())) {
+      return null;
+    }
+    return classInfos.get(methodReference.getHolderClass()).methodInfos.get(methodReference);
+  }
+
+  public KeepFieldInfo getKeepFieldInfo(FieldReference fieldReference) {
+    if (!classInfos.containsKey(fieldReference.getHolderClass())) {
+      return null;
+    }
+    return classInfos.get(fieldReference.getHolderClass()).fieldInfos.get(fieldReference);
+  }
+
   private ExportedClassInfo.Builder getBuilder(
       Map<TypeReference, ExportedClassInfo.Builder> classInfosBuilder,
       TypeReference typeReference) {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index f50b1db..6b75e1d 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -118,6 +118,8 @@
       generator.forSingleMethod("DesugaredLibraryBridge");
   public final SyntheticKind NON_STARTUP_IN_STARTUP_OUTLINE =
       generator.forSingleMethodWithGlobalMerging("NonStartupInStartupOutline");
+  public final SyntheticKind THROW_BLOCK_OUTLINE =
+      generator.forSingleMethodWithGlobalMerging("ThrowBlockOutline");
 
   private final List<SyntheticKind> ALL_KINDS;
   private String lazyVersionHash = null;
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 6bedcb4..91abc0b 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -72,6 +72,7 @@
 import com.android.tools.r8.ir.desugar.nest.Nest;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutlinerOptions;
 import com.android.tools.r8.metadata.D8BuildMetadata;
 import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.naming.ClassNameMapper;
@@ -100,6 +101,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.GlobalKeepInfoConfiguration;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
 import com.android.tools.r8.shaking.KeepSpecificationSource;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
@@ -118,6 +120,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -1042,6 +1045,8 @@
       new VerticalClassMergerOptions(this);
   private final OpenClosedInterfacesOptions openClosedInterfacesOptions =
       new OpenClosedInterfacesOptions();
+  private final ThrowBlockOutlinerOptions throwBlockOutlinerOptions =
+      new ThrowBlockOutlinerOptions();
   private final ProtoShrinkingOptions protoShrinking = new ProtoShrinkingOptions();
   private final RedundantBridgeRemovalOptions redundantBridgeRemovalOptions =
       new RedundantBridgeRemovalOptions();
@@ -1196,6 +1201,10 @@
     return syntheticItemsOptions;
   }
 
+  public ThrowBlockOutlinerOptions getThrowBlockOutlinerOptions() {
+    return throwBlockOutlinerOptions;
+  }
+
   public TraceReferencesOptions getTraceReferencesOptions() {
     return traceReferencesOptions;
   }
@@ -2188,6 +2197,11 @@
     public boolean enableEmbeddedKeepAnnotations =
         System.getProperty("com.android.tools.r8.enableKeepAnnotations") != null;
     public boolean reverseClassSortingForDeterminism = false;
+    public Path exportFinalKeepInfoCollectionToDirectory =
+        System.getProperty("com.android.tools.r8.exportInitialKeepInfoCollection") != null
+            ? Paths.get(System.getProperty("com.android.tools.r8.exportInitialKeepInfoCollection"))
+            : null;
+    public Consumer<KeepInfoCollectionExported> finalKeepInfoCollectionConsumer = null;
 
     public boolean enableAutoCloseableDesugaring = true;
     public boolean enableNumberUnboxer = false;
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 b5802d9..acc0349 100644
--- a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.threading.TaskCollection;
 import com.android.tools.r8.threading.ThreadingModule;
 import com.android.tools.r8.utils.ListUtils.ReferenceAndIntConsumer;
+import com.android.tools.r8.utils.collections.DexClassAndMemberMap;
 import java.util.Collection;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
@@ -193,9 +194,16 @@
       ThreadingModule threadingModule,
       ExecutorService executorService)
       throws ExecutionException {
+    if (items instanceof DexClassAndMemberMap) {
+      return processItemsWithResults(
+          (Consumer<T> keyConsumer) -> items.forEach((k, v) -> keyConsumer.accept(k)),
+          (key, index) -> consumer.apply(key, items.get(key)),
+          threadingModule,
+          executorService);
+    }
     return processItemsWithResults(
         items.entrySet(),
-        arg -> consumer.apply(arg.getKey(), arg.getValue()),
+        entry -> consumer.apply(entry.getKey(), entry.getValue()),
         threadingModule,
         executorService);
   }
diff --git a/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterJsonTest.java b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterJsonTest.java
new file mode 100644
index 0000000..c3b55b4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterJsonTest.java
@@ -0,0 +1,96 @@
+// 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.JavaLangClassTestClass.Bar;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Foo;
+import com.android.tools.r8.assistant.postprocessing.ReflectiveOperationJsonParser;
+import com.android.tools.r8.assistant.postprocessing.model.AtomicFieldUpdaterNewUpdater;
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+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.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 AtomicUpdaterJsonTest 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()
+        .addProgramClasses(AtomicUpdaterTestClass.class, Foo.class, Bar.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(), AtomicUpdaterTestClass.class)
+        .assertSuccess();
+    List<ReflectiveEvent> reflectiveEvents =
+        new ReflectiveOperationJsonParser(factoryBox.get()).parse(path);
+    assertEquals(3, reflectiveEvents.size());
+    String name = AtomicUpdaterTestClass.class.getName();
+
+    assertTrue(reflectiveEvents.get(0).isAtomicFieldUpdaterNewUpdater());
+    AtomicFieldUpdaterNewUpdater updater0 =
+        reflectiveEvents.get(0).asAtomicFieldUpdaterNewUpdater();
+    assertEquals("int " + name + ".i", updater0.getField().toSourceString());
+
+    assertTrue(reflectiveEvents.get(1).isAtomicFieldUpdaterNewUpdater());
+    AtomicFieldUpdaterNewUpdater updater1 =
+        reflectiveEvents.get(1).asAtomicFieldUpdaterNewUpdater();
+    assertEquals("long " + name + ".l", updater1.getField().toSourceString());
+
+    assertTrue(reflectiveEvents.get(2).isAtomicFieldUpdaterNewUpdater());
+    AtomicFieldUpdaterNewUpdater updater2 =
+        reflectiveEvents.get(2).asAtomicFieldUpdaterNewUpdater();
+    assertEquals("java.lang.Object " + name + ".o", updater2.getField().toSourceString());
+
+    Box<KeepInfoCollectionExported> keepInfoBox = new Box<>();
+    testForR8(parameters)
+        .addProgramClasses(AtomicUpdaterTestClass.class, Foo.class, Bar.class)
+        .addOptionsModification(
+            opt -> opt.testing.finalKeepInfoCollectionConsumer = keepInfoBox::set)
+        .setMinApi(parameters)
+        .addKeepMainRule(AtomicUpdaterTestClass.class)
+        .run(parameters.getRuntime(), AtomicUpdaterTestClass.class)
+        .assertSuccessWithOutputLines("42", "42", "42");
+
+    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/assistant/AtomicUpdaterTest.java b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterTest.java
index 360ea26..616bfb3 100644
--- a/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterTest.java
@@ -49,18 +49,8 @@
   public static class Instrumentation extends EmptyReflectiveOperationReceiver {
 
     @Override
-    public void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
-      System.out.println("int " + clazz.getName() + "#" + name);
-    }
-
-    @Override
-    public void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
-      System.out.println("long " + clazz.getName() + "#" + name);
-    }
-
-    @Override
-    public void onAtomicReferenceFieldUpdaterNewUpdater(
-        Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {
+    public void onAtomicFieldUpdaterNewUpdater(
+        Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {
       System.out.println(fieldClass.getName() + " " + clazz.getName() + "#" + name);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java b/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java
new file mode 100644
index 0000000..089824f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java
@@ -0,0 +1,161 @@
+// 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.JavaLangClassTestClass.Bar;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Foo;
+import com.android.tools.r8.assistant.postprocessing.ReflectiveOperationJsonParser;
+import com.android.tools.r8.assistant.postprocessing.model.ClassGetMember;
+import com.android.tools.r8.assistant.postprocessing.model.ClassGetMembers;
+import com.android.tools.r8.assistant.postprocessing.model.ClassGetName;
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.references.Reference;
+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 JavaLangClassJsonTest 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()
+        .addProgramClasses(JavaLangClassTestClass.class, Foo.class, Bar.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(), JavaLangClassTestClass.class)
+        .assertSuccess();
+    List<ReflectiveEvent> reflectiveEvents =
+        new ReflectiveOperationJsonParser(factoryBox.get()).parse(path);
+    Assert.assertEquals(28, reflectiveEvents.size());
+
+    assertTrue(reflectiveEvents.get(3).isClassGetMember());
+    ClassGetMember updater00 = reflectiveEvents.get(3).asClassGetMember();
+    assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_METHOD, updater00.getEventType());
+    assertEquals(
+        Reference.methodFromMethod(Foo.class.getDeclaredMethod("barr")),
+        updater00.getMember().asDexMethod().asMethodReference());
+
+    assertTrue(reflectiveEvents.get(4).isClassGetMember());
+    ClassGetMember updater01 = reflectiveEvents.get(4).asClassGetMember();
+    assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_FIELD, updater01.getEventType());
+    assertEquals(
+        Reference.fieldFromField(Foo.class.getDeclaredField("a")),
+        updater01.getMember().asDexField().asFieldReference());
+
+    assertTrue(reflectiveEvents.get(6).isClassGetMembers());
+    ClassGetMembers updater02 = reflectiveEvents.get(6).asClassGetMembers();
+    assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_METHODS, updater02.getEventType());
+    assertEquals(Foo.class.getName(), updater02.getHolder().toSourceString());
+
+    assertTrue(reflectiveEvents.get(7).isClassGetMembers());
+    ClassGetMembers updater03 = reflectiveEvents.get(7).asClassGetMembers();
+    assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_FIELDS, updater03.getEventType());
+    assertEquals(Foo.class.getName(), updater03.getHolder().toSourceString());
+
+    assertTrue(reflectiveEvents.get(8).isClassGetMember());
+    ClassGetMember updater04 = reflectiveEvents.get(8).asClassGetMember();
+    assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTOR, updater04.getEventType());
+    assertEquals(
+        Reference.methodFromMethod(Foo.class.getDeclaredConstructor()),
+        updater04.getMember().asDexMethod().asMethodReference());
+
+    assertTrue(reflectiveEvents.get(9).isClassGetMembers());
+    ClassGetMembers updater05 = reflectiveEvents.get(9).asClassGetMembers();
+    assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTORS, updater05.getEventType());
+    assertEquals(Foo.class.getName(), updater05.getHolder().toSourceString());
+
+    assertTrue(reflectiveEvents.get(10).isClassGetName());
+    ClassGetName updater0 = reflectiveEvents.get(10).asClassGetName();
+    assertEquals(Foo.class.getName(), updater0.getType().toSourceString());
+    assertEquals(NameLookupType.NAME, updater0.getNameLookupType());
+
+    assertTrue(reflectiveEvents.get(11).isClassGetName());
+    ClassGetName updater1 = reflectiveEvents.get(11).asClassGetName();
+    assertEquals(Foo.class.getName(), updater1.getType().toSourceString());
+    assertEquals(NameLookupType.CANONICAL_NAME, updater1.getNameLookupType());
+
+    assertTrue(reflectiveEvents.get(12).isClassGetName());
+    ClassGetName updater2 = reflectiveEvents.get(12).asClassGetName();
+    assertEquals(Foo.class.getName(), updater2.getType().toSourceString());
+    assertEquals(NameLookupType.SIMPLE_NAME, updater2.getNameLookupType());
+
+    assertTrue(reflectiveEvents.get(13).isClassGetName());
+    ClassGetName updater3 = reflectiveEvents.get(13).asClassGetName();
+    assertEquals(Foo.class.getName(), updater3.getType().toSourceString());
+    assertEquals(NameLookupType.TYPE_NAME, updater3.getNameLookupType());
+
+    assertTrue(reflectiveEvents.get(19).isClassGetMembers());
+    ClassGetMembers updater19 = reflectiveEvents.get(19).asClassGetMembers();
+    assertEquals(ReflectiveEventType.CLASS_GET_METHODS, updater19.getEventType());
+    assertEquals(Bar.class.getName(), updater19.getHolder().toSourceString());
+
+    assertTrue(reflectiveEvents.get(20).isClassGetMembers());
+    ClassGetMembers updater20 = reflectiveEvents.get(20).asClassGetMembers();
+    assertEquals(ReflectiveEventType.CLASS_GET_FIELDS, updater20.getEventType());
+    assertEquals(Bar.class.getName(), updater20.getHolder().toSourceString());
+
+    assertTrue(reflectiveEvents.get(21).isClassGetMembers());
+    ClassGetMembers updater21 = reflectiveEvents.get(21).asClassGetMembers();
+    assertEquals(ReflectiveEventType.CLASS_GET_CONSTRUCTORS, updater21.getEventType());
+    assertEquals(Bar.class.getName(), updater21.getHolder().toSourceString());
+
+    assertTrue(reflectiveEvents.get(22).isClassGetMember());
+    ClassGetMember updater22 = reflectiveEvents.get(22).asClassGetMember();
+    assertEquals(ReflectiveEventType.CLASS_GET_METHOD, updater22.getEventType());
+    assertEquals(
+        Reference.methodFromMethod(Bar.class.getMethod("bar")),
+        updater22.getMember().asDexMethod().asMethodReference());
+
+    assertTrue(reflectiveEvents.get(23).isClassGetMember());
+    ClassGetMember updater23 = reflectiveEvents.get(23).asClassGetMember();
+    assertEquals(ReflectiveEventType.CLASS_GET_FIELD, updater23.getEventType());
+    assertEquals(
+        Reference.fieldFromField(Bar.class.getField("i")),
+        updater23.getMember().asDexField().asFieldReference());
+
+    assertTrue(reflectiveEvents.get(24).isClassGetMember());
+    ClassGetMember updater24 = reflectiveEvents.get(24).asClassGetMember();
+    assertEquals(ReflectiveEventType.CLASS_GET_CONSTRUCTOR, updater24.getEventType());
+    assertEquals(
+        Reference.methodFromMethod(Bar.class.getConstructor()),
+        updater24.getMember().asDexMethod().asMethodReference());
+  }
+
+  public static class Instrumentation extends ReflectiveOperationJsonLogger {
+    public Instrumentation() throws IOException {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java b/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java
index 6f6e8ed..f4f0eff 100644
--- a/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java
@@ -74,7 +74,7 @@
 
     @Override
     public void onClassGetDeclaredMethod(
-        Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+        Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
       printNumIfTrue(clazz.getName().endsWith("Foo"), 2);
     }
 
@@ -84,7 +84,8 @@
     }
 
     @Override
-    public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {
+    public void onClassGetDeclaredField(
+        Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
       printNumIfTrue(
           clazz.getName().endsWith("Foo") && (fieldName.equals("a") || fieldName.equals("b")), 3);
     }
@@ -106,7 +107,7 @@
 
     @Override
     public void onClassGetMethod(
-        Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+        Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
       printNumIfTrue(clazz.getName().endsWith("Bar"), 20);
     }
 
@@ -116,7 +117,7 @@
     }
 
     @Override
-    public void onClassGetField(Stack stack, Class<?> clazz, String fieldName) {
+    public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
       printNumIfTrue(clazz.getName().endsWith("Bar"), 21);
     }
 
diff --git a/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java b/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
index 2332575..bcf3dc6 100644
--- a/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
@@ -159,7 +159,7 @@
 
     @Override
     public void onClassGetDeclaredMethod(
-        Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+        Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
       if (!clazz.equals(Bar.class) || !method.equals("callMe")) {
         throw new RuntimeException("Wrong method passed");
       }
@@ -200,7 +200,7 @@
 
     @Override
     public void onClassGetDeclaredMethod(
-        Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+        Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
       System.out.println("Custom receiver method " + method);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest.java
new file mode 100644
index 0000000..7c8d366
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest.java
@@ -0,0 +1,111 @@
+// 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.classmerging.horizontal;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.apimodel.ApiModelingTestHelper;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+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 DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testInterfaceOnProgram() throws Exception {
+    testForR8(parameters)
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .applyIf(
+                        !parameters.canUseDefaultAndStaticInterfaceMethods(),
+                        i -> i.assertIsCompleteMergeGroup(A.class, B.class))
+                    .assertNoOtherClassesMerged())
+        .enableInliningAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("I", "B");
+  }
+
+  @Test
+  public void testInterfaceOnLibrary() throws Exception {
+    testForR8(parameters)
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addLibraryClasses(I.class)
+        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+        .addKeepClassAndMembersRules(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .apply(ApiModelingTestHelper.setMockApiLevelForClass(I.class, AndroidApiLevel.B))
+        .apply(
+            ApiModelingTestHelper.setMockApiLevelForMethod(
+                I.class.getDeclaredMethod("m"), AndroidApiLevel.B))
+        .compile()
+        .addRunClasspathClasses(I.class)
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.canUseDefaultAndStaticInterfaceMethods(),
+            rr -> rr.assertSuccessWithOutputLines("I", "B"),
+            rr -> rr.assertSuccessWithOutputLines("Caught java.lang.AbstractMethodError", "B"));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      try {
+        test(new A());
+      } catch (AbstractMethodError e) {
+        System.out.println("Caught " + e.getClass().getName());
+      }
+      test(new B());
+    }
+
+    // @Keep
+    static void test(I i) {
+      i.m();
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {
+
+    @NeverInline
+    default void m() {
+      System.out.println("I");
+    }
+  }
+
+  static class A implements I {}
+
+  static class B implements I {
+
+    @Override
+    public void m() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticGeneratorAGPUseTest.java b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticGeneratorAGPUseTest.java
new file mode 100644
index 0000000..6a277f2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticGeneratorAGPUseTest.java
@@ -0,0 +1,80 @@
+// 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.globalsynthetics;
+
+import static com.android.tools.r8.ToolHelper.getAndroidJar;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.GlobalSyntheticsGenerator;
+import com.android.tools.r8.GlobalSyntheticsGeneratorCommand;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 GlobalSyntheticGeneratorAGPUseTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean emitLambdaMethodAnnotations;
+
+  @Parameters(name = "{0}, emitLambdaMethodAnnotations = {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withNoneRuntime().build(), BooleanUtils.values());
+  }
+
+  @Test
+  public void test() throws Exception {
+    try {
+      if (emitLambdaMethodAnnotations) {
+        assertNull(System.getProperty("com.android.tools.r8.emitLambdaMethodAnnotations"));
+        System.setProperty("com.android.tools.r8.emitLambdaMethodAnnotations", "");
+      }
+      Path globals = temp.newFile("all.globals").toPath();
+      GlobalSyntheticsGenerator.run(
+          GlobalSyntheticsGeneratorCommand.builder()
+              .addLibraryFiles(getAndroidJar(36))
+              .setGlobalSyntheticsOutput(globals)
+              .build());
+
+      Path globalsDex = temp.newFile("globals.zip").toPath();
+      D8.run(
+          D8Command.builder()
+              .addLibraryFiles(getAndroidJar(36))
+              .setMinApiLevel(21)
+              .addGlobalSyntheticsFiles(globals)
+              .setOutput(globalsDex, OutputMode.DexIndexed)
+              .build());
+
+      CodeInspector inspector = new CodeInspector(globalsDex);
+      assertThat(inspector.clazz("java.lang.Record"), isPresent());
+      // Added in API level 24.
+      assertThat(inspector.clazz("android.os.HardwarePropertiesManager"), isPresent());
+      // Added in API level 36.
+      assertThat(inspector.clazz("android.os.Build$VERSION_CODES_FULL"), isPresent());
+      // TODO(b/417709154): Should this always be part of global synthetics?
+      assertThat(
+          inspector.clazz("com.android.tools.r8.annotations.LambdaMethod"),
+          isPresentIf(emitLambdaMethodAnnotations));
+    } finally {
+      System.clearProperty("com.android.tools.r8.emitLambdaMethodAnnotations");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerNoArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerNoArgumentsTest.java
new file mode 100644
index 0000000..ed330c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerNoArgumentsTest.java
@@ -0,0 +1,135 @@
+// 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.ir.optimize.outliner.exceptions;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.isInvokeWithTarget;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import java.util.Collection;
+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 ThrowBlockOutlinerNoArgumentsTest extends TestBase {
+
+  @Parameter(0)
+  public boolean minimizeSyntheticNames;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, minimizeSyntheticNames: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimesAndAllApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    BooleanBox receivedCallback = new BooleanBox();
+    TestCompileResult<?, ?> compileResult =
+        testForD8(parameters)
+            .addInnerClasses(getClass())
+            .addOptionsModification(
+                options -> {
+                  options.desugarSpecificOptions().minimizeSyntheticNames = minimizeSyntheticNames;
+                  assertFalse(options.getThrowBlockOutlinerOptions().enable);
+                  options.getThrowBlockOutlinerOptions().enable = true;
+                  options.getThrowBlockOutlinerOptions().outlineConsumerForTesting =
+                      outlines -> {
+                        inspectOutlines(outlines);
+                        receivedCallback.set();
+                      };
+                })
+            .release()
+            .setMinApi(parameters)
+            .compile()
+            .inspect(this::inspectOutput);
+    assertTrue(receivedCallback.isTrue());
+
+    for (int i = 0; i < 3; i++) {
+      compileResult
+          .run(parameters.getRuntime(), Main.class, Integer.toString(i))
+          .assertFailureWithErrorThatThrows(IllegalArgumentException.class);
+    }
+    compileResult
+        .run(parameters.getRuntime(), Main.class, Integer.toString(3))
+        .assertFailureWithErrorThatThrows(RuntimeException.class);
+    compileResult
+        .run(parameters.getRuntime(), Main.class, Integer.toString(42))
+        .assertSuccessWithEmptyOutput();
+  }
+
+  private void inspectOutlines(Collection<ThrowBlockOutline> outlines) {
+    // Verify that we have two outlines with one and three users, respectively.
+    assertEquals(2, outlines.size());
+    IntSet numberOfUsers = new IntArraySet();
+    for (ThrowBlockOutline outline : outlines) {
+      numberOfUsers.add(outline.getNumberOfUsers());
+    }
+    assertTrue(numberOfUsers.contains(1));
+    assertTrue(numberOfUsers.contains(3));
+  }
+
+  private void inspectOutput(CodeInspector inspector) {
+    assertEquals(2, inspector.allClasses().size());
+
+    MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+
+    ClassSubject outlineClassSubject =
+        inspector.clazz(
+            minimizeSyntheticNames
+                ? SyntheticItemsTestUtils.syntheticClassWithMinimalName(Main.class, 0)
+                : SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(Main.class, 0));
+    assertThat(outlineClassSubject, isPresent());
+    assertEquals(1, outlineClassSubject.allMethods().size());
+
+    MethodSubject outlineMethodSubject = outlineClassSubject.uniqueMethod();
+    assertEquals(
+        3,
+        mainMethodSubject
+            .streamInstructions()
+            .filter(isInvokeWithTarget(outlineMethodSubject))
+            .count());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      int i = Integer.parseInt(args[0]);
+      if (i == 0) {
+        throw new IllegalArgumentException();
+      }
+      if (i == 1) {
+        throw new IllegalArgumentException();
+      }
+      if (i == 2) {
+        throw new IllegalArgumentException();
+      }
+      if (i == 3) {
+        throw new RuntimeException();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
index 2497a5b..1cdc98e 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
@@ -437,7 +437,8 @@
       KotlinCompileMemoizer compilation,
       List<byte[]> classFileData,
       String mainClass,
-      ExpectedRules expectedRules)
+      ExpectedRules expectedRules,
+      String expectedOutput)
       throws Exception {
     // TODO(b/392865072): Legacy R8 fails with AssertionError: Synthetic class kinds should agree.
     assumeFalse(parameters.isLegacyR8());
@@ -491,20 +492,22 @@
               }
             })
         .run(mainClass)
-        .assertSuccessWithOutput(getExpectedOutputForKotlin());
+        .assertSuccessWithOutput(expectedOutput);
   }
 
   protected void runTestExtractedRulesKotlin(
       KotlinCompileMemoizer compilation, String mainClass, ExpectedRules expectedRules)
       throws Exception {
-    runTestExtractedRulesKotlin(compilation, ImmutableList.of(), mainClass, expectedRules);
+    runTestExtractedRulesKotlin(
+        compilation, ImmutableList.of(), mainClass, expectedRules, getExpectedOutputForKotlin());
   }
 
   protected void runTestExtractedRulesKotlin(
       KotlinCompileMemoizer compilation,
       BiFunction<ClassReference, byte[], byte[]> transformerForClass,
       String mainClass,
-      ExpectedRules expectedRules)
+      ExpectedRules expectedRules,
+      String expectedOutput)
       throws Exception {
     List<byte[]> result = new ArrayList<>();
     ZipUtils.iter(
@@ -517,6 +520,16 @@
           result.add(
               transformerForClass.apply(classReference, ByteStreams.toByteArray(inputStream)));
         });
-    runTestExtractedRulesKotlin(null, result, mainClass, expectedRules);
+    runTestExtractedRulesKotlin(null, result, mainClass, expectedRules, expectedOutput);
+  }
+
+  protected void runTestExtractedRulesKotlin(
+      KotlinCompileMemoizer compilation,
+      BiFunction<ClassReference, byte[], byte[]> transformerForClass,
+      String mainClass,
+      ExpectedRules expectedRules)
+      throws Exception {
+    runTestExtractedRulesKotlin(
+        compilation, transformerForClass, mainClass, expectedRules, getExpectedOutputForKotlin());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java
new file mode 100644
index 0000000..dfb602f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java
@@ -0,0 +1,224 @@
+// 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.keepanno.androidx;
+
+import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+
+import androidx.annotation.keep.UsesReflectionToConstruct;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.ClassFileTransformer.AnnotationBuilder;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.function.Consumer;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Type;
+
+@RunWith(Parameterized.class)
+public class KeepUsesReflectionForInstantiationAnyArgsConstructorTest
+    extends KeepAnnoTestExtractedRulesBase {
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static Collection<Object[]> data() {
+    // Test with Android 14, which has `java.lang.ClassValue` to avoid having to deal with R8
+    // missing class warnings for tests using the kotlin-reflect library.
+    return buildParameters(
+        createParameters(
+            getTestParameters()
+                .withDexRuntime(Version.V14_0_0)
+                .withDefaultCfRuntime()
+                .withMaximumApiLevel()
+                .build()),
+        getKotlinTestParameters().withLatestCompiler().build());
+  }
+
+  @Override
+  protected String getExpectedOutputForJava() {
+    return StringUtils.lines("4");
+  }
+
+  @Override
+  protected String getExpectedOutputForKotlin() {
+    return StringUtils.lines(
+        "fun `<init>`(): com.android.tools.r8.keepanno.androidx.kt.KeptClass", "<init>()", "4");
+  }
+
+  private static Collection<Path> getKotlinSources() {
+    try {
+      return getFilesInTestFolderRelativeToClass(
+          KeepUsesReflectionForInstantiationAnyArgsConstructorTest.class,
+          "kt",
+          "AnyArgsConstructor.kt",
+          "KeptClass.kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources());
+  }
+
+  private static ExpectedRules getExpectedRulesJava(Class<?> conditionClass) {
+    return getExpectedRulesJava(conditionClass, null);
+  }
+
+  private static ExpectedRules getExpectedRulesJava(
+      Class<?> conditionClass, String conditionMembers) {
+    Consumer<ExpectedKeepRule.Builder> setCondition =
+        b -> b.setConditionClass(conditionClass).setConditionMembers(conditionMembers);
+    ExpectedRules.Builder builder =
+        ExpectedRules.builder()
+            .add(
+                ExpectedKeepRule.builder()
+                    .apply(setCondition)
+                    .setConsequentClass(KeptClass.class)
+                    .setConsequentMembers("{ void <init>(...); }")
+                    .build());
+    addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+    return builder.build();
+  }
+
+  private static ExpectedRules getExpectedRulesKotlin(String conditionClass) {
+    return getExpectedRulesKotlin(conditionClass, null);
+  }
+
+  private static ExpectedRules getExpectedRulesKotlin(
+      String conditionClass, String conditionMembers) {
+    Consumer<ExpectedKeepRule.Builder> setCondition =
+        b ->
+            b.setConditionClass("com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor")
+                .setConditionMembers(conditionMembers);
+    ExpectedRules.Builder builder =
+        ExpectedRules.builder()
+            .add(
+                ExpectedKeepRule.builder()
+                    .apply(setCondition)
+                    .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass")
+                    .setConsequentMembers("{ void <init>(...); }")
+                    .build());
+    addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+    return builder.build();
+  }
+
+  private static void buildAnyConstructor(AnnotationBuilder builder, Object clazz) {
+    if (clazz instanceof String) {
+      builder.setField("className", clazz);
+    } else {
+      assert clazz instanceof Class<?> || clazz instanceof Type;
+      builder.setField("classConstant", clazz);
+    }
+    // No parameterTypes or parameterTypeNames means any constructor.
+  }
+
+  @Test
+  public void testAnyConstructor() throws Exception {
+    runTestExtractedRulesJava(
+        AnyConstructor.class,
+        ImmutableList.of(KeptClass.class),
+        ImmutableList.of(
+            setAnnotationOnMethod(
+                AnyConstructor.class,
+                MethodPredicate.onName("foo"),
+                UsesReflectionToConstruct.class,
+                builder -> buildAnyConstructor(builder, KeptClass.class))),
+        getExpectedRulesJava(AnyConstructor.class, "{ void foo(java.lang.Class); }"));
+  }
+
+  @Test
+  public void testAnyConstructorAnnotateClass() throws Exception {
+    runTestExtractedRulesJava(
+        AnyConstructor.class,
+        ImmutableList.of(KeptClass.class),
+        ImmutableList.of(
+            setAnnotationOnClass(
+                AnyConstructor.class,
+                UsesReflectionToConstruct.class,
+                builder -> buildAnyConstructor(builder, KeptClass.class))),
+        getExpectedRulesJava(AnyConstructor.class));
+  }
+
+  @Test
+  public void testAnyConstructorKotlin() throws Exception {
+    runTestExtractedRulesKotlin(
+        compilationResults,
+        (classReference, classFileBytes) ->
+            setAnnotationOnMethod(
+                classReference,
+                classFileBytes,
+                Reference.classFromTypeName(
+                    "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+                MethodPredicate.onName("foo"),
+                UsesReflectionToConstruct.class,
+                builder ->
+                    buildAnyConstructor(
+                        builder,
+                        Type.getType(
+                            DescriptorUtils.javaTypeToDescriptor(
+                                "com.android.tools.r8.keepanno.androidx.kt.KeptClass")))),
+        "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructorKt",
+        getExpectedRulesKotlin(
+            "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor",
+            "{ void foo(kotlin.reflect.KClass); }"));
+  }
+
+  @Test
+  public void testAnyConstructorKotlinAnnotateClass() throws Exception {
+    runTestExtractedRulesKotlin(
+        compilationResults,
+        (classReference, classFileBytes) ->
+            setAnnotationOnClass(
+                classReference,
+                classFileBytes,
+                Reference.classFromTypeName(
+                    "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+                UsesReflectionToConstruct.class,
+                builder ->
+                    buildAnyConstructor(
+                        builder,
+                        Type.getType(
+                            DescriptorUtils.javaTypeToDescriptor(
+                                "com.android.tools.r8.keepanno.androidx.kt.KeptClass")))),
+        "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructorKt",
+        getExpectedRulesKotlin("com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+        // TODO(b/437277192): Constructors should be kept.
+        parameters.isExtractRules()
+            ? StringUtils.lines("null", "0")
+            : getExpectedOutputForKotlin());
+  }
+
+  // Test class without annotation to be used by multiple tests inserting annotations using a
+  // transformer.
+  static class AnyConstructor {
+
+    public void foo(Class<KeptClass> clazz) throws Exception {
+      if (clazz != null) {
+        System.out.println(clazz.getDeclaredConstructors().length);
+      }
+    }
+
+    public static void main(String[] args) throws Exception {
+      new AnyConstructor().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+    }
+  }
+
+  static class KeptClass {
+    KeptClass() {}
+
+    KeptClass(int i) {}
+
+    KeptClass(long j) {}
+
+    KeptClass(String s1, String s2, String s3) {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt
new file mode 100644
index 0000000..c891829
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt
@@ -0,0 +1,19 @@
+// 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.keepanno.androidx.kt
+
+import kotlin.reflect.KClass
+import kotlin.reflect.full.primaryConstructor
+
+class AnyArgsConstructor {
+  fun foo(clazz: KClass<KeptClass>?) {
+    println(clazz?.primaryConstructor)
+    clazz?.primaryConstructor?.call()
+    println(clazz?.constructors?.size)
+  }
+}
+
+fun main() {
+  AnyArgsConstructor().foo(if (System.nanoTime() > 0) KeptClass::class else null)
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
index 89e2d02..e96643d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
@@ -16,7 +16,7 @@
     println("<init>(Long)")
   }
 
-  constructor(s: String) : this() {
-    println("<init>(String)")
+  constructor(s1: String, s2: String, s3: String) : this() {
+    println("<init>(String, String, String)")
   }
 }
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
index e87a04d..89d12eb 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
@@ -8,6 +8,8 @@
 import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.memberrebinding.testclasses.MemberRebindingClasspathSplitTestClasses;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
@@ -24,20 +26,22 @@
 
   private static class TestConfig {
     private final String desc;
-
     private final ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath;
     private final ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath;
     private final ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath;
+    private final boolean expectFailure;
 
     private TestConfig(
         String desc,
         ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath,
         ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath,
-        ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath) {
+        ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath,
+        boolean expectFailure) {
       this.desc = desc;
       this.addToClasspath = addToClasspath;
       this.addToProgrampath = addToProgrampath;
       this.addToRunClasspath = addToRunClasspath;
+      this.expectFailure = expectFailure;
     }
 
     @Override
@@ -66,7 +70,8 @@
                   b.addRunClasspathClasses(
                       MemberRebindingClasspathSplitTestClasses.getA(),
                       MemberRebindingClasspathSplitTestClasses.getB());
-                }),
+                },
+                false),
             new TestConfig(
                 "Both A and B on classpath, m() bridge removed from B",
                 b -> {
@@ -83,7 +88,8 @@
                       transformer(MemberRebindingClasspathSplitTestClasses.getB())
                           .removeMethodsWithName("m")
                           .transform());
-                }),
+                },
+                false),
             new TestConfig(
                 "A on classpath and B on programpath",
                 b -> {
@@ -94,7 +100,8 @@
                 },
                 b -> {
                   b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
-                }),
+                },
+                false),
             new TestConfig(
                 "A on classpath and B on programpath, m() bridge removed from B",
                 b -> {
@@ -108,7 +115,26 @@
                 },
                 b -> {
                   b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
-                })));
+                },
+                false),
+            new TestConfig(
+                "Both A and B on classpath but neither A or B is public",
+                b -> {
+                  b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                  b.addClasspathClassFileData(
+                      transformer(MemberRebindingClasspathSplitTestClasses.getB())
+                          .setAccessFlags(AccessFlags::unsetPublic)
+                          .transform());
+                },
+                b -> {},
+                b -> {
+                  b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+                  b.addRunClasspathClassFileData(
+                      transformer(MemberRebindingClasspathSplitTestClasses.getB())
+                          .setAccessFlags(AccessFlags::unsetPublic)
+                          .transform());
+                },
+                true)));
   }
 
   @Test
@@ -124,7 +150,12 @@
         .compile()
         .apply(split.addToRunClasspath)
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("A", "A");
+        .applyIf(
+            split.expectFailure && parameters.isDexRuntimeVersionOlderThanOrEqual(Version.V4_4_4),
+            rr -> rr.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+            split.expectFailure,
+            rr -> rr.assertFailureWithErrorThatThrows(IllegalAccessError.class),
+            rr -> rr.assertSuccessWithOutputLines("A", "A"));
   }
 
   public static class C extends MemberRebindingClasspathSplitTestClasses.B {
diff --git a/src/test/java/com/android/tools/r8/partial/ExperimentalApiPatternParseTest.java b/src/test/java/com/android/tools/r8/partial/ExperimentalApiPatternParseTest.java
new file mode 100644
index 0000000..64447c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/ExperimentalApiPatternParseTest.java
@@ -0,0 +1,72 @@
+// 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.assertFalse;
+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.graph.DexItemFactory;
+import com.android.tools.r8.partial.predicate.R8PartialPredicateCollection;
+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 ExperimentalApiPatternParseTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @Test
+  public void testPackagePrefix() throws Exception {
+    DexItemFactory factory = new DexItemFactory();
+    for (String pattern :
+        new String[] {
+          "a.**,b.**", "a.**, b.**", " a.**,b.** ", "\ta.**\t,\nb.**\u2000\u2001\u2002"
+        }) {
+      R8PartialCompilationConfiguration config =
+          R8PartialCompilationConfiguration.fromIncludeExcludePatterns(pattern, pattern);
+      for (R8PartialPredicateCollection predicate :
+          new R8PartialPredicateCollection[] {
+            config.getIncludePredicates(), config.getExcludePredicates()
+          }) {
+        assertTrue(predicate.test(factory.createString("La/A;")));
+        assertTrue(predicate.test(factory.createString("La/a/A;")));
+        assertTrue(predicate.test(factory.createString("Lb/A;")));
+        assertTrue(predicate.test(factory.createString("Lb/a/A;")));
+      }
+    }
+  }
+
+  @Test
+  public void testClassPrefix() throws Exception {
+    DexItemFactory factory = new DexItemFactory();
+    for (String pattern :
+        new String[] {"a.*,b.A*", "a.*, b.A*", " a.*,b.A* ", "\ta.*\t,\nb.A*\u2000\u2001\u2002"}) {
+      R8PartialCompilationConfiguration config =
+          R8PartialCompilationConfiguration.fromIncludeExcludePatterns(pattern, pattern);
+      for (R8PartialPredicateCollection predicate :
+          new R8PartialPredicateCollection[] {
+            config.getIncludePredicates(), config.getExcludePredicates()
+          }) {
+        assertTrue(predicate.test(factory.createString("La/A;")));
+        assertTrue(predicate.test(factory.createString("La/Aa;")));
+        assertFalse(predicate.test(factory.createString("La/a/A;")));
+        assertTrue(predicate.test(factory.createString("Lb/A;")));
+        assertTrue(predicate.test(factory.createString("Lb/Aa;")));
+        assertFalse(predicate.test(factory.createString("Lb/Aa/a;")));
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithMethodTest.java b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithMethodTest.java
new file mode 100644
index 0000000..9f8760f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithMethodTest.java
@@ -0,0 +1,94 @@
+// 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.shaking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+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.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+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 SoftPinClassInConditionWithMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("2");
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules(
+            "-if class "
+                + A.class.getTypeName()
+                + " { void foo(java.lang.Class); }"
+                + " -keepclasseswithmembers class "
+                + B.class.getTypeName()
+                + " { <init>(...); }")
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(
+            inspector -> {
+              // Class A is still present with no members and no reference to it at all. It could
+              // have been removed.
+              assertThat(inspector.clazz(A.class), isPresent());
+              assertTrue(inspector.clazz(A.class).allMethods().isEmpty());
+              assertEquals(
+                  0,
+                  inspector
+                      .clazz(TestClass.class)
+                      .mainMethod()
+                      .streamInstructions()
+                      .filter(InstructionSubject::isNewInstance)
+                      .count());
+            })
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  static class A {
+    void foo(Class<?> clazz) {
+      System.out.println(clazz.getDeclaredConstructors().length);
+    }
+  }
+
+  static class B {
+    B() {}
+
+    B(int i) {}
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new A().foo(System.nanoTime() > 0 ? B.class : null);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithOnlyInstanceInitializerTest.java b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithOnlyInstanceInitializerTest.java
new file mode 100644
index 0000000..c84c6bf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithOnlyInstanceInitializerTest.java
@@ -0,0 +1,86 @@
+// 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.shaking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+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.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SoftPinClassInConditionWithOnlyInstanceInitializerTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("2");
+  private static final String UNEXPECTED_OUTPUT = StringUtils.lines("0");
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  // TODO(b/437277192): Reproduction.
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules(
+            "-if class "
+                + A.class.getTypeName()
+                + " -keepclasseswithmembers class "
+                + B.class.getTypeName()
+                + " { <init>(...); }")
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(
+            inspector -> {
+              // Class A is removed (which is fine, but the B constructors should still be there).
+              assertThat(inspector.clazz(A.class), isAbsent());
+              assertThat(inspector.clazz(B.class), isPresent());
+              assertTrue(inspector.clazz(B.class).allMethods().isEmpty());
+            })
+        .assertSuccessWithOutput(UNEXPECTED_OUTPUT);
+  }
+
+  static class A {
+    void foo(Class<?> clazz) {
+      System.out.println(clazz.getDeclaredConstructors().length);
+    }
+  }
+
+  static class B {
+    B() {}
+
+    B(int i) {}
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new A().foo(System.nanoTime() > 0 ? B.class : null);
+    }
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java
index c007ec3..e4fd8b3 100644
--- a/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.benchmarks.BenchmarkResults;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -100,7 +101,13 @@
     if (customReflectiveOperationReceiver != null) {
       builder.setReflectiveReceiverClassDescriptor(customReflectiveOperationReceiver);
     }
-    R8Assistant.run(builder.build());
+    R8AssistantCommand build = builder.build();
+    InternalOptions options = build.getInternalOptions();
+    if (optionsConsumer != null) {
+      ExceptionUtils.withCompilationHandler(
+          options.reporter, () -> optionsConsumer.accept(options));
+    }
+    R8Assistant.runForTest(build, options);
     return new AssistantTestCompileResult(
         initialCompilation,
         getState(),
diff --git a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
index 753027f..bf980e0 100644
--- a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -101,6 +101,10 @@
         methodWithReceiverForForwarding.getMethodDescriptor());
   }
 
+  public static ClassReference syntheticThrowBlockOutlineClass(Class<?> clazz, int id) {
+    return syntheticClass(clazz, naming.THROW_BLOCK_OUTLINE, id);
+  }
+
   public static ClassReference syntheticOutlineClass(Class<?> clazz, int id) {
     return syntheticClass(clazz, naming.OUTLINE, id);
   }
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 598ff46..1d9ecb1 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -17,7 +17,7 @@
 
 import utils
 
-R8_DEV_BRANCH = '8.13'
+R8_DEV_BRANCH = '9.0'
 R8_VERSION_FILE = os.path.join('src', 'main', 'java', 'com', 'android', 'tools',
                                'r8', 'Version.java')
 THIS_FILE_RELATIVE = os.path.join('tools', 'r8_release.py')