Merge commit 'd2c333f958b9987fb598dca4df09975210c13035' into dev-release
diff --git a/.gitignore b/.gitignore
index 052f0a6..17af682 100644
--- a/.gitignore
+++ b/.gitignore
@@ -183,6 +183,10 @@
 third_party/opensource-apps/applymapping.tar.gz
 third_party/opensource-apps/chanu
 third_party/opensource-apps/chanu.tar.gz
+third_party/opensource-apps/empty-activity
+third_party/opensource-apps/empty-activity.tar.gz
+third_party/opensource-apps/empty-compose-activity
+third_party/opensource-apps/empty-compose-activity.tar.gz
 third_party/opensource-apps/friendlyeats
 third_party/opensource-apps/friendlyeats.tar.gz
 third_party/opensource-apps/iosched
diff --git a/build.gradle b/build.gradle
index 2045d5a..46548e4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,7 +2,6 @@
 // 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.
 
-import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
 import dx.DexMergerTask
 import dx.DxTask
 import net.ltgt.gradle.errorprone.CheckSeverity
@@ -21,9 +20,6 @@
         gradlePluginPortal()
         jcenter()
     }
-    dependencies {
-        classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
-    }
 }
 
 plugins {
@@ -270,6 +266,8 @@
     examplesTestNGRunnerCompile group: 'org.testng', name: 'testng', version: testngVersion
     testCompile sourceSets.examples.output
     testCompile "junit:junit:$junitVersion"
+    testCompile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+    testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
     testCompile group: 'org.smali', name: 'smali', version: smaliVersion
     testCompile files('third_party/jasmin/jasmin-2.4.jar')
     testCompile files('third_party/jdwp-tests/apache-harmony-jdwp-tests-host.jar')
@@ -290,8 +288,6 @@
     apiUsageSampleCompile "com.google.guava:guava:$guavaVersion"
     kotlinR8TestResourcesCompileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
     errorprone("com.google.errorprone:error_prone_core:$errorproneVersion")
-    testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
-    testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
 }
 
 def r8LibPath = "$buildDir/libs/r8lib.jar"
@@ -741,56 +737,56 @@
     }
 }
 
-static mergeServiceFiles(ShadowJar task) {
-    // Everything under META-INF is not included by default.
-    // Should include before 'relocate' so that the service file path and its content
-    // are properly relocated as well.
-    task.mergeServiceFiles {
-        include 'META-INF/services/*'
+def repackageDepFile(file) {
+    if (file.getName().endsWith('.jar')) {
+        return zipTree(file).matching {
+            exclude '**/module-info.class'
+            exclude 'META-INF/maven/**'
+            exclude 'META-INF/LICENSE.txt'
+            exclude 'META-INF/MANIFEST.MF'
+        }
+    } else {
+        return fileTree(file)
     }
 }
 
-task repackageDeps(type: ShadowJar) {
+task repackageDeps(type: Jar) {
     dependsOn downloadCloudDeps
-    configurations = [project.configurations.runtimeClasspath]
-    mergeServiceFiles(it)
-    exclude { it.getRelativePath().getPathString().endsWith("module-info.class") }
-    exclude { it.getRelativePath().getPathString().startsWith("META-INF/maven/") }
-    exclude { it.getRelativePath().getPathString().equals("META-INF/LICENSE.txt") }
-    baseName 'deps_all'
+    dependsOn project.configurations.runtimeClasspath
+    project.configurations.runtimeClasspath.forEach {
+        from repackageDepFile(it)
+    }
+    archiveFileName = 'deps_all.jar'
 }
 
-task repackageTestDeps(type: ShadowJar) {
+task repackageTestDeps(type: Jar) {
     dependsOn downloadCloudDeps
-    configurations = [project.configurations.testCompile]
-    mergeServiceFiles(it)
-    exclude { it.getRelativePath().getPathString().endsWith("module-info.class") }
-    exclude { it.getRelativePath().getPathString().startsWith("META-INF/maven/") }
-    baseName 'test_deps_all'
+    dependsOn project.configurations.testCompile
+    project.configurations.testCompile.forEach {
+        from repackageDepFile(it)
+    }
+    archiveFileName = 'test_deps_all.jar'
 }
 
-task repackageSources(type: ShadowJar) {
+task repackageSources(type: Jar) {
     // If this fails then remove all generated folders from
     // build/classes/java/test that is not {com,dalvik}
     from sourceSets.main.output
-    mergeServiceFiles(it)
-    baseName 'sources_main'
+    archiveFileName = 'sources_main.jar'
 }
 
-task repackageSources11(type: ShadowJar) {
+task repackageSources11(type: Jar) {
     from sourceSets.main11.output
-    mergeServiceFiles(it)
-    baseName 'sources_main_11'
+    archiveFileName = 'sources_main_11.jar'
 }
 
-def r8CreateTask(name, baseNameName, sources, includeSwissArmyKnife) {
-    return tasks.create("r8Create${name}", ShadowJar) {
+def r8CreateTask(name, baseName, sources, includeSwissArmyKnife) {
+    return tasks.create("r8Create${name}", Jar) {
+        dependsOn sources
         from consolidatedLicense.outputs.files
-        from sources
+        from sources.collect { zipTree(it) }
         exclude "$buildDir/classes/**"
-        baseName baseNameName
-        classifier = null
-        version = null
+        archiveFileName = baseName
         if (includeSwissArmyKnife) {
             manifest {
                 attributes 'Main-Class': 'com.android.tools.r8.SwissArmyKnife'
@@ -846,7 +842,7 @@
     inputs.files ([repackageSources.outputs, repackageDeps.outputs])
     def r8Task = r8CreateTask(
             'WithDeps',
-            'r8_with_deps',
+            'r8_with_deps.jar',
             repackageSources.outputs.files + repackageDeps.outputs.files,
             true)
     dependsOn r8Task
@@ -859,7 +855,7 @@
     inputs.files ([repackageSources11.outputs, repackageDeps.outputs])
     def r8Task = r8CreateTask(
             'WithDeps11',
-            'r8_with_deps_11',
+            'r8_with_deps_11.jar',
             repackageSources11.outputs.files + repackageDeps.outputs.files,
             true)
     dependsOn r8Task
@@ -885,7 +881,7 @@
     inputs.files repackageSources.outputs
     def r8Task = r8CreateTask(
             'WithoutDeps',
-            'r8_without_deps',
+            'r8_without_deps.jar',
             repackageSources.outputs.files,
             true)
     dependsOn r8Task
@@ -907,7 +903,7 @@
     inputs.files repackageSources.outputs
     def r8Task = r8CreateTask(
             'NoManifestWithoutDeps',
-            'r8_no_manifest_without_deps',
+            'r8_no_manifest_without_deps.jar',
             repackageSources.outputs.files,
             false)
     dependsOn r8Task
@@ -919,7 +915,7 @@
     inputs.files ([repackageSources.outputs, repackageDeps.outputs])
     def r8Task = r8CreateTask(
             'NoManifestWithDeps',
-            'r8_no_manifest_with_deps',
+            'r8_no_manifest_with_deps.jar',
             repackageSources.outputs.files + repackageDeps.outputs.files,
             false)
     dependsOn r8Task
@@ -943,10 +939,10 @@
     outputs.file "${buildDir}/libs/r8_no_manifest.jar"
 }
 
-task D8(type: ShadowJar) {
+task D8(type: Jar) {
     dependsOn r8
-    from r8.outputs.files[0]
-    baseName 'd8'
+    from zipTree(r8.outputs.files[0])
+    archiveFileName = 'd8.jar'
     manifest {
         attributes 'Main-Class': 'com.android.tools.r8.D8'
     }
@@ -1015,8 +1011,8 @@
     destinationDir file('build/libs')
 }
 
-task testJarSources(type: ShadowJar, dependsOn: [testClasses, buildLibraryDesugarConversions]) {
-    baseName = "r8testsbase"
+task testJarSources(type: Jar, dependsOn: [testClasses, buildLibraryDesugarConversions]) {
+    archiveFileName = "r8testsbase.jar"
     from sourceSets.test.output
     // We only want to include tests that use R8 when generating keep rules for applymapping.
     include "com/android/tools/r8/**"
@@ -1152,7 +1148,7 @@
 
 task jctfCommonJar(type: Jar) {
     from sourceSets.jctfCommon.output
-    baseName 'jctfCommon'
+    archiveFileName = 'jctfCommon.jar'
 }
 
 artifacts {
@@ -1186,13 +1182,13 @@
 
 task buildCfSegments(type: Jar, dependsOn: downloadDeps) {
     from sourceSets.cfSegments.output
-    baseName 'cf_segments'
+    archiveFileName = 'cf_segments.jar'
     destinationDir file('build/libs')
 }
 
 task buildR8ApiUsageSample(type: Jar) {
     from sourceSets.apiUsageSample.output
-    baseName 'r8_api_usage_sample'
+    archiveFileName = 'r8_api_usage_sample.jar'
     destinationDir file('tests')
 }
 
@@ -1763,7 +1759,7 @@
 }
 
 task buildPreNJdwpTestsJar(type: Jar) {
-    baseName = 'jdwp-tests-preN'
+    archiveFileName = 'jdwp-tests-preN.jar'
     from zipTree('third_party/jdwp-tests/apache-harmony-jdwp-tests-host.jar')
     // Exclude the classes containing java8
     exclude 'org/apache/harmony/jpda/tests/jdwp/InterfaceType/*.class'
diff --git a/infra/config/global/generated/luci-scheduler.cfg b/infra/config/global/generated/luci-scheduler.cfg
index bf973f3..6d92824 100644
--- a/infra/config/global/generated/luci-scheduler.cfg
+++ b/infra/config/global/generated/luci-scheduler.cfg
@@ -98,6 +98,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -126,6 +127,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -154,6 +156,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -182,6 +185,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -210,6 +214,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -238,6 +243,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -266,6 +272,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -294,6 +301,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -322,6 +330,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -350,6 +359,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -378,6 +388,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -434,6 +445,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -462,6 +474,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -490,6 +503,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -546,6 +560,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -574,6 +589,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -602,6 +618,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
@@ -630,6 +647,7 @@
   triggering_policy {
     kind: GREEDY_BATCHING
     max_concurrent_invocations: 4
+    max_batch_size: 1
   }
   buildbucket {
     server: "cr-buildbucket.appspot.com"
diff --git a/infra/config/global/generated/project.cfg b/infra/config/global/generated/project.cfg
index 525fcd4..b87c601 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.30.4"
+  version: "1.30.5"
   package_dir: ".."
   config_dir: "generated"
   entry_point: "main.star"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index a438dbc..02b45d5 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -173,6 +173,7 @@
       else ["main-gitiles-trigger"]
   triggering_policy = triggering_policy or scheduler.policy(
       kind = scheduler.GREEDY_BATCHING_KIND,
+      max_batch_size = 1 if release else None,
       max_concurrent_invocations = 4)
 
   luci.builder(
diff --git a/src/main/java/com/android/tools/r8/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
index 660b1ef..01e1163 100644
--- a/src/main/java/com/android/tools/r8/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
@@ -128,13 +128,7 @@
 
   private CfCode buildEmptyThrowingCfCode(DexMethod method) {
     CfInstruction insn[] = {new CfConstNull(), new CfThrow()};
-    return new CfCode(
-        method.holder,
-        1,
-        method.proto.parameters.size() + 1,
-        Arrays.asList(insn),
-        Collections.emptyList(),
-        Collections.emptyList());
+    return new CfCode(method.holder, 1, method.proto.parameters.size() + 1, Arrays.asList(insn));
   }
 
   private void addMethodsToHeaderJar(
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 171d1bc..5e43f29 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -630,6 +630,7 @@
             new BridgeHoisting(appViewWithLiveness).run();
 
             assert Inliner.verifyAllSingleCallerMethodsHaveBeenPruned(appViewWithLiveness);
+            assert Inliner.verifyAllMultiCallerInlinedMethodsHaveBeenPruned(appView);
 
             assert appView.allMergedClasses().verifyAllSourcesPruned(appViewWithLiveness);
             assert appView.validateUnboxedEnumsHaveBeenPruned();
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
index 644ed7d..c4bbada 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
@@ -75,6 +75,9 @@
       NamingLens namingLens,
       LensCodeRewriterUtils rewriter,
       MethodVisitor visitor) {
+    if (context.getName().toSourceString().equals("recordNewFieldSignature")) {
+      System.currentTimeMillis();
+    }
     DexCallSite rewrittenCallSite = rewriter.rewriteCallSite(callSite, context);
     DexMethodHandle bootstrapMethod = rewrittenCallSite.bootstrapMethod;
     List<DexValue> bootstrapArgs = rewrittenCallSite.bootstrapArgs;
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 a48fb5c..7c7824d 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -149,6 +149,7 @@
   private final List<CfTryCatch> tryCatchRanges;
   private final List<LocalVariableInfo> localVariables;
   private StackMapStatus stackMapStatus = StackMapStatus.NOT_VERIFIED;
+  private final com.android.tools.r8.position.Position diagnosticPosition;
 
   public CfCode(
       DexType originalHolder, int maxStack, int maxLocals, List<CfInstruction> instructions) {
@@ -168,12 +169,31 @@
       List<CfInstruction> instructions,
       List<CfTryCatch> tryCatchRanges,
       List<LocalVariableInfo> localVariables) {
+    this(
+        originalHolder,
+        maxStack,
+        maxLocals,
+        instructions,
+        tryCatchRanges,
+        localVariables,
+        com.android.tools.r8.position.Position.UNKNOWN);
+  }
+
+  public CfCode(
+      DexType originalHolder,
+      int maxStack,
+      int maxLocals,
+      List<CfInstruction> instructions,
+      List<CfTryCatch> tryCatchRanges,
+      List<LocalVariableInfo> localVariables,
+      com.android.tools.r8.position.Position diagnosticPosition) {
     this.originalHolder = originalHolder;
     this.maxStack = maxStack;
     this.maxLocals = maxLocals;
     this.instructions = instructions;
     this.tryCatchRanges = tryCatchRanges;
     this.localVariables = localVariables;
+    this.diagnosticPosition = diagnosticPosition;
   }
 
   @Override
@@ -208,6 +228,10 @@
     return stackMapStatus;
   }
 
+  public com.android.tools.r8.position.Position getDiagnosticPosition() {
+    return diagnosticPosition;
+  }
+
   public void setMaxLocals(int newMaxLocals) {
     maxLocals = newMaxLocals;
   }
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 e25cb47..8803346 100644
--- a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
@@ -134,7 +134,10 @@
 
   @Override
   public IRCode buildIR(ProgramMethod method, AppView<?> appView, Origin origin) {
-    DefaultInstanceInitializerSourceCode source = new DefaultInstanceInitializerSourceCode(method);
+    DexMethod originalMethod =
+        appView.graphLens().getOriginalMethodSignature(method.getReference());
+    DefaultInstanceInitializerSourceCode source =
+        new DefaultInstanceInitializerSourceCode(originalMethod);
     return IRBuilder.create(method, appView, source, origin).build(method);
   }
 
@@ -148,8 +151,10 @@
       Position callerPosition,
       Origin origin,
       RewrittenPrototypeDescription protoChanges) {
+    DexMethod originalMethod =
+        appView.graphLens().getOriginalMethodSignature(method.getReference());
     DefaultInstanceInitializerSourceCode source =
-        new DefaultInstanceInitializerSourceCode(method, callerPosition);
+        new DefaultInstanceInitializerSourceCode(originalMethod, callerPosition);
     return IRBuilder.createForInlining(
             method, appView, codeLens, source, origin, valueNumberGenerator, protoChanges)
         .build(context);
@@ -379,16 +384,16 @@
 
   static class DefaultInstanceInitializerSourceCode extends SyntheticStraightLineSourceCode {
 
-    DefaultInstanceInitializerSourceCode(ProgramMethod method) {
+    DefaultInstanceInitializerSourceCode(DexMethod method) {
       this(method, null);
     }
 
-    DefaultInstanceInitializerSourceCode(ProgramMethod method, Position callerPosition) {
+    DefaultInstanceInitializerSourceCode(DexMethod method, Position callerPosition) {
       super(
           getInstructionBuilders(),
           SyntheticPosition.builder()
               .setLine(0)
-              .setMethod(method.getReference())
+              .setMethod(method)
               .setCallerPosition(callerPosition)
               .build());
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 4b5152b..289f255 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -65,7 +65,6 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.NamingLens;
-import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.ConsumerUtils;
@@ -82,7 +81,6 @@
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.function.BiConsumer;
@@ -505,6 +503,11 @@
     return !isPrivateMethod() && isVirtualMethod();
   }
 
+  public boolean isNonStaticPrivateMethod() {
+    checkIfObsolete();
+    return isInstance() && isPrivate();
+  }
+
   /**
    * Returns true if this method can be invoked via invoke-virtual, invoke-super or invoke-interface
    * and is non-abstract.
@@ -958,9 +961,7 @@
         getReference().holder,
         1 + BooleanUtils.intValue(negate),
         getReference().getArity() + 1,
-        Arrays.asList(instructions),
-        Collections.emptyList(),
-        Collections.emptyList());
+        Arrays.asList(instructions));
   }
 
   public DexCode buildInstanceOfDexCode(DexType type, boolean negate) {
@@ -1078,13 +1079,7 @@
         .add(new CfConstString(message))
         .add(new CfInvoke(Opcodes.INVOKESPECIAL, exceptionInitMethod, false))
         .add(new CfThrow());
-    return new CfCode(
-        getReference().holder,
-        3,
-        locals,
-        instructionBuilder.build(),
-        Collections.emptyList(),
-        Collections.emptyList());
+    return new CfCode(getReference().holder, 3, locals, instructionBuilder.build());
   }
 
   public DexEncodedMethod toTypeSubstitutedMethod(DexMethod method) {
@@ -1241,10 +1236,6 @@
     return code == null ? "<no code>" : code.toString(this, null);
   }
 
-  public MethodPosition getPosition() {
-    return new MethodPosition(getReference().asMethodReference());
-  }
-
   @Override
   public boolean isDexEncodedMethod() {
     checkIfObsolete();
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index a6c1c5c..4c76629 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -421,11 +421,30 @@
     return lookupMethod(method, context.getReference(), Type.VIRTUAL);
   }
 
-  /** Lookup a rebound or non-rebound method reference using the current graph lens. */
-  public abstract MethodLookupResult lookupMethod(DexMethod method, DexMethod context, Type type);
+  public final MethodLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
+    return lookupMethod(method, context, type, null);
+  }
+
+  /**
+   * Lookup a rebound or non-rebound method reference using the current graph lens.
+   *
+   * @param codeLens Specifies the graph lens which has already been applied to the code object. The
+   *     lookup procedure will not recurse beyond this graph lens to ensure that each mapping is
+   *     applied at most once.
+   *     <p>Note: since the compiler currently inserts {@link ClearCodeRewritingGraphLens} it is
+   *     generally valid to pass null for the {@param codeLens}. The removal of {@link
+   *     ClearCodeRewritingGraphLens} is tracked by b/202368283. After this is removed, the compiler
+   *     should generally use the result of calling {@link AppView#codeLens()}.
+   */
+  public abstract MethodLookupResult lookupMethod(
+      DexMethod method, DexMethod context, Type type, GraphLens codeLens);
 
   protected abstract MethodLookupResult internalLookupMethod(
-      DexMethod reference, DexMethod context, Type type, LookupMethodContinuation continuation);
+      DexMethod reference,
+      DexMethod context,
+      Type type,
+      GraphLens codeLens,
+      LookupMethodContinuation continuation);
 
   interface LookupMethodContinuation {
 
@@ -435,21 +454,31 @@
   public abstract RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
       DexMethod method);
 
-  /** Lookup a rebound or non-rebound field reference using the current graph lens. */
-  public DexField lookupField(DexField field) {
-    // Lookup the field using the graph lens and return the (non-rebound) reference from the lookup
-    // result.
-    return lookupFieldResult(field).getReference();
+  public final DexField lookupField(DexField field) {
+    return lookupField(field, null);
   }
 
   /** Lookup a rebound or non-rebound field reference using the current graph lens. */
-  public FieldLookupResult lookupFieldResult(DexField field) {
+  public DexField lookupField(DexField field, GraphLens codeLens) {
+    // Lookup the field using the graph lens and return the (non-rebound) reference from the lookup
+    // result.
+    return lookupFieldResult(field, codeLens).getReference();
+  }
+
+  /** Lookup a rebound or non-rebound field reference using the current graph lens. */
+  public final FieldLookupResult lookupFieldResult(DexField field) {
     // Lookup the field using the graph lens and return the lookup result.
-    return internalLookupField(field, x -> x);
+    return lookupFieldResult(field, null);
+  }
+
+  /** Lookup a rebound or non-rebound field reference using the current graph lens. */
+  public final FieldLookupResult lookupFieldResult(DexField field, GraphLens codeLens) {
+    // Lookup the field using the graph lens and return the lookup result.
+    return internalLookupField(field, codeLens, x -> x);
   }
 
   protected abstract FieldLookupResult internalLookupField(
-      DexField reference, LookupFieldContinuation continuation);
+      DexField reference, GraphLens codeLens, LookupFieldContinuation continuation);
 
   interface LookupFieldContinuation {
 
@@ -759,7 +788,8 @@
     }
 
     @Override
-    public MethodLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
+    public MethodLookupResult lookupMethod(
+        DexMethod method, DexMethod context, Type type, GraphLens codeLens) {
       if (method.getHolderType().isArrayType()) {
         assert lookupType(method.getReturnType()) == method.getReturnType();
         assert method.getParameters().stream()
@@ -770,7 +800,7 @@
             .build();
       }
       assert method.getHolderType().isClassType();
-      return internalLookupMethod(method, context, type, result -> result);
+      return internalLookupMethod(method, context, type, codeLens, result -> result);
     }
 
     @Override
@@ -810,18 +840,33 @@
 
     @Override
     protected FieldLookupResult internalLookupField(
-        DexField reference, LookupFieldContinuation continuation) {
+        DexField reference, GraphLens codeLens, LookupFieldContinuation continuation) {
+      if (this == codeLens) {
+        return getIdentityLens().internalLookupField(reference, codeLens, continuation);
+      }
       return previousLens.internalLookupField(
-          reference, previous -> continuation.lookupField(internalDescribeLookupField(previous)));
+          reference,
+          codeLens,
+          previous -> continuation.lookupField(internalDescribeLookupField(previous)));
     }
 
     @Override
     protected MethodLookupResult internalLookupMethod(
-        DexMethod reference, DexMethod context, Type type, LookupMethodContinuation continuation) {
+        DexMethod reference,
+        DexMethod context,
+        Type type,
+        GraphLens codeLens,
+        LookupMethodContinuation continuation) {
+      if (this == codeLens) {
+        GraphLens identityLens = getIdentityLens();
+        return identityLens.internalLookupMethod(
+            reference, context, type, identityLens, continuation);
+      }
       return previousLens.internalLookupMethod(
           reference,
           internalGetPreviousMethodSignature(context),
           type,
+          codeLens,
           previous -> continuation.lookupMethod(internalDescribeLookupMethod(previous, context)));
     }
 
@@ -912,7 +957,9 @@
     }
 
     @Override
-    public MethodLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
+    public MethodLookupResult lookupMethod(
+        DexMethod method, DexMethod context, Type type, GraphLens codeLens) {
+      assert codeLens == null || codeLens.isIdentityLens();
       return MethodLookupResult.builder(this).setReference(method).setType(type).build();
     }
 
@@ -924,7 +971,7 @@
 
     @Override
     protected FieldLookupResult internalLookupField(
-        DexField reference, LookupFieldContinuation continuation) {
+        DexField reference, GraphLens codeLens, LookupFieldContinuation continuation) {
       // Passes the field reference back to the next graph lens. The identity lens intentionally
       // does not set the rebound field reference, since it does not know what that is.
       return continuation.lookupField(
@@ -933,11 +980,14 @@
 
     @Override
     protected MethodLookupResult internalLookupMethod(
-        DexMethod reference, DexMethod context, Type type, LookupMethodContinuation continuation) {
+        DexMethod reference,
+        DexMethod context,
+        Type type,
+        GraphLens codeLens,
+        LookupMethodContinuation continuation) {
       // Passes the method reference back to the next graph lens. The identity lens intentionally
       // does not set the rebound method reference, since it does not know what that is.
-      return continuation.lookupMethod(
-          MethodLookupResult.builder(this).setReference(reference).setType(type).build());
+      return continuation.lookupMethod(lookupMethod(reference, context, type, codeLens));
     }
 
     @Override
@@ -1004,8 +1054,8 @@
 
     @Override
     protected FieldLookupResult internalLookupField(
-        DexField reference, LookupFieldContinuation continuation) {
-      return getIdentityLens().internalLookupField(reference, continuation);
+        DexField reference, GraphLens codeLens, LookupFieldContinuation continuation) {
+      return getIdentityLens().internalLookupField(reference, codeLens, continuation);
     }
 
     @Override
@@ -1015,8 +1065,15 @@
 
     @Override
     protected MethodLookupResult internalLookupMethod(
-        DexMethod reference, DexMethod context, Type type, LookupMethodContinuation continuation) {
-      return getIdentityLens().internalLookupMethod(reference, context, type, continuation);
+        DexMethod reference,
+        DexMethod context,
+        Type type,
+        GraphLens codeLens,
+        LookupMethodContinuation continuation) {
+      assert codeLens == null || codeLens == this;
+      GraphLens identityLens = getIdentityLens();
+      return identityLens.internalLookupMethod(
+          reference, context, type, identityLens, continuation);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index 2403804..ae16c5a 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -62,6 +62,8 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.MethodPosition;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.position.TextRange;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.ExceptionUtils;
@@ -368,6 +370,8 @@
     private final LazyCfCode code;
     private final DexMethod method;
     private final Origin origin;
+    private int minLine = Integer.MAX_VALUE;
+    private int maxLine = -1;
 
     MethodCodeVisitor(
         JarApplicationReader application,
@@ -406,7 +410,7 @@
         throw new CompilationError(
             "Absent Code attribute in method that is not native or abstract",
             origin,
-            new MethodPosition(method.asMethodReference()));
+            MethodPosition.create(method.asMethodReference(), getDiagnosticPosition()));
       }
       code.setCode(
           new CfCode(
@@ -415,7 +419,20 @@
               maxLocals,
               instructions,
               tryCatchRanges,
-              localVariables));
+              localVariables,
+              getDiagnosticPosition()));
+    }
+
+    private com.android.tools.r8.position.Position getDiagnosticPosition() {
+      if (minLine == Integer.MAX_VALUE) {
+        return com.android.tools.r8.position.Position.UNKNOWN;
+      } else if (minLine == maxLine) {
+        return new TextPosition(0, minLine, TextPosition.UNKNOWN_COLUMN);
+      } else {
+        return new TextRange(
+            new TextPosition(0, minLine, TextPosition.UNKNOWN_COLUMN),
+            new TextPosition(0, maxLine, TextPosition.UNKNOWN_COLUMN));
+      }
     }
 
     @Override
@@ -1015,6 +1032,8 @@
 
     @Override
     public void visitLineNumber(int line, Label start) {
+      minLine = Math.min(line, minLine);
+      maxLine = Math.max(line, maxLine);
       if (debugParsingOptions.lineInfo) {
         instructions.add(
             new CfPosition(
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
index cf866d4..9bd0c4a 100644
--- a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.kotlin.KotlinMethodLevelInfo;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 /** Type representing a method definition in the programs compilation unit and its holder. */
@@ -171,8 +170,4 @@
   public KotlinMethodLevelInfo getKotlinInfo() {
     return getDefinition().getKotlinInfo();
   }
-
-  public MethodPosition getPosition() {
-    return getDefinition().getPosition();
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/UseRegistry.java b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
index 8ac7c59..09d477d 100644
--- a/src/main/java/com/android/tools/r8/graph/UseRegistry.java
+++ b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
@@ -40,6 +40,10 @@
     continuation = TraversalContinuation.BREAK;
   }
 
+  public GraphLens getCodeLens() {
+    return appView.codeLens();
+  }
+
   public final T getContext() {
     return context;
   }
@@ -69,7 +73,7 @@
 
   public void registerInvokeSpecial(DexMethod method) {
     DexClassAndMethod context = getMethodContext();
-    Invoke.Type type = Invoke.Type.fromInvokeSpecial(method, context, appView);
+    Invoke.Type type = Invoke.Type.fromInvokeSpecial(method, context, appView, getCodeLens());
     if (type.isDirect()) {
       registerInvokeDirect(method);
     } else {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
index b1219c4..bb8f046 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
@@ -42,7 +42,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Set;
@@ -136,12 +135,7 @@
           SyntheticPosition.builder().setLine(0).setMethod(syntheticMethodReference).build();
       List<CfInstruction> instructions = buildInstructions(callerPosition);
       return new CfCode(
-          syntheticMethodReference.getHolderType(),
-          maxStack,
-          maxLocals,
-          instructions,
-          Collections.emptyList(),
-          Collections.emptyList());
+          syntheticMethodReference.getHolderType(), maxStack, maxLocals, instructions);
     }
 
     private List<CfInstruction> buildInstructions(Position callerPosition) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java
index 0e35bf3..95f0385 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/deadlock/SingleCallerInformation.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraph;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
 import java.util.IdentityHashMap;
@@ -26,9 +27,9 @@
  * since computing single caller information for such methods is expensive (it involves computing
  * the possible dispatch targets for each virtual invoke).
  *
- * <p>Unlike the {@link com.android.tools.r8.ir.conversion.CallGraph} that is used to determine if a
- * method can be single caller inlined, this considers a method that is called from multiple call
- * sites in the same method to have a single caller.
+ * <p>Unlike the {@link CallGraph} that is used to determine if a method can be single caller
+ * inlined, this considers a method that is called from multiple call sites in the same method to
+ * have a single caller.
  */
 // TODO(b/205611444): account for -keep rules.
 public class SingleCallerInformation {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
index 3aa2dac..b5eec8c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
@@ -31,9 +31,9 @@
 import com.android.tools.r8.ir.code.SafeCheckCast;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.callgraph.Node;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.enums.EnumValueOptimizer;
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java
index 8c71936..337b88b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoInliningReasonStrategy.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.inliner.InliningReasonStrategy;
 
@@ -38,6 +39,7 @@
       InvokeMethod invoke,
       ProgramMethod target,
       ProgramMethod context,
+      DefaultInliningOracle oracle,
       MethodProcessor methodProcessor) {
     if (references.isAbstractGeneratedMessageLiteBuilder(context.getHolder())
         && invoke.isInvokeSuper()) {
@@ -48,7 +50,7 @@
     }
     return references.isDynamicMethod(target) || references.isDynamicMethodBridge(target)
         ? computeInliningReasonForDynamicMethod(invoke, target, context)
-        : parent.computeInliningReason(invoke, target, context, methodProcessor);
+        : parent.computeInliningReason(invoke, target, context, oracle, methodProcessor);
   }
 
   private Reason computeInliningReasonForDynamicMethod(
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldGet.java b/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
index 3f62f1f..aecb3eb 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
@@ -4,7 +4,11 @@
 
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.graph.DexField;
+
 public interface FieldGet {
 
+  DexField getField();
+
   Value outValue();
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
deleted file mode 100644
index 6ed9ef6..0000000
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ /dev/null
@@ -1,303 +0,0 @@
-// Copyright (c) 2017, 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.conversion;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator.CycleEliminationResult;
-import com.android.tools.r8.ir.conversion.CallSiteInformation.CallGraphBasedCallSiteInformation;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.Sets;
-import java.util.Iterator;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * Call graph representation.
- *
- * <p>Each node in the graph contain the methods called and the calling methods. For virtual and
- * interface calls all potential calls from subtypes are recorded.
- *
- * <p>Only methods in the program - not library methods - are represented.
- *
- * <p>The directional edges are represented as sets of nodes in each node (called methods and
- * callees).
- *
- * <p>A call from method <code>a</code> to method <code>b</code> is only present once no matter how
- * many calls of <code>a</code> there are in <code>a</code>.
- *
- * <p>Recursive calls are not present.
- */
-public class CallGraph {
-
-  public static class Node implements Comparable<Node> {
-
-    public static Node[] EMPTY_ARRAY = {};
-
-    private final ProgramMethod method;
-    private int numberOfCallSites = 0;
-
-    // Outgoing calls from this method.
-    private final Set<Node> callees = new TreeSet<>();
-
-    // Incoming calls to this method.
-    private final Set<Node> callers = new TreeSet<>();
-
-    // Incoming field read edges to this method (i.e., the set of methods that read a field written
-    // by the current method).
-    private final Set<Node> readers = new TreeSet<>();
-
-    // Outgoing field read edges from this method (i.e., the set of methods that write a field read
-    // by the current method).
-    private final Set<Node> writers = new TreeSet<>();
-
-    public Node(ProgramMethod method) {
-      this.method = method;
-    }
-
-    public void addCallerConcurrently(Node caller) {
-      addCallerConcurrently(caller, false);
-    }
-
-    public void addCallerConcurrently(Node caller, boolean likelySpuriousCallEdge) {
-      if (caller != this && !likelySpuriousCallEdge) {
-        boolean changedCallers;
-        synchronized (callers) {
-          changedCallers = callers.add(caller);
-          numberOfCallSites++;
-        }
-        if (changedCallers) {
-          synchronized (caller.callees) {
-            caller.callees.add(this);
-          }
-          // Avoid redundant field read edges (call edges are considered stronger).
-          removeReaderConcurrently(caller);
-        }
-      } else {
-        synchronized (callers) {
-          numberOfCallSites++;
-        }
-      }
-    }
-
-    public void addReaderConcurrently(Node reader) {
-      if (reader != this) {
-        synchronized (callers) {
-          if (callers.contains(reader)) {
-            // Avoid redundant field read edges (call edges are considered stronger).
-            return;
-          }
-          boolean readersChanged;
-          synchronized (readers) {
-            readersChanged = readers.add(reader);
-          }
-          if (readersChanged) {
-            synchronized (reader.writers) {
-              reader.writers.add(this);
-            }
-          }
-        }
-      }
-    }
-
-    private void removeReaderConcurrently(Node reader) {
-      synchronized (readers) {
-        readers.remove(reader);
-      }
-      synchronized (reader.writers) {
-        reader.writers.remove(this);
-      }
-    }
-
-    public void removeCaller(Node caller) {
-      boolean callersChanged = callers.remove(caller);
-      assert callersChanged;
-      boolean calleesChanged = caller.callees.remove(this);
-      assert calleesChanged;
-      assert !hasReader(caller);
-    }
-
-    public void removeReader(Node reader) {
-      boolean readersChanged = readers.remove(reader);
-      assert readersChanged;
-      boolean writersChanged = reader.writers.remove(this);
-      assert writersChanged;
-      assert !hasCaller(reader);
-    }
-
-    public void cleanCalleesAndWritersForRemoval() {
-      assert callers.isEmpty();
-      assert readers.isEmpty();
-      for (Node callee : callees) {
-        boolean changed = callee.callers.remove(this);
-        assert changed;
-      }
-      for (Node writer : writers) {
-        boolean changed = writer.readers.remove(this);
-        assert changed;
-      }
-    }
-
-    public void cleanCallersAndReadersForRemoval() {
-      assert callees.isEmpty();
-      assert writers.isEmpty();
-      for (Node caller : callers) {
-        boolean changed = caller.callees.remove(this);
-        assert changed;
-      }
-      for (Node reader : readers) {
-        boolean changed = reader.writers.remove(this);
-        assert changed;
-      }
-    }
-
-    public Set<Node> getCallersWithDeterministicOrder() {
-      return callers;
-    }
-
-    public Set<Node> getCalleesWithDeterministicOrder() {
-      return callees;
-    }
-
-    public Set<Node> getReadersWithDeterministicOrder() {
-      return readers;
-    }
-
-    public Set<Node> getWritersWithDeterministicOrder() {
-      return writers;
-    }
-
-    public int getNumberOfCallSites() {
-      return numberOfCallSites;
-    }
-
-    public boolean hasCallee(Node method) {
-      return callees.contains(method);
-    }
-
-    public boolean hasCaller(Node method) {
-      return callers.contains(method);
-    }
-
-    public boolean hasReader(Node method) {
-      return readers.contains(method);
-    }
-
-    public boolean hasWriter(Node method) {
-      return writers.contains(method);
-    }
-
-    public boolean isRoot() {
-      return callers.isEmpty() && readers.isEmpty();
-    }
-
-    public boolean isLeaf() {
-      return callees.isEmpty() && writers.isEmpty();
-    }
-
-    @Override
-    public int compareTo(Node other) {
-      return getProgramMethod().getReference().compareTo(other.getProgramMethod().getReference());
-    }
-
-    @Override
-    public String toString() {
-      StringBuilder builder = new StringBuilder();
-      builder.append("MethodNode for: ");
-      builder.append(getProgramMethod().toSourceString());
-      builder.append(" (");
-      builder.append(callees.size());
-      builder.append(" callees, ");
-      builder.append(callers.size());
-      builder.append(" callers");
-      builder.append(", invoke count ").append(numberOfCallSites);
-      builder.append(").");
-      builder.append(System.lineSeparator());
-      if (callees.size() > 0) {
-        builder.append("Callees:");
-        builder.append(System.lineSeparator());
-        for (Node call : callees) {
-          builder.append("  ");
-          builder.append(call.getProgramMethod().toSourceString());
-          builder.append(System.lineSeparator());
-        }
-      }
-      if (callers.size() > 0) {
-        builder.append("Callers:");
-        builder.append(System.lineSeparator());
-        for (Node caller : callers) {
-          builder.append("  ");
-          builder.append(caller.getProgramMethod().toSourceString());
-          builder.append(System.lineSeparator());
-        }
-      }
-      return builder.toString();
-    }
-
-    public DexEncodedMethod getMethod() {
-      return method.getDefinition();
-    }
-
-    public ProgramMethod getProgramMethod() {
-      return method;
-    }
-  }
-
-  final Set<Node> nodes;
-  final CycleEliminationResult cycleEliminationResult;
-
-  CallGraph(Set<Node> nodes) {
-    this(nodes, null);
-  }
-
-  CallGraph(Set<Node> nodes, CycleEliminationResult cycleEliminationResult) {
-    this.nodes = nodes;
-    this.cycleEliminationResult = cycleEliminationResult;
-  }
-
-  static CallGraphBuilder builder(AppView<AppInfoWithLiveness> appView) {
-    return new CallGraphBuilder(appView);
-  }
-
-  CallSiteInformation createCallSiteInformation(AppView<AppInfoWithLiveness> appView) {
-    // Don't leverage single/dual call site information when we are not tree shaking.
-    return appView.options().isShrinking()
-        ? new CallGraphBasedCallSiteInformation(appView, this)
-        : CallSiteInformation.empty();
-  }
-
-  public boolean isEmpty() {
-    return nodes.isEmpty();
-  }
-
-  public ProgramMethodSet extractLeaves() {
-    return extractNodes(Node::isLeaf, Node::cleanCallersAndReadersForRemoval);
-  }
-
-  public ProgramMethodSet extractRoots() {
-    return extractNodes(Node::isRoot, Node::cleanCalleesAndWritersForRemoval);
-  }
-
-  private ProgramMethodSet extractNodes(Predicate<Node> predicate, Consumer<Node> clean) {
-    ProgramMethodSet result = ProgramMethodSet.create();
-    Set<Node> removed = Sets.newIdentityHashSet();
-    Iterator<Node> nodeIterator = nodes.iterator();
-    while (nodeIterator.hasNext()) {
-      Node node = nodeIterator.next();
-      if (predicate.test(node)) {
-        result.add(node.getProgramMethod());
-        nodeIterator.remove();
-        removed.add(node);
-      }
-    }
-    removed.forEach(clean);
-    assert !result.isEmpty();
-    return result;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
deleted file mode 100644
index 58bfafa..0000000
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
+++ /dev/null
@@ -1,808 +0,0 @@
-// Copyright (c) 2019, 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.conversion;
-
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexCallSite;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.FieldAccessInfo;
-import com.android.tools.r8.graph.FieldAccessInfoCollection;
-import com.android.tools.r8.graph.GraphLens.MethodLookupResult;
-import com.android.tools.r8.graph.LookupResult;
-import com.android.tools.r8.graph.MethodResolutionResult;
-import com.android.tools.r8.graph.ProgramField;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
-import com.android.tools.r8.ir.code.Invoke;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
-import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator.CycleEliminationResult;
-import com.android.tools.r8.logging.Log;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.Iterators;
-import com.google.common.collect.Sets;
-import java.util.ArrayDeque;
-import java.util.Collection;
-import java.util.Deque;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-abstract class CallGraphBuilderBase {
-
-  final AppView<AppInfoWithLiveness> appView;
-  private final FieldAccessInfoCollection<?> fieldAccessInfoCollection;
-  final Map<DexMethod, Node> nodes = new IdentityHashMap<>();
-  private final Map<DexMethod, ProgramMethodSet> possibleProgramTargetsCache =
-      new ConcurrentHashMap<>();
-
-  CallGraphBuilderBase(AppView<AppInfoWithLiveness> appView) {
-    this.appView = appView;
-    this.fieldAccessInfoCollection = appView.appInfo().getFieldAccessInfoCollection();
-  }
-
-  public CallGraph build(ExecutorService executorService, Timing timing) throws ExecutionException {
-    timing.begin("Build IR processing order constraints");
-    timing.begin("Build call graph");
-    populateGraph(executorService);
-    assert verifyNoRedundantFieldReadEdges();
-    timing.end();
-    assert verifyAllMethodsWithCodeExists();
-
-    appView.withGeneratedMessageLiteBuilderShrinker(
-        shrinker -> shrinker.preprocessCallGraphBeforeCycleElimination(nodes));
-
-    timing.begin("Cycle elimination");
-    // Sort the nodes for deterministic cycle elimination.
-    Set<Node> nodesWithDeterministicOrder = Sets.newTreeSet(nodes.values());
-    CycleEliminator cycleEliminator = new CycleEliminator();
-    CycleEliminationResult cycleEliminationResult =
-        cycleEliminator.breakCycles(nodesWithDeterministicOrder);
-    timing.end();
-    timing.end();
-    assert cycleEliminator.breakCycles(nodesWithDeterministicOrder).numberOfRemovedCallEdges()
-        == 0; // The cycles should be gone.
-
-    return new CallGraph(nodesWithDeterministicOrder, cycleEliminationResult);
-  }
-
-  abstract void populateGraph(ExecutorService executorService) throws ExecutionException;
-
-  /** Verify that there are no field read edges in the graph if there is also a call graph edge. */
-  private boolean verifyNoRedundantFieldReadEdges() {
-    for (Node writer : nodes.values()) {
-      for (Node reader : writer.getReadersWithDeterministicOrder()) {
-        assert !writer.hasCaller(reader);
-      }
-    }
-    return true;
-  }
-
-  Node getOrCreateNode(ProgramMethod method) {
-    synchronized (nodes) {
-      return nodes.computeIfAbsent(method.getReference(), ignore -> new Node(method));
-    }
-  }
-
-  abstract boolean verifyAllMethodsWithCodeExists();
-
-  class InvokeExtractor extends UseRegistry<ProgramMethod> {
-
-    private final Node currentMethod;
-    private final Predicate<ProgramMethod> targetTester;
-
-    InvokeExtractor(Node currentMethod, Predicate<ProgramMethod> targetTester) {
-      super(appView, currentMethod.getProgramMethod());
-      this.currentMethod = currentMethod;
-      this.targetTester = targetTester;
-    }
-
-    private void addClassInitializerTarget(DexProgramClass clazz) {
-      assert clazz != null;
-      if (clazz.hasClassInitializer()) {
-        addCallEdge(clazz.getProgramClassInitializer(), false);
-      }
-    }
-
-    private void addClassInitializerTarget(DexType type) {
-      assert type.isClassType();
-      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
-      if (clazz != null) {
-        addClassInitializerTarget(clazz);
-      }
-    }
-
-    private void addCallEdge(ProgramMethod callee, boolean likelySpuriousCallEdge) {
-      if (!targetTester.test(callee)) {
-        return;
-      }
-      if (callee.getDefinition().isAbstract()) {
-        // Not a valid target.
-        return;
-      }
-      if (callee.getDefinition().isNative()) {
-        // We don't care about calls to native methods.
-        return;
-      }
-      if (!appView.getKeepInfo(callee).isInliningAllowed(appView.options())) {
-        // Since the callee is kept and optimizations are disallowed, we cannot inline it into the
-        // caller, and we also cannot collect any optimization info for the method. Therefore, we
-        // drop the call edge to reduce the total number of call graph edges, which should lead to
-        // fewer call graph cycles.
-        return;
-      }
-      getOrCreateNode(callee).addCallerConcurrently(currentMethod, likelySpuriousCallEdge);
-    }
-
-    private void addFieldReadEdge(DexEncodedMethod writer) {
-      addFieldReadEdge(writer.asProgramMethod(appView));
-    }
-
-    private void addFieldReadEdge(ProgramMethod writer) {
-      assert !writer.getDefinition().isAbstract();
-      if (!targetTester.test(writer)) {
-        return;
-      }
-      getOrCreateNode(writer).addReaderConcurrently(currentMethod);
-    }
-
-    private void processInvoke(Invoke.Type originalType, DexMethod originalMethod) {
-      ProgramMethod context = currentMethod.getProgramMethod();
-      MethodLookupResult result =
-          appView.graphLens().lookupMethod(originalMethod, context.getReference(), originalType);
-      DexMethod method = result.getReference();
-      Invoke.Type type = result.getType();
-      if (type == Invoke.Type.INTERFACE || type == Invoke.Type.VIRTUAL) {
-        // For virtual and interface calls add all potential targets that could be called.
-        MethodResolutionResult resolutionResult =
-            appView.appInfo().resolveMethod(method, type == Invoke.Type.INTERFACE);
-        DexEncodedMethod target = resolutionResult.getSingleTarget();
-        if (target != null) {
-          processInvokeWithDynamicDispatch(type, target, context);
-        }
-      } else {
-        ProgramMethod singleTarget =
-            appView.appInfo().lookupSingleProgramTarget(type, method, context, appView);
-        if (singleTarget != null) {
-          assert !context.getDefinition().isBridge()
-              || singleTarget.getDefinition() != context.getDefinition();
-          // For static invokes, the class could be initialized.
-          if (type.isStatic()) {
-            addClassInitializerTarget(singleTarget.getHolder());
-          }
-          addCallEdge(singleTarget, false);
-        }
-      }
-    }
-
-    private void processInvokeWithDynamicDispatch(
-        Invoke.Type type, DexEncodedMethod encodedTarget, ProgramMethod context) {
-      DexMethod target = encodedTarget.getReference();
-      DexClass clazz = appView.definitionFor(target.holder);
-      if (clazz == null) {
-        assert false : "Unable to lookup holder of `" + target.toSourceString() + "`";
-        return;
-      }
-
-      if (!appView.options().testing.addCallEdgesForLibraryInvokes) {
-        if (clazz.isLibraryClass()) {
-          // Likely to have many possible targets.
-          return;
-        }
-      }
-
-      boolean isInterface = type == Invoke.Type.INTERFACE;
-      ProgramMethodSet possibleProgramTargets =
-          possibleProgramTargetsCache.computeIfAbsent(
-              target,
-              method -> {
-                MethodResolutionResult resolution =
-                    appView.appInfo().resolveMethod(method, isInterface);
-                if (resolution.isVirtualTarget()) {
-                  LookupResult lookupResult =
-                      resolution.lookupVirtualDispatchTargets(
-                          context.getHolder(), appView.appInfo());
-                  if (lookupResult.isLookupResultSuccess()) {
-                    ProgramMethodSet targets = ProgramMethodSet.create();
-                    lookupResult
-                        .asLookupResultSuccess()
-                        .forEach(
-                            methodTarget -> {
-                              if (methodTarget.isProgramMethod()) {
-                                targets.add(methodTarget.asProgramMethod());
-                              }
-                            },
-                            lambdaTarget -> {
-                              // The call target will ultimately be the implementation method.
-                              DexClassAndMethod implementationMethod =
-                                  lambdaTarget.getImplementationMethod();
-                              if (implementationMethod.isProgramMethod()) {
-                                targets.add(implementationMethod.asProgramMethod());
-                              }
-                            });
-                    return targets;
-                  }
-                }
-                return null;
-              });
-      if (possibleProgramTargets != null) {
-        boolean likelySpuriousCallEdge =
-            possibleProgramTargets.size()
-                >= appView.options().callGraphLikelySpuriousCallEdgeThreshold;
-        for (ProgramMethod possibleTarget : possibleProgramTargets) {
-          addCallEdge(possibleTarget, likelySpuriousCallEdge);
-        }
-      }
-    }
-
-    private void processFieldRead(DexField reference) {
-      if (!reference.holder.isClassType()) {
-        return;
-      }
-
-      ProgramField field = appView.appInfo().resolveField(reference).getProgramField();
-      if (field == null || appView.appInfo().isPinned(field)) {
-        return;
-      }
-
-      // Each static field access implicitly triggers the class initializer.
-      if (field.getAccessFlags().isStatic()) {
-        addClassInitializerTarget(field.getHolder());
-      }
-
-      FieldAccessInfo fieldAccessInfo = fieldAccessInfoCollection.get(field.getReference());
-      if (fieldAccessInfo != null && fieldAccessInfo.hasKnownWriteContexts()) {
-        if (fieldAccessInfo.getNumberOfWriteContexts() == 1) {
-          fieldAccessInfo.forEachWriteContext(this::addFieldReadEdge);
-        }
-      }
-    }
-
-    private void processFieldWrite(DexField reference) {
-      if (reference.getHolderType().isClassType()) {
-        ProgramField field = appView.appInfo().resolveField(reference).getProgramField();
-        if (field != null && field.getAccessFlags().isStatic()) {
-          // Each static field access implicitly triggers the class initializer.
-          addClassInitializerTarget(field.getHolder());
-        }
-      }
-    }
-
-    private void processInitClass(DexType type) {
-      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
-      if (clazz == null) {
-        assert false;
-        return;
-      }
-      addClassInitializerTarget(clazz);
-    }
-
-    @Override
-    public void registerInitClass(DexType clazz) {
-      processInitClass(clazz);
-    }
-
-    @Override
-    public void registerInvokeVirtual(DexMethod method) {
-      processInvoke(Invoke.Type.VIRTUAL, method);
-    }
-
-    @Override
-    public void registerInvokeDirect(DexMethod method) {
-      processInvoke(Invoke.Type.DIRECT, method);
-    }
-
-    @Override
-    public void registerInvokeStatic(DexMethod method) {
-      processInvoke(Invoke.Type.STATIC, method);
-    }
-
-    @Override
-    public void registerInvokeInterface(DexMethod method) {
-      processInvoke(Invoke.Type.INTERFACE, method);
-    }
-
-    @Override
-    public void registerInvokeSuper(DexMethod method) {
-      processInvoke(Invoke.Type.SUPER, method);
-    }
-
-    @Override
-    public void registerInstanceFieldRead(DexField field) {
-      processFieldRead(field);
-    }
-
-    @Override
-    public void registerInstanceFieldWrite(DexField field) {
-      processFieldWrite(field);
-    }
-
-    @Override
-    public void registerNewInstance(DexType type) {
-      if (type.isClassType()) {
-        addClassInitializerTarget(type);
-      }
-    }
-
-    @Override
-    public void registerStaticFieldRead(DexField field) {
-      processFieldRead(field);
-    }
-
-    @Override
-    public void registerStaticFieldWrite(DexField field) {
-      processFieldWrite(field);
-    }
-
-    @Override
-    public void registerTypeReference(DexType type) {}
-
-    @Override
-    public void registerInstanceOf(DexType type) {}
-
-    @Override
-    public void registerCallSite(DexCallSite callSite) {
-      registerMethodHandle(
-          callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
-    }
-  }
-
-  static class CycleEliminator {
-
-    static final String CYCLIC_FORCE_INLINING_MESSAGE =
-        "Unable to satisfy force inlining constraints due to cyclic force inlining";
-
-    private static class CallEdge {
-
-      private final Node caller;
-      private final Node callee;
-
-      CallEdge(Node caller, Node callee) {
-        this.caller = caller;
-        this.callee = callee;
-      }
-    }
-
-    static class StackEntryInfo {
-
-      final int index;
-      final Node predecessor;
-
-      boolean processed;
-
-      StackEntryInfo(int index, Node predecessor) {
-        this.index = index;
-        this.predecessor = predecessor;
-      }
-    }
-
-    static class CycleEliminationResult {
-
-      private Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges;
-
-      CycleEliminationResult(Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges) {
-        this.removedCallEdges = removedCallEdges;
-      }
-
-      void forEachRemovedCaller(ProgramMethod callee, Consumer<ProgramMethod> fn) {
-        removedCallEdges.getOrDefault(callee.getDefinition(), ProgramMethodSet.empty()).forEach(fn);
-      }
-
-      int numberOfRemovedCallEdges() {
-        int numberOfRemovedCallEdges = 0;
-        for (ProgramMethodSet nodes : removedCallEdges.values()) {
-          numberOfRemovedCallEdges += nodes.size();
-        }
-        return numberOfRemovedCallEdges;
-      }
-    }
-
-    // DFS stack.
-    private Deque<Node> stack = new ArrayDeque<>();
-
-    // Nodes on the DFS stack.
-    private Map<Node, StackEntryInfo> stackEntryInfo = new IdentityHashMap<>();
-
-    // Subset of the DFS stack, where the nodes on the stack are class initializers.
-    //
-    // This stack is used to efficiently compute if there is a class initializer on the stack.
-    private Deque<Node> clinitCallStack = new ArrayDeque<>();
-
-    // Subset of the DFS stack, where the nodes on the stack satisfy that the edge from the
-    // predecessor to the node itself is a field read edge.
-    //
-    // This stack is used to efficiently compute if there is a field read edge inside a cycle when
-    // a cycle is found.
-    private Deque<Node> writerStack = new ArrayDeque<>();
-
-    // Set of nodes that have been visited entirely.
-    private Set<Node> marked = Sets.newIdentityHashSet();
-
-    // Call edges that should be removed when the caller has been processed. These are not removed
-    // directly since that would lead to ConcurrentModificationExceptions.
-    private Map<Node, Set<Node>> calleesToBeRemoved = new IdentityHashMap<>();
-
-    // Field read edges that should be removed when the reader has been processed. These are not
-    // removed directly since that would lead to ConcurrentModificationExceptions.
-    private Map<Node, Set<Node>> writersToBeRemoved = new IdentityHashMap<>();
-
-    // Mapping from callee to the set of callers that were removed from the callee.
-    private Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges = new IdentityHashMap<>();
-
-    // Set of nodes from which cycle elimination must be rerun to ensure that all cycles will be
-    // removed.
-    private LinkedHashSet<Node> revisit = new LinkedHashSet<>();
-
-    CycleEliminationResult breakCycles(Collection<Node> roots) {
-      // Break cycles in this call graph by removing edges causing cycles. We do this in a fixpoint
-      // because the algorithm does not guarantee that all cycles will be removed from the graph
-      // when we remove an edge in the middle of a cycle that contains another cycle.
-      do {
-        traverse(roots);
-        roots = revisit;
-        prepareForNewTraversal();
-      } while (!roots.isEmpty());
-
-      CycleEliminationResult result = new CycleEliminationResult(removedCallEdges);
-      if (Log.ENABLED) {
-        Log.info(getClass(), "# call graph cycles broken: %s", result.numberOfRemovedCallEdges());
-      }
-      reset();
-      return result;
-    }
-
-    private void prepareForNewTraversal() {
-      assert calleesToBeRemoved.isEmpty();
-      assert clinitCallStack.isEmpty();
-      assert stack.isEmpty();
-      assert stackEntryInfo.isEmpty();
-      assert writersToBeRemoved.isEmpty();
-      assert writerStack.isEmpty();
-      marked.clear();
-      revisit = new LinkedHashSet<>();
-    }
-
-    private void reset() {
-      assert clinitCallStack.isEmpty();
-      assert marked.isEmpty();
-      assert revisit.isEmpty();
-      assert stack.isEmpty();
-      assert stackEntryInfo.isEmpty();
-      assert writerStack.isEmpty();
-      removedCallEdges = new IdentityHashMap<>();
-    }
-
-    private static class WorkItem {
-      boolean isNode() {
-        return false;
-      }
-
-      NodeWorkItem asNode() {
-        return null;
-      }
-
-      boolean isIterator() {
-        return false;
-      }
-
-      IteratorWorkItem asIterator() {
-        return null;
-      }
-    }
-
-    private static class NodeWorkItem extends WorkItem {
-      private final Node node;
-
-      NodeWorkItem(Node node) {
-        this.node = node;
-      }
-
-      @Override
-      boolean isNode() {
-        return true;
-      }
-
-      @Override
-      NodeWorkItem asNode() {
-        return this;
-      }
-    }
-
-    private static class IteratorWorkItem extends WorkItem {
-      private final Node callerOrReader;
-      private final Iterator<Node> calleesAndWriters;
-
-      IteratorWorkItem(Node callerOrReader, Iterator<Node> calleesAndWriters) {
-        this.callerOrReader = callerOrReader;
-        this.calleesAndWriters = calleesAndWriters;
-      }
-
-      @Override
-      boolean isIterator() {
-        return true;
-      }
-
-      @Override
-      IteratorWorkItem asIterator() {
-        return this;
-      }
-    }
-
-    private void traverse(Collection<Node> roots) {
-      Deque<WorkItem> workItems = new ArrayDeque<>(roots.size());
-      for (Node node : roots) {
-        workItems.addLast(new NodeWorkItem(node));
-      }
-      while (!workItems.isEmpty()) {
-        WorkItem workItem = workItems.removeFirst();
-        if (workItem.isNode()) {
-          Node node = workItem.asNode().node;
-          if (marked.contains(node)) {
-            // Already visited all nodes that can be reached from this node.
-            continue;
-          }
-
-          Node predecessor = stack.isEmpty() ? null : stack.peek();
-          push(node, predecessor);
-
-          // The callees and writers must be sorted before calling traverse recursively.
-          // This ensures that cycles are broken the same way across multiple compilations.
-          Iterator<Node> calleesAndWriterIterator =
-              Iterators.concat(
-                  node.getCalleesWithDeterministicOrder().iterator(),
-                  node.getWritersWithDeterministicOrder().iterator());
-          workItems.addFirst(new IteratorWorkItem(node, calleesAndWriterIterator));
-        } else {
-          assert workItem.isIterator();
-          IteratorWorkItem iteratorWorkItem = workItem.asIterator();
-          Node newCallerOrReader =
-              iterateCalleesAndWriters(
-                  iteratorWorkItem.calleesAndWriters, iteratorWorkItem.callerOrReader);
-          if (newCallerOrReader != null) {
-            // We did not finish the work on this iterator, so add it again.
-            workItems.addFirst(iteratorWorkItem);
-            workItems.addFirst(new NodeWorkItem(newCallerOrReader));
-          } else {
-            assert !iteratorWorkItem.calleesAndWriters.hasNext();
-            pop(iteratorWorkItem.callerOrReader);
-            marked.add(iteratorWorkItem.callerOrReader);
-
-            Collection<Node> calleesToBeRemovedFromCaller =
-                calleesToBeRemoved.remove(iteratorWorkItem.callerOrReader);
-            if (calleesToBeRemovedFromCaller != null) {
-              calleesToBeRemovedFromCaller.forEach(
-                  callee -> {
-                    callee.removeCaller(iteratorWorkItem.callerOrReader);
-                    recordCallEdgeRemoval(iteratorWorkItem.callerOrReader, callee);
-                  });
-            }
-
-            Collection<Node> writersToBeRemovedFromReader =
-                writersToBeRemoved.remove(iteratorWorkItem.callerOrReader);
-            if (writersToBeRemovedFromReader != null) {
-              writersToBeRemovedFromReader.forEach(
-                  writer -> writer.removeReader(iteratorWorkItem.callerOrReader));
-            }
-          }
-        }
-      }
-    }
-
-    private Node iterateCalleesAndWriters(
-        Iterator<Node> calleeOrWriterIterator, Node callerOrReader) {
-      while (calleeOrWriterIterator.hasNext()) {
-        Node calleeOrWriter = calleeOrWriterIterator.next();
-        StackEntryInfo calleeOrWriterStackEntryInfo = stackEntryInfo.get(calleeOrWriter);
-        boolean foundCycle = calleeOrWriterStackEntryInfo != null;
-        if (!foundCycle) {
-          return calleeOrWriter;
-        }
-
-        // Found a cycle that needs to be eliminated. If it is a field read edge, then remove it
-        // right away.
-        boolean isFieldReadEdge = calleeOrWriter.hasReader(callerOrReader);
-        if (isFieldReadEdge) {
-          removeFieldReadEdge(callerOrReader, calleeOrWriter);
-          continue;
-        }
-
-        // Otherwise, it is a call edge. Check if there is a field read edge in the cycle, and if
-        // so, remove that edge.
-        if (!writerStack.isEmpty()
-            && removeIncomingEdgeOnStack(
-                writerStack.peek(),
-                calleeOrWriter,
-                calleeOrWriterStackEntryInfo,
-                this::removeFieldReadEdge)) {
-          continue;
-        }
-
-        // It is a call edge and the cycle does not contain any field read edges.
-        // If it is a call edge to a <clinit>, then remove it.
-        if (calleeOrWriter.getMethod().isClassInitializer()) {
-          // Calls to class initializers are always safe to remove.
-          assert callEdgeRemovalIsSafe(callerOrReader, calleeOrWriter);
-          removeCallEdge(callerOrReader, calleeOrWriter);
-          continue;
-        }
-
-        // Otherwise, check if there is a call edge to a <clinit> method in the cycle, and if so,
-        // remove that edge.
-        if (!clinitCallStack.isEmpty()
-            && removeIncomingEdgeOnStack(
-                clinitCallStack.peek(),
-                calleeOrWriter,
-                calleeOrWriterStackEntryInfo,
-                this::removeCallEdge)) {
-          continue;
-        }
-
-        // Otherwise, we remove the call edge if it is safe according to force inlining.
-        if (callEdgeRemovalIsSafe(callerOrReader, calleeOrWriter)) {
-          // Break the cycle by removing the edge node->calleeOrWriter.
-          // Need to remove `calleeOrWriter` from `node.callees` using the iterator to prevent a
-          // ConcurrentModificationException.
-          removeCallEdge(callerOrReader, calleeOrWriter);
-          continue;
-        }
-
-        // The call edge cannot be removed due to force inlining. Find another call edge in the
-        // cycle that can safely be removed instead.
-        LinkedList<Node> cycle = extractCycle(calleeOrWriter);
-
-        // Break the cycle by finding an edge that can be removed without breaking force
-        // inlining. If that is not possible, this call fails with a compilation error.
-        CallEdge edge = findCallEdgeForRemoval(cycle);
-
-        // The edge will be null if this cycle has already been eliminated as a result of
-        // another cycle elimination.
-        if (edge != null) {
-          assert callEdgeRemovalIsSafe(edge.caller, edge.callee);
-
-          // Break the cycle by removing the edge caller->callee.
-          removeCallEdge(edge.caller, edge.callee);
-          revisit.add(edge.callee);
-        }
-
-        // Recover the stack.
-        recoverStack(cycle);
-      }
-      return null;
-    }
-
-    private void push(Node node, Node predecessor) {
-      stack.push(node);
-      assert !stackEntryInfo.containsKey(node);
-      stackEntryInfo.put(node, new StackEntryInfo(stack.size() - 1, predecessor));
-      if (predecessor != null) {
-        if (node.getMethod().isClassInitializer() && node.hasCaller(predecessor)) {
-          clinitCallStack.push(node);
-        } else if (predecessor.getWritersWithDeterministicOrder().contains(node)) {
-          writerStack.push(node);
-        }
-      }
-    }
-
-    private void pop(Node node) {
-      Node popped = stack.pop();
-      assert popped == node;
-      assert stackEntryInfo.containsKey(node);
-      stackEntryInfo.remove(node);
-      if (clinitCallStack.peek() == popped) {
-        assert writerStack.peek() != popped;
-        clinitCallStack.pop();
-      } else if (writerStack.peek() == popped) {
-        writerStack.pop();
-      }
-    }
-
-    private void removeCallEdge(Node caller, Node callee) {
-      calleesToBeRemoved.computeIfAbsent(caller, ignore -> Sets.newIdentityHashSet()).add(callee);
-    }
-
-    private void removeFieldReadEdge(Node reader, Node writer) {
-      writersToBeRemoved.computeIfAbsent(reader, ignore -> Sets.newIdentityHashSet()).add(writer);
-    }
-
-    private boolean removeIncomingEdgeOnStack(
-        Node target,
-        Node currentCalleeOrWriter,
-        StackEntryInfo currentCalleeOrWriterStackEntryInfo,
-        BiConsumer<Node, Node> edgeRemover) {
-      StackEntryInfo targetStackEntryInfo = stackEntryInfo.get(target);
-      boolean cycleContainsTarget =
-          targetStackEntryInfo.index > currentCalleeOrWriterStackEntryInfo.index;
-      if (cycleContainsTarget) {
-        assert verifyCycleSatisfies(
-            currentCalleeOrWriter,
-            cycle -> cycle.contains(target) && cycle.contains(targetStackEntryInfo.predecessor));
-        if (!targetStackEntryInfo.processed) {
-          edgeRemover.accept(targetStackEntryInfo.predecessor, target);
-          revisit.add(target);
-          targetStackEntryInfo.processed = true;
-        }
-        return true;
-      }
-      return false;
-    }
-
-    private LinkedList<Node> extractCycle(Node entry) {
-      LinkedList<Node> cycle = new LinkedList<>();
-      do {
-        assert !stack.isEmpty();
-        cycle.add(stack.pop());
-      } while (cycle.getLast() != entry);
-      return cycle;
-    }
-
-    private boolean verifyCycleSatisfies(Node entry, Predicate<LinkedList<Node>> predicate) {
-      LinkedList<Node> cycle = extractCycle(entry);
-      assert predicate.test(cycle);
-      recoverStack(cycle);
-      return true;
-    }
-
-    private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) {
-      Node callee = extractedCycle.getLast();
-      for (Node caller : extractedCycle) {
-        if (caller.hasWriter(callee)) {
-          // Not a call edge.
-          assert !caller.hasCallee(callee);
-          assert !callee.hasCaller(caller);
-          callee = caller;
-          continue;
-        }
-        if (!caller.hasCallee(callee)) {
-          // No need to break any edges since this cycle has already been broken previously.
-          assert !callee.hasCaller(caller);
-          return null;
-        }
-        if (callEdgeRemovalIsSafe(caller, callee)) {
-          return new CallEdge(caller, callee);
-        }
-        callee = caller;
-      }
-      throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE);
-    }
-
-    private static boolean callEdgeRemovalIsSafe(Node callerOrReader, Node calleeOrWriter) {
-      // All call edges where the callee is a method that should be force inlined must be kept,
-      // to guarantee that the IR converter will process the callee before the caller.
-      assert calleeOrWriter.hasCaller(callerOrReader);
-      return !calleeOrWriter.getMethod().getOptimizationInfo().forceInline();
-    }
-
-    private void recordCallEdgeRemoval(Node caller, Node callee) {
-      removedCallEdges
-          .computeIfAbsent(callee.getMethod(), ignore -> ProgramMethodSet.create(2))
-          .add(caller.getProgramMethod());
-    }
-
-    private void recoverStack(LinkedList<Node> extractedCycle) {
-      Iterator<Node> descendingIt = extractedCycle.descendingIterator();
-      while (descendingIt.hasNext()) {
-        stack.push(descendingIt.next());
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index b358828..34142c2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -371,13 +371,19 @@
         localVariablesTable.add(info);
       }
     }
+    com.android.tools.r8.position.Position diagnosticPosition =
+        com.android.tools.r8.position.Position.UNKNOWN;
+    if (method.getCode().isCfCode()) {
+      diagnosticPosition = method.getCode().asCfCode().getDiagnosticPosition();
+    }
     return new CfCode(
         method.getHolderType(),
         stackHeightTracker.maxHeight,
         registerAllocator.registersUsed(),
         instructions,
         tryCatchRanges,
-        localVariablesTable);
+        localVariablesTable,
+        diagnosticPosition);
   }
 
   private static boolean isNopInstruction(Instruction instruction, BasicBlock nextBlock) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java
index 22b279b..5737361 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/D8MethodProcessor.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer.D8CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
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 dc97590..327326c 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
@@ -678,6 +678,7 @@
           this::waveDone,
           timing,
           executorService);
+      lastWaveDone(postMethodProcessorBuilder, executorService);
       assert appView.graphLens() == graphLensForPrimaryOptimizationPass;
       timing.end();
     }
@@ -713,9 +714,6 @@
       libraryMethodOverrideAnalysis.finish();
     }
 
-    ConsumerUtils.acceptIfNotNull(
-        inliner, inliner -> inliner.enqueueMethodsForReprocessing(postMethodProcessorBuilder));
-
     if (!options.debug) {
       new TrivialFieldAccessReprocessor(appView.withLiveness(), postMethodProcessorBuilder)
           .run(executorService, feedback, timing);
@@ -848,6 +846,14 @@
     }
   }
 
+  private void lastWaveDone(
+      PostMethodProcessor.Builder postMethodProcessorBuilder, ExecutorService executorService)
+      throws ExecutionException {
+    if (inliner != null) {
+      inliner.onLastWaveDone(postMethodProcessorBuilder, executorService, timing);
+    }
+  }
+
   public void addWaveDoneAction(com.android.tools.r8.utils.Action action) {
     if (!appView.enableWholeProgramOptimizations()) {
       throw new Unreachable("addWaveDoneAction() should never be used in D8.");
@@ -1532,6 +1538,10 @@
       enumUnboxer.analyzeEnums(code, conversionOptions);
     }
 
+    if (inliner != null) {
+      inliner.recordCallEdgesForMultiCallerInlining(method, code, methodProcessor, timing);
+    }
+
     if (libraryMethodOverrideAnalysis != null) {
       timing.begin("Analyze library method overrides");
       libraryMethodOverrideAnalysis.analyze(code);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
index d0b0349..dbe72e8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation;
 
 public abstract class MethodProcessor {
 
@@ -12,6 +13,10 @@
     return false;
   }
 
+  public PrimaryMethodProcessor asPrimaryMethodProcessor() {
+    return null;
+  }
+
   public boolean isPostMethodProcessor() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessorWithWave.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessorWithWave.java
index f0c766d..c3953a2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessorWithWave.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessorWithWave.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 
 public abstract class MethodProcessorWithWave extends MethodProcessor {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
index c0f6e90..6e69b3c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -15,6 +15,8 @@
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.PrimaryMethodProcessor.MethodAction;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraph;
+import com.android.tools.r8.ir.conversion.callgraph.PartialCallGraphBuilder;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -36,9 +38,7 @@
   private final Deque<ProgramMethodSet> waves;
   private final ProgramMethodSet processed = ProgramMethodSet.create();
 
-  private PostMethodProcessor(
-      AppView<AppInfoWithLiveness> appView,
-      CallGraph callGraph) {
+  private PostMethodProcessor(AppView<AppInfoWithLiveness> appView, CallGraph callGraph) {
     this.processorContext = appView.createProcessorContext();
     this.waves = createWaves(callGraph);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
index be8f65e..c1ce6d2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
@@ -8,7 +8,9 @@
 import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraph;
+import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation;
+import com.android.tools.r8.ir.conversion.callgraph.Node;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
@@ -19,7 +21,6 @@
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.Deque;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -27,7 +28,7 @@
  * A {@link MethodProcessor} that processes methods in the whole program in a bottom-up manner,
  * i.e., from leaves to roots.
  */
-class PrimaryMethodProcessor extends MethodProcessorWithWave {
+public class PrimaryMethodProcessor extends MethodProcessorWithWave {
 
   interface WaveStartAction {
 
@@ -46,9 +47,7 @@
 
   private ProcessorContext processorContext;
 
-  private PrimaryMethodProcessor(
-      AppView<AppInfoWithLiveness> appView,
-      CallGraph callGraph) {
+  private PrimaryMethodProcessor(AppView<AppInfoWithLiveness> appView, CallGraph callGraph) {
     this.appView = appView;
     this.callSiteInformation = callGraph.createCallSiteInformation(appView);
     this.waves = createWaves(appView, callGraph);
@@ -74,6 +73,11 @@
   }
 
   @Override
+  public PrimaryMethodProcessor asPrimaryMethodProcessor() {
+    return this;
+  }
+
+  @Override
   public boolean shouldApplyCodeRewritings(ProgramMethod method) {
     assert !wave.contains(method);
     return !method.getDefinition().isProcessed();
@@ -87,7 +91,7 @@
   private Deque<ProgramMethodSet> createWaves(AppView<?> appView, CallGraph callGraph) {
     InternalOptions options = appView.options();
     Deque<ProgramMethodSet> waves = new ArrayDeque<>();
-    Set<Node> nodes = callGraph.nodes;
+    Collection<Node> nodes = callGraph.getNodes();
     int waveCount = 1;
     while (!nodes.isEmpty()) {
       ProgramMethodSet wave = callGraph.extractLeaves();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraph.java
new file mode 100644
index 0000000..5560d5b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraph.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2017, 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.conversion.callgraph;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation.CallGraphBasedCallSiteInformation;
+import com.android.tools.r8.ir.conversion.callgraph.CycleEliminator.CycleEliminationResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Call graph representation.
+ *
+ * <p>Each node in the graph contain the methods called and the calling methods. For virtual and
+ * interface calls all potential calls from subtypes are recorded.
+ *
+ * <p>Only methods in the program - not library methods - are represented.
+ *
+ * <p>The directional edges are represented as sets of nodes in each node (called methods and
+ * callees).
+ *
+ * <p>A call from method <code>a</code> to method <code>b</code> is only present once no matter how
+ * many calls of <code>a</code> there are in <code>a</code>.
+ *
+ * <p>Recursive calls are not present.
+ */
+public class CallGraph extends CallGraphBase<Node> {
+
+  private final CycleEliminationResult cycleEliminationResult;
+
+  CallGraph(Map<DexMethod, Node> nodes) {
+    this(nodes, null);
+  }
+
+  CallGraph(Map<DexMethod, Node> nodes, CycleEliminationResult cycleEliminationResult) {
+    super(nodes);
+    this.cycleEliminationResult = cycleEliminationResult;
+  }
+
+  public static CallGraphBuilder builder(AppView<AppInfoWithLiveness> appView) {
+    return new CallGraphBuilder(appView);
+  }
+
+  public static CallGraph createForTesting(Collection<Node> nodes) {
+    return new CallGraph(
+        nodes.stream()
+            .collect(
+                Collectors.toMap(
+                    node -> node.getProgramMethod().getReference(), Function.identity())));
+  }
+
+  public CallSiteInformation createCallSiteInformation(AppView<AppInfoWithLiveness> appView) {
+    // Don't leverage single/dual call site information when we are not tree shaking.
+    return appView.options().isShrinking()
+        ? new CallGraphBasedCallSiteInformation(appView, this)
+        : CallSiteInformation.empty();
+  }
+
+  public ProgramMethodSet extractLeaves() {
+    return extractNodes(Node::isLeaf, Node::cleanCallersAndReadersForRemoval);
+  }
+
+  public ProgramMethodSet extractRoots() {
+    return extractNodes(Node::isRoot, Node::cleanCalleesAndWritersForRemoval);
+  }
+
+  private ProgramMethodSet extractNodes(Predicate<Node> predicate, Consumer<Node> clean) {
+    ProgramMethodSet result = ProgramMethodSet.create();
+    Set<Node> removed = Sets.newIdentityHashSet();
+    Iterator<Node> nodeIterator = nodes.values().iterator();
+    while (nodeIterator.hasNext()) {
+      Node node = nodeIterator.next();
+      if (predicate.test(node)) {
+        result.add(node.getProgramMethod());
+        nodeIterator.remove();
+        removed.add(node);
+      }
+    }
+    removed.forEach(clean);
+    assert !result.isEmpty();
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBase.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBase.java
new file mode 100644
index 0000000..3996903
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBase.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2021, 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.conversion.callgraph;
+
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import java.util.Collection;
+import java.util.Map;
+
+public abstract class CallGraphBase<N extends NodeBase<N>> {
+
+  final Map<DexMethod, N> nodes;
+
+  public CallGraphBase(Map<DexMethod, N> nodes) {
+    this.nodes = nodes;
+  }
+
+  public boolean isEmpty() {
+    return nodes.isEmpty();
+  }
+
+  public N getNode(ProgramMethod method) {
+    return nodes.get(method.getReference());
+  }
+
+  public Collection<N> getNodes() {
+    return nodes.values();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBuilder.java
similarity index 73%
rename from src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
rename to src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBuilder.java
index 4be3d83..cc1f711 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBuilder.java
@@ -2,7 +2,7 @@
 // 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.conversion;
+package com.android.tools.r8.ir.conversion.callgraph;
 
 import static com.google.common.base.Predicates.alwaysTrue;
 
@@ -15,9 +15,9 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
-public class CallGraphBuilder extends CallGraphBuilderBase {
+public class CallGraphBuilder extends IRProcessingCallGraphBuilderBase {
 
-  CallGraphBuilder(AppView<AppInfoWithLiveness> appView) {
+  public CallGraphBuilder(AppView<AppInfoWithLiveness> appView) {
     super(appView);
   }
 
@@ -31,7 +31,14 @@
   }
 
   private void processMethod(ProgramMethod method) {
-    method.registerCodeReferences(new InvokeExtractor(getOrCreateNode(method), alwaysTrue()));
+    IRProcessingCallGraphUseRegistry<Node> registry =
+        new IRProcessingCallGraphUseRegistry<>(
+            appView,
+            getOrCreateNode(method),
+            this::getOrCreateNode,
+            possibleProgramTargetsCache,
+            alwaysTrue());
+    method.registerCodeReferences(registry);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBuilderBase.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBuilderBase.java
new file mode 100644
index 0000000..1c9b944
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallGraphBuilderBase.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2019, 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.conversion.callgraph;
+
+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.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public abstract class CallGraphBuilderBase<N extends NodeBase<N>> {
+
+  protected final AppView<AppInfoWithLiveness> appView;
+
+  protected final Map<DexMethod, N> nodes = new ConcurrentHashMap<>();
+  protected final Map<DexMethod, ProgramMethodSet> possibleProgramTargetsCache =
+      new ConcurrentHashMap<>();
+
+  public CallGraphBuilderBase(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  protected abstract N createNode(ProgramMethod method);
+
+  protected N getOrCreateNode(ProgramMethod method) {
+    return nodes.computeIfAbsent(method.getReference(), ignore -> createNode(method));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java
rename to src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
index 5d93463..a46a6bc 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
@@ -1,13 +1,12 @@
 // Copyright (c) 2017, 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.conversion;
+package com.android.tools.r8.ir.conversion.callgraph;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.classhierarchy.MethodOverridesCollector;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
@@ -24,7 +23,7 @@
    */
   public abstract boolean hasSingleCallSite(ProgramMethod method);
 
-  public abstract boolean hasDoubleCallSite(ProgramMethod method);
+  public abstract boolean isMultiCallerInlineCandidate(ProgramMethod method);
 
   public abstract void unsetCallSiteInformation(ProgramMethod method);
 
@@ -42,7 +41,7 @@
     }
 
     @Override
-    public boolean hasDoubleCallSite(ProgramMethod method) {
+    public boolean isMultiCallerInlineCandidate(ProgramMethod method) {
       return false;
     }
 
@@ -54,8 +53,8 @@
 
   static class CallGraphBasedCallSiteInformation extends CallSiteInformation {
 
-    private final Set<DexMethod> singleCallSite = Sets.newIdentityHashSet();
-    private final Set<DexMethod> doubleCallSite = Sets.newIdentityHashSet();
+    private final Set<DexMethod> singleCallerMethods = Sets.newIdentityHashSet();
+    private final Set<DexMethod> multiCallerInlineCandidates = Sets.newIdentityHashSet();
 
     CallGraphBasedCallSiteInformation(AppView<AppInfoWithLiveness> appView, CallGraph graph) {
       ProgramMethodSet pinned =
@@ -67,7 +66,7 @@
                   appView.getKeepInfo(method).isPinned(appView.options())
                       || appView.appInfo().isMethodTargetedByInvokeDynamic(method));
 
-      for (Node node : graph.nodes) {
+      for (Node node : graph.getNodes()) {
         ProgramMethod method = node.getProgramMethod();
         DexMethod reference = method.getReference();
 
@@ -90,9 +89,9 @@
 
         int numberOfCallSites = node.getNumberOfCallSites();
         if (numberOfCallSites == 1) {
-          singleCallSite.add(reference);
-        } else if (numberOfCallSites == 2) {
-          doubleCallSite.add(reference);
+          singleCallerMethods.add(reference);
+        } else if (numberOfCallSites > 1) {
+          multiCallerInlineCandidates.add(reference);
         }
       }
     }
@@ -105,7 +104,7 @@
      */
     @Override
     public boolean hasSingleCallSite(ProgramMethod method) {
-      return singleCallSite.contains(method.getReference());
+      return singleCallerMethods.contains(method.getReference());
     }
 
     /**
@@ -115,14 +114,14 @@
      * library method this always returns false.
      */
     @Override
-    public boolean hasDoubleCallSite(ProgramMethod method) {
-      return doubleCallSite.contains(method.getReference());
+    public boolean isMultiCallerInlineCandidate(ProgramMethod method) {
+      return multiCallerInlineCandidates.contains(method.getReference());
     }
 
     @Override
     public void unsetCallSiteInformation(ProgramMethod method) {
-      singleCallSite.remove(method.getReference());
-      doubleCallSite.remove(method.getReference());
+      singleCallerMethods.remove(method.getReference());
+      multiCallerInlineCandidates.remove(method.getReference());
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CycleEliminator.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CycleEliminator.java
new file mode 100644
index 0000000..c96def4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CycleEliminator.java
@@ -0,0 +1,458 @@
+// Copyright (c) 2019, 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.conversion.callgraph;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
+
+public class CycleEliminator {
+
+  public static final String CYCLIC_FORCE_INLINING_MESSAGE =
+      "Unable to satisfy force inlining constraints due to cyclic force inlining";
+
+  private static class CallEdge {
+
+    private final Node caller;
+    private final Node callee;
+
+    CallEdge(Node caller, Node callee) {
+      this.caller = caller;
+      this.callee = callee;
+    }
+  }
+
+  static class StackEntryInfo {
+
+    final int index;
+    final Node predecessor;
+
+    boolean processed;
+
+    StackEntryInfo(int index, Node predecessor) {
+      this.index = index;
+      this.predecessor = predecessor;
+    }
+  }
+
+  public static class CycleEliminationResult {
+
+    private Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges;
+
+    CycleEliminationResult(Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges) {
+      this.removedCallEdges = removedCallEdges;
+    }
+
+    public int numberOfRemovedCallEdges() {
+      int numberOfRemovedCallEdges = 0;
+      for (ProgramMethodSet nodes : removedCallEdges.values()) {
+        numberOfRemovedCallEdges += nodes.size();
+      }
+      return numberOfRemovedCallEdges;
+    }
+  }
+
+  // DFS stack.
+  private Deque<Node> stack = new ArrayDeque<>();
+
+  // Nodes on the DFS stack.
+  private Map<Node, StackEntryInfo> stackEntryInfo = new IdentityHashMap<>();
+
+  // Subset of the DFS stack, where the nodes on the stack are class initializers.
+  //
+  // This stack is used to efficiently compute if there is a class initializer on the stack.
+  private Deque<Node> clinitCallStack = new ArrayDeque<>();
+
+  // Subset of the DFS stack, where the nodes on the stack satisfy that the edge from the
+  // predecessor to the node itself is a field read edge.
+  //
+  // This stack is used to efficiently compute if there is a field read edge inside a cycle when
+  // a cycle is found.
+  private Deque<Node> writerStack = new ArrayDeque<>();
+
+  // Set of nodes that have been visited entirely.
+  private Set<Node> marked = Sets.newIdentityHashSet();
+
+  // Call edges that should be removed when the caller has been processed. These are not removed
+  // directly since that would lead to ConcurrentModificationExceptions.
+  private Map<Node, Set<Node>> calleesToBeRemoved = new IdentityHashMap<>();
+
+  // Field read edges that should be removed when the reader has been processed. These are not
+  // removed directly since that would lead to ConcurrentModificationExceptions.
+  private Map<Node, Set<Node>> writersToBeRemoved = new IdentityHashMap<>();
+
+  // Mapping from callee to the set of callers that were removed from the callee.
+  private Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges = new IdentityHashMap<>();
+
+  // Set of nodes from which cycle elimination must be rerun to ensure that all cycles will be
+  // removed.
+  private LinkedHashSet<Node> revisit = new LinkedHashSet<>();
+
+  public CycleEliminationResult breakCycles(Collection<Node> roots) {
+    // Break cycles in this call graph by removing edges causing cycles. We do this in a fixpoint
+    // because the algorithm does not guarantee that all cycles will be removed from the graph
+    // when we remove an edge in the middle of a cycle that contains another cycle.
+    do {
+      traverse(roots);
+      roots = revisit;
+      prepareForNewTraversal();
+    } while (!roots.isEmpty());
+
+    CycleEliminationResult result = new CycleEliminationResult(removedCallEdges);
+    if (Log.ENABLED) {
+      Log.info(getClass(), "# call graph cycles broken: %s", result.numberOfRemovedCallEdges());
+    }
+    reset();
+    return result;
+  }
+
+  private void prepareForNewTraversal() {
+    assert calleesToBeRemoved.isEmpty();
+    assert clinitCallStack.isEmpty();
+    assert stack.isEmpty();
+    assert stackEntryInfo.isEmpty();
+    assert writersToBeRemoved.isEmpty();
+    assert writerStack.isEmpty();
+    marked.clear();
+    revisit = new LinkedHashSet<>();
+  }
+
+  private void reset() {
+    assert clinitCallStack.isEmpty();
+    assert marked.isEmpty();
+    assert revisit.isEmpty();
+    assert stack.isEmpty();
+    assert stackEntryInfo.isEmpty();
+    assert writerStack.isEmpty();
+    removedCallEdges = new IdentityHashMap<>();
+  }
+
+  private static class WorkItem {
+    boolean isNode() {
+      return false;
+    }
+
+    NodeWorkItem asNode() {
+      return null;
+    }
+
+    boolean isIterator() {
+      return false;
+    }
+
+    IteratorWorkItem asIterator() {
+      return null;
+    }
+  }
+
+  private static class NodeWorkItem extends WorkItem {
+    private final Node node;
+
+    NodeWorkItem(Node node) {
+      this.node = node;
+    }
+
+    @Override
+    boolean isNode() {
+      return true;
+    }
+
+    @Override
+    NodeWorkItem asNode() {
+      return this;
+    }
+  }
+
+  private static class IteratorWorkItem extends WorkItem {
+    private final Node callerOrReader;
+    private final Iterator<Node> calleesAndWriters;
+
+    IteratorWorkItem(Node callerOrReader, Iterator<Node> calleesAndWriters) {
+      this.callerOrReader = callerOrReader;
+      this.calleesAndWriters = calleesAndWriters;
+    }
+
+    @Override
+    boolean isIterator() {
+      return true;
+    }
+
+    @Override
+    IteratorWorkItem asIterator() {
+      return this;
+    }
+  }
+
+  private void traverse(Collection<Node> roots) {
+    Deque<WorkItem> workItems = new ArrayDeque<>(roots.size());
+    for (Node node : roots) {
+      workItems.addLast(new NodeWorkItem(node));
+    }
+    while (!workItems.isEmpty()) {
+      WorkItem workItem = workItems.removeFirst();
+      if (workItem.isNode()) {
+        Node node = workItem.asNode().node;
+        if (marked.contains(node)) {
+          // Already visited all nodes that can be reached from this node.
+          continue;
+        }
+
+        Node predecessor = stack.isEmpty() ? null : stack.peek();
+        push(node, predecessor);
+
+        // The callees and writers must be sorted before calling traverse recursively.
+        // This ensures that cycles are broken the same way across multiple compilations.
+        Iterator<Node> calleesAndWriterIterator =
+            Iterators.concat(
+                node.getCalleesWithDeterministicOrder().iterator(),
+                node.getWritersWithDeterministicOrder().iterator());
+        workItems.addFirst(new IteratorWorkItem(node, calleesAndWriterIterator));
+      } else {
+        assert workItem.isIterator();
+        IteratorWorkItem iteratorWorkItem = workItem.asIterator();
+        Node newCallerOrReader =
+            iterateCalleesAndWriters(
+                iteratorWorkItem.calleesAndWriters, iteratorWorkItem.callerOrReader);
+        if (newCallerOrReader != null) {
+          // We did not finish the work on this iterator, so add it again.
+          workItems.addFirst(iteratorWorkItem);
+          workItems.addFirst(new NodeWorkItem(newCallerOrReader));
+        } else {
+          assert !iteratorWorkItem.calleesAndWriters.hasNext();
+          pop(iteratorWorkItem.callerOrReader);
+          marked.add(iteratorWorkItem.callerOrReader);
+
+          Collection<Node> calleesToBeRemovedFromCaller =
+              calleesToBeRemoved.remove(iteratorWorkItem.callerOrReader);
+          if (calleesToBeRemovedFromCaller != null) {
+            calleesToBeRemovedFromCaller.forEach(
+                callee -> {
+                  callee.removeCaller(iteratorWorkItem.callerOrReader);
+                  recordCallEdgeRemoval(iteratorWorkItem.callerOrReader, callee);
+                });
+          }
+
+          Collection<Node> writersToBeRemovedFromReader =
+              writersToBeRemoved.remove(iteratorWorkItem.callerOrReader);
+          if (writersToBeRemovedFromReader != null) {
+            writersToBeRemovedFromReader.forEach(
+                writer -> writer.removeReader(iteratorWorkItem.callerOrReader));
+          }
+        }
+      }
+    }
+  }
+
+  private Node iterateCalleesAndWriters(
+      Iterator<Node> calleeOrWriterIterator, Node callerOrReader) {
+    while (calleeOrWriterIterator.hasNext()) {
+      Node calleeOrWriter = calleeOrWriterIterator.next();
+      StackEntryInfo calleeOrWriterStackEntryInfo = stackEntryInfo.get(calleeOrWriter);
+      boolean foundCycle = calleeOrWriterStackEntryInfo != null;
+      if (!foundCycle) {
+        return calleeOrWriter;
+      }
+
+      // Found a cycle that needs to be eliminated. If it is a field read edge, then remove it
+      // right away.
+      boolean isFieldReadEdge = calleeOrWriter.hasReader(callerOrReader);
+      if (isFieldReadEdge) {
+        removeFieldReadEdge(callerOrReader, calleeOrWriter);
+        continue;
+      }
+
+      // Otherwise, it is a call edge. Check if there is a field read edge in the cycle, and if
+      // so, remove that edge.
+      if (!writerStack.isEmpty()
+          && removeIncomingEdgeOnStack(
+              writerStack.peek(),
+              calleeOrWriter,
+              calleeOrWriterStackEntryInfo,
+              this::removeFieldReadEdge)) {
+        continue;
+      }
+
+      // It is a call edge and the cycle does not contain any field read edges.
+      // If it is a call edge to a <clinit>, then remove it.
+      if (calleeOrWriter.getMethod().isClassInitializer()) {
+        // Calls to class initializers are always safe to remove.
+        assert callEdgeRemovalIsSafe(callerOrReader, calleeOrWriter);
+        removeCallEdge(callerOrReader, calleeOrWriter);
+        continue;
+      }
+
+      // Otherwise, check if there is a call edge to a <clinit> method in the cycle, and if so,
+      // remove that edge.
+      if (!clinitCallStack.isEmpty()
+          && removeIncomingEdgeOnStack(
+              clinitCallStack.peek(),
+              calleeOrWriter,
+              calleeOrWriterStackEntryInfo,
+              this::removeCallEdge)) {
+        continue;
+      }
+
+      // Otherwise, we remove the call edge if it is safe according to force inlining.
+      if (callEdgeRemovalIsSafe(callerOrReader, calleeOrWriter)) {
+        // Break the cycle by removing the edge node->calleeOrWriter.
+        // Need to remove `calleeOrWriter` from `node.callees` using the iterator to prevent a
+        // ConcurrentModificationException.
+        removeCallEdge(callerOrReader, calleeOrWriter);
+        continue;
+      }
+
+      // The call edge cannot be removed due to force inlining. Find another call edge in the
+      // cycle that can safely be removed instead.
+      LinkedList<Node> cycle = extractCycle(calleeOrWriter);
+
+      // Break the cycle by finding an edge that can be removed without breaking force
+      // inlining. If that is not possible, this call fails with a compilation error.
+      CallEdge edge = findCallEdgeForRemoval(cycle);
+
+      // The edge will be null if this cycle has already been eliminated as a result of
+      // another cycle elimination.
+      if (edge != null) {
+        assert callEdgeRemovalIsSafe(edge.caller, edge.callee);
+
+        // Break the cycle by removing the edge caller->callee.
+        removeCallEdge(edge.caller, edge.callee);
+        revisit.add(edge.callee);
+      }
+
+      // Recover the stack.
+      recoverStack(cycle);
+    }
+    return null;
+  }
+
+  private void push(Node node, Node predecessor) {
+    stack.push(node);
+    assert !stackEntryInfo.containsKey(node);
+    stackEntryInfo.put(node, new StackEntryInfo(stack.size() - 1, predecessor));
+    if (predecessor != null) {
+      if (node.getMethod().isClassInitializer() && node.hasCaller(predecessor)) {
+        clinitCallStack.push(node);
+      } else if (predecessor.getWritersWithDeterministicOrder().contains(node)) {
+        writerStack.push(node);
+      }
+    }
+  }
+
+  private void pop(Node node) {
+    Node popped = stack.pop();
+    assert popped == node;
+    assert stackEntryInfo.containsKey(node);
+    stackEntryInfo.remove(node);
+    if (clinitCallStack.peek() == popped) {
+      assert writerStack.peek() != popped;
+      clinitCallStack.pop();
+    } else if (writerStack.peek() == popped) {
+      writerStack.pop();
+    }
+  }
+
+  private void removeCallEdge(Node caller, Node callee) {
+    calleesToBeRemoved.computeIfAbsent(caller, ignore -> Sets.newIdentityHashSet()).add(callee);
+  }
+
+  private void removeFieldReadEdge(Node reader, Node writer) {
+    writersToBeRemoved.computeIfAbsent(reader, ignore -> Sets.newIdentityHashSet()).add(writer);
+  }
+
+  private boolean removeIncomingEdgeOnStack(
+      Node target,
+      Node currentCalleeOrWriter,
+      StackEntryInfo currentCalleeOrWriterStackEntryInfo,
+      BiConsumer<Node, Node> edgeRemover) {
+    StackEntryInfo targetStackEntryInfo = stackEntryInfo.get(target);
+    boolean cycleContainsTarget =
+        targetStackEntryInfo.index > currentCalleeOrWriterStackEntryInfo.index;
+    if (cycleContainsTarget) {
+      assert verifyCycleSatisfies(
+          currentCalleeOrWriter,
+          cycle -> cycle.contains(target) && cycle.contains(targetStackEntryInfo.predecessor));
+      if (!targetStackEntryInfo.processed) {
+        edgeRemover.accept(targetStackEntryInfo.predecessor, target);
+        revisit.add(target);
+        targetStackEntryInfo.processed = true;
+      }
+      return true;
+    }
+    return false;
+  }
+
+  private LinkedList<Node> extractCycle(Node entry) {
+    LinkedList<Node> cycle = new LinkedList<>();
+    do {
+      assert !stack.isEmpty();
+      cycle.add(stack.pop());
+    } while (cycle.getLast() != entry);
+    return cycle;
+  }
+
+  private boolean verifyCycleSatisfies(Node entry, Predicate<LinkedList<Node>> predicate) {
+    LinkedList<Node> cycle = extractCycle(entry);
+    assert predicate.test(cycle);
+    recoverStack(cycle);
+    return true;
+  }
+
+  private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) {
+    Node callee = extractedCycle.getLast();
+    for (Node caller : extractedCycle) {
+      if (caller.hasWriter(callee)) {
+        // Not a call edge.
+        assert !caller.hasCallee(callee);
+        assert !callee.hasCaller(caller);
+        callee = caller;
+        continue;
+      }
+      if (!caller.hasCallee(callee)) {
+        // No need to break any edges since this cycle has already been broken previously.
+        assert !callee.hasCaller(caller);
+        return null;
+      }
+      if (callEdgeRemovalIsSafe(caller, callee)) {
+        return new CallEdge(caller, callee);
+      }
+      callee = caller;
+    }
+    throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE);
+  }
+
+  private static boolean callEdgeRemovalIsSafe(Node callerOrReader, Node calleeOrWriter) {
+    // All call edges where the callee is a method that should be force inlined must be kept,
+    // to guarantee that the IR converter will process the callee before the caller.
+    assert calleeOrWriter.hasCaller(callerOrReader);
+    return !calleeOrWriter.getMethod().getOptimizationInfo().forceInline();
+  }
+
+  private void recordCallEdgeRemoval(Node caller, Node callee) {
+    removedCallEdges
+        .computeIfAbsent(callee.getMethod(), ignore -> ProgramMethodSet.create(2))
+        .add(caller.getProgramMethod());
+  }
+
+  private void recoverStack(LinkedList<Node> extractedCycle) {
+    Iterator<Node> descendingIt = extractedCycle.descendingIterator();
+    while (descendingIt.hasNext()) {
+      stack.push(descendingIt.next());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/IRProcessingCallGraphBuilderBase.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/IRProcessingCallGraphBuilderBase.java
new file mode 100644
index 0000000..a580f04
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/IRProcessingCallGraphBuilderBase.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2021, 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.conversion.callgraph;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.callgraph.CycleEliminator.CycleEliminationResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Sets;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+abstract class IRProcessingCallGraphBuilderBase extends CallGraphBuilderBase<Node> {
+
+  IRProcessingCallGraphBuilderBase(AppView<AppInfoWithLiveness> appView) {
+    super(appView);
+  }
+
+  public CallGraph build(ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Build IR processing order constraints");
+    timing.begin("Build call graph");
+    populateGraph(executorService);
+    assert verifyNoRedundantFieldReadEdges();
+    timing.end();
+    assert verifyAllMethodsWithCodeExists();
+
+    appView.withGeneratedMessageLiteBuilderShrinker(
+        shrinker -> shrinker.preprocessCallGraphBeforeCycleElimination(nodes));
+
+    timing.begin("Cycle elimination");
+    // Sort the nodes for deterministic cycle elimination.
+    Set<Node> nodesWithDeterministicOrder = Sets.newTreeSet(nodes.values());
+    CycleEliminator cycleEliminator = new CycleEliminator();
+    CycleEliminationResult cycleEliminationResult =
+        cycleEliminator.breakCycles(nodesWithDeterministicOrder);
+    timing.end();
+    timing.end();
+    assert cycleEliminator.breakCycles(nodesWithDeterministicOrder).numberOfRemovedCallEdges()
+        == 0; // The cycles should be gone.
+
+    return new CallGraph(nodes, cycleEliminationResult);
+  }
+
+  @Override
+  protected Node createNode(ProgramMethod method) {
+    return new Node(method);
+  }
+
+  abstract void populateGraph(ExecutorService executorService) throws ExecutionException;
+
+  /** Verify that there are no field read edges in the graph if there is also a call graph edge. */
+  private boolean verifyNoRedundantFieldReadEdges() {
+    for (Node writer : nodes.values()) {
+      for (Node reader : writer.getReadersWithDeterministicOrder()) {
+        assert !writer.hasCaller(reader);
+      }
+    }
+    return true;
+  }
+
+  abstract boolean verifyAllMethodsWithCodeExists();
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/IRProcessingCallGraphUseRegistry.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/IRProcessingCallGraphUseRegistry.java
new file mode 100644
index 0000000..b88c9d0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/IRProcessingCallGraphUseRegistry.java
@@ -0,0 +1,164 @@
+// Copyright (c) 2021, 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.conversion.callgraph;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessInfo;
+import com.android.tools.r8.graph.FieldAccessInfoCollection;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class IRProcessingCallGraphUseRegistry<N extends NodeBase<N>> extends InvokeExtractor<N> {
+
+  private final FieldAccessInfoCollection<?> fieldAccessInfoCollection;
+
+  IRProcessingCallGraphUseRegistry(
+      AppView<AppInfoWithLiveness> appView,
+      N currentMethod,
+      Function<ProgramMethod, N> nodeFactory,
+      Map<DexMethod, ProgramMethodSet> possibleProgramTargetsCache,
+      Predicate<ProgramMethod> targetTester) {
+    super(appView, currentMethod, nodeFactory, possibleProgramTargetsCache, targetTester);
+    this.fieldAccessInfoCollection = appView.appInfo().getFieldAccessInfoCollection();
+  }
+
+  protected void addClassInitializerTarget(DexProgramClass clazz) {
+    assert clazz != null;
+    if (clazz.hasClassInitializer()) {
+      addCallEdge(clazz.getProgramClassInitializer(), false);
+    }
+  }
+
+  protected void addClassInitializerTarget(DexType type) {
+    assert type.isClassType();
+    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+    if (clazz != null) {
+      addClassInitializerTarget(clazz);
+    }
+  }
+
+  private void addFieldReadEdge(ProgramMethod writer) {
+    assert !writer.getDefinition().isAbstract();
+    if (!targetTester.test(writer)) {
+      return;
+    }
+    nodeFactory.apply(writer).addReaderConcurrently(currentMethod);
+  }
+
+  private void processFieldRead(DexField reference) {
+    DexField rewrittenReference = appView.graphLens().lookupField(reference, getCodeLens());
+    if (!rewrittenReference.getHolderType().isClassType()) {
+      return;
+    }
+
+    ProgramField field = appView.appInfo().resolveField(rewrittenReference).getProgramField();
+    if (field == null || appView.appInfo().isPinned(field)) {
+      return;
+    }
+
+    // Each static field access implicitly triggers the class initializer.
+    if (field.getAccessFlags().isStatic()) {
+      addClassInitializerTarget(field.getHolder());
+    }
+
+    FieldAccessInfo fieldAccessInfo = fieldAccessInfoCollection.get(field.getReference());
+    if (fieldAccessInfo != null && fieldAccessInfo.hasKnownWriteContexts()) {
+      if (fieldAccessInfo.getNumberOfWriteContexts() == 1) {
+        fieldAccessInfo.forEachWriteContext(this::addFieldReadEdge);
+      }
+    }
+  }
+
+  private void processFieldWrite(DexField reference) {
+    DexField rewrittenReference = appView.graphLens().lookupField(reference, getCodeLens());
+    if (!rewrittenReference.getHolderType().isClassType()) {
+      return;
+    }
+
+    ProgramField field = appView.appInfo().resolveField(rewrittenReference).getProgramField();
+    if (field == null || appView.appInfo().isPinned(field)) {
+      return;
+    }
+
+    // Each static field access implicitly triggers the class initializer.
+    if (field.getAccessFlags().isStatic()) {
+      addClassInitializerTarget(field.getHolder());
+    }
+  }
+
+  private void processInitClass(DexType type) {
+    DexType rewrittenType = appView.graphLens().lookupType(type);
+    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(rewrittenType));
+    if (clazz == null) {
+      assert false;
+      return;
+    }
+    addClassInitializerTarget(clazz);
+  }
+
+  @Override
+  protected void processSingleTarget(ProgramMethod singleTarget, ProgramMethod context) {
+    super.processSingleTarget(singleTarget, context);
+    if (singleTarget.getAccessFlags().isStatic()) {
+      addClassInitializerTarget(singleTarget.getHolder());
+    }
+  }
+
+  @Override
+  public void registerInitClass(DexType clazz) {
+    processInitClass(clazz);
+  }
+
+  @Override
+  public void registerInstanceFieldRead(DexField field) {
+    processFieldRead(field);
+  }
+
+  @Override
+  public void registerInstanceFieldWrite(DexField field) {
+    processFieldWrite(field);
+  }
+
+  @Override
+  public void registerInstanceOf(DexType type) {}
+
+  @Override
+  public void registerNewInstance(DexType type) {
+    if (type.isClassType()) {
+      addClassInitializerTarget(type);
+    }
+  }
+
+  @Override
+  public void registerStaticFieldRead(DexField field) {
+    processFieldRead(field);
+  }
+
+  @Override
+  public void registerStaticFieldWrite(DexField field) {
+    processFieldWrite(field);
+  }
+
+  @Override
+  public void registerTypeReference(DexType type) {}
+
+  @Override
+  public void registerCallSite(DexCallSite callSite) {
+    registerMethodHandle(
+        callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
new file mode 100644
index 0000000..ecd06de
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/InvokeExtractor.java
@@ -0,0 +1,215 @@
+// Copyright (c) 2019, 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.conversion.callgraph;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens.MethodLookupResult;
+import com.android.tools.r8.graph.LookupResult;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.Invoke;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class InvokeExtractor<N extends NodeBase<N>> extends UseRegistry<ProgramMethod> {
+
+  protected final AppView<AppInfoWithLiveness> appView;
+  protected final N currentMethod;
+  protected final Function<ProgramMethod, N> nodeFactory;
+  protected final Map<DexMethod, ProgramMethodSet> possibleProgramTargetsCache;
+  protected final Predicate<ProgramMethod> targetTester;
+
+  public InvokeExtractor(
+      AppView<AppInfoWithLiveness> appView,
+      N currentMethod,
+      Function<ProgramMethod, N> nodeFactory,
+      Map<DexMethod, ProgramMethodSet> possibleProgramTargetsCache,
+      Predicate<ProgramMethod> targetTester) {
+    super(appView, currentMethod.getProgramMethod());
+    this.appView = appView;
+    this.currentMethod = currentMethod;
+    this.nodeFactory = nodeFactory;
+    this.possibleProgramTargetsCache = possibleProgramTargetsCache;
+    this.targetTester = targetTester;
+  }
+
+  protected void addCallEdge(ProgramMethod callee, boolean likelySpuriousCallEdge) {
+    if (!targetTester.test(callee)) {
+      return;
+    }
+    if (callee.getDefinition().isAbstract()) {
+      // Not a valid target.
+      return;
+    }
+    if (callee.getDefinition().isNative()) {
+      // We don't care about calls to native methods.
+      return;
+    }
+    if (!appView.getKeepInfo(callee).isInliningAllowed(appView.options())) {
+      // Since the callee is kept and optimizations are disallowed, we cannot inline it into the
+      // caller, and we also cannot collect any optimization info for the method. Therefore, we
+      // drop the call edge to reduce the total number of call graph edges, which should lead to
+      // fewer call graph cycles.
+      return;
+    }
+    nodeFactory.apply(callee).addCallerConcurrently(currentMethod, likelySpuriousCallEdge);
+  }
+
+  private void processInvoke(Invoke.Type originalType, DexMethod originalMethod) {
+    ProgramMethod context = currentMethod.getProgramMethod();
+    MethodLookupResult result =
+        appView
+            .graphLens()
+            .lookupMethod(originalMethod, context.getReference(), originalType, getCodeLens());
+    DexMethod method = result.getReference();
+    Invoke.Type type = result.getType();
+    if (type == Invoke.Type.INTERFACE || type == Invoke.Type.VIRTUAL) {
+      // For virtual and interface calls add all potential targets that could be called.
+      MethodResolutionResult resolutionResult =
+          appView.appInfo().resolveMethod(method, type == Invoke.Type.INTERFACE);
+      DexClassAndMethod target = resolutionResult.getResolutionPair();
+      if (target != null) {
+        processInvokeWithDynamicDispatch(type, target, context);
+      }
+    } else {
+      ProgramMethod singleTarget =
+          appView.appInfo().lookupSingleProgramTarget(type, method, context, appView);
+      if (singleTarget != null) {
+        processSingleTarget(singleTarget, context);
+      }
+    }
+  }
+
+  protected void processSingleTarget(ProgramMethod singleTarget, ProgramMethod context) {
+    assert !context.getDefinition().isBridge()
+        || singleTarget.getDefinition() != context.getDefinition();
+    addCallEdge(singleTarget, false);
+  }
+
+  protected void processInvokeWithDynamicDispatch(
+      Invoke.Type type, DexClassAndMethod encodedTarget, ProgramMethod context) {
+    DexMethod target = encodedTarget.getReference();
+    DexClass clazz = encodedTarget.getHolder();
+    if (!appView.options().testing.addCallEdgesForLibraryInvokes) {
+      if (clazz.isLibraryClass()) {
+        // Likely to have many possible targets.
+        return;
+      }
+    }
+
+    boolean isInterface = type == Invoke.Type.INTERFACE;
+    ProgramMethodSet possibleProgramTargets =
+        possibleProgramTargetsCache.computeIfAbsent(
+            target,
+            method -> {
+              MethodResolutionResult resolution =
+                  appView.appInfo().resolveMethod(method, isInterface);
+              if (resolution.isVirtualTarget()) {
+                LookupResult lookupResult =
+                    resolution.lookupVirtualDispatchTargets(context.getHolder(), appView.appInfo());
+                if (lookupResult.isLookupResultSuccess()) {
+                  ProgramMethodSet targets = ProgramMethodSet.create();
+                  lookupResult
+                      .asLookupResultSuccess()
+                      .forEach(
+                          methodTarget -> {
+                            if (methodTarget.isProgramMethod()) {
+                              targets.add(methodTarget.asProgramMethod());
+                            }
+                          },
+                          lambdaTarget -> {
+                            // The call target will ultimately be the implementation method.
+                            DexClassAndMethod implementationMethod =
+                                lambdaTarget.getImplementationMethod();
+                            if (implementationMethod.isProgramMethod()) {
+                              targets.add(implementationMethod.asProgramMethod());
+                            }
+                          });
+                  return targets;
+                }
+              }
+              return null;
+            });
+    if (possibleProgramTargets != null) {
+      boolean likelySpuriousCallEdge =
+          possibleProgramTargets.size()
+              >= appView.options().callGraphLikelySpuriousCallEdgeThreshold;
+      for (ProgramMethod possibleTarget : possibleProgramTargets) {
+        addCallEdge(possibleTarget, likelySpuriousCallEdge);
+      }
+    }
+  }
+
+  @Override
+  public void registerCallSite(DexCallSite callSite) {
+    registerMethodHandle(
+        callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
+  }
+
+  @Override
+  public void registerInvokeDirect(DexMethod method) {
+    processInvoke(Invoke.Type.DIRECT, method);
+  }
+
+  @Override
+  public void registerInvokeInterface(DexMethod method) {
+    processInvoke(Invoke.Type.INTERFACE, method);
+  }
+
+  @Override
+  public void registerInvokeStatic(DexMethod method) {
+    processInvoke(Invoke.Type.STATIC, method);
+  }
+
+  @Override
+  public void registerInvokeSuper(DexMethod method) {
+    processInvoke(Invoke.Type.SUPER, method);
+  }
+
+  @Override
+  public void registerInvokeVirtual(DexMethod method) {
+    processInvoke(Invoke.Type.VIRTUAL, method);
+  }
+
+  @Override
+  public void registerInitClass(DexType type) {
+    // Intentionally empty. This use registry is only tracing method calls.
+  }
+
+  @Override
+  public void registerInstanceFieldRead(DexField field) {
+    // Intentionally empty. This use registry is only tracing method calls.
+  }
+
+  @Override
+  public void registerInstanceFieldWrite(DexField field) {
+    // Intentionally empty. This use registry is only tracing method calls.
+  }
+
+  @Override
+  public void registerStaticFieldRead(DexField field) {
+    // Intentionally empty. This use registry is only tracing method calls.
+  }
+
+  @Override
+  public void registerStaticFieldWrite(DexField field) {
+    // Intentionally empty. This use registry is only tracing method calls.
+  }
+
+  @Override
+  public void registerTypeReference(DexType type) {
+    // Intentionally empty. This use registry is only tracing method calls.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/Node.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/Node.java
new file mode 100644
index 0000000..7752ad9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/Node.java
@@ -0,0 +1,215 @@
+// Copyright (c) 2017, 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.conversion.callgraph;
+
+import com.android.tools.r8.graph.ProgramMethod;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class Node extends NodeBase<Node> implements Comparable<Node> {
+
+  public static Node[] EMPTY_ARRAY = {};
+
+  private int numberOfCallSites = 0;
+
+  // Outgoing calls from this method.
+  private final Set<Node> callees = new TreeSet<>();
+
+  // Incoming calls to this method.
+  private final Set<Node> callers = new TreeSet<>();
+
+  // Incoming field read edges to this method (i.e., the set of methods that read a field written
+  // by the current method).
+  private final Set<Node> readers = new TreeSet<>();
+
+  // Outgoing field read edges from this method (i.e., the set of methods that write a field read
+  // by the current method).
+  private final Set<Node> writers = new TreeSet<>();
+
+  public Node(ProgramMethod method) {
+    super(method);
+  }
+
+  public void addCallerConcurrently(Node caller) {
+    addCallerConcurrently(caller, false);
+  }
+
+  @Override
+  public void addCallerConcurrently(Node caller, boolean likelySpuriousCallEdge) {
+    if (caller != this && !likelySpuriousCallEdge) {
+      boolean changedCallers;
+      synchronized (callers) {
+        changedCallers = callers.add(caller);
+        numberOfCallSites++;
+      }
+      if (changedCallers) {
+        synchronized (caller.callees) {
+          caller.callees.add(this);
+        }
+        // Avoid redundant field read edges (call edges are considered stronger).
+        removeReaderConcurrently(caller);
+      }
+    } else {
+      synchronized (callers) {
+        numberOfCallSites++;
+      }
+    }
+  }
+
+  @Override
+  public void addReaderConcurrently(Node reader) {
+    if (reader != this) {
+      synchronized (callers) {
+        if (callers.contains(reader)) {
+          // Avoid redundant field read edges (call edges are considered stronger).
+          return;
+        }
+        boolean readersChanged;
+        synchronized (readers) {
+          readersChanged = readers.add(reader);
+        }
+        if (readersChanged) {
+          synchronized (reader.writers) {
+            reader.writers.add(this);
+          }
+        }
+      }
+    }
+  }
+
+  private void removeReaderConcurrently(Node reader) {
+    synchronized (readers) {
+      readers.remove(reader);
+    }
+    synchronized (reader.writers) {
+      reader.writers.remove(this);
+    }
+  }
+
+  public void removeCaller(Node caller) {
+    boolean callersChanged = callers.remove(caller);
+    assert callersChanged;
+    boolean calleesChanged = caller.callees.remove(this);
+    assert calleesChanged;
+    assert !hasReader(caller);
+  }
+
+  public void removeReader(Node reader) {
+    boolean readersChanged = readers.remove(reader);
+    assert readersChanged;
+    boolean writersChanged = reader.writers.remove(this);
+    assert writersChanged;
+    assert !hasCaller(reader);
+  }
+
+  public void cleanCalleesAndWritersForRemoval() {
+    assert callers.isEmpty();
+    assert readers.isEmpty();
+    for (Node callee : callees) {
+      boolean changed = callee.callers.remove(this);
+      assert changed;
+    }
+    for (Node writer : writers) {
+      boolean changed = writer.readers.remove(this);
+      assert changed;
+    }
+  }
+
+  public void cleanCallersAndReadersForRemoval() {
+    assert callees.isEmpty();
+    assert writers.isEmpty();
+    for (Node caller : callers) {
+      boolean changed = caller.callees.remove(this);
+      assert changed;
+    }
+    for (Node reader : readers) {
+      boolean changed = reader.writers.remove(this);
+      assert changed;
+    }
+  }
+
+  public Set<Node> getCallersWithDeterministicOrder() {
+    return callers;
+  }
+
+  public Set<Node> getCalleesWithDeterministicOrder() {
+    return callees;
+  }
+
+  public Set<Node> getReadersWithDeterministicOrder() {
+    return readers;
+  }
+
+  public Set<Node> getWritersWithDeterministicOrder() {
+    return writers;
+  }
+
+  public int getNumberOfCallSites() {
+    return numberOfCallSites;
+  }
+
+  public boolean hasCallee(Node method) {
+    return callees.contains(method);
+  }
+
+  public boolean hasCaller(Node method) {
+    return callers.contains(method);
+  }
+
+  public boolean hasReader(Node method) {
+    return readers.contains(method);
+  }
+
+  public boolean hasWriter(Node method) {
+    return writers.contains(method);
+  }
+
+  public boolean isRoot() {
+    return callers.isEmpty() && readers.isEmpty();
+  }
+
+  public boolean isLeaf() {
+    return callees.isEmpty() && writers.isEmpty();
+  }
+
+  @Override
+  public int compareTo(Node other) {
+    return getProgramMethod().getReference().compareTo(other.getProgramMethod().getReference());
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    builder.append("MethodNode for: ");
+    builder.append(getProgramMethod().toSourceString());
+    builder.append(" (");
+    builder.append(callees.size());
+    builder.append(" callees, ");
+    builder.append(callers.size());
+    builder.append(" callers");
+    builder.append(", invoke count ").append(numberOfCallSites);
+    builder.append(").");
+    builder.append(System.lineSeparator());
+    if (callees.size() > 0) {
+      builder.append("Callees:");
+      builder.append(System.lineSeparator());
+      for (Node call : callees) {
+        builder.append("  ");
+        builder.append(call.getProgramMethod().toSourceString());
+        builder.append(System.lineSeparator());
+      }
+    }
+    if (callers.size() > 0) {
+      builder.append("Callers:");
+      builder.append(System.lineSeparator());
+      for (Node caller : callers) {
+        builder.append("  ");
+        builder.append(caller.getProgramMethod().toSourceString());
+        builder.append(System.lineSeparator());
+      }
+    }
+    return builder.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/NodeBase.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/NodeBase.java
new file mode 100644
index 0000000..2f59f65
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/NodeBase.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2021, 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.conversion.callgraph;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+
+public abstract class NodeBase<N extends NodeBase<N>> {
+
+  private final ProgramMethod method;
+
+  public NodeBase(ProgramMethod method) {
+    this.method = method;
+  }
+
+  public abstract void addCallerConcurrently(N caller, boolean likelySpuriousCallEdge);
+
+  public abstract void addReaderConcurrently(N reader);
+
+  public DexEncodedMethod getMethod() {
+    return getProgramMethod().getDefinition();
+  }
+
+  public ProgramMethod getProgramMethod() {
+    return method;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/PartialCallGraphBuilder.java
similarity index 68%
rename from src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java
rename to src/main/java/com/android/tools/r8/ir/conversion/callgraph/PartialCallGraphBuilder.java
index e094867..afab94b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/PartialCallGraphBuilder.java
@@ -1,7 +1,8 @@
 // Copyright (c) 2019, 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.conversion;
+
+package com.android.tools.r8.ir.conversion.callgraph;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -11,11 +12,11 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
-public class PartialCallGraphBuilder extends CallGraphBuilderBase {
+public class PartialCallGraphBuilder extends IRProcessingCallGraphBuilderBase {
 
   private final ProgramMethodSet seeds;
 
-  PartialCallGraphBuilder(AppView<AppInfoWithLiveness> appView, ProgramMethodSet seeds) {
+  public PartialCallGraphBuilder(AppView<AppInfoWithLiveness> appView, ProgramMethodSet seeds) {
     super(appView);
     assert seeds != null && !seeds.isEmpty();
     this.seeds = seeds;
@@ -27,7 +28,14 @@
   }
 
   private void processMethod(ProgramMethod method) {
-    method.registerCodeReferences(new InvokeExtractor(getOrCreateNode(method), seeds::contains));
+    IRProcessingCallGraphUseRegistry<Node> registry =
+        new IRProcessingCallGraphUseRegistry<>(
+            appView,
+            getOrCreateNode(method),
+            this::getOrCreateNode,
+            possibleProgramTargetsCache,
+            seeds::contains);
+    method.registerCodeReferences(registry);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClassConstructorSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClassConstructorSourceCode.java
index b0abf2a..ddcb68d 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClassConstructorSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClassConstructorSourceCode.java
@@ -30,8 +30,6 @@
             new CfStackInstruction(Opcode.Dup),
             new CfInvoke(Opcodes.INVOKESPECIAL, lambda.constructor, false),
             new CfStaticFieldWrite(lambda.lambdaField, lambda.lambdaField),
-            new CfReturnVoid()),
-        ImmutableList.of(),
-        ImmutableList.of());
+            new CfReturnVoid()));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index a8d8b14..69cc40b 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.desugar.records.RecordDesugaring;
 import com.android.tools.r8.ir.desugar.stringconcat.StringConcatInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.twr.TwrInstructionDesugaring;
+import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
@@ -49,6 +50,8 @@
 
   private final AppView<?> appView;
   private final List<CfInstructionDesugaring> desugarings = new ArrayList<>();
+  // A special collection of desugarings that yield to all other desugarings.
+  private final List<CfInstructionDesugaring> yieldingDesugarings = new ArrayList<>();
 
   private final NestBasedAccessDesugaring nestBasedAccessDesugaring;
   private final RecordDesugaring recordRewriter;
@@ -89,7 +92,7 @@
       backportedMethodRewriter = new BackportedMethodRewriter(appView);
     }
     if (appView.options().apiModelingOptions().enableOutliningOfMethods) {
-      desugarings.add(new ApiInvokeOutlinerDesugaring(appView, apiLevelCompute));
+      yieldingDesugarings.add(new ApiInvokeOutlinerDesugaring(appView, apiLevelCompute));
     }
     if (appView.options().enableTryWithResourcesDesugaring()) {
       desugarings.add(new TwrInstructionDesugaring(appView));
@@ -167,7 +170,7 @@
               new StringDiagnostic(
                   "Unsupported attempt to desugar non-CF code",
                   method.getOrigin(),
-                  method.getPosition()));
+                  MethodPosition.create(method)));
     }
   }
 
@@ -267,7 +270,38 @@
       ProgramMethod context,
       MethodProcessingContext methodProcessingContext) {
     // TODO(b/177810578): Migrate other cf-to-cf based desugaring here.
-    Iterator<CfInstructionDesugaring> iterator = desugarings.iterator();
+    Collection<CfInstruction> replacement =
+        applyDesugaring(
+            instruction,
+            freshLocalProvider,
+            localStackAllocator,
+            eventConsumer,
+            context,
+            methodProcessingContext,
+            desugarings.iterator());
+    if (replacement != null) {
+      return replacement;
+    }
+    // If we made it here there it is because a yielding desugaring reported that it needs
+    // desugaring and no other desugaring happened.
+    return applyDesugaring(
+        instruction,
+        freshLocalProvider,
+        localStackAllocator,
+        eventConsumer,
+        context,
+        methodProcessingContext,
+        yieldingDesugarings.iterator());
+  }
+
+  private Collection<CfInstruction> applyDesugaring(
+      CfInstruction instruction,
+      FreshLocalProvider freshLocalProvider,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext,
+      Iterator<CfInstructionDesugaring> iterator) {
     while (iterator.hasNext()) {
       CfInstructionDesugaring desugaring = iterator.next();
       Collection<CfInstruction> replacement =
@@ -310,7 +344,9 @@
 
   private boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
     return Iterables.any(
-        desugarings, desugaring -> desugaring.needsDesugaring(instruction, context));
+            desugarings, desugaring -> desugaring.needsDesugaring(instruction, context))
+        || Iterables.any(
+            yieldingDesugarings, desugaring -> desugaring.needsDesugaring(instruction, context));
   }
 
   private boolean verifyNoOtherDesugaringNeeded(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
index 62c0445..d56be1a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
@@ -31,6 +31,10 @@
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 
+/**
+ * This desugaring will outline calls to library methods that are introduced after the min-api
+ * level. For classes introduced after the min-api level see ApiReferenceStubber.
+ */
 public class ApiInvokeOutlinerDesugaring implements CfInstructionDesugaring {
 
   private final AppView<?> appView;
@@ -55,7 +59,7 @@
     if (computedApiLevel.isGreaterThan(appView.computedMinApiLevel())) {
       return desugarLibraryCall(
           methodProcessingContext.createUniqueContext(),
-          instruction,
+          instruction.asInvoke(),
           computedApiLevel,
           dexItemFactory);
     }
@@ -92,14 +96,6 @@
         apiLevelCompute.computeApiLevelForLibraryReference(
             cfInvoke.getMethod(), ComputedApiLevel.unknown());
     if (apiLevel.isGreaterThan(appView.computedMinApiLevel())) {
-      ComputedApiLevel holderApiLevel =
-          apiLevelCompute.computeApiLevelForLibraryReference(
-              holderType, ComputedApiLevel.unknown());
-      if (holderApiLevel.isGreaterThan(appView.computedMinApiLevel())) {
-        // Do not outline where the holder is unknown or introduced later then min api.
-        // TODO(b/208978971): Describe where mocking is done when landing.
-        return appView.computedMinApiLevel();
-      }
       return apiLevel;
     }
     return appView.computedMinApiLevel();
@@ -107,11 +103,12 @@
 
   private Collection<CfInstruction> desugarLibraryCall(
       UniqueContext context,
-      CfInstruction instruction,
+      CfInvoke invoke,
       ComputedApiLevel computedApiLevel,
       DexItemFactory factory) {
-    DexMethod method = instruction.asInvoke().getMethod();
-    ProgramMethod programMethod = ensureOutlineMethod(context, method, computedApiLevel, factory);
+    DexMethod method = invoke.getMethod();
+    ProgramMethod programMethod =
+        ensureOutlineMethod(context, method, computedApiLevel, factory, invoke);
     return ImmutableList.of(new CfInvoke(INVOKESTATIC, programMethod.getReference(), false));
   }
 
@@ -119,12 +116,13 @@
       UniqueContext context,
       DexMethod apiMethod,
       ComputedApiLevel apiLevel,
-      DexItemFactory factory) {
+      DexItemFactory factory,
+      CfInvoke invoke) {
     DexClass libraryHolder = appView.definitionFor(apiMethod.getHolderType());
     assert libraryHolder != null;
-    DexEncodedMethod libraryApiMethodDefinition = libraryHolder.lookupMethod(apiMethod);
-    DexProto proto =
-        factory.prependHolderToProtoIf(apiMethod, libraryApiMethodDefinition.isVirtualMethod());
+    boolean isVirtualMethod = invoke.isInvokeVirtual() || invoke.isInvokeInterface();
+    assert verifyLibraryHolderAndInvoke(libraryHolder, apiMethod, isVirtualMethod);
+    DexProto proto = factory.prependHolderToProtoIf(apiMethod, isVirtualMethod);
     return appView
         .getSyntheticItems()
         .createMethod(
@@ -145,18 +143,25 @@
                   .setApiLevelForCode(apiLevel)
                   .setCode(
                       m -> {
-                        if (libraryApiMethodDefinition.isStatic()) {
-                          return ForwardMethodBuilder.builder(factory)
-                              .setStaticTarget(apiMethod, libraryHolder.isInterface())
-                              .setStaticSource(apiMethod)
-                              .build();
-                        } else {
+                        if (isVirtualMethod) {
                           return ForwardMethodBuilder.builder(factory)
                               .setVirtualTarget(apiMethod, libraryHolder.isInterface())
                               .setNonStaticSource(apiMethod)
                               .build();
+                        } else {
+                          return ForwardMethodBuilder.builder(factory)
+                              .setStaticTarget(apiMethod, libraryHolder.isInterface())
+                              .setStaticSource(apiMethod)
+                              .build();
                         }
                       });
             });
   }
+
+  private boolean verifyLibraryHolderAndInvoke(
+      DexClass libraryHolder, DexMethod apiMethod, boolean isVirtualInvoke) {
+    DexEncodedMethod libraryApiMethodDefinition = libraryHolder.lookupMethod(apiMethod);
+    return libraryApiMethodDefinition == null
+        || libraryApiMethodDefinition.isVirtualMethod() == isVirtualInvoke;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/backports/CollectionMethodGenerators.java b/src/main/java/com/android/tools/r8/ir/desugar/backports/CollectionMethodGenerators.java
index ee175f9..cd385e8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/backports/CollectionMethodGenerators.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/backports/CollectionMethodGenerators.java
@@ -60,13 +60,7 @@
             false),
         new CfReturn(ValueType.OBJECT));
 
-    return new CfCode(
-        method.holder,
-        4,
-        formalCount,
-        builder.build(),
-        ImmutableList.of(),
-        ImmutableList.of());
+    return new CfCode(method.holder, 4, formalCount, builder.build());
   }
 
   public static CfCode generateMapOf(
@@ -111,12 +105,6 @@
             false),
         new CfReturn(ValueType.OBJECT));
 
-    return new CfCode(
-        method.holder,
-        7,
-        formalCount * 2,
-        builder.build(),
-        ImmutableList.of(),
-        ImmutableList.of());
+    return new CfCode(method.holder, 7, formalCount * 2, builder.build());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecification.java
new file mode 100644
index 0000000..6a6a2d4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecification.java
@@ -0,0 +1,170 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.PrefixRewritingMapper;
+import com.android.tools.r8.ir.desugar.PrefixRewritingMapper.DesugarPrefixRewritingMapper;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class HumanDesugaredLibrarySpecification {
+
+  private final boolean libraryCompilation;
+  private final HumanTopLevelFlags topLevelFlags;
+  private final HumanRewritingFlags rewritingFlags;
+  private final PrefixRewritingMapper prefixRewritingMapper;
+
+  public static HumanDesugaredLibrarySpecification withOnlyRewritePrefixForTesting(
+      Map<String, String> prefix, InternalOptions options) {
+    return new HumanDesugaredLibrarySpecification(
+        HumanTopLevelFlags.empty(),
+        HumanRewritingFlags.withOnlyRewritePrefixForTesting(prefix, options),
+        true,
+        options.itemFactory);
+  }
+
+  public static HumanDesugaredLibrarySpecification empty() {
+    return new HumanDesugaredLibrarySpecification(
+        HumanTopLevelFlags.empty(), HumanRewritingFlags.empty(), false, null) {
+
+      @Override
+      public boolean isSupported(DexReference reference, AppView<?> appView) {
+        return false;
+      }
+
+      @Override
+      public boolean isEmptyConfiguration() {
+        return true;
+      }
+    };
+  }
+
+  public HumanDesugaredLibrarySpecification(
+      HumanTopLevelFlags topLevelFlags,
+      HumanRewritingFlags rewritingFlags,
+      boolean libraryCompilation,
+      DexItemFactory factory) {
+    this.libraryCompilation = libraryCompilation;
+    this.topLevelFlags = topLevelFlags;
+    this.rewritingFlags = rewritingFlags;
+    this.prefixRewritingMapper =
+        rewritingFlags.getRewritePrefix().isEmpty()
+            ? PrefixRewritingMapper.empty()
+            : new DesugarPrefixRewritingMapper(
+                rewritingFlags.getRewritePrefix(), factory, libraryCompilation);
+  }
+
+  public boolean supportAllCallbacksFromLibrary() {
+    return topLevelFlags.supportAllCallbacksFromLibrary();
+  }
+
+  public PrefixRewritingMapper getPrefixRewritingMapper() {
+    return prefixRewritingMapper;
+  }
+
+  public AndroidApiLevel getRequiredCompilationApiLevel() {
+    return topLevelFlags.getRequiredCompilationAPILevel();
+  }
+
+  public boolean isLibraryCompilation() {
+    return libraryCompilation;
+  }
+
+  public String getSynthesizedLibraryClassesPackagePrefix() {
+    return topLevelFlags.getSynthesizedLibraryClassesPackagePrefix();
+  }
+
+  public HumanTopLevelFlags getTopLevelFlags() {
+    return topLevelFlags;
+  }
+
+  public HumanRewritingFlags getRewritingFlags() {
+    return rewritingFlags;
+  }
+
+  public String getIdentifier() {
+    return topLevelFlags.getIdentifier();
+  }
+
+  public Map<String, String> getRewritePrefix() {
+    return rewritingFlags.getRewritePrefix();
+  }
+
+  public boolean hasEmulatedLibraryInterfaces() {
+    return !getEmulateLibraryInterface().isEmpty();
+  }
+
+  public Map<DexType, DexType> getEmulateLibraryInterface() {
+    return rewritingFlags.getEmulateLibraryInterface();
+  }
+
+  public boolean isSupported(DexReference reference, AppView<?> appView) {
+    return prefixRewritingMapper.hasRewrittenType(reference.getContextType(), appView);
+  }
+
+  // If the method is retargeted, answers the retargeted method, else null.
+  public DexMethod retargetMethod(DexEncodedMethod method, AppView<?> appView) {
+    Map<DexMethod, DexType> retargetCoreLibMember = rewritingFlags.getRetargetCoreLibMember();
+    DexType dexType = retargetCoreLibMember.get(method.getReference());
+    if (dexType != null) {
+      return appView
+          .dexItemFactory()
+          .createMethod(
+              dexType,
+              appView.dexItemFactory().prependHolderToProto(method.getReference()),
+              method.getName());
+    }
+    return null;
+  }
+
+  public DexMethod retargetMethod(DexClassAndMethod method, AppView<?> appView) {
+    return retargetMethod(method.getDefinition(), appView);
+  }
+
+  public Map<DexMethod, DexType> getRetargetCoreLibMember() {
+    return rewritingFlags.getRetargetCoreLibMember();
+  }
+
+  public Map<DexType, DexType> getBackportCoreLibraryMember() {
+    return rewritingFlags.getBackportCoreLibraryMember();
+  }
+
+  public Map<DexType, DexType> getCustomConversions() {
+    return rewritingFlags.getCustomConversions();
+  }
+
+  public Set<DexType> getWrapperConversions() {
+    return rewritingFlags.getWrapperConversions();
+  }
+
+  public Set<DexMethod> getDontRewriteInvocation() {
+    return rewritingFlags.getDontRewriteInvocation();
+  }
+
+  public Set<DexType> getDontRetargetLibMember() {
+    return rewritingFlags.getDontRetargetLibMember();
+  }
+
+  public List<String> getExtraKeepRules() {
+    return topLevelFlags.getExtraKeepRules();
+  }
+
+  public String getJsonSource() {
+    return topLevelFlags.getJsonSource();
+  }
+
+  public boolean isEmptyConfiguration() {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
new file mode 100644
index 0000000..a612177
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
@@ -0,0 +1,236 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class HumanDesugaredLibrarySpecificationParser {
+
+  static final String IDENTIFIER_KEY = "identifier";
+  static final String REQUIRED_COMPILATION_API_LEVEL_KEY = "required_compilation_api_level";
+  static final String SYNTHESIZED_LIBRARY_CLASSES_PACKAGE_PREFIX_KEY =
+      "synthesized_library_classes_package_prefix";
+
+  static final String COMMON_FLAGS_KEY = "common_flags";
+  static final String LIBRARY_FLAGS_KEY = "library_flags";
+  static final String PROGRAM_FLAGS_KEY = "program_flags";
+
+  static final String API_LEVEL_BELOW_OR_EQUAL_KEY = "api_level_below_or_equal";
+  static final String WRAPPER_CONVERSION_KEY = "wrapper_conversion";
+  static final String CUSTOM_CONVERSION_KEY = "custom_conversion";
+  static final String REWRITE_PREFIX_KEY = "rewrite_prefix";
+  static final String RETARGET_LIB_MEMBER_KEY = "retarget_lib_member";
+  static final String EMULATE_INTERFACE_KEY = "emulate_interface";
+  static final String DONT_REWRITE_KEY = "dont_rewrite";
+  static final String DONT_RETARGET_LIB_MEMBER_KEY = "dont_retarget_lib_member";
+  static final String BACKPORT_KEY = "backport";
+  static final String SHRINKER_CONFIG_KEY = "shrinker_config";
+  static final String SUPPORT_ALL_CALLBACKS_FROM_LIBRARY_KEY = "support_all_callbacks_from_library";
+
+  private final DexItemFactory dexItemFactory;
+  private final Reporter reporter;
+  private final boolean libraryCompilation;
+  private final int minAPILevel;
+
+  private Origin origin;
+  private JsonObject jsonConfig;
+
+  public HumanDesugaredLibrarySpecificationParser(
+      DexItemFactory dexItemFactory,
+      Reporter reporter,
+      boolean libraryCompilation,
+      int minAPILevel) {
+    this.dexItemFactory = dexItemFactory;
+    this.reporter = reporter;
+    this.minAPILevel = minAPILevel;
+    this.libraryCompilation = libraryCompilation;
+  }
+
+  public DexItemFactory dexItemFactory() {
+    return dexItemFactory;
+  }
+
+  public Reporter reporter() {
+    return reporter;
+  }
+
+  public JsonObject getJsonConfig() {
+    return jsonConfig;
+  }
+
+  public Origin getOrigin() {
+    assert origin != null;
+    return origin;
+  }
+
+  JsonElement required(JsonObject json, String key) {
+    if (!json.has(key)) {
+      throw reporter.fatalError(
+          new StringDiagnostic(
+              "Invalid desugared library configuration. Expected required key '" + key + "'",
+              origin));
+    }
+    return json.get(key);
+  }
+
+  public HumanDesugaredLibrarySpecification parse(StringResource stringResource) {
+    return parse(stringResource, builder -> {});
+  }
+
+  public HumanDesugaredLibrarySpecification parse(
+      StringResource stringResource, Consumer<HumanTopLevelFlags.Builder> topLevelFlagAmender) {
+    String jsonConfigString = parseJson(stringResource);
+
+    HumanTopLevelFlags topLevelFlags = parseTopLevelFlags(jsonConfigString, topLevelFlagAmender);
+
+    HumanRewritingFlags legacyRewritingFlags = parseRewritingFlags();
+
+    HumanDesugaredLibrarySpecification config =
+        new HumanDesugaredLibrarySpecification(
+            topLevelFlags, legacyRewritingFlags, libraryCompilation, dexItemFactory);
+    origin = null;
+    return config;
+  }
+
+  String parseJson(StringResource stringResource) {
+    setOrigin(stringResource);
+    String jsonConfigString;
+    try {
+      jsonConfigString = stringResource.getString();
+      JsonParser parser = new JsonParser();
+      jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
+    } catch (Exception e) {
+      throw reporter.fatalError(new ExceptionDiagnostic(e, origin));
+    }
+    return jsonConfigString;
+  }
+
+  void setOrigin(StringResource stringResource) {
+    origin = stringResource.getOrigin();
+    assert origin != null;
+  }
+
+  private HumanRewritingFlags parseRewritingFlags() {
+    HumanRewritingFlags.Builder builder =
+        HumanRewritingFlags.builder(dexItemFactory, reporter, origin);
+    JsonElement commonFlags = required(jsonConfig, COMMON_FLAGS_KEY);
+    JsonElement libraryFlags = required(jsonConfig, LIBRARY_FLAGS_KEY);
+    JsonElement programFlags = required(jsonConfig, PROGRAM_FLAGS_KEY);
+    parseFlagsList(commonFlags.getAsJsonArray(), builder);
+    parseFlagsList(
+        libraryCompilation ? libraryFlags.getAsJsonArray() : programFlags.getAsJsonArray(),
+        builder);
+    return builder.build();
+  }
+
+  HumanTopLevelFlags parseTopLevelFlags(
+      String jsonConfigString, Consumer<HumanTopLevelFlags.Builder> topLevelFlagAmender) {
+    HumanTopLevelFlags.Builder builder = HumanTopLevelFlags.builder();
+
+    builder.setJsonSource(jsonConfigString);
+
+    String identifier = required(jsonConfig, IDENTIFIER_KEY).getAsString();
+    builder.setDesugaredLibraryIdentifier(identifier);
+    builder.setSynthesizedLibraryClassesPackagePrefix(
+        required(jsonConfig, SYNTHESIZED_LIBRARY_CLASSES_PACKAGE_PREFIX_KEY).getAsString());
+
+    int required_compilation_api_level =
+        required(jsonConfig, REQUIRED_COMPILATION_API_LEVEL_KEY).getAsInt();
+    builder.setRequiredCompilationAPILevel(
+        AndroidApiLevel.getAndroidApiLevel(required_compilation_api_level));
+    if (jsonConfig.has(SHRINKER_CONFIG_KEY)) {
+      JsonArray jsonKeepRules = jsonConfig.get(SHRINKER_CONFIG_KEY).getAsJsonArray();
+      List<String> extraKeepRules = new ArrayList<>(jsonKeepRules.size());
+      for (JsonElement keepRule : jsonKeepRules) {
+        extraKeepRules.add(keepRule.getAsString());
+      }
+      builder.setExtraKeepRules(extraKeepRules);
+    }
+
+    if (jsonConfig.has(SUPPORT_ALL_CALLBACKS_FROM_LIBRARY_KEY)) {
+      boolean supportAllCallbacksFromLibrary =
+          jsonConfig.get(SUPPORT_ALL_CALLBACKS_FROM_LIBRARY_KEY).getAsBoolean();
+      builder.setSupportAllCallbacksFromLibrary(supportAllCallbacksFromLibrary);
+    }
+
+    topLevelFlagAmender.accept(builder);
+
+    return builder.build();
+  }
+
+  private void parseFlagsList(JsonArray jsonFlags, HumanRewritingFlags.Builder builder) {
+    for (JsonElement jsonFlagSet : jsonFlags) {
+      JsonObject flag = jsonFlagSet.getAsJsonObject();
+      int api_level_below_or_equal = required(flag, API_LEVEL_BELOW_OR_EQUAL_KEY).getAsInt();
+      if (minAPILevel <= api_level_below_or_equal) {
+        parseFlags(flag, builder);
+      }
+    }
+  }
+
+  void parseFlags(JsonObject jsonFlagSet, HumanRewritingFlags.Builder builder) {
+    if (jsonFlagSet.has(REWRITE_PREFIX_KEY)) {
+      for (Map.Entry<String, JsonElement> rewritePrefix :
+          jsonFlagSet.get(REWRITE_PREFIX_KEY).getAsJsonObject().entrySet()) {
+        builder.putRewritePrefix(rewritePrefix.getKey(), rewritePrefix.getValue().getAsString());
+      }
+    }
+    if (jsonFlagSet.has(RETARGET_LIB_MEMBER_KEY)) {
+      for (Map.Entry<String, JsonElement> retarget :
+          jsonFlagSet.get(RETARGET_LIB_MEMBER_KEY).getAsJsonObject().entrySet()) {
+        builder.putRetargetCoreLibMember(retarget.getKey(), retarget.getValue().getAsString());
+      }
+    }
+    if (jsonFlagSet.has(BACKPORT_KEY)) {
+      for (Map.Entry<String, JsonElement> backport :
+          jsonFlagSet.get(BACKPORT_KEY).getAsJsonObject().entrySet()) {
+        builder.putBackportCoreLibraryMember(backport.getKey(), backport.getValue().getAsString());
+      }
+    }
+    if (jsonFlagSet.has(EMULATE_INTERFACE_KEY)) {
+      for (Map.Entry<String, JsonElement> itf :
+          jsonFlagSet.get(EMULATE_INTERFACE_KEY).getAsJsonObject().entrySet()) {
+        builder.putEmulateLibraryInterface(itf.getKey(), itf.getValue().getAsString());
+      }
+    }
+    if (jsonFlagSet.has(CUSTOM_CONVERSION_KEY)) {
+      for (Map.Entry<String, JsonElement> conversion :
+          jsonFlagSet.get(CUSTOM_CONVERSION_KEY).getAsJsonObject().entrySet()) {
+        builder.putCustomConversion(conversion.getKey(), conversion.getValue().getAsString());
+      }
+    }
+    if (jsonFlagSet.has(WRAPPER_CONVERSION_KEY)) {
+      for (JsonElement wrapper : jsonFlagSet.get(WRAPPER_CONVERSION_KEY).getAsJsonArray()) {
+        builder.addWrapperConversion(wrapper.getAsString());
+      }
+    }
+    if (jsonFlagSet.has(DONT_REWRITE_KEY)) {
+      JsonArray dontRewrite = jsonFlagSet.get(DONT_REWRITE_KEY).getAsJsonArray();
+      for (JsonElement rewrite : dontRewrite) {
+        builder.addDontRewriteInvocation(rewrite.getAsString());
+      }
+    }
+    if (jsonFlagSet.has(DONT_RETARGET_LIB_MEMBER_KEY)) {
+      JsonArray dontRetarget = jsonFlagSet.get(DONT_RETARGET_LIB_MEMBER_KEY).getAsJsonArray();
+      for (JsonElement rewrite : dontRetarget) {
+        builder.addDontRetargetLibMember(rewrite.getAsString());
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
new file mode 100644
index 0000000..bf87cd8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
@@ -0,0 +1,354 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class HumanRewritingFlags {
+
+  private final Map<String, String> rewritePrefix;
+  private final Map<DexType, DexType> emulateLibraryInterface;
+  private final Map<DexMethod, DexType> retargetCoreLibMember;
+  private final Map<DexType, DexType> backportCoreLibraryMember;
+  private final Map<DexType, DexType> customConversions;
+  private final Set<DexMethod> dontRewriteInvocation;
+  private final Set<DexType> dontRetargetLibMember;
+  private final Set<DexType> wrapperConversions;
+
+  HumanRewritingFlags(
+      Map<String, String> rewritePrefix,
+      Map<DexType, DexType> emulateLibraryInterface,
+      Map<DexMethod, DexType> retargetCoreLibMember,
+      Map<DexType, DexType> backportCoreLibraryMember,
+      Map<DexType, DexType> customConversions,
+      Set<DexMethod> dontRewriteInvocation,
+      Set<DexType> dontRetargetLibMember,
+      Set<DexType> wrapperConversions) {
+    this.rewritePrefix = rewritePrefix;
+    this.emulateLibraryInterface = emulateLibraryInterface;
+    this.retargetCoreLibMember = retargetCoreLibMember;
+    this.backportCoreLibraryMember = backportCoreLibraryMember;
+    this.customConversions = customConversions;
+    this.dontRewriteInvocation = dontRewriteInvocation;
+    this.dontRetargetLibMember = dontRetargetLibMember;
+    this.wrapperConversions = wrapperConversions;
+  }
+
+  public static HumanRewritingFlags empty() {
+    return new HumanRewritingFlags(
+        ImmutableMap.of(),
+        ImmutableMap.of(),
+        ImmutableMap.of(),
+        ImmutableMap.of(),
+        ImmutableMap.of(),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableSet.of());
+  }
+
+  public static HumanRewritingFlags withOnlyRewritePrefixForTesting(
+      Map<String, String> prefix, InternalOptions options) {
+    Builder builder = builder(options.dexItemFactory(), options.reporter, Origin.unknown());
+    prefix.forEach(builder::putRewritePrefix);
+    return builder.build();
+  }
+
+  public static Builder builder(DexItemFactory dexItemFactory, Reporter reporter, Origin origin) {
+    return new Builder(dexItemFactory, reporter, origin);
+  }
+
+  public Builder newBuilder(DexItemFactory dexItemFactory, Reporter reporter, Origin origin) {
+    return new Builder(
+        dexItemFactory,
+        reporter,
+        origin,
+        rewritePrefix,
+        emulateLibraryInterface,
+        retargetCoreLibMember,
+        backportCoreLibraryMember,
+        customConversions,
+        dontRewriteInvocation,
+        dontRetargetLibMember,
+        wrapperConversions);
+  }
+
+  public Map<String, String> getRewritePrefix() {
+    return rewritePrefix;
+  }
+
+  public Map<DexType, DexType> getEmulateLibraryInterface() {
+    return emulateLibraryInterface;
+  }
+
+  public Map<DexMethod, DexType> getRetargetCoreLibMember() {
+    return retargetCoreLibMember;
+  }
+
+  public Map<DexType, DexType> getBackportCoreLibraryMember() {
+    return backportCoreLibraryMember;
+  }
+
+  public Map<DexType, DexType> getCustomConversions() {
+    return customConversions;
+  }
+
+  public Set<DexMethod> getDontRewriteInvocation() {
+    return dontRewriteInvocation;
+  }
+
+  public Set<DexType> getDontRetargetLibMember() {
+    return dontRetargetLibMember;
+  }
+
+  public Set<DexType> getWrapperConversions() {
+    return wrapperConversions;
+  }
+
+  public static class Builder {
+
+    private static final String SEPARATORS = "\\s+|,\\s+|#|\\(|\\)";
+
+    private final DexItemFactory factory;
+    private final Reporter reporter;
+    private final Origin origin;
+
+    private final Map<String, String> rewritePrefix;
+    private final Map<DexType, DexType> emulateLibraryInterface;
+    private final Map<DexMethod, DexType> retargetCoreLibMember;
+    private final Map<DexType, DexType> backportCoreLibraryMember;
+    private final Map<DexType, DexType> customConversions;
+    private final Set<DexMethod> dontRewriteInvocation;
+    private final Set<DexType> dontRetargetLibMember;
+    private final Set<DexType> wrapperConversions;
+
+    Builder(DexItemFactory factory, Reporter reporter, Origin origin) {
+      this(
+          factory,
+          reporter,
+          origin,
+          new HashMap<>(),
+          new IdentityHashMap<>(),
+          new IdentityHashMap<>(),
+          new IdentityHashMap<>(),
+          new IdentityHashMap<>(),
+          Sets.newIdentityHashSet(),
+          Sets.newIdentityHashSet(),
+          Sets.newIdentityHashSet());
+    }
+
+    Builder(
+        DexItemFactory factory,
+        Reporter reporter,
+        Origin origin,
+        Map<String, String> rewritePrefix,
+        Map<DexType, DexType> emulateLibraryInterface,
+        Map<DexMethod, DexType> retargetCoreLibMember,
+        Map<DexType, DexType> backportCoreLibraryMember,
+        Map<DexType, DexType> customConversions,
+        Set<DexMethod> dontRewriteInvocation,
+        Set<DexType> dontRetargetLibMember,
+        Set<DexType> wrapperConversions) {
+      this.factory = factory;
+      this.reporter = reporter;
+      this.origin = origin;
+      this.rewritePrefix = new HashMap<>(rewritePrefix);
+      this.emulateLibraryInterface = new IdentityHashMap<>(emulateLibraryInterface);
+      this.retargetCoreLibMember = new IdentityHashMap<>(retargetCoreLibMember);
+      this.backportCoreLibraryMember = new IdentityHashMap<>(backportCoreLibraryMember);
+      this.customConversions = new IdentityHashMap<>(customConversions);
+      this.dontRewriteInvocation = Sets.newIdentityHashSet();
+      this.dontRewriteInvocation.addAll(dontRewriteInvocation);
+      this.dontRetargetLibMember = Sets.newIdentityHashSet();
+      this.dontRetargetLibMember.addAll(dontRetargetLibMember);
+      this.wrapperConversions = Sets.newIdentityHashSet();
+      this.wrapperConversions.addAll(wrapperConversions);
+    }
+
+    // Utility to set values.
+    private <K, V> void put(Map<K, V> map, K key, V value, String desc) {
+      if (map.containsKey(key) && !map.get(key).equals(value)) {
+        throw reporter.fatalError(
+            new StringDiagnostic(
+                "Invalid desugared library configuration. "
+                    + " Duplicate assignment of key: '"
+                    + key
+                    + "' in sections for '"
+                    + desc
+                    + "'",
+                origin));
+      }
+      map.put(key, value);
+    }
+
+    public Builder putRewritePrefix(String prefix, String rewrittenPrefix) {
+      put(
+          rewritePrefix,
+          prefix,
+          rewrittenPrefix,
+          HumanDesugaredLibrarySpecificationParser.REWRITE_PREFIX_KEY);
+      return this;
+    }
+
+    public Builder putEmulateLibraryInterface(
+        String emulateLibraryItf, String rewrittenEmulateLibraryItf) {
+      DexType interfaceType = stringClassToDexType(emulateLibraryItf);
+      DexType rewrittenType = stringClassToDexType(rewrittenEmulateLibraryItf);
+      putEmulateLibraryInterface(interfaceType, rewrittenType);
+      return this;
+    }
+
+    public Builder putEmulateLibraryInterface(DexType interfaceType, DexType rewrittenType) {
+      put(
+          emulateLibraryInterface,
+          interfaceType,
+          rewrittenType,
+          HumanDesugaredLibrarySpecificationParser.EMULATE_INTERFACE_KEY);
+      return this;
+    }
+
+    public Builder putCustomConversion(String type, String conversionHolder) {
+      DexType dexType = stringClassToDexType(type);
+      DexType conversionType = stringClassToDexType(conversionHolder);
+      putCustomConversion(dexType, conversionType);
+      return this;
+    }
+
+    public Builder putCustomConversion(DexType dexType, DexType conversionType) {
+      put(
+          customConversions,
+          dexType,
+          conversionType,
+          HumanDesugaredLibrarySpecificationParser.CUSTOM_CONVERSION_KEY);
+      return this;
+    }
+
+    public Builder addWrapperConversion(String type) {
+      DexType dexType = stringClassToDexType(type);
+      addWrapperConversion(dexType);
+      return this;
+    }
+
+    public Builder addWrapperConversion(DexType dexType) {
+      wrapperConversions.add(dexType);
+      return this;
+    }
+
+    public Builder putRetargetCoreLibMember(String retarget, String rewrittenRetarget) {
+      DexMethod key = parseMethod(retarget);
+      DexType rewrittenType = stringClassToDexType(rewrittenRetarget);
+      putRetargetCoreLibMember(key, rewrittenType);
+      return this;
+    }
+
+    public Builder putRetargetCoreLibMember(DexMethod key, DexType rewrittenType) {
+      put(
+          retargetCoreLibMember,
+          key,
+          rewrittenType,
+          HumanDesugaredLibrarySpecificationParser.RETARGET_LIB_MEMBER_KEY);
+      return this;
+    }
+
+    public Builder putBackportCoreLibraryMember(String backport, String rewrittenBackport) {
+      DexType backportType = stringClassToDexType(backport);
+      DexType rewrittenBackportType = stringClassToDexType(rewrittenBackport);
+      putBackportCoreLibraryMember(backportType, rewrittenBackportType);
+      return this;
+    }
+
+    public Builder putBackportCoreLibraryMember(
+        DexType backportType, DexType rewrittenBackportType) {
+      put(
+          backportCoreLibraryMember,
+          backportType,
+          rewrittenBackportType,
+          HumanDesugaredLibrarySpecificationParser.BACKPORT_KEY);
+      return this;
+    }
+
+    public Builder addDontRewriteInvocation(String dontRewriteInvocation) {
+      DexMethod dontRewrite = parseMethod(dontRewriteInvocation);
+      addDontRewriteInvocation(dontRewrite);
+      return this;
+    }
+
+    public Builder addDontRewriteInvocation(DexMethod dontRewrite) {
+      this.dontRewriteInvocation.add(dontRewrite);
+      return this;
+    }
+
+    public Builder addDontRetargetLibMember(String dontRetargetLibMember) {
+      addDontRetargetLibMember(stringClassToDexType(dontRetargetLibMember));
+      return this;
+    }
+
+    public Builder addDontRetargetLibMember(DexType dontRetargetLibMember) {
+      this.dontRetargetLibMember.add(dontRetargetLibMember);
+      return this;
+    }
+
+    private DexMethod parseMethod(String signature) {
+      String[] split = signature.split(SEPARATORS);
+      assert split.length >= 3;
+      DexType returnType = factory.createType(DescriptorUtils.javaTypeToDescriptor(split[0]));
+      DexType holderType = factory.createType(DescriptorUtils.javaTypeToDescriptor(split[1]));
+      DexString name = factory.createString(split[2]);
+      DexType[] argTypes = new DexType[split.length - 3];
+      for (int i = 3; i < split.length; i++) {
+        argTypes[i - 3] = factory.createType(DescriptorUtils.javaTypeToDescriptor(split[i]));
+      }
+      DexProto proto = factory.createProto(returnType, argTypes);
+      return factory.createMethod(holderType, proto, name);
+    }
+
+    private DexType stringClassToDexType(String stringClass) {
+      return factory.createType(DescriptorUtils.javaTypeToDescriptor(stringClass));
+    }
+
+    public HumanRewritingFlags build() {
+      validate();
+      return new HumanRewritingFlags(
+          ImmutableMap.copyOf(rewritePrefix),
+          ImmutableMap.copyOf(emulateLibraryInterface),
+          ImmutableMap.copyOf(retargetCoreLibMember),
+          ImmutableMap.copyOf(backportCoreLibraryMember),
+          ImmutableMap.copyOf(customConversions),
+          ImmutableSet.copyOf(dontRewriteInvocation),
+          ImmutableSet.copyOf(dontRetargetLibMember),
+          ImmutableSet.copyOf(wrapperConversions));
+    }
+
+    private void validate() {
+      SetView<DexType> dups = Sets.intersection(customConversions.keySet(), wrapperConversions);
+      if (!dups.isEmpty()) {
+        throw reporter.fatalError(
+            new StringDiagnostic(
+                "Invalid desugared library configuration. "
+                    + "Duplicate types in custom conversions and wrapper conversions: "
+                    + String.join(
+                        ", ", dups.stream().map(DexType::toString).collect(Collectors.toSet())),
+                origin));
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanTopLevelFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanTopLevelFlags.java
new file mode 100644
index 0000000..80a13b5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanTopLevelFlags.java
@@ -0,0 +1,133 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+public class HumanTopLevelFlags {
+
+  private final AndroidApiLevel requiredCompilationAPILevel;
+  private final String synthesizedLibraryClassesPackagePrefix;
+  private final String identifier;
+  private final String jsonSource;
+  // Setting supportAllCallbacksFromLibrary reduces the number of generated call-backs,
+  // more specifically:
+  // - no call-back is generated for emulated interface method overrides (forEach, etc.)
+  // - no call-back is generated inside the desugared library itself.
+  // Such setting decreases significantly the desugared library dex file, but virtual calls from
+  // within the library to desugared library classes instances as receiver may be incorrect, for
+  // example the method forEach in Iterable may be executed over a concrete implementation.
+  private final boolean supportAllCallbacksFromLibrary;
+  private final List<String> extraKeepRules;
+
+  HumanTopLevelFlags(
+      AndroidApiLevel requiredCompilationAPILevel,
+      String synthesizedLibraryClassesPackagePrefix,
+      String identifier,
+      String jsonSource,
+      boolean supportAllCallbacksFromLibrary,
+      List<String> extraKeepRules) {
+    this.requiredCompilationAPILevel = requiredCompilationAPILevel;
+    this.synthesizedLibraryClassesPackagePrefix = synthesizedLibraryClassesPackagePrefix;
+    this.identifier = identifier;
+    this.jsonSource = jsonSource;
+    this.supportAllCallbacksFromLibrary = supportAllCallbacksFromLibrary;
+    this.extraKeepRules = extraKeepRules;
+  }
+
+  public static HumanTopLevelFlags empty() {
+    return new HumanTopLevelFlags(
+        AndroidApiLevel.B, "unused", null, null, true, ImmutableList.of());
+  }
+
+  public static HumanTopLevelFlags testing() {
+    return new HumanTopLevelFlags(
+        AndroidApiLevel.B, "unused", "testing", null, true, ImmutableList.of());
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public AndroidApiLevel getRequiredCompilationAPILevel() {
+    return requiredCompilationAPILevel;
+  }
+
+  public String getSynthesizedLibraryClassesPackagePrefix() {
+    return synthesizedLibraryClassesPackagePrefix;
+  }
+
+  public String getIdentifier() {
+    return identifier;
+  }
+
+  public String getJsonSource() {
+    return jsonSource;
+  }
+
+  public boolean supportAllCallbacksFromLibrary() {
+    return supportAllCallbacksFromLibrary;
+  }
+
+  public List<String> getExtraKeepRules() {
+    return extraKeepRules;
+  }
+
+  public static class Builder {
+
+    private AndroidApiLevel requiredCompilationAPILevel;
+    private String synthesizedLibraryClassesPackagePrefix;
+    private String identifier;
+    private String jsonSource;
+    private Boolean supportAllCallbacksFromLibrary;
+    private List<String> extraKeepRules;
+
+    Builder() {}
+
+    public Builder setRequiredCompilationAPILevel(AndroidApiLevel requiredCompilationAPILevel) {
+      this.requiredCompilationAPILevel = requiredCompilationAPILevel;
+      return this;
+    }
+
+    public Builder setSynthesizedLibraryClassesPackagePrefix(String prefix) {
+      this.synthesizedLibraryClassesPackagePrefix = prefix.replace('.', '/');
+      return this;
+    }
+
+    public Builder setDesugaredLibraryIdentifier(String identifier) {
+      this.identifier = identifier;
+      return this;
+    }
+
+    public Builder setJsonSource(String jsonSource) {
+      this.jsonSource = jsonSource;
+      return this;
+    }
+
+    public Builder setSupportAllCallbacksFromLibrary(boolean supportAllCallbacksFromLibrary) {
+      this.supportAllCallbacksFromLibrary = supportAllCallbacksFromLibrary;
+      return this;
+    }
+
+    public Builder setExtraKeepRules(List<String> rules) {
+      extraKeepRules = rules;
+      return this;
+    }
+
+    public HumanTopLevelFlags build() {
+      assert synthesizedLibraryClassesPackagePrefix != null;
+      assert supportAllCallbacksFromLibrary != null;
+      return new HumanTopLevelFlags(
+          requiredCompilationAPILevel,
+          synthesizedLibraryClassesPackagePrefix,
+          identifier,
+          jsonSource,
+          supportAllCallbacksFromLibrary,
+          extraKeepRules);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecification.java
new file mode 100644
index 0000000..69c82a9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecification.java
@@ -0,0 +1,64 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import com.android.tools.r8.origin.Origin;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import java.util.Map;
+
+public class MultiAPILevelHumanDesugaredLibrarySpecification {
+
+  private final Origin origin;
+  private final HumanTopLevelFlags topLevelFlags;
+  private final Int2ObjectMap<HumanRewritingFlags> commonFlags;
+  private final Int2ObjectMap<HumanRewritingFlags> libraryFlags;
+  private final Int2ObjectMap<HumanRewritingFlags> programFlags;
+
+  public MultiAPILevelHumanDesugaredLibrarySpecification(
+      Origin origin,
+      HumanTopLevelFlags topLevelFlags,
+      Int2ObjectMap<HumanRewritingFlags> commonFlags,
+      Int2ObjectMap<HumanRewritingFlags> libraryFlags,
+      Int2ObjectMap<HumanRewritingFlags> programFlags) {
+    this.origin = origin;
+    this.topLevelFlags = topLevelFlags;
+    this.commonFlags = commonFlags;
+    this.libraryFlags = libraryFlags;
+    this.programFlags = programFlags;
+  }
+
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  public HumanTopLevelFlags getTopLevelFlags() {
+    return topLevelFlags;
+  }
+
+  public Int2ObjectMap<HumanRewritingFlags> getCommonFlags() {
+    return commonFlags;
+  }
+
+  public Int2ObjectMap<HumanRewritingFlags> getLibraryFlags() {
+    return libraryFlags;
+  }
+
+  public Int2ObjectMap<HumanRewritingFlags> getProgramFlags() {
+    return programFlags;
+  }
+
+  public Map<Integer, HumanRewritingFlags> getCommonFlagsForTesting() {
+    return commonFlags;
+  }
+
+  public Map<Integer, HumanRewritingFlags> getLibraryFlagsForTesting() {
+    return libraryFlags;
+  }
+
+  public Map<Integer, HumanRewritingFlags> getProgramFlagsForTesting() {
+    return programFlags;
+  }
+
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
new file mode 100644
index 0000000..c79fad3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
@@ -0,0 +1,162 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.Reporter;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator {
+
+  public static void deduplicateFlags(
+      MultiAPILevelHumanDesugaredLibrarySpecification specification,
+      DexItemFactory factory,
+      Reporter reporter) {
+
+    IntArraySet apis = new IntArraySet();
+    apis.addAll(specification.getCommonFlags().keySet());
+    apis.addAll(specification.getLibraryFlags().keySet());
+    apis.addAll(specification.getProgramFlags().keySet());
+
+    for (Integer api : apis) {
+      deduplicateFlags(specification, factory, reporter, api);
+    }
+  }
+
+  private static void deduplicateFlags(
+      MultiAPILevelHumanDesugaredLibrarySpecification specification,
+      DexItemFactory factory,
+      Reporter reporter,
+      int api) {
+
+    Int2ObjectMap<HumanRewritingFlags> commonFlags = specification.getCommonFlags();
+    Int2ObjectMap<HumanRewritingFlags> libraryFlags = specification.getLibraryFlags();
+    Int2ObjectMap<HumanRewritingFlags> programFlags = specification.getProgramFlags();
+
+    HumanRewritingFlags library = libraryFlags.get(api);
+    HumanRewritingFlags program = programFlags.get(api);
+
+    if (library == null || program == null) {
+      return;
+    }
+
+    Origin origin = specification.getOrigin();
+    HumanRewritingFlags.Builder commonBuilder =
+        commonFlags.get(api) == null
+            ? HumanRewritingFlags.builder(factory, reporter, origin)
+            : commonFlags.get(api).newBuilder(factory, reporter, origin);
+    HumanRewritingFlags.Builder libraryBuilder =
+        HumanRewritingFlags.builder(factory, reporter, origin);
+    HumanRewritingFlags.Builder programBuilder =
+        HumanRewritingFlags.builder(factory, reporter, origin);
+
+    // Iterate over all library/program flags, add them in common if also in the other, else add
+    // them to library/program.
+    deduplicateFlags(library, program, commonBuilder, libraryBuilder);
+    deduplicateFlags(program, library, commonBuilder, programBuilder);
+
+    commonFlags.put(api, commonBuilder.build());
+    libraryFlags.put(api, libraryBuilder.build());
+    programFlags.put(api, programBuilder.build());
+  }
+
+  private static void deduplicateFlags(
+      HumanRewritingFlags flags,
+      HumanRewritingFlags otherFlags,
+      HumanRewritingFlags.Builder commonBuilder,
+      HumanRewritingFlags.Builder builder) {
+    deduplicateRewritePrefix(flags, otherFlags, commonBuilder, builder);
+
+    deduplicateFlags(
+        flags.getEmulateLibraryInterface(),
+        otherFlags.getEmulateLibraryInterface(),
+        commonBuilder::putEmulateLibraryInterface,
+        builder::putEmulateLibraryInterface);
+    deduplicateFlags(
+        flags.getRetargetCoreLibMember(),
+        otherFlags.getRetargetCoreLibMember(),
+        commonBuilder::putRetargetCoreLibMember,
+        builder::putRetargetCoreLibMember);
+    deduplicateFlags(
+        flags.getBackportCoreLibraryMember(),
+        otherFlags.getBackportCoreLibraryMember(),
+        commonBuilder::putBackportCoreLibraryMember,
+        builder::putBackportCoreLibraryMember);
+    deduplicateFlags(
+        flags.getCustomConversions(),
+        otherFlags.getCustomConversions(),
+        commonBuilder::putCustomConversion,
+        builder::putCustomConversion);
+
+    deduplicateFlags(
+        flags.getDontRewriteInvocation(),
+        otherFlags.getDontRewriteInvocation(),
+        commonBuilder::addDontRewriteInvocation,
+        builder::addDontRewriteInvocation);
+    deduplicateFlags(
+        flags.getDontRetargetLibMember(),
+        otherFlags.getDontRetargetLibMember(),
+        commonBuilder::addDontRetargetLibMember,
+        builder::addDontRetargetLibMember);
+    deduplicateFlags(
+        flags.getWrapperConversions(),
+        otherFlags.getWrapperConversions(),
+        commonBuilder::addWrapperConversion,
+        builder::addWrapperConversion);
+  }
+
+  private static void deduplicateRewritePrefix(
+      HumanRewritingFlags flags,
+      HumanRewritingFlags otherFlags,
+      HumanRewritingFlags.Builder commonBuilder,
+      HumanRewritingFlags.Builder builder) {
+    flags
+        .getRewritePrefix()
+        .forEach(
+            (k, v) -> {
+              if (otherFlags.getRewritePrefix().get(k) != null
+                  && otherFlags.getRewritePrefix().get(k).equals(v)) {
+                commonBuilder.putRewritePrefix(k, v);
+              } else {
+                builder.putRewritePrefix(k, v);
+              }
+            });
+  }
+
+  private static <T extends DexItem> void deduplicateFlags(
+      Map<T, DexType> flags,
+      Map<T, DexType> otherFlags,
+      BiConsumer<T, DexType> common,
+      BiConsumer<T, DexType> specific) {
+    flags.forEach(
+        (k, v) -> {
+          if (otherFlags.get(k) == v) {
+            common.accept(k, v);
+          } else {
+            specific.accept(k, v);
+          }
+        });
+  }
+
+  private static <T extends DexItem> void deduplicateFlags(
+      Set<T> flags, Set<T> otherFlags, Consumer<T> common, Consumer<T> specific) {
+    flags.forEach(
+        e -> {
+          if (otherFlags.contains(e)) {
+            common.accept(e);
+          } else {
+            specific.accept(e);
+          }
+        });
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
new file mode 100644
index 0000000..fd76244
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
@@ -0,0 +1,133 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import static com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanDesugaredLibrarySpecificationParser.*;
+import static com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanDesugaredLibrarySpecificationParser.IDENTIFIER_KEY;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.google.common.collect.Sets;
+import com.google.gson.Gson;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter {
+
+  public static void export(
+      MultiAPILevelHumanDesugaredLibrarySpecification specification, StringConsumer output) {
+    new MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter()
+        .internalExport(specification, output);
+  }
+
+  private void internalExport(
+      MultiAPILevelHumanDesugaredLibrarySpecification humanSpec, StringConsumer output) {
+    HashMap<String, Object> toJson = new LinkedHashMap<>();
+    toJson.put(IDENTIFIER_KEY, humanSpec.getTopLevelFlags().getIdentifier());
+    toJson.put(
+        REQUIRED_COMPILATION_API_LEVEL_KEY,
+        humanSpec.getTopLevelFlags().getRequiredCompilationAPILevel().getLevel());
+    toJson.put(
+        SYNTHESIZED_LIBRARY_CLASSES_PACKAGE_PREFIX_KEY,
+        humanSpec.getTopLevelFlags().getSynthesizedLibraryClassesPackagePrefix());
+    toJson.put(
+        SUPPORT_ALL_CALLBACKS_FROM_LIBRARY_KEY,
+        humanSpec.getTopLevelFlags().supportAllCallbacksFromLibrary());
+
+    toJson.put(COMMON_FLAGS_KEY, rewritingFlagsToString(humanSpec.getCommonFlags()));
+    toJson.put(PROGRAM_FLAGS_KEY, rewritingFlagsToString(humanSpec.getProgramFlags()));
+    toJson.put(LIBRARY_FLAGS_KEY, rewritingFlagsToString(humanSpec.getLibraryFlags()));
+
+    toJson.put(SHRINKER_CONFIG_KEY, humanSpec.getTopLevelFlags().getExtraKeepRules());
+
+    Gson gson = new Gson();
+    String export = gson.toJson(toJson);
+    output.accept(export, new DiagnosticsHandler() {});
+  }
+
+  private List<Object> rewritingFlagsToString(
+      Int2ObjectMap<HumanRewritingFlags> rewritingFlagsMap) {
+    ArrayList<Object> list = new ArrayList<>();
+    rewritingFlagsMap.forEach(
+        (apiBelowOrEqual, flags) -> {
+          HashMap<String, Object> toJson = new LinkedHashMap<>();
+          toJson.put(API_LEVEL_BELOW_OR_EQUAL_KEY, apiBelowOrEqual);
+          if (!flags.getRewritePrefix().isEmpty()) {
+            toJson.put(REWRITE_PREFIX_KEY, new TreeMap<>(flags.getRewritePrefix()));
+          }
+          if (!flags.getEmulateLibraryInterface().isEmpty()) {
+            toJson.put(EMULATE_INTERFACE_KEY, mapToString(flags.getEmulateLibraryInterface()));
+          }
+          if (!flags.getDontRewriteInvocation().isEmpty()) {
+            toJson.put(DONT_REWRITE_KEY, setToString(flags.getDontRewriteInvocation()));
+          }
+          if (!flags.getRetargetCoreLibMember().isEmpty()) {
+            toJson.put(RETARGET_LIB_MEMBER_KEY, mapToString(flags.getRetargetCoreLibMember()));
+          }
+          if (!flags.getDontRetargetLibMember().isEmpty()) {
+            toJson.put(DONT_RETARGET_LIB_MEMBER_KEY, setToString(flags.getDontRetargetLibMember()));
+          }
+          if (!flags.getBackportCoreLibraryMember().isEmpty()) {
+            toJson.put(BACKPORT_KEY, mapToString(flags.getBackportCoreLibraryMember()));
+          }
+          if (!flags.getWrapperConversions().isEmpty()) {
+            toJson.put(WRAPPER_CONVERSION_KEY, setToString(flags.getWrapperConversions()));
+          }
+          if (!flags.getCustomConversions().isEmpty()) {
+            toJson.put(CUSTOM_CONVERSION_KEY, mapToString(flags.getCustomConversions()));
+          }
+          list.add(toJson);
+        });
+    return list;
+  }
+
+  private Set<String> setToString(Set<? extends DexItem> set) {
+    Set<String> stringSet = Sets.newHashSet();
+    set.forEach(e -> stringSet.add(toString(e)));
+    return stringSet;
+  }
+
+  private Map<String, String> mapToString(Map<? extends DexItem, ? extends DexItem> map) {
+    Map<String, String> stringMap = new TreeMap<>();
+    map.forEach((k, v) -> stringMap.put(toString(k), toString(v)));
+    return stringMap;
+  }
+
+  private String toString(DexItem o) {
+    if (o instanceof DexType) {
+      return o.toString();
+    }
+    if (o instanceof DexMethod) {
+      DexMethod method = (DexMethod) o;
+      StringBuilder sb =
+          new StringBuilder()
+              .append(method.getReturnType())
+              .append(" ")
+              .append(method.getHolderType())
+              .append("#")
+              .append(method.getName())
+              .append("(");
+      for (int i = 0; i < method.getParameters().size(); i++) {
+        sb.append(method.getParameter(i));
+        if (i != method.getParameters().size() - 1) {
+          sb.append(", ");
+        }
+      }
+      sb.append(")");
+      return sb.toString();
+    }
+    throw new Unreachable();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationParser.java
new file mode 100644
index 0000000..140609d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationParser.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.humanspecification;
+
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.utils.Reporter;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+
+public class MultiAPILevelHumanDesugaredLibrarySpecificationParser
+    extends HumanDesugaredLibrarySpecificationParser {
+
+  public MultiAPILevelHumanDesugaredLibrarySpecificationParser(
+      DexItemFactory dexItemFactory, Reporter reporter) {
+    super(dexItemFactory, reporter, false, 1);
+  }
+
+  public MultiAPILevelHumanDesugaredLibrarySpecification parseMultiLevelConfiguration(
+      StringResource stringResource) {
+
+    String jsonConfigString = parseJson(stringResource);
+
+    HumanTopLevelFlags topLevelFlags = parseTopLevelFlags(jsonConfigString, builder -> {});
+
+    Int2ObjectMap<HumanRewritingFlags> commonFlags = parseAllFlags(COMMON_FLAGS_KEY);
+    Int2ObjectMap<HumanRewritingFlags> libraryFlags = parseAllFlags(LIBRARY_FLAGS_KEY);
+    Int2ObjectMap<HumanRewritingFlags> programFlags = parseAllFlags(PROGRAM_FLAGS_KEY);
+
+    return new MultiAPILevelHumanDesugaredLibrarySpecification(
+        getOrigin(), topLevelFlags, commonFlags, libraryFlags, programFlags);
+  }
+
+  private Int2ObjectMap<HumanRewritingFlags> parseAllFlags(String flagKey) {
+    JsonElement jsonFlags = required(getJsonConfig(), flagKey);
+    Int2ObjectMap<HumanRewritingFlags> flags = new Int2ObjectArrayMap<>();
+    for (JsonElement jsonFlagSet : jsonFlags.getAsJsonArray()) {
+      JsonObject flag = jsonFlagSet.getAsJsonObject();
+      int api_level_below_or_equal = required(flag, API_LEVEL_BELOW_OR_EQUAL_KEY).getAsInt();
+      HumanRewritingFlags.Builder builder =
+          flags.containsKey(api_level_below_or_equal)
+              ? flags
+                  .get(api_level_below_or_equal)
+                  .newBuilder(dexItemFactory(), reporter(), getOrigin())
+              : HumanRewritingFlags.builder(dexItemFactory(), reporter(), getOrigin());
+      parseFlags(flag, builder);
+      flags.put(api_level_below_or_equal, builder.build());
+    }
+    return flags;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecification.java
index 6198ce5..3da0b97 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecification.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecification.java
@@ -68,6 +68,14 @@
                 rewritingFlags.getRewritePrefix(), factory, libraryCompilation);
   }
 
+  public LegacyTopLevelFlags getTopLevelFlags() {
+    return topLevelFlags;
+  }
+
+  public LegacyRewritingFlags getRewritingFlags() {
+    return rewritingFlags;
+  }
+
   public boolean supportAllCallbacksFromLibrary() {
     return topLevelFlags.supportAllCallbacksFromLibrary();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecification.java
index 76a0b07..2226f88 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecification.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecification.java
@@ -4,23 +4,47 @@
 
 package com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification;
 
+import com.android.tools.r8.origin.Origin;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 
 public class MultiAPILevelLegacyDesugaredLibrarySpecification {
 
+  private final Origin origin;
   private final LegacyTopLevelFlags topLevelFlags;
   private final Int2ObjectMap<LegacyRewritingFlags> commonFlags;
   private final Int2ObjectMap<LegacyRewritingFlags> libraryFlags;
   private final Int2ObjectMap<LegacyRewritingFlags> programFlags;
 
   public MultiAPILevelLegacyDesugaredLibrarySpecification(
+      Origin origin,
       LegacyTopLevelFlags topLevelFlags,
       Int2ObjectMap<LegacyRewritingFlags> commonFlags,
       Int2ObjectMap<LegacyRewritingFlags> libraryFlags,
       Int2ObjectMap<LegacyRewritingFlags> programFlags) {
+    this.origin = origin;
     this.topLevelFlags = topLevelFlags;
     this.commonFlags = commonFlags;
     this.libraryFlags = libraryFlags;
     this.programFlags = programFlags;
   }
+
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  public LegacyTopLevelFlags getTopLevelFlags() {
+    return topLevelFlags;
+  }
+
+  public Int2ObjectMap<LegacyRewritingFlags> getCommonFlags() {
+    return commonFlags;
+  }
+
+  public Int2ObjectMap<LegacyRewritingFlags> getLibraryFlags() {
+    return libraryFlags;
+  }
+
+  public Int2ObjectMap<LegacyRewritingFlags> getProgramFlags() {
+    return programFlags;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecificationParser.java
index 708b300..447272e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecificationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/MultiAPILevelLegacyDesugaredLibrarySpecificationParser.java
@@ -32,7 +32,7 @@
     Int2ObjectMap<LegacyRewritingFlags> programFlags = parseAllFlags(PROGRAM_FLAGS_KEY);
 
     return new MultiAPILevelLegacyDesugaredLibrarySpecification(
-        topLevelFlags, commonFlags, libraryFlags, programFlags);
+        getOrigin(), topLevelFlags, commonFlags, libraryFlags, programFlags);
   }
 
   private Int2ObjectMap<LegacyRewritingFlags> parseAllFlags(String flagKey) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LegacyToHumanSpecificationConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LegacyToHumanSpecificationConverter.java
new file mode 100644
index 0000000..14898fc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LegacyToHumanSpecificationConverter.java
@@ -0,0 +1,226 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.specificationconversion;
+
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanRewritingFlags;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanTopLevelFlags;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.MultiAPILevelHumanDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.LegacyDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.LegacyRewritingFlags;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.LegacyTopLevelFlags;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.MultiAPILevelLegacyDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.MultiAPILevelLegacyDesugaredLibrarySpecificationParser;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+public class LegacyToHumanSpecificationConverter implements SpecificationConverter {
+
+  @Override
+  public void convertAllAPILevels(
+      StringResource inputSpecification, Path androidLib, StringConsumer output)
+      throws IOException {
+    InternalOptions options = new InternalOptions();
+    MultiAPILevelLegacyDesugaredLibrarySpecification legacySpec =
+        new MultiAPILevelLegacyDesugaredLibrarySpecificationParser(
+                options.dexItemFactory(), options.reporter)
+            .parseMultiLevelConfiguration(inputSpecification);
+    MultiAPILevelHumanDesugaredLibrarySpecification humanSpec =
+        convertAllAPILevels(legacySpec, androidLib, options);
+    MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.deduplicateFlags(
+        humanSpec, options.dexItemFactory(), options.reporter);
+    MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.export(humanSpec, output);
+  }
+
+  public MultiAPILevelHumanDesugaredLibrarySpecification convertAllAPILevels(
+      MultiAPILevelLegacyDesugaredLibrarySpecification legacySpec,
+      Path androidLib,
+      InternalOptions options)
+      throws IOException {
+    Origin origin = legacySpec.getOrigin();
+    DexApplication app = readApp(androidLib, options);
+    HumanTopLevelFlags humanTopLevelFlags = convertTopLevelFlags(legacySpec.getTopLevelFlags());
+    Int2ObjectArrayMap<HumanRewritingFlags> commonFlags =
+        convertRewritingFlagMap(legacySpec.getCommonFlags(), app, origin);
+    Int2ObjectArrayMap<HumanRewritingFlags> programFlags =
+        convertRewritingFlagMap(legacySpec.getProgramFlags(), app, origin);
+    Int2ObjectArrayMap<HumanRewritingFlags> libraryFlags =
+        convertRewritingFlagMap(legacySpec.getLibraryFlags(), app, origin);
+
+    legacyLibraryFlagHacks(libraryFlags, app, origin);
+
+    return new MultiAPILevelHumanDesugaredLibrarySpecification(
+        origin, humanTopLevelFlags, commonFlags, libraryFlags, programFlags);
+  }
+
+  public HumanDesugaredLibrarySpecification convert(
+      LegacyDesugaredLibrarySpecification legacySpec, Path androidLib, InternalOptions options)
+      throws IOException {
+    DexApplication app = readApp(androidLib, options);
+    HumanTopLevelFlags humanTopLevelFlags = convertTopLevelFlags(legacySpec.getTopLevelFlags());
+
+    // The origin is not maintained in non multi-level specifications.
+    // It should not matter since the origin is used to report invalid specifications, and
+    // converting non multi-level specifications should be performed only with *valid*
+    // specifications in practical cases.
+    Origin origin = Origin.unknown();
+
+    HumanRewritingFlags humanRewritingFlags =
+        convertRewritingFlags(legacySpec.getRewritingFlags(), app, origin);
+    return new HumanDesugaredLibrarySpecification(
+        humanTopLevelFlags,
+        humanRewritingFlags,
+        legacySpec.isLibraryCompilation(),
+        app.dexItemFactory());
+  }
+
+  private void legacyLibraryFlagHacks(
+      Int2ObjectArrayMap<HumanRewritingFlags> libraryFlags, DexApplication app, Origin origin) {
+    int level = AndroidApiLevel.N_MR1.getLevel();
+    HumanRewritingFlags humanRewritingFlags = libraryFlags.get(level);
+    HumanRewritingFlags.Builder builder =
+        humanRewritingFlags.newBuilder(app.dexItemFactory(), app.options.reporter, origin);
+    DexItemFactory itemFactory = app.dexItemFactory();
+
+    // TODO(b/177977763): This is only a workaround rewriting invokes of j.u.Arrays.deepEquals0
+    // to j.u.DesugarArrays.deepEquals0.
+    DexString name = itemFactory.createString("deepEquals0");
+    DexProto proto =
+        itemFactory.createProto(
+            itemFactory.booleanType, itemFactory.objectType, itemFactory.objectType);
+    DexMethod source =
+        itemFactory.createMethod(itemFactory.createType(itemFactory.arraysDescriptor), proto, name);
+    DexType target = itemFactory.createType("Ljava/util/DesugarArrays;");
+    builder.putRetargetCoreLibMember(source, target);
+
+    // TODO(b/181629049): This is only a workaround rewriting invokes of
+    //  j.u.TimeZone.getTimeZone taking a java.time.ZoneId.
+    name = itemFactory.createString("getTimeZone");
+    proto =
+        itemFactory.createProto(
+            itemFactory.createType("Ljava/util/TimeZone;"),
+            itemFactory.createType("Ljava/time/ZoneId;"));
+    source = itemFactory.createMethod(itemFactory.createType("Ljava/util/TimeZone;"), proto, name);
+    target = itemFactory.createType("Ljava/util/DesugarTimeZone;");
+    builder.putRetargetCoreLibMember(source, target);
+
+    libraryFlags.put(level, builder.build());
+  }
+
+  private DirectMappedDexApplication readApp(Path androidLib, InternalOptions options)
+      throws IOException {
+    AndroidApp androidApp = AndroidApp.builder().addLibraryFile(androidLib).build();
+    ApplicationReader applicationReader =
+        new ApplicationReader(androidApp, options, Timing.empty());
+    ExecutorService executorService = ThreadUtils.getExecutorService(options);
+    return applicationReader.read(executorService).toDirect();
+  }
+
+  private Int2ObjectArrayMap<HumanRewritingFlags> convertRewritingFlagMap(
+      Int2ObjectMap<LegacyRewritingFlags> libFlags, DexApplication app, Origin origin) {
+    Int2ObjectArrayMap<HumanRewritingFlags> map = new Int2ObjectArrayMap<>();
+    libFlags.forEach((key, flags) -> map.put((int) key, convertRewritingFlags(flags, app, origin)));
+    return map;
+  }
+
+  private HumanRewritingFlags convertRewritingFlags(
+      LegacyRewritingFlags flags, DexApplication app, Origin origin) {
+    HumanRewritingFlags.Builder builder =
+        HumanRewritingFlags.builder(app.dexItemFactory(), app.options.reporter, origin);
+
+    flags.getRewritePrefix().forEach(builder::putRewritePrefix);
+    flags.getEmulateLibraryInterface().forEach(builder::putEmulateLibraryInterface);
+    flags.getBackportCoreLibraryMember().forEach(builder::putBackportCoreLibraryMember);
+    flags.getCustomConversions().forEach(builder::putCustomConversion);
+    flags.getDontRetargetLibMember().forEach(builder::addDontRetargetLibMember);
+    flags.getWrapperConversions().forEach(builder::addWrapperConversion);
+
+    flags
+        .getRetargetCoreLibMember()
+        .forEach((name, typeMap) -> convertRetargetCoreLibMember(builder, app, name, typeMap));
+    flags
+        .getDontRewriteInvocation()
+        .forEach(pair -> convertDontRewriteInvocation(builder, app, pair));
+
+    return builder.build();
+  }
+
+  private void convertDontRewriteInvocation(
+      HumanRewritingFlags.Builder builder, DexApplication app, Pair<DexType, DexString> pair) {
+    DexClass dexClass = app.definitionFor(pair.getFirst());
+    assert dexClass != null;
+    List<DexClassAndMethod> methodsWithName = findMethodsWithName(pair.getSecond(), dexClass);
+    for (DexClassAndMethod dexClassAndMethod : methodsWithName) {
+      builder.addDontRewriteInvocation(dexClassAndMethod.getReference());
+    }
+  }
+
+  private void convertRetargetCoreLibMember(
+      HumanRewritingFlags.Builder builder,
+      DexApplication app,
+      DexString name,
+      Map<DexType, DexType> typeMap) {
+    typeMap.forEach(
+        (type, rewrittenType) -> {
+          DexClass dexClass = app.definitionFor(type);
+          assert dexClass != null;
+          List<DexClassAndMethod> methodsWithName = findMethodsWithName(name, dexClass);
+          for (DexClassAndMethod dexClassAndMethod : methodsWithName) {
+            builder.putRetargetCoreLibMember(dexClassAndMethod.getReference(), rewrittenType);
+          }
+        });
+  }
+
+  private List<DexClassAndMethod> findMethodsWithName(DexString methodName, DexClass clazz) {
+    List<DexClassAndMethod> found = new ArrayList<>();
+    clazz.forEachClassMethodMatching(definition -> definition.getName() == methodName, found::add);
+    assert !found.isEmpty()
+        : "Should have found a method (library specifications) for "
+            + clazz.toSourceString()
+            + "."
+            + methodName
+            + ". Maybe the library used for the compilation should be newer.";
+    return found;
+  }
+
+  private HumanTopLevelFlags convertTopLevelFlags(LegacyTopLevelFlags topLevelFlags) {
+    return HumanTopLevelFlags.builder()
+        .setDesugaredLibraryIdentifier(topLevelFlags.getIdentifier())
+        .setExtraKeepRules(topLevelFlags.getExtraKeepRules())
+        .setJsonSource(topLevelFlags.getJsonSource())
+        .setRequiredCompilationAPILevel(topLevelFlags.getRequiredCompilationAPILevel())
+        .setSupportAllCallbacksFromLibrary(topLevelFlags.supportAllCallbacksFromLibrary())
+        .setSynthesizedLibraryClassesPackagePrefix(
+            topLevelFlags.getSynthesizedLibraryClassesPackagePrefix())
+        .build();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/SpecificationConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/SpecificationConverter.java
new file mode 100644
index 0000000..85a8c8b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/SpecificationConverter.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.specificationconversion;
+
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.StringResource;
+import java.io.IOException;
+import java.nio.file.Path;
+
+public interface SpecificationConverter {
+
+  void convertAllAPILevels(
+      StringResource inputSpecification, Path androidLib, StringConsumer output) throws IOException;
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
index a4800c3..616cc07 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
@@ -481,9 +481,7 @@
                     ImmutableList.of(
                         new CfInitClass(iface.getType()),
                         new CfStackInstruction(Opcode.Pop),
-                        new CfReturnVoid()),
-                    ImmutableList.of(),
-                    ImmutableList.of());
+                        new CfReturnVoid()));
               }
               DexEncodedField clinitField =
                   ensureStaticClinitFieldToTriggerInterfaceInitialization(iface);
@@ -497,9 +495,7 @@
                       isWide
                           ? new CfStackInstruction(Opcode.Pop2)
                           : new CfStackInstruction(Opcode.Pop),
-                      new CfReturnVoid()),
-                  ImmutableList.of(),
-                  ImmutableList.of());
+                      new CfReturnVoid()));
             });
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index d612c5f..35e919e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -39,15 +39,14 @@
 import com.android.tools.r8.ir.optimize.Inliner.InlineeWithReason;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.optimize.Inliner.RetryAction;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.InliningReasonStrategy;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
-import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexInfo;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
-import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.BitSet;
@@ -57,8 +56,8 @@
 public final class DefaultInliningOracle implements InliningOracle, InliningStrategy {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final Inliner inliner;
   private final InlinerOptions inlinerOptions;
+  private final MainDexInfo mainDexInfo;
   private final ProgramMethod method;
   private final MethodProcessor methodProcessor;
   private final InliningReasonStrategy reasonStrategy;
@@ -66,21 +65,25 @@
 
   DefaultInliningOracle(
       AppView<AppInfoWithLiveness> appView,
-      Inliner inliner,
       InliningReasonStrategy inliningReasonStrategy,
       ProgramMethod method,
       MethodProcessor methodProcessor,
       int inliningInstructionAllowance) {
     this.appView = appView;
-    this.inliner = inliner;
     this.inlinerOptions = appView.options().inlinerOptions();
     this.reasonStrategy = inliningReasonStrategy;
+    this.mainDexInfo = appView.appInfo().getMainDexInfo();
     this.method = method;
     this.methodProcessor = methodProcessor;
     this.instructionAllowance = inliningInstructionAllowance;
   }
 
   @Override
+  public AppView<AppInfoWithLiveness> appView() {
+    return appView;
+  }
+
+  @Override
   public boolean isForcedInliningOracle() {
     return false;
   }
@@ -190,19 +193,18 @@
     // Don't inline code with references beyond root main dex classes into a root main dex class.
     // If we do this it can increase the size of the main dex dependent classes.
     if (reason != Reason.FORCE
-        && inliner.mainDexInfo.disallowInliningIntoContext(
+        && mainDexInfo.disallowInliningIntoContext(
             appView, method, singleTarget, appView.getSyntheticItems())) {
       whyAreYouNotInliningReporter.reportInlineeRefersToClassesNotInMainDex();
       return false;
     }
     assert reason != Reason.FORCE
-        || !inliner.mainDexInfo.disallowInliningIntoContext(
+        || !mainDexInfo.disallowInliningIntoContext(
             appView, method, singleTarget, appView.getSyntheticItems());
     return true;
   }
 
-  private boolean satisfiesRequirementsForSimpleInlining(
-      InvokeMethod invoke, ProgramMethod target) {
+  public boolean satisfiesRequirementsForSimpleInlining(InvokeMethod invoke, ProgramMethod target) {
     // If we are looking for a simple method, only inline if actually simple.
     Code code = target.getDefinition().getCode();
     int instructionLimit =
@@ -252,17 +254,19 @@
 
   @Override
   public InlineResult computeInlining(
+      IRCode code,
       InvokeMethod invoke,
       SingleResolutionResult resolutionResult,
       ProgramMethod singleTarget,
       ProgramMethod context,
       ClassInitializationAnalysis classInitializationAnalysis,
+      InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     if (isSingleTargetInvalid(invoke, singleTarget, whyAreYouNotInliningReporter)) {
       return null;
     }
 
-    if (inliner.neverInline(invoke, resolutionResult, singleTarget, whyAreYouNotInliningReporter)) {
+    if (neverInline(invoke, resolutionResult, singleTarget, whyAreYouNotInliningReporter)) {
       if (singleTarget.getDefinition().getOptimizationInfo().forceInline()) {
         throw new Unreachable(
             "Unexpected attempt to force inline method `"
@@ -275,7 +279,7 @@
     }
 
     Reason reason =
-        reasonStrategy.computeInliningReason(invoke, singleTarget, context, methodProcessor);
+        reasonStrategy.computeInliningReason(invoke, singleTarget, context, this, methodProcessor);
     if (reason == Reason.NEVER) {
       return null;
     }
@@ -300,8 +304,60 @@
       return null;
     }
 
-    return invoke.computeInlining(
-        singleTarget, reason, this, classInitializationAnalysis, whyAreYouNotInliningReporter);
+    InlineAction action =
+        invoke.computeInlining(
+            singleTarget, reason, this, classInitializationAnalysis, whyAreYouNotInliningReporter);
+    if (action == null) {
+      return null;
+    }
+
+    if (!setDowncastTypeIfNeeded(appView, action, invoke, singleTarget, context)) {
+      return null;
+    }
+
+    // Make sure constructor inlining is legal.
+    if (singleTarget.getDefinition().isInstanceInitializer()
+        && !canInlineInstanceInitializer(
+            code,
+            invoke.asInvokeDirect(),
+            singleTarget,
+            inliningIRProvider,
+            whyAreYouNotInliningReporter)) {
+      return null;
+    }
+
+    return action;
+  }
+
+  private boolean neverInline(
+      InvokeMethod invoke,
+      SingleResolutionResult resolutionResult,
+      ProgramMethod singleTarget,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (!appView.getKeepInfo(singleTarget).isInliningAllowed(appView.options())) {
+      whyAreYouNotInliningReporter.reportPinned();
+      return true;
+    }
+
+    if (appInfo.isNeverInlineMethod(singleTargetReference)) {
+      whyAreYouNotInliningReporter.reportMarkedAsNeverInline();
+      return true;
+    }
+
+    if (appInfo.noSideEffects.containsKey(invoke.getInvokedMethod())
+        || appInfo.noSideEffects.containsKey(resolutionResult.getResolvedMethod().getReference())
+        || appInfo.noSideEffects.containsKey(singleTargetReference)) {
+      return !singleTarget.getDefinition().getOptimizationInfo().forceInline();
+    }
+
+    if (!appView.testing().allowInliningOfSynthetics
+        && appView.getSyntheticItems().isSyntheticClass(singleTarget.getHolder())) {
+      return true;
+    }
+
+    return false;
   }
 
   public InlineAction computeForInvokeWithReceiver(
@@ -407,17 +463,6 @@
   }
 
   @Override
-  public void ensureMethodProcessed(
-      ProgramMethod target, IRCode inlinee, OptimizationFeedback feedback) {
-    if (!target.getDefinition().isProcessed()) {
-      if (Log.ENABLED) {
-        Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
-      }
-      inliner.performInlining(target, inlinee, feedback, methodProcessor, Timing.empty());
-    }
-  }
-
-  @Override
   public boolean allowInliningOfInvokeInInlinee(
       InlineAction action,
       int inliningDepth,
@@ -440,9 +485,14 @@
   @Override
   public boolean canInlineInstanceInitializer(
       IRCode code,
-      IRCode inlinee,
       InvokeDirect invoke,
+      ProgramMethod singleTarget,
+      InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
+    boolean removeInnerFramesIfThrowingNpe = false;
+    IRCode inlinee =
+        inliningIRProvider.getInliningIR(invoke, singleTarget, removeInnerFramesIfThrowingNpe);
+
     // In the Java VM Specification section "4.10.2.4. Instance Initialization Methods and
     // Newly Created Objects" it says:
     //
@@ -453,13 +503,14 @@
     // Allow inlining a constructor into a constructor of the same class, as the constructor code
     // is expected to adhere to the VM specification.
     DexType callerMethodHolder = method.getHolderType();
-    DexType calleeMethodHolder = inlinee.method().getHolderType();
+    DexType calleeMethodHolder = singleTarget.getHolderType();
 
     // Forwarding constructor calls that target a constructor in the same class can always be
     // inlined.
     if (method.getDefinition().isInstanceInitializer()
         && callerMethodHolder == calleeMethodHolder
         && invoke.getReceiver() == code.getThis()) {
+      inliningIRProvider.cacheInliningIR(invoke, inlinee);
       return true;
     }
 
@@ -548,6 +599,7 @@
     }
 
     inlinee.returnMarkingColor(markingColor);
+    inliningIRProvider.cacheInliningIR(invoke, inlinee);
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index 0e26fa3..493b052 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -17,7 +17,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
 import com.android.tools.r8.ir.optimize.Inliner.InlineeWithReason;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Map;
@@ -38,6 +38,11 @@
   }
 
   @Override
+  public AppView<AppInfoWithLiveness> appView() {
+    return appView;
+  }
+
+  @Override
   public boolean isForcedInliningOracle() {
     return true;
   }
@@ -63,13 +68,22 @@
 
   @Override
   public InlineResult computeInlining(
+      IRCode code,
       InvokeMethod invoke,
       SingleResolutionResult resolutionResult,
       ProgramMethod singleTarget,
       ProgramMethod context,
       ClassInitializationAnalysis classInitializationAnalysis,
+      InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    return computeForInvoke(invoke, resolutionResult, whyAreYouNotInliningReporter);
+    InlineAction action = computeForInvoke(invoke, resolutionResult, whyAreYouNotInliningReporter);
+    if (action == null) {
+      return null;
+    }
+    if (!setDowncastTypeIfNeeded(appView, action, invoke, singleTarget, context)) {
+      return null;
+    }
+    return action;
   }
 
   private InlineAction computeForInvoke(
@@ -87,13 +101,6 @@
   }
 
   @Override
-  public void ensureMethodProcessed(
-      ProgramMethod target, IRCode inlinee, OptimizationFeedback feedback) {
-    // Do nothing. If the method is not yet processed, we still should
-    // be able to build IR for inlining, though.
-  }
-
-  @Override
   public boolean allowInliningOfInvokeInInlinee(
       InlineAction action,
       int inliningDepth,
@@ -105,8 +112,9 @@
   @Override
   public boolean canInlineInstanceInitializer(
       IRCode code,
-      IRCode inlinee,
       InvokeDirect invoke,
+      ProgramMethod singleTarget,
+      InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index b4a067e..2c185bf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -8,7 +8,6 @@
 import static com.google.common.base.Predicates.not;
 
 import com.android.tools.r8.androidapi.AvailableApiExceptions;
-import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
@@ -61,6 +60,7 @@
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
 import com.android.tools.r8.utils.IteratorUtils;
@@ -79,6 +79,9 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 
 public class Inliner {
 
@@ -91,6 +94,8 @@
   // due to not being processed at the time of inlining.
   private final LongLivedProgramMethodSetBuilder<ProgramMethodSet> singleInlineCallers;
 
+  private final MultiCallerInliner multiCallerInliner;
+
   // The set of methods that have been single caller inlined in the current wave. These need to be
   // pruned when the wave ends.
   private final Map<DexProgramClass, ProgramMethodSet> singleCallerInlinedMethodsInWave =
@@ -108,6 +113,7 @@
     this.converter = converter;
     this.lensCodeRewriter = lensCodeRewriter;
     this.mainDexInfo = appView.appInfo().getMainDexInfo();
+    this.multiCallerInliner = new MultiCallerInliner(appView);
     this.singleInlineCallers =
         LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet(appView.graphLens());
     availableApiExceptions =
@@ -116,41 +122,6 @@
             : null;
   }
 
-  boolean neverInline(
-      InvokeMethod invoke,
-      SingleResolutionResult resolutionResult,
-      ProgramMethod singleTarget,
-      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    AppInfoWithLiveness appInfo = appView.appInfo();
-    DexMethod singleTargetReference = singleTarget.getReference();
-    if (!appView.getKeepInfo(singleTarget).isInliningAllowed(appView.options())) {
-      whyAreYouNotInliningReporter.reportPinned();
-      return true;
-    }
-
-    if (appInfo.isNeverInlineMethod(singleTargetReference)) {
-      whyAreYouNotInliningReporter.reportMarkedAsNeverInline();
-      return true;
-    }
-
-    if (appInfo.noSideEffects.containsKey(invoke.getInvokedMethod())
-        || appInfo.noSideEffects.containsKey(resolutionResult.getResolvedMethod().getReference())
-        || appInfo.noSideEffects.containsKey(singleTargetReference)) {
-      return !singleTarget.getDefinition().getOptimizationInfo().forceInline();
-    }
-
-    if (!appView.testing().allowInliningOfSynthetics
-        && appView.getSyntheticItems().isSyntheticClass(singleTarget.getHolder())) {
-      return true;
-    }
-
-    return false;
-  }
-
-  boolean isDoubleInliningEnabled(MethodProcessor methodProcessor) {
-    return methodProcessor.isPostMethodProcessor();
-  }
-
   private ConstraintWithTarget instructionAllowedForInlining(
       Instruction instruction, InliningConstraints inliningConstraints, ProgramMethod context) {
     ConstraintWithTarget result = instruction.inliningConstraint(inliningConstraints, context);
@@ -203,16 +174,9 @@
     return false;
   }
 
-  public void enqueueMethodsForReprocessing(
-      PostMethodProcessor.Builder postMethodProcessorBuilder) {
-    // The double inline callers are always rewritten up until the graph lens of the primary
-    // optimization pass, so we can safely merge them into the methods to reprocess (which may be
-    // rewritten with a newer graph lens).
-    postMethodProcessorBuilder
-        .getMethodsToReprocessBuilder()
-        .rewrittenWithLens(appView)
-        .merge(singleInlineCallers);
-    singleInlineCallers.clear();
+  public void recordCallEdgesForMultiCallerInlining(
+      ProgramMethod method, IRCode code, MethodProcessor methodProcessor, Timing timing) {
+    multiCallerInliner.recordCallEdgesForMultiCallerInlining(method, code, methodProcessor, timing);
   }
 
   /**
@@ -521,12 +485,14 @@
     FORCE,         // Inlinee is marked for forced inlining (bridge method or renamed constructor).
     ALWAYS,        // Inlinee is marked for inlining due to alwaysinline directive.
     SINGLE_CALLER, // Inlinee has precisely one caller.
-    DUAL_CALLER,   // Inlinee has precisely two callers.
+    // Inlinee has multiple callers and should not be inlined. Only used during the primary
+    // optimization pass.
+    MULTI_CALLER_CANDIDATE,
     SIMPLE,        // Inlinee has simple code suitable for inlining.
     NEVER;         // Inlinee must not be inlined.
 
     public boolean mustBeInlined() {
-      // TODO(118734615): Include SINGLE_CALLER and DUAL_CALLER here as well?
+      // TODO(118734615): Include SINGLE_CALLER here as well?
       return this == FORCE || this == ALWAYS;
     }
   }
@@ -551,6 +517,8 @@
     private boolean shouldSynthesizeInitClass;
     private boolean shouldSynthesizeNullCheckForReceiver;
 
+    private DexProgramClass downcastClass;
+
     InlineAction(ProgramMethod target, Invoke invoke, Reason reason) {
       this.target = target;
       this.invoke = invoke;
@@ -562,6 +530,14 @@
       return this;
     }
 
+    DexProgramClass getDowncastClass() {
+      return downcastClass;
+    }
+
+    void setDowncastClass(DexProgramClass downcastClass) {
+      this.downcastClass = downcastClass;
+    }
+
     void setShouldSynthesizeInitClass() {
       assert !shouldSynthesizeNullCheckForReceiver;
       shouldSynthesizeInitClass = true;
@@ -850,6 +826,7 @@
       IRCode code,
       Map<? extends InvokeMethod, InliningInfo> invokesToInline,
       InliningIRProvider inliningIRProvider,
+      MethodProcessor methodProcessor,
       Timing timing) {
     ForcedInliningOracle oracle = new ForcedInliningOracle(appView, method, invokesToInline);
     performInliningImpl(
@@ -859,6 +836,7 @@
         code,
         OptimizationFeedbackIgnore.getInstance(),
         inliningIRProvider,
+        methodProcessor,
         timing);
   }
 
@@ -894,7 +872,8 @@
     InliningIRProvider inliningIRProvider =
         new InliningIRProvider(appView, method, code, methodProcessor);
     assert inliningIRProvider.verifyIRCacheIsEmpty();
-    performInliningImpl(oracle, oracle, method, code, feedback, inliningIRProvider, timing);
+    performInliningImpl(
+        oracle, oracle, method, code, feedback, inliningIRProvider, methodProcessor, timing);
   }
 
   public InliningReasonStrategy createDefaultInliningReasonStrategy(
@@ -924,7 +903,6 @@
       InliningReasonStrategy inliningReasonStrategy) {
     return new DefaultInliningOracle(
         appView,
-        this,
         inliningReasonStrategy,
         method,
         methodProcessor,
@@ -938,6 +916,7 @@
       IRCode code,
       OptimizationFeedback feedback,
       InliningIRProvider inliningIRProvider,
+      MethodProcessor methodProcessor,
       Timing timing) {
     AssumeRemover assumeRemover = new AssumeRemover(appView, code);
     Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
@@ -990,11 +969,13 @@
                   : WhyAreYouNotInliningReporter.createFor(singleTarget, appView, context);
           InlineResult inlineResult =
               oracle.computeInlining(
+                  code,
                   invoke,
                   resolutionResult,
                   singleTarget,
                   context,
                   classInitializationAnalysis,
+                  inliningIRProvider,
                   whyAreYouNotInliningReporter);
           if (inlineResult == null) {
             assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
@@ -1007,11 +988,8 @@
           }
 
           InlineAction action = inlineResult.asInlineAction();
-
-          DexProgramClass downcastClass = getDowncastTypeIfNeeded(strategy, invoke, singleTarget);
-          if (downcastClass != null
-              && AccessControl.isClassAccessible(downcastClass, context, appView)
-                  .isPossiblyFalse()) {
+          if (action.reason == Reason.MULTI_CALLER_CANDIDATE) {
+            assert methodProcessor.isPrimaryMethodProcessor();
             continue;
           }
 
@@ -1036,18 +1014,8 @@
             continue;
           }
 
-          // If this code did not go through the full pipeline, apply inlining to make sure
-          // that force inline targets get processed.
-          strategy.ensureMethodProcessed(singleTarget, inlinee.code, feedback);
-
-          // Make sure constructor inlining is legal.
-          assert !singleTargetMethod.isClassInitializer();
-          if (singleTargetMethod.isInstanceInitializer()
-              && !strategy.canInlineInstanceInitializer(
-                  code, inlinee.code, invoke.asInvokeDirect(), whyAreYouNotInliningReporter)) {
-            assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
-            continue;
-          }
+          // Verify this code went through the full pipeline.
+          assert singleTarget.getDefinition().isProcessed();
 
           // Mark AssumeDynamicType instruction for the out-value for removal, if any.
           Value outValue = invoke.outValue();
@@ -1062,7 +1030,12 @@
           iterator.previous();
           strategy.markInlined(inlinee);
           iterator.inlineInvoke(
-              appView, code, inlinee.code, blockIterator, blocksToRemove, downcastClass);
+              appView,
+              code,
+              inlinee.code,
+              blockIterator,
+              blocksToRemove,
+              action.getDowncastClass());
 
           if (inlinee.reason == Reason.SINGLE_CALLER) {
             assert converter.isInWave();
@@ -1077,7 +1050,7 @@
           }
 
           classInitializationAnalysis.notifyCodeHasChanged();
-          postProcessInlineeBlocks(code, inlinee.code, blockIterator, block, timing);
+          postProcessInlineeBlocks(code, blockIterator, block, blocksToRemove, timing);
 
           // The synthetic and bridge flags are maintained only if the inlinee has also these flags.
           if (context.getDefinition().isBridge() && !inlinee.code.method().isBridge()) {
@@ -1180,33 +1153,38 @@
   /** Applies member rebinding to the inlinee and inserts assume instructions. */
   private void postProcessInlineeBlocks(
       IRCode code,
-      IRCode inlinee,
       BasicBlockIterator blockIterator,
       BasicBlock block,
+      Set<BasicBlock> blocksToRemove,
       Timing timing) {
     BasicBlock state = IteratorUtils.peekNext(blockIterator);
 
-    Set<BasicBlock> inlineeBlocks = SetUtils.newIdentityHashSet(inlinee.blocks);
+    Set<BasicBlock> inlineeBlocks = Sets.newIdentityHashSet();
 
     // Run member value propagation on the inlinee blocks.
-    rewindBlockIteratorToFirstInlineeBlock(blockIterator, block);
-    applyMemberValuePropagationToInlinee(code, blockIterator, block, inlineeBlocks);
+    rewindBlockIterator(
+        blockIterator,
+        block,
+        inlineeBlock -> {
+          if (!blocksToRemove.contains(inlineeBlock)) {
+            inlineeBlocks.add(inlineeBlock);
+          }
+        });
+    applyMemberValuePropagationToInlinee(code, blockIterator, inlineeBlocks);
 
     // Add non-null IRs only to the inlinee blocks.
-    insertAssumeInstructions(code, blockIterator, block, inlineeBlocks, timing);
+    rewindBlockIterator(blockIterator, block);
+    insertAssumeInstructions(code, blockIterator, inlineeBlocks, timing);
 
     // Restore the old state of the iterator.
-    rewindBlockIteratorToFirstInlineeBlock(blockIterator, state);
-    // TODO(b/72693244): need a test where refined env in inlinee affects the caller.
+    rewindBlockIterator(blockIterator, state);
   }
 
   private void insertAssumeInstructions(
       IRCode code,
       BasicBlockIterator blockIterator,
-      BasicBlock block,
       Set<BasicBlock> inlineeBlocks,
       Timing timing) {
-    rewindBlockIteratorToFirstInlineeBlock(blockIterator, block);
     new AssumeInserter(appView)
         .insertAssumeInstructionsInBlocks(code, blockIterator, inlineeBlocks::contains, timing);
     assert !blockIterator.hasNext();
@@ -1215,9 +1193,7 @@
   private void applyMemberValuePropagationToInlinee(
       IRCode code,
       ListIterator<BasicBlock> blockIterator,
-      BasicBlock block,
       Set<BasicBlock> inlineeBlocks) {
-    assert IteratorUtils.peekNext(blockIterator) == block;
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     new MemberValuePropagation(appView)
         .run(code, blockIterator, affectedValues, inlineeBlocks::contains);
@@ -1227,13 +1203,23 @@
     assert !blockIterator.hasNext();
   }
 
-  private void rewindBlockIteratorToFirstInlineeBlock(
-      ListIterator<BasicBlock> blockIterator, BasicBlock firstInlineeBlock) {
+  private void rewindBlockIterator(ListIterator<BasicBlock> blockIterator, BasicBlock callerBlock) {
+    rewindBlockIterator(blockIterator, callerBlock, ConsumerUtils.emptyConsumer());
+  }
+
+  private void rewindBlockIterator(
+      ListIterator<BasicBlock> blockIterator,
+      BasicBlock callerBlock,
+      Consumer<BasicBlock> consumer) {
     // Move the cursor back to where the first inlinee block was added.
-    while (blockIterator.hasPrevious() && blockIterator.previous() != firstInlineeBlock) {
-      // Do nothing.
+    while (blockIterator.hasPrevious()) {
+      BasicBlock previous = blockIterator.previous();
+      if (previous == callerBlock) {
+        break;
+      }
+      consumer.accept(previous);
     }
-    assert IteratorUtils.peekNext(blockIterator) == firstInlineeBlock;
+    assert IteratorUtils.peekNext(blockIterator) == callerBlock;
   }
 
   public void enqueueMethodForReprocessing(ProgramMethod method) {
@@ -1242,6 +1228,7 @@
 
   public void onMethodPruned(ProgramMethod method) {
     onMethodCodePruned(method);
+    multiCallerInliner.onMethodPruned(method);
   }
 
   public void onMethodCodePruned(ProgramMethod method) {
@@ -1281,6 +1268,24 @@
     singleCallerInlinedMethodsInWave.clear();
   }
 
+  public void onLastWaveDone(
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    postMethodProcessorBuilder
+        .getMethodsToReprocessBuilder()
+        .rewrittenWithLens(appView)
+        .merge(
+            singleInlineCallers
+                .rewrittenWithLens(appView)
+                .removeIf(
+                    appView,
+                    method -> method.getOptimizationInfo().hasBeenInlinedIntoSingleCallSite()));
+    singleInlineCallers.clear();
+    multiCallerInliner.onLastWaveDone(postMethodProcessorBuilder, executorService, timing);
+  }
+
   public static boolean verifyAllSingleCallerMethodsHaveBeenPruned(
       AppView<AppInfoWithLiveness> appView) {
     for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
@@ -1298,4 +1303,16 @@
     assert singleCallerInlinedPrunedMethodsForTesting.contains(method);
     return true;
   }
+
+  public static boolean verifyAllMultiCallerInlinedMethodsHaveBeenPruned(AppView<?> appView) {
+    for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
+      for (DexEncodedMethod method : clazz.methods()) {
+        if (method.hasCode() && method.getOptimizationInfo().isMultiCallerMethod()) {
+          // TODO(b/142300882): Ensure soundness of multi caller inlining.
+          // assert false;
+        }
+      }
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index f839991..ddc31c2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -7,9 +7,11 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 
 /**
@@ -30,10 +32,12 @@
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 
   InlineResult computeInlining(
+      IRCode code,
       InvokeMethod invoke,
       SingleResolutionResult resolutionResult,
       ProgramMethod singleTarget,
       ProgramMethod context,
       ClassInitializationAnalysis classInitializationAnalysis,
+      InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
index b6d33ea..564fcf8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -4,19 +4,26 @@
 
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.graph.AccessControl;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.Inliner.InlineeWithReason;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 interface InliningStrategy {
 
+  AppView<AppInfoWithLiveness> appView();
+
   boolean allowInliningOfInvokeInInlinee(
       InlineAction action,
       int inliningDepth,
@@ -24,8 +31,9 @@
 
   boolean canInlineInstanceInitializer(
       IRCode code,
-      IRCode inlinee,
       InvokeDirect invoke,
+      ProgramMethod singleTarget,
+      InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 
   /** Return true if there is still budget for inlining into this method. */
@@ -47,7 +55,41 @@
   /** Inform the strategy that the inlinee has been inlined. */
   void markInlined(InlineeWithReason inlinee);
 
-  void ensureMethodProcessed(ProgramMethod target, IRCode inlinee, OptimizationFeedback feedback);
+  default boolean setDowncastTypeIfNeeded(
+      AppView<AppInfoWithLiveness> appView,
+      InlineAction action,
+      InvokeMethod invoke,
+      ProgramMethod singleTarget,
+      ProgramMethod context) {
+    DexProgramClass downcastClass = getDowncastTypeIfNeeded(invoke, singleTarget);
+    if (downcastClass != null) {
+      if (AccessControl.isClassAccessible(downcastClass, context, appView).isPossiblyFalse()) {
+        return false;
+      }
+      action.setDowncastClass(downcastClass);
+    }
+    return true;
+  }
+
+  default DexProgramClass getDowncastTypeIfNeeded(InvokeMethod invoke, ProgramMethod target) {
+    if (invoke.isInvokeMethodWithReceiver()) {
+      // If the invoke has a receiver but the actual type of the receiver is different from the
+      // computed target holder, inlining requires a downcast of the receiver. In case we don't know
+      // the exact type of the receiver we use the static type of the receiver.
+      Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
+      if (!receiver.getType().isClassType()) {
+        return target.getHolder();
+      }
+
+      ClassTypeElement receiverType =
+          getReceiverTypeOrDefault(invoke, receiver.getType().asClassType());
+      ClassTypeElement targetType = target.getHolderType().toTypeElement(appView()).asClassType();
+      if (!receiverType.lessThanOrEqualUpToNullability(targetType, appView())) {
+        return target.getHolder();
+      }
+    }
+    return null;
+  }
 
   ClassTypeElement getReceiverTypeOrDefault(InvokeMethod invoke, ClassTypeElement defaultValue);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java
new file mode 100644
index 0000000..afe3547
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java
@@ -0,0 +1,272 @@
+// Copyright (c) 2021, 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;
+
+import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor;
+import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.InlineResult;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.ir.optimize.inliner.FixedInliningReasonStrategy;
+import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
+import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
+import com.android.tools.r8.ir.optimize.inliner.multicallerinliner.MultiCallerInlinerCallGraph;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.LazyBox;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
+import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.android.tools.r8.utils.collections.ProgramMethodMultiset;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+// TODO(b/142300882): If a method is selected for multi caller inlining, then if it is reprocessed
+//  and we inline into it, we should potentially disable multi caller inlining for that method (or
+//  we should disallow inlining into it).
+public class MultiCallerInliner {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  // Maps each method to the set of inlineable call sites targeting the method, or Optional.empty()
+  // if we have stopped tracking the inlineable call sites.
+  private final ProgramMethodMap<Optional<ProgramMethodMultiset>> multiInlineCallEdges =
+      ProgramMethodMap.createConcurrent();
+
+  private final int[] multiCallerInliningInstructionLimits;
+
+  MultiCallerInliner(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.multiCallerInliningInstructionLimits =
+        appView.options().inlinerOptions().multiCallerInliningInstructionLimits;
+  }
+
+  void recordCallEdgesForMultiCallerInlining(
+      ProgramMethod method, IRCode code, MethodProcessor methodProcessor, Timing timing) {
+    if (!methodProcessor.isPrimaryMethodProcessor()) {
+      return;
+    }
+
+    timing.time(
+        "Multi caller inliner: Record call edges",
+        () -> recordCallEdgesForMultiCallerInlining(method, code, methodProcessor));
+  }
+
+  private void recordCallEdgesForMultiCallerInlining(
+      ProgramMethod method, IRCode code, MethodProcessor methodProcessor) {
+    LazyBox<DefaultInliningOracle> lazyOracle =
+        new LazyBox<>(
+            () -> {
+              int inliningInstructionAllowance = Integer.MAX_VALUE;
+              return new DefaultInliningOracle(
+                  appView,
+                  new FixedInliningReasonStrategy(Reason.MULTI_CALLER_CANDIDATE),
+                  method,
+                  methodProcessor,
+                  inliningInstructionAllowance);
+            });
+    for (InvokeMethod invoke : code.<InvokeMethod>instructions(Instruction::isInvokeMethod)) {
+      // Don't attempt to multi caller inline constructors. To determine if a constructor is
+      // eligible for inlining, the inliner builds IR for the constructor, which we want to avoid
+      // here for build speed.
+      if (invoke.isInvokeConstructor(appView.dexItemFactory())) {
+        continue;
+      }
+
+      SingleResolutionResult resolutionResult =
+          appView
+              .appInfo()
+              .resolveMethod(invoke.getInvokedMethod(), invoke.getInterfaceBit())
+              .asSingleResolution();
+      if (resolutionResult == null
+          || resolutionResult.isAccessibleFrom(method, appView).isPossiblyFalse()) {
+        continue;
+      }
+
+      ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, method);
+      if (singleTarget == null
+          || !methodProcessor.getCallSiteInformation().isMultiCallerInlineCandidate(singleTarget)) {
+        continue;
+      }
+
+      InlineResult inlineResult =
+          lazyOracle
+              .computeIfAbsent()
+              .computeInlining(
+                  code,
+                  invoke,
+                  resolutionResult,
+                  singleTarget,
+                  method,
+                  ClassInitializationAnalysis.trivial(),
+                  InliningIRProvider.getThrowingInstance(),
+                  NopWhyAreYouNotInliningReporter.getInstance());
+      if (inlineResult == null || inlineResult.isRetryAction()) {
+        stopTrackingCallSitesForMethod(singleTarget);
+        continue;
+      }
+
+      InlineAction action = inlineResult.asInlineAction();
+      assert action.reason == Reason.MULTI_CALLER_CANDIDATE;
+      recordCallEdgeForMultiCallerInlining(method, singleTarget, methodProcessor);
+    }
+  }
+
+  void recordCallEdgeForMultiCallerInlining(
+      ProgramMethod method, ProgramMethod singleTarget, MethodProcessor methodProcessor) {
+    Optional<ProgramMethodMultiset> value =
+        multiInlineCallEdges.computeIfAbsent(
+            singleTarget, ignoreKey(() -> Optional.of(ProgramMethodMultiset.createConcurrent())));
+
+    // If we are not tracking the callers for the single target, then just return. In this case, we
+    // have previously found that the single target is ineligible for multi caller inlining.
+    if (!value.isPresent()) {
+      return;
+    }
+
+    // Record that we have seen a call site that dispatched to the single target which is eligible
+    // for inlining.
+    ProgramMethodMultiset callers = value.get();
+    callers.add(method);
+
+    // We track up to n call sites, where n is the size of multiCallerInliningInstructionLimits.
+    if (callers.size() > multiCallerInliningInstructionLimits.length) {
+      stopTrackingCallSitesForMethodIfDefinitelyIneligibleForMultiCallerInlining(
+          method, singleTarget, methodProcessor, callers);
+    }
+  }
+
+  private void stopTrackingCallSitesForMethodIfDefinitelyIneligibleForMultiCallerInlining(
+      ProgramMethod method,
+      ProgramMethod singleTarget,
+      MethodProcessor methodProcessor,
+      ProgramMethodMultiset callers) {
+    // First remove the call sites that no longer exist due to single caller inlining.
+    callers.removeIf(caller -> caller.getOptimizationInfo().hasBeenInlinedIntoSingleCallSite());
+
+    // Then compute the minimum number of call sites that are guaranteed to be present at the end
+    // of the primary optimization pass.
+    IntBox minimumCallers = new IntBox();
+    callers.forEachEntry(
+        (caller, calls) -> {
+          // If these call sites are inside a method that has a single caller, then the call sites
+          // could potentially disappear as a result of single caller inlining, so don't include
+          // them.
+          if (!methodProcessor.getCallSiteInformation().hasSingleCallSite(caller)) {
+            minimumCallers.increment(calls);
+          }
+        });
+
+    // If the threshold is definitely exceeded, then mark as ineligible for multi caller inlining.
+    if (minimumCallers.get() > multiCallerInliningInstructionLimits.length) {
+      stopTrackingCallSitesForMethod(singleTarget);
+    }
+  }
+
+  private void stopTrackingCallSitesForMethod(ProgramMethod method) {
+    multiInlineCallEdges.put(method, Optional.empty());
+  }
+
+  void onMethodPruned(ProgramMethod method) {
+    assert !multiInlineCallEdges.containsKey(method);
+  }
+
+  public void onLastWaveDone(
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    timing.begin("Multi caller inliner");
+    MultiCallerInlinerCallGraph callGraph =
+        timing.time(
+            "Call graph construction",
+            () -> MultiCallerInlinerCallGraph.builder(appView).build(executorService));
+    LongLivedProgramMethodSetBuilder<ProgramMethodSet> multiInlineCallers =
+        timing.time("Needs inlining analysis", () -> computeMultiInlineCallerMethods(callGraph));
+    postMethodProcessorBuilder
+        .getMethodsToReprocessBuilder()
+        .rewrittenWithLens(appView)
+        .merge(multiInlineCallers);
+    timing.end();
+  }
+
+  private LongLivedProgramMethodSetBuilder<ProgramMethodSet> computeMultiInlineCallerMethods(
+      MultiCallerInlinerCallGraph callGraph) {
+    // The multi inline callers are always rewritten up until the graph lens of the primary
+    // optimization pass, so we can safely merge them into the methods to reprocess (which may be
+    // rewritten with a newer graph lens).
+    GraphLens currentGraphLens = appView.graphLens();
+    LongLivedProgramMethodSetBuilder<ProgramMethodSet> multiInlineCallers =
+        LongLivedProgramMethodSetBuilder.createForIdentitySet(currentGraphLens);
+    multiInlineCallEdges.forEach(
+        (singleTarget, value) -> {
+          if (singleTarget.getDefinition().isLibraryMethodOverride().isPossiblyTrue()) {
+            return;
+          }
+
+          if (!value.isPresent()) {
+            return;
+          }
+
+          if (singleTarget.getDefinition().isInstance()
+              && !appView.appInfo().isInstantiatedDirectlyOrIndirectly(singleTarget.getHolder())) {
+            return;
+          }
+
+          ProgramMethodMultiset callers = value.get();
+          callers.removeIf(
+              method -> method.getOptimizationInfo().hasBeenInlinedIntoSingleCallSite());
+          if (callers.size() == 0 || callers.size() > multiCallerInliningInstructionLimits.length) {
+            return;
+          }
+
+          int numberOfCallSites = callGraph.getNode(singleTarget).getNumberOfCallSites();
+          // TODO(b/142300882): The number of call sites according to the call graph should
+          //  generally be >= the number of calls that the multi caller inliner has seen. This may
+          //  not hold when calls are removed due to identical block prefix/suffix sharing, however.
+          //  When this happens, the inliner may think that it can inline all call sites found in
+          //  the call graph, although this may not actually be true.
+          if (numberOfCallSites < callers.size()) {
+            return;
+          }
+          if (callers.size() < numberOfCallSites) {
+            // Can't inline all call sites.
+            return;
+          }
+
+          int multiCallerInliningInstructionLimit =
+              multiCallerInliningInstructionLimits[callers.size() - 1];
+          if (!singleTarget
+              .getDefinition()
+              .getCode()
+              .estimatedSizeForInliningAtMost(multiCallerInliningInstructionLimit)) {
+            // Multi caller inlining could lead to a size increase according to the heuristic.
+            return;
+          }
+          callers.forEachEntry(
+              (caller, count) -> {
+                if (!caller.getOptimizationInfo().hasBeenInlinedIntoSingleCallSite()) {
+                  multiInlineCallers.add(caller, currentGraphLens);
+                }
+              });
+          getSimpleFeedback().setMultiCallerMethod(singleTarget);
+        });
+    multiInlineCallEdges.clear();
+    return multiInlineCallers;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index b3f74c5..0e3dfdb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.analysis.value.SingleValue;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.FieldGet;
 import com.android.tools.r8.ir.code.FieldInstruction;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InitClass;
@@ -91,6 +92,10 @@
 
   private interface FieldValue {
 
+    default ExistingValue asExistingValue() {
+      return null;
+    }
+
     void eliminateRedundantRead(InstructionListIterator it, FieldInstruction redundant);
   }
 
@@ -103,6 +108,11 @@
     }
 
     @Override
+    public ExistingValue asExistingValue() {
+      return this;
+    }
+
+    @Override
     public void eliminateRedundantRead(InstructionListIterator it, FieldInstruction redundant) {
       affectedValues.addAll(redundant.value().affectedValues());
       redundant.value().replaceUsers(value);
@@ -110,6 +120,10 @@
       value.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
     }
 
+    public Value getValue() {
+      return value;
+    }
+
     @Override
     public String toString() {
       return "ExistingValue(v" + value.getNumber() + ")";
@@ -411,7 +425,7 @@
     FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
     FieldValue replacement = activeState.getInstanceFieldValue(fieldAndObject);
     if (replacement != null) {
-      assumeRemover.markAssumeDynamicTypeUsersForRemoval(instanceGet.outValue());
+      markAssumeDynamicTypeUsersForRemoval(instanceGet, replacement, assumeRemover);
       replacement.eliminateRedundantRead(it, instanceGet);
       return;
     }
@@ -430,6 +444,21 @@
     }
   }
 
+  private void markAssumeDynamicTypeUsersForRemoval(
+      FieldGet fieldGet, FieldValue replacement, AssumeRemover assumeRemover) {
+    ExistingValue existingValue = replacement.asExistingValue();
+    if (existingValue == null
+        || !existingValue
+            .getValue()
+            .isDefinedByInstructionSatisfying(
+                definition ->
+                    definition.isFieldGet()
+                        && definition.asFieldGet().getField().getType()
+                            == fieldGet.getField().getType())) {
+      assumeRemover.markAssumeDynamicTypeUsersForRemoval(fieldGet.outValue());
+    }
+  }
+
   private void handleInstancePut(InstancePut instancePut, DexClassAndField field) {
     // An instance-put instruction can potentially write the given field on all objects because of
     // aliases.
@@ -480,7 +509,7 @@
 
     FieldValue replacement = activeState.getStaticFieldValue(field.getReference());
     if (replacement != null) {
-      assumeRemover.markAssumeDynamicTypeUsersForRemoval(staticGet.outValue());
+      markAssumeDynamicTypeUsersForRemoval(staticGet, replacement, assumeRemover);
       replacement.eliminateRedundantRead(instructionIterator, staticGet);
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index aede4f7..d317655 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -401,7 +401,7 @@
     }
 
     inliner.performForcedInlining(
-        method, code, directMethodCalls, inliningIRProvider, Timing.empty());
+        method, code, directMethodCalls, inliningIRProvider, methodProcessor, Timing.empty());
 
     // In case we are class inlining an object allocation that does not inherit directly from
     // java.lang.Object, we need keep force inlining the constructor until we reach
@@ -450,7 +450,7 @@
         }
         if (!directMethodCalls.isEmpty()) {
           inliner.performForcedInlining(
-              method, code, directMethodCalls, inliningIRProvider, Timing.empty());
+              method, code, directMethodCalls, inliningIRProvider, methodProcessor, Timing.empty());
         }
       } while (!directMethodCalls.isEmpty());
     }
@@ -504,7 +504,7 @@
 
     if (!methodCallsOnInstance.isEmpty()) {
       inliner.performForcedInlining(
-          method, code, methodCallsOnInstance, inliningIRProvider, Timing.empty());
+          method, code, methodCallsOnInstance, inliningIRProvider, methodProcessor, Timing.empty());
     } else {
       assert indirectMethodCallsOnInstance.stream()
           .filter(method -> method.getDefinition().getOptimizationInfo().mayHaveSideEffects())
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index 353d8b7..3875ddf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -1154,12 +1154,15 @@
     DexClass targetHolder = singleTarget.getHolder();
     if (targetHolder.isProgramClass()) {
       if (targetHolder.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) {
-        if (code.context().getHolder() == targetHolder && code.method().isClassInitializer()) {
-          // The enum instance initializer is allowed to be called only from the enum clinit.
-          return Reason.ELIGIBLE;
-        } else {
+        // The enum instance initializer is only allowed to be called from an initializer of the
+        // enum itself.
+        if (code.context().getHolder() != targetHolder || !code.method().isInitializer()) {
           return Reason.INVALID_INIT;
         }
+        if (code.method().isInstanceInitializer() && !invoke.getFirstArgument().isThis()) {
+          return Reason.INVALID_INIT;
+        }
+        return Reason.ELIGIBLE;
       }
 
       // Check if this is a checkNotNull() user. In this case, we can create a copy of the method
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
index 182b415..096c2cf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
@@ -40,7 +40,6 @@
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import org.objectweb.asm.Opcodes;
@@ -271,13 +270,7 @@
 
       int maxStack = 4;
       int maxLocals = 0;
-      return new CfCode(
-          sharedUtilityClassType,
-          maxStack,
-          maxLocals,
-          instructions,
-          Collections.emptyList(),
-          Collections.emptyList());
+      return new CfCode(sharedUtilityClassType, maxStack, maxLocals, instructions);
     }
 
     private DexEncodedMethod createValuesMethod(
@@ -325,9 +318,7 @@
                   Opcodes.INVOKESTATIC, dexItemFactory.javaLangSystemMethods.arraycopy, false),
               // return result
               new CfLoad(ValueType.OBJECT, resultLocalSlot),
-              new CfReturn(ValueType.OBJECT)),
-          Collections.emptyList(),
-          Collections.emptyList());
+              new CfReturn(ValueType.OBJECT)));
     }
 
     private static DexProgramClass findDeterministicContextType(Set<DexProgramClass> contexts) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index 1ce6568..09b1cc3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -157,6 +157,11 @@
   }
 
   @Override
+  public boolean isMultiCallerMethod() {
+    return false;
+  }
+
+  @Override
   public boolean forceInline() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index b2d7be0..a4667c9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -22,7 +22,7 @@
     implements MemberOptimizationInfo<MutableMethodOptimizationInfo> {
 
   enum InlinePreference {
-    NeverInline,
+    MultiCallerInline,
     ForceInline,
     Default
   }
@@ -84,6 +84,8 @@
 
   public abstract BitSet getUnusedArguments();
 
+  public abstract boolean isMultiCallerMethod();
+
   public abstract boolean forceInline();
 
   public abstract boolean checksNullReceiverBeforeAnySideEffect();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 968cc66..62db1f4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -468,6 +468,11 @@
   }
 
   @Override
+  public boolean isMultiCallerMethod() {
+    return inlining == InlinePreference.MultiCallerInline;
+  }
+
+  @Override
   public boolean forceInline() {
     return inlining == InlinePreference.ForceInline;
   }
@@ -644,13 +649,18 @@
     inlining = InlinePreference.ForceInline;
   }
 
-  // TODO(b/140214568): Should be package-private.
-  public void unsetForceInline() {
-    // For concurrent scenarios we should allow the flag to be already unset
-    assert inlining == InlinePreference.Default || inlining == InlinePreference.ForceInline;
+  void unsetForceInline() {
     inlining = InlinePreference.Default;
   }
 
+  void setMultiCallerMethod() {
+    if (inlining == InlinePreference.Default) {
+      inlining = InlinePreference.MultiCallerInline;
+    } else {
+      assert inlining == InlinePreference.ForceInline;
+    }
+  }
+
   void markCheckNullReceiverBeforeAnySideEffect(boolean mark) {
     setFlag(CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT_FLAG, mark);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 5fd4800..c6cd18d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -67,6 +67,10 @@
     }
   }
 
+  public void setMultiCallerMethod(ProgramMethod method) {
+    method.getDefinition().getMutableOptimizationInfo().setMultiCallerMethod();
+  }
+
   // METHOD OPTIMIZATION INFO.
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java
index ef00071..3a9c668 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java
@@ -9,8 +9,9 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.conversion.CallSiteInformation;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.callgraph.CallSiteInformation;
+import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
@@ -33,6 +34,7 @@
       InvokeMethod invoke,
       ProgramMethod target,
       ProgramMethod context,
+      DefaultInliningOracle oracle,
       MethodProcessor methodProcessor) {
     DexEncodedMethod targetMethod = target.getDefinition();
     DexMethod targetReference = target.getReference();
@@ -54,9 +56,10 @@
     if (isSingleCallerInliningTarget(target)) {
       return Reason.SINGLE_CALLER;
     }
-    if (isDoubleInliningTarget(target)) {
-      assert methodProcessor.isPrimaryMethodProcessor();
-      return Reason.DUAL_CALLER;
+    if (isMultiCallerInlineCandidate(invoke, target, oracle, methodProcessor)) {
+      return methodProcessor.isPrimaryMethodProcessor()
+          ? Reason.MULTI_CALLER_CANDIDATE
+          : Reason.ALWAYS;
     }
     return Reason.SIMPLE;
   }
@@ -75,11 +78,20 @@
     return true;
   }
 
-  private boolean isDoubleInliningTarget(ProgramMethod candidate) {
-    return callSiteInformation.hasDoubleCallSite(candidate)
-        && candidate
-            .getDefinition()
-            .getCode()
-            .estimatedSizeForInliningAtMost(options.getDoubleInliningInstructionLimit());
+  private boolean isMultiCallerInlineCandidate(
+      InvokeMethod invoke,
+      ProgramMethod singleTarget,
+      DefaultInliningOracle oracle,
+      MethodProcessor methodProcessor) {
+    if (oracle.satisfiesRequirementsForSimpleInlining(invoke, singleTarget)) {
+      return false;
+    }
+    if (methodProcessor.isPrimaryMethodProcessor()) {
+      return callSiteInformation.isMultiCallerInlineCandidate(singleTarget);
+    }
+    if (methodProcessor.isPostMethodProcessor()) {
+      return singleTarget.getOptimizationInfo().isMultiCallerMethod();
+    }
+    return false;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java
index aed242b..ec998a7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/FixedInliningReasonStrategy.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 
 public class FixedInliningReasonStrategy implements InliningReasonStrategy {
@@ -22,6 +23,7 @@
       InvokeMethod invoke,
       ProgramMethod target,
       ProgramMethod context,
+      DefaultInliningOracle oracle,
       MethodProcessor methodProcessor) {
     return reason;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
index 1387311..677ccdd 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.inliner;
 
+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.code.IRCode;
@@ -24,6 +25,13 @@
 
   private final Map<InvokeMethod, IRCode> cache = new IdentityHashMap<>();
 
+  private InliningIRProvider() {
+    this.appView = null;
+    this.context = null;
+    this.valueNumberGenerator = null;
+    this.methodProcessor = null;
+  }
+
   public InliningIRProvider(
       AppView<?> appView, ProgramMethod context, IRCode code, MethodProcessor methodProcessor) {
     this.appView = appView;
@@ -32,6 +40,42 @@
     this.methodProcessor = methodProcessor;
   }
 
+  public static InliningIRProvider getThrowingInstance() {
+    return new InliningIRProvider() {
+      @Override
+      public IRCode getInliningIR(
+          InvokeMethod invoke, ProgramMethod method, boolean removeInnerFramesIfNpe) {
+        throw new Unreachable();
+      }
+
+      @Override
+      public IRCode getAndCacheInliningIR(
+          InvokeMethod invoke, ProgramMethod method, boolean removeInnerFrameIfThrowingNpe) {
+        throw new Unreachable();
+      }
+
+      @Override
+      public void cacheInliningIR(InvokeMethod invoke, IRCode code) {
+        throw new Unreachable();
+      }
+
+      @Override
+      public MethodProcessor getMethodProcessor() {
+        throw new Unreachable();
+      }
+
+      @Override
+      public boolean verifyIRCacheIsEmpty() {
+        throw new Unreachable();
+      }
+
+      @Override
+      public boolean shouldApplyCodeRewritings(ProgramMethod method) {
+        throw new Unreachable();
+      }
+    };
+  }
+
   public IRCode getInliningIR(
       InvokeMethod invoke, ProgramMethod method, boolean removeInnerFramesIfNpe) {
     IRCode cached = cache.remove(invoke);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java
index 00ff53c..c749be9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningReasonStrategy.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.DefaultInliningOracle;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 
 public interface InliningReasonStrategy {
@@ -15,5 +16,6 @@
       InvokeMethod invoke,
       ProgramMethod target,
       ProgramMethod context,
+      DefaultInliningOracle oracle,
       MethodProcessor methodProcessor);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerCallGraph.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerCallGraph.java
new file mode 100644
index 0000000..9f77a05
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerCallGraph.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2021, 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.inliner.multicallerinliner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraphBase;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Map;
+
+public class MultiCallerInlinerCallGraph extends CallGraphBase<MultiCallerInlinerNode> {
+
+  MultiCallerInlinerCallGraph(Map<DexMethod, MultiCallerInlinerNode> nodes) {
+    super(nodes);
+  }
+
+  public static MultiCallerInlinerCallGraphBuilder builder(AppView<AppInfoWithLiveness> appView) {
+    return new MultiCallerInlinerCallGraphBuilder(appView);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerCallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerCallGraphBuilder.java
new file mode 100644
index 0000000..d54fc87
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerCallGraphBuilder.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2021, 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.inliner.multicallerinliner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraphBuilderBase;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class MultiCallerInlinerCallGraphBuilder
+    extends CallGraphBuilderBase<MultiCallerInlinerNode> {
+
+  MultiCallerInlinerCallGraphBuilder(AppView<AppInfoWithLiveness> appView) {
+    super(appView);
+  }
+
+  @Override
+  protected MultiCallerInlinerNode createNode(ProgramMethod method) {
+    return new MultiCallerInlinerNode(method);
+  }
+
+  public MultiCallerInlinerCallGraph build(ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processItems(appView.appInfo().classes(), this::processClass, executorService);
+    return new MultiCallerInlinerCallGraph(nodes);
+  }
+
+  private void processClass(DexProgramClass clazz) {
+    clazz.forEachProgramMethodMatching(DexEncodedMethod::hasCode, this::processMethod);
+  }
+
+  private void processMethod(ProgramMethod method) {
+    MultiCallerInlinerInvokeRegistry registry =
+        new MultiCallerInlinerInvokeRegistry(
+            appView, getOrCreateNode(method), this::getOrCreateNode, possibleProgramTargetsCache);
+    method.registerCodeReferences(registry);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerInvokeRegistry.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerInvokeRegistry.java
new file mode 100644
index 0000000..e60c8b5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerInvokeRegistry.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2021, 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.inliner.multicallerinliner;
+
+import static com.google.common.base.Predicates.alwaysTrue;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.conversion.callgraph.InvokeExtractor;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.Map;
+import java.util.function.Function;
+
+public class MultiCallerInlinerInvokeRegistry extends InvokeExtractor<MultiCallerInlinerNode> {
+
+  MultiCallerInlinerInvokeRegistry(
+      AppView<AppInfoWithLiveness> appView,
+      MultiCallerInlinerNode currentMethod,
+      Function<ProgramMethod, MultiCallerInlinerNode> nodeFactory,
+      Map<DexMethod, ProgramMethodSet> possibleProgramTargetsCache) {
+    super(appView, currentMethod, nodeFactory, possibleProgramTargetsCache, alwaysTrue());
+  }
+
+  @Override
+  public GraphLens getCodeLens() {
+    return appView.graphLens();
+  }
+
+  @Override
+  protected void processInvokeWithDynamicDispatch(
+      Type type, DexClassAndMethod resolutionResult, ProgramMethod context) {
+    // Skip calls that dispatch to library methods or library method overrides.
+    if (resolutionResult.isProgramMethod()
+        && resolutionResult.getDefinition().isLibraryMethodOverride().isPossiblyFalse()) {
+      super.processInvokeWithDynamicDispatch(type, resolutionResult, context);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerNode.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerNode.java
new file mode 100644
index 0000000..c08b1a7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/multicallerinliner/MultiCallerInlinerNode.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, 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.inliner.multicallerinliner;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.callgraph.NodeBase;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class MultiCallerInlinerNode extends NodeBase<MultiCallerInlinerNode> {
+
+  private final AtomicInteger numberOfCallSites = new AtomicInteger();
+
+  public MultiCallerInlinerNode(ProgramMethod method) {
+    super(method);
+  }
+
+  @Override
+  public void addCallerConcurrently(MultiCallerInlinerNode caller, boolean likelySpuriousCallEdge) {
+    assert !getMethod().isClassInitializer();
+    numberOfCallSites.incrementAndGet();
+  }
+
+  @Override
+  public void addReaderConcurrently(MultiCallerInlinerNode reader) {
+    throw new Unreachable();
+  }
+
+  public int getNumberOfCallSites() {
+    return numberOfCallSites.get();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java
index 05f88a1..8e166d1 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java
@@ -5,11 +5,9 @@
 package com.android.tools.r8.ir.synthetic;
 
 import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfTryCatch;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexType;
-import com.google.common.collect.ImmutableList;
 import java.util.List;
 
 public abstract class SyntheticCfCodeProvider {
@@ -29,13 +27,7 @@
   public abstract CfCode generateCfCode();
 
   protected CfCode standardCfCodeFromInstructions(List<CfInstruction> instructions) {
-    return new CfCode(
-        holder,
-        defaultMaxStack(),
-        defaultMaxLocals(),
-        instructions,
-        defaultTryCatchs(),
-        ImmutableList.of());
+    return new CfCode(holder, defaultMaxStack(), defaultMaxLocals(), instructions);
   }
 
   protected int defaultMaxStack() {
@@ -45,8 +37,4 @@
   protected int defaultMaxLocals() {
     return 16;
   }
-
-  protected List<CfTryCatch> defaultTryCatchs() {
-    return ImmutableList.of();
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
index 2e21467..0ad986c 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
@@ -13,9 +13,11 @@
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.DexValue.DexItemBasedValueString;
 import com.android.tools.r8.graph.DexValue.DexValueString;
@@ -23,6 +25,7 @@
 import com.android.tools.r8.ir.desugar.records.RecordCfToCfRewriter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardClassFilter;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import java.util.List;
@@ -94,19 +97,36 @@
           cnst.BBBB = getRenamedStringLiteral(cnst.getString());
         }
       }
-    } else {
-      assert code.isCfCode();
+    } else if (code.isCfCode()) {
       for (CfInstruction instruction : code.asCfCode().getInstructions()) {
         if (instruction.isConstString()) {
           CfConstString cnst = instruction.asConstString();
           cnst.setString(getRenamedStringLiteral(cnst.getString()));
         }
       }
+    } else {
+      assert code.isCfWritableCode() || code.isDexWritableCode();
     }
   }
 
   private DexString getRenamedStringLiteral(DexString originalLiteral) {
-    DexString rewrittenString = lens.lookupDescriptorForJavaTypeName(originalLiteral.toString());
+    String descriptor =
+        DescriptorUtils.javaTypeToDescriptorIfValidJavaType(originalLiteral.toString());
+    if (descriptor == null) {
+      return originalLiteral;
+    }
+    DexType type = appView.dexItemFactory().createType(descriptor);
+    DexType originalType = appView.graphLens().getOriginalType(type);
+    if (originalType != type) {
+      // The type has changed to something clashing with the string.
+      return originalLiteral;
+    }
+    DexType rewrittenType = appView.graphLens().lookupType(type);
+    DexClass clazz = appView.appInfo().definitionForWithoutExistenceAssert(rewrittenType);
+    if (clazz == null || clazz.isNotProgramClass()) {
+      return originalLiteral;
+    }
+    DexString rewrittenString = lens.lookupClassDescriptor(rewrittenType);
     return rewrittenString == null
         ? originalLiteral
         : appView.dexItemFactory().createString(descriptorToJavaType(rewrittenString.toString()));
diff --git a/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java b/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
index cc64864..bf18111 100644
--- a/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
@@ -34,7 +34,7 @@
       ClassRenaming classRenaming,
       MethodRenaming methodRenaming,
       FieldRenaming fieldRenaming) {
-    super(appView.dexItemFactory(), classRenaming.classRenaming);
+    super(appView.dexItemFactory());
     this.appView = appView;
     this.packageRenaming = classRenaming.packageRenaming;
     renaming.putAll(classRenaming.classRenaming);
diff --git a/src/main/java/com/android/tools/r8/naming/NamingLens.java b/src/main/java/com/android/tools/r8/naming/NamingLens.java
index b6e94ee..5b24eb9 100644
--- a/src/main/java/com/android/tools/r8/naming/NamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/NamingLens.java
@@ -21,8 +21,6 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
 import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Set;
 
 /**
@@ -42,8 +40,6 @@
 
   public abstract DexString lookupDescriptor(DexType type);
 
-  public abstract DexString lookupDescriptorForJavaTypeName(String typeName);
-
   public DexString lookupClassDescriptor(DexType type) {
     assert type.isClassType();
     return internalLookupClassDescriptor(type);
@@ -185,13 +181,9 @@
   public abstract static class NonIdentityNamingLens extends NamingLens {
 
     private final DexItemFactory dexItemFactory;
-    private final Map<String, DexString> typeStringMapping;
 
-    protected NonIdentityNamingLens(
-        DexItemFactory dexItemFactory, Map<DexType, DexString> typeMapping) {
+    protected NonIdentityNamingLens(DexItemFactory dexItemFactory) {
       this.dexItemFactory = dexItemFactory;
-      typeStringMapping = new HashMap<>();
-      typeMapping.forEach((k, v) -> typeStringMapping.put(k.toSourceString(), v));
     }
 
     protected DexItemFactory dexItemFactory() {
@@ -211,11 +203,6 @@
       assert type.isClassType();
       return lookupClassDescriptor(type);
     }
-
-    @Override
-    public DexString lookupDescriptorForJavaTypeName(String typeName) {
-      return typeStringMapping.get(typeName);
-    }
   }
 
   private static final class IdentityLens extends NamingLens {
@@ -230,11 +217,6 @@
     }
 
     @Override
-    public DexString lookupDescriptorForJavaTypeName(String typeName) {
-      return null;
-    }
-
-    @Override
     protected DexString internalLookupClassDescriptor(DexType type) {
       return type.descriptor;
     }
diff --git a/src/main/java/com/android/tools/r8/naming/PrefixRewritingNamingLens.java b/src/main/java/com/android/tools/r8/naming/PrefixRewritingNamingLens.java
index 04f235f..5ab9772 100644
--- a/src/main/java/com/android/tools/r8/naming/PrefixRewritingNamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/PrefixRewritingNamingLens.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.NamingLens.NonIdentityNamingLens;
 import com.android.tools.r8.utils.InternalOptions;
-import java.util.IdentityHashMap;
 
 // Naming lens for rewriting type prefixes.
 public class PrefixRewritingNamingLens extends NonIdentityNamingLens {
@@ -34,7 +33,7 @@
   }
 
   public PrefixRewritingNamingLens(NamingLens namingLens, AppView<?> appView) {
-    super(appView.dexItemFactory(), new IdentityHashMap<>());
+    super(appView.dexItemFactory());
     this.appView = appView;
     this.namingLens = namingLens;
     this.options = appView.options();
@@ -96,18 +95,6 @@
   }
 
   @Override
-  public DexString lookupDescriptorForJavaTypeName(String typeName) {
-    if (appView.rewritePrefix.shouldRewriteTypeName(typeName)) {
-      DexType rewrittenType =
-          appView.rewritePrefix.rewrittenType(dexItemFactory().createType(typeName), appView);
-      if (rewrittenType != null) {
-        return rewrittenType.descriptor;
-      }
-    }
-    return namingLens.lookupDescriptorForJavaTypeName(typeName);
-  }
-
-  @Override
   public String lookupPackageName(String packageName) {
     // Used for resource shrinking.
     // Desugared libraries do not have resources.
diff --git a/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java b/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
index f48cfc7..fe2654f 100644
--- a/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/RecordRewritingNamingLens.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.NamingLens.NonIdentityNamingLens;
 import com.android.tools.r8.utils.InternalOptions;
-import java.util.IdentityHashMap;
 
 // Naming lens for rewriting java.lang.Record to the internal RecordTag type.
 public class RecordRewritingNamingLens extends NonIdentityNamingLens {
@@ -34,7 +33,7 @@
   }
 
   public RecordRewritingNamingLens(NamingLens namingLens, AppView<?> appView) {
-    super(appView.dexItemFactory(), new IdentityHashMap<>());
+    super(appView.dexItemFactory());
     this.namingLens = namingLens;
     factory = appView.dexItemFactory();
   }
@@ -75,14 +74,6 @@
   }
 
   @Override
-  public DexString lookupDescriptorForJavaTypeName(String typeName) {
-    if (typeName.equals(factory.recordType.toSourceString())) {
-      return factory.recordTagType.descriptor;
-    }
-    return namingLens.lookupDescriptorForJavaTypeName(typeName);
-  }
-
-  @Override
   public boolean hasPrefixRewritingLogic() {
     return namingLens.hasPrefixRewritingLogic();
   }
diff --git a/src/main/java/com/android/tools/r8/position/MethodPosition.java b/src/main/java/com/android/tools/r8/position/MethodPosition.java
index fa86ac8..12a7d8d 100644
--- a/src/main/java/com/android/tools/r8/position/MethodPosition.java
+++ b/src/main/java/com/android/tools/r8/position/MethodPosition.java
@@ -4,7 +4,9 @@
 package com.android.tools.r8.position;
 
 import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.TypeReference;
 import java.util.List;
@@ -15,14 +17,41 @@
 public class MethodPosition implements Position {
 
   private final MethodReference method;
+  private final Position textPosition;
 
   @Deprecated
   public MethodPosition(DexMethod method) {
     this(method.asMethodReference());
   }
 
+  @Deprecated
   public MethodPosition(MethodReference method) {
+    this(method, Position.UNKNOWN);
+  }
+
+  private MethodPosition(MethodReference method, Position textPosition) {
     this.method = method;
+    this.textPosition = textPosition;
+  }
+
+  public static MethodPosition create(ProgramMethod method) {
+    return create(method.getDefinition());
+  }
+
+  public static MethodPosition create(DexEncodedMethod method) {
+    Position position = UNKNOWN;
+    if (method.hasCode() && method.getCode().isCfCode()) {
+      position = method.getCode().asCfCode().getDiagnosticPosition();
+    }
+    return create(method.getReference().asMethodReference(), position);
+  }
+
+  public static MethodPosition create(MethodReference method) {
+    return new MethodPosition(method, Position.UNKNOWN);
+  }
+
+  public static MethodPosition create(MethodReference method, Position position) {
+    return new MethodPosition(method, position);
   }
 
   /** The method */
@@ -51,6 +80,10 @@
         .collect(Collectors.toList());
   }
 
+  public Position getTextPosition() {
+    return textPosition;
+  }
+
   @Override
   public String toString() {
     return method.toString();
diff --git a/src/main/java/com/android/tools/r8/relocator/SimplePackagesRewritingMapper.java b/src/main/java/com/android/tools/r8/relocator/SimplePackagesRewritingMapper.java
index 3a5902a..7afb7e3 100644
--- a/src/main/java/com/android/tools/r8/relocator/SimplePackagesRewritingMapper.java
+++ b/src/main/java/com/android/tools/r8/relocator/SimplePackagesRewritingMapper.java
@@ -102,7 +102,7 @@
         Map<DexType, DexString> typeMappings,
         Map<String, String> packageMappings,
         DexItemFactory factory) {
-      super(factory, typeMappings);
+      super(factory);
       this.typeMappings = typeMappings;
       this.packageMappings = packageMappings;
     }
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 7765953..c7a2bab 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1017,6 +1017,7 @@
     markLambdaAsInstantiated(descriptor, context);
     transitionMethodsForInstantiatedLambda(descriptor);
     callSites.computeIfAbsent(callSite, ignore -> ProgramMethodSet.create()).add(context);
+    descriptor.captures.forEach(type -> markTypeAsLive(type, context));
 
     // For call sites representing a lambda, we link the targeted method
     // or field as if it were referenced from the current method.
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 2a06ead..6d51c04 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.code.Invoke.Type.DIRECT;
 import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
+import static com.android.tools.r8.ir.code.Invoke.Type.VIRTUAL;
 import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
 
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
@@ -1061,7 +1062,7 @@
           }
         }
 
-        DexEncodedMethod resultingDirectMethod;
+        DexEncodedMethod resultingMethod;
         if (source.accessFlags.isInterface()) {
           // Moving a default interface method into its subtype. This method could be hit directly
           // via an invoke-super instruction from any of the transitive subtypes of this interface,
@@ -1070,7 +1071,7 @@
           // method name that does not collide with one in the hierarchy of this class.
           MemberPool<DexMethod> methodPoolForTarget =
               methodPoolCollection.buildForHierarchy(target, executorService, timing);
-          resultingDirectMethod =
+          resultingMethod =
               renameMethod(
                   virtualMethod,
                   method ->
@@ -1079,26 +1080,32 @@
                               MethodSignatureEquivalence.get().wrap(method)),
                   Rename.ALWAYS,
                   appView.dexItemFactory().prependHolderToProto(virtualMethod.getReference()));
-          makeStatic(resultingDirectMethod);
+          makeStatic(resultingMethod);
 
           // Update method pool collection now that we are adding a new public method.
-          methodPoolForTarget.seen(resultingDirectMethod.getReference());
+          methodPoolForTarget.seen(resultingMethod.getReference());
         } else {
           // This virtual method could be called directly from a sub class via an invoke-super in-
-          // struction. Therefore, we translate this virtual method into a direct method, such that
-          // relevant invoke-super instructions can be rewritten into invoke-direct instructions.
-          resultingDirectMethod =
-              renameMethod(virtualMethod, availableMethodSignatures, Rename.ALWAYS);
-          makePrivate(resultingDirectMethod);
+          // struction. Therefore, we translate this virtual method into an instance method with a
+          // unique name, such that relevant invoke-super instructions can be rewritten to target
+          // this method directly.
+          resultingMethod = renameMethod(virtualMethod, availableMethodSignatures, Rename.ALWAYS);
+          if (appView.options().getProguardConfiguration().isAccessModificationAllowed()) {
+            makePublic(resultingMethod);
+          } else {
+            makePrivate(resultingMethod);
+          }
         }
 
-        add(directMethods, resultingDirectMethod, MethodSignatureEquivalence.get());
+        add(
+            resultingMethod.belongsToDirectPool() ? directMethods : virtualMethods,
+            resultingMethod,
+            MethodSignatureEquivalence.get());
 
         // Record that invoke-super instructions in the target class should be redirected to the
         // newly created direct method.
-        redirectSuperCallsInTarget(
-            virtualMethod.getReference(), resultingDirectMethod.getReference());
-        blockRedirectionOfSuperCalls(resultingDirectMethod.getReference());
+        redirectSuperCallsInTarget(virtualMethod, resultingMethod);
+        blockRedirectionOfSuperCalls(resultingMethod.getReference());
 
         if (shadowedBy == null) {
           // In addition to the newly added direct method, create a virtual method such that we do
@@ -1106,15 +1113,14 @@
           // Note that this method is added independently of whether it will actually be used. If
           // it turns out that the method is never used, it will be removed by the final round
           // of tree shaking.
-          shadowedBy = buildBridgeMethod(virtualMethod, resultingDirectMethod);
+          shadowedBy = buildBridgeMethod(virtualMethod, resultingMethod);
           deferredRenamings.recordCreationOfBridgeMethod(
               virtualMethod.getReference(), shadowedBy.getReference());
           add(virtualMethods, shadowedBy, MethodSignatureEquivalence.get());
         }
 
         deferredRenamings.map(virtualMethod.getReference(), shadowedBy.getReference());
-        deferredRenamings.recordMove(
-            virtualMethod.getReference(), resultingDirectMethod.getReference());
+        deferredRenamings.recordMove(virtualMethod.getReference(), resultingMethod.getReference());
       }
 
       if (abortMerge) {
@@ -1369,7 +1375,11 @@
       return synthesizedBridges;
     }
 
-    private void redirectSuperCallsInTarget(DexMethod oldTarget, DexMethod newTarget) {
+    private void redirectSuperCallsInTarget(
+        DexEncodedMethod oldTarget, DexEncodedMethod newTarget) {
+      DexMethod oldTargetReference = oldTarget.getReference();
+      DexMethod newTargetReference = newTarget.getReference();
+      Type newTargetType = newTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT;
       if (source.accessFlags.isInterface()) {
         // If we merge a default interface method from interface I to its subtype C, then we need
         // to rewrite invocations on the form "invoke-super I.m()" to "invoke-direct C.m$I()".
@@ -1379,21 +1389,23 @@
         // if I has a supertype J. This is due to the fact that invoke-super instructions that
         // resolve to a method on an interface never hit an implementation below that interface.
         deferredRenamings.mapVirtualMethodToDirectInType(
-            oldTarget,
-            prototypeChanges -> new MethodLookupResult(newTarget, null, STATIC, prototypeChanges),
+            oldTargetReference,
+            prototypeChanges ->
+                new MethodLookupResult(newTargetReference, null, STATIC, prototypeChanges),
             target.type);
       } else {
         // If we merge class B into class C, and class C contains an invocation super.m(), then it
-        // is insufficient to rewrite "invoke-super B.m()" to "invoke-direct C.m$B()" (the method
-        // C.m$B denotes the direct method that has been created in C for B.m). In particular, there
-        // might be an instruction "invoke-super A.m()" in C that resolves to B.m at runtime (A is
-        // a superclass of B), which also needs to be rewritten to "invoke-direct C.m$B()".
+        // is insufficient to rewrite "invoke-super B.m()" to "invoke-{direct,virtual} C.m$B()" (the
+        // method C.m$B denotes the direct/virtual method that has been created in C for B.m). In
+        // particular, there might be an instruction "invoke-super A.m()" in C that resolves to B.m
+        // at runtime (A is a superclass of B), which also needs to be rewritten to
+        // "invoke-{direct,virtual} C.m$B()".
         //
         // We handle this by adding a mapping for [target] and all of its supertypes.
         DexProgramClass holder = target;
         while (holder != null && holder.isProgramClass()) {
           DexMethod signatureInHolder =
-              application.dexItemFactory.createMethod(holder.type, oldTarget.proto, oldTarget.name);
+              oldTargetReference.withHolder(holder, appView.dexItemFactory());
           // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
           boolean resolutionSucceeds =
               holder.lookupVirtualMethod(signatureInHolder) != null
@@ -1402,7 +1414,8 @@
             deferredRenamings.mapVirtualMethodToDirectInType(
                 signatureInHolder,
                 prototypeChanges ->
-                    new MethodLookupResult(newTarget, null, DIRECT, prototypeChanges),
+                    new MethodLookupResult(
+                        newTargetReference, null, newTargetType, prototypeChanges),
                 target.type);
           } else {
             break;
@@ -1416,7 +1429,7 @@
           Set<DexType> mergedTypes = mergedClasses.getKeys(holder.type);
           for (DexType type : mergedTypes) {
             DexMethod signatureInType =
-                application.dexItemFactory.createMethod(type, oldTarget.proto, oldTarget.name);
+                oldTargetReference.withHolder(type, appView.dexItemFactory());
             // Resolution would have succeeded if the method used to be in [type], or if one of
             // its super classes declared the method.
             boolean resolutionSucceededBeforeMerge =
@@ -1426,7 +1439,8 @@
               deferredRenamings.mapVirtualMethodToDirectInType(
                   signatureInType,
                   prototypeChanges ->
-                      new MethodLookupResult(newTarget, null, DIRECT, prototypeChanges),
+                      new MethodLookupResult(
+                          newTargetReference, null, newTargetType, prototypeChanges),
                   target.type);
             }
           }
@@ -1469,13 +1483,17 @@
       accessFlags.setSynthetic();
       accessFlags.unsetAbstract();
 
-      assert invocationTarget.isPrivateMethod() == !invocationTarget.isStatic();
+      assert invocationTarget.isStatic()
+          || invocationTarget.isNonPrivateVirtualMethod()
+          || invocationTarget.isNonStaticPrivateMethod();
       SynthesizedBridgeCode code =
           new SynthesizedBridgeCode(
               newMethod,
               appView.graphLens().getOriginalMethodSignature(method.getReference()),
               invocationTarget.getReference(),
-              invocationTarget.isPrivateMethod() ? DIRECT : STATIC,
+              invocationTarget.isStatic()
+                  ? STATIC
+                  : (invocationTarget.isNonPrivateVirtualMethod() ? VIRTUAL : DIRECT),
               target.isInterface());
 
       // Add the bridge to the list of synthesized bridges such that the method signatures will
@@ -1694,6 +1712,14 @@
     method.accessFlags.setPrivate();
   }
 
+  private static void makePublic(DexEncodedMethod method) {
+    MethodAccessFlags accessFlags = method.getAccessFlags();
+    assert !accessFlags.isAbstract();
+    accessFlags.unsetPrivate();
+    accessFlags.unsetProtected();
+    accessFlags.setPublic();
+  }
+
   private static class VerticalClassMergerTreeFixer extends TreeFixerBase {
 
     private final AppView<AppInfoWithLiveness> appView;
@@ -1959,10 +1985,11 @@
     }
 
     @Override
-    public MethodLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
+    public MethodLookupResult lookupMethod(
+        DexMethod method, DexMethod context, Type type, GraphLens codeLens) {
       // First look up the method using the existing graph lens (for example, the type will have
       // changed if the method was publicized by ClassAndMemberPublicizer).
-      MethodLookupResult lookup = appView.graphLens().lookupMethod(method, context, type);
+      MethodLookupResult lookup = appView.graphLens().lookupMethod(method, context, type, codeLens);
       // Then check if there is a renaming due to the vertical class merger.
       DexMethod newMethod = lensBuilder.methodMap.get(lookup.getReference());
       if (newMethod == null) {
@@ -1979,7 +2006,7 @@
         DexClass clazz = appInfo.definitionFor(newMethod.holder);
         if (clazz != null && !clazz.accessFlags.isInterface()) {
           assert appInfo.definitionFor(method.holder).accessFlags.isInterface();
-          methodLookupResultBuilder.setType(Type.VIRTUAL);
+          methodLookupResultBuilder.setType(VIRTUAL);
         }
       }
       return methodLookupResultBuilder.build();
@@ -1999,7 +2026,7 @@
     }
 
     @Override
-    public DexField lookupField(DexField field) {
+    public DexField lookupField(DexField field, GraphLens codeLens) {
       return lensBuilder.fieldMap.getOrDefault(field, field);
     }
 
@@ -2087,14 +2114,14 @@
     @Override
     public void registerInvokeVirtual(DexMethod method) {
       MethodLookupResult lookup =
-          appView.graphLens().lookupMethod(method, getContext().getReference(), Type.VIRTUAL);
+          appView.graphLens().lookupMethod(method, getContext().getReference(), VIRTUAL);
       checkMethodReference(lookup.getReference(), OptionalBool.FALSE);
     }
 
     @Override
     public void registerInvokeDirect(DexMethod method) {
       MethodLookupResult lookup =
-          appView.graphLens().lookupMethod(method, getContext().getReference(), Type.DIRECT);
+          appView.graphLens().lookupMethod(method, getContext().getReference(), DIRECT);
       checkMethodReference(lookup.getReference(), OptionalBool.UNKNOWN);
     }
 
@@ -2255,7 +2282,7 @@
       forwardSourceCodeBuilder
           .setReceiver(method.holder)
           .setOriginalMethod(originalMethod)
-          .setTargetReceiver(type == DIRECT ? method.holder : null)
+          .setTargetReceiver(type.isStatic() ? null : method.holder)
           .setTarget(invocationTarget)
           .setInvokeType(type)
           .setIsInterface(isInterface);
@@ -2270,11 +2297,12 @@
           case DIRECT:
             registry.registerInvokeDirect(invocationTarget);
             break;
-
           case STATIC:
             registry.registerInvokeStatic(invocationTarget);
             break;
-
+          case VIRTUAL:
+            registry.registerInvokeVirtual(invocationTarget);
+            break;
           default:
             throw new Unreachable("Unexpected invocation type: " + type);
         }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 9548f78..5cc5876 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -602,7 +602,11 @@
                         externalSyntheticTypePrefix,
                         generators,
                         appView,
-                        equivalences::containsKey);
+                        candidateType ->
+                            equivalences.containsKey(candidateType)
+                                || appView
+                                    .horizontallyMergedClasses()
+                                    .hasBeenMergedIntoDifferentType(candidateType));
             equivalences.put(representativeType, group);
           }
         });
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index 9a863be..24c6a60 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -91,6 +91,10 @@
     return true;
   }
 
+  public static int last(int[] array) {
+    return array[array.length - 1];
+  }
+
   public static <T> T last(T[] array) {
     return array[array.length - 1];
   }
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 7db36b3..7bc38a4 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1317,10 +1317,8 @@
         parseSystemPropertyForDevelopmentOrDefault(
             "com.android.tools.r8.inliningInstructionLimit", -1);
 
-    // This defines the limit of instructions in the inlinee
-    public int doubleInliningInstructionLimit =
-        parseSystemPropertyForDevelopmentOrDefault(
-            "com.android.tools.r8.doubleInliningInstructionLimit", -1);
+    public int[] multiCallerInliningInstructionLimits =
+        new int[] {Integer.MAX_VALUE, 28, 16, 12, 10};
 
     // This defines how many instructions of inlinees we can inlinee overall.
     public int inliningInstructionAllowance = 1500;
@@ -1356,20 +1354,6 @@
       assert isGeneratingDex();
       return 5;
     }
-
-    public int getDoubleInliningInstructionLimit() {
-      // If a custom double inlining instruction limit is set, then use that.
-      if (doubleInliningInstructionLimit >= 0) {
-        return doubleInliningInstructionLimit;
-      }
-      // Allow 10 instructions when generating to class files.
-      if (isGeneratingClassFiles()) {
-        return 10;
-      }
-      // Allow the size of the dex code to be up to 20 bytes.
-      assert isGeneratingDex();
-      return 20;
-    }
   }
 
   public class HorizontalClassMergerOptions {
diff --git a/src/main/java/com/android/tools/r8/utils/Timing.java b/src/main/java/com/android/tools/r8/utils/Timing.java
index 5367c26..04d801c 100644
--- a/src/main/java/com/android/tools/r8/utils/Timing.java
+++ b/src/main/java/com/android/tools/r8/utils/Timing.java
@@ -21,7 +21,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Stack;
-import java.util.function.Supplier;
 
 public class Timing {
 
@@ -380,7 +379,7 @@
     }
   }
 
-  public <T> T time(String title, Supplier<T> supplier) {
+  public <T, E extends Exception> T time(String title, ThrowingSupplier<T, E> supplier) throws E {
     begin(title);
     try {
       return supplier.get();
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMultiset.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMultiset.java
index ae8974c..cfaa16f 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMultiset.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMultiset.java
@@ -9,9 +9,11 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.utils.ProgramMethodEquivalence;
 import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.ConcurrentHashMultiset;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.Multiset;
 import java.util.function.ObjIntConsumer;
+import java.util.function.Predicate;
 
 public class ProgramMethodMultiset {
 
@@ -21,10 +23,18 @@
     this.backing = backing;
   }
 
+  public static ProgramMethodMultiset createConcurrent() {
+    return new ProgramMethodMultiset(ConcurrentHashMultiset.create());
+  }
+
   public static ProgramMethodMultiset createHash() {
     return new ProgramMethodMultiset(HashMultiset.create());
   }
 
+  public void add(ProgramMethod method) {
+    backing.add(wrap(method));
+  }
+
   public void createAndAdd(DexProgramClass holder, DexEncodedMethod method, int occurrences) {
     backing.add(wrap(new ProgramMethod(holder, method)), occurrences);
   }
@@ -33,6 +43,14 @@
     backing.forEachEntry((wrapper, occurrences) -> consumer.accept(wrapper.get(), occurrences));
   }
 
+  public boolean removeIf(Predicate<ProgramMethod> predicate) {
+    return backing.removeIf(wrapper -> predicate.test(wrapper.get()));
+  }
+
+  public int size() {
+    return backing.size();
+  }
+
   private static Wrapper<ProgramMethod> wrap(ProgramMethod method) {
     return ProgramMethodEquivalence.get().wrap(method);
   }
diff --git a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
index 9b81396..b6bcf5c 100644
--- a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -130,6 +130,10 @@
       return compilerVersion == version;
     }
 
+    public boolean isOneOf(KotlinCompilerVersion... versions) {
+      return Arrays.stream(versions).anyMatch(this::is);
+    }
+
     public boolean isNot(KotlinCompilerVersion version) {
       return !is(version);
     }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index 45d03e7..f695dbb 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -204,7 +204,7 @@
             b ->
                 b.addProguardConfiguration(
                     getProguardOptionsNPlus(enableProguardCompatibilityMode), Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 2, "lambdadesugaringnplus"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaringnplus"))
         .run();
   }
 
@@ -244,7 +244,7 @@
             b ->
                 b.addProguardConfiguration(
                     getProguardOptionsNPlus(enableProguardCompatibilityMode), Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 2, "lambdadesugaringnplus"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaringnplus"))
         .run();
   }
 
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineDuplicateMethodTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineDuplicateMethodTest.java
index 71bb770..e0afd34 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineDuplicateMethodTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineDuplicateMethodTest.java
@@ -7,11 +7,11 @@
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
 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 static org.junit.Assume.assumeFalse;
 
@@ -22,7 +22,6 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.lang.reflect.Method;
@@ -81,27 +80,30 @@
         .inspect(
             inspector -> {
               // No need to check further on CF.
-              Optional<FoundMethodSubject> synthesizedAddedOn23 =
-                  inspector.allClasses().stream()
-                      .flatMap(clazz -> clazz.allMethods().stream())
-                      .filter(
-                          methodSubject ->
-                              methodSubject.isSynthetic()
-                                  && invokesMethodWithName("addedOn23").matches(methodSubject))
-                      .findFirst();
-              if (parameters.isCfRuntime() || parameters.getApiLevel().isLessThan(classApiLevel)) {
-                assertFalse(synthesizedAddedOn23.isPresent());
-                assertEquals(3, inspector.allClasses().size());
-              } else if (parameters.getApiLevel().isLessThan(methodApiLevel)) {
+              int classCount =
+                  parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(methodApiLevel)
+                      ? 4
+                      : 3;
+              assertEquals(classCount, inspector.allClasses().size());
+              Method testMethod = TestClass.class.getDeclaredMethod("test");
+              verifyThat(parameters, adeddOn23).isOutlinedFromUntil(testMethod, methodApiLevel);
+              if (parameters.isDexRuntime()
+                  && parameters.getApiLevel().isLessThan(methodApiLevel)) {
+                // Verify that we invoke the synthesized outline addedOn23 twice.
+                Optional<FoundMethodSubject> synthesizedAddedOn23 =
+                    inspector.allClasses().stream()
+                        .flatMap(clazz -> clazz.allMethods().stream())
+                        .filter(
+                            methodSubject ->
+                                methodSubject.isSynthetic()
+                                    && invokesMethodWithName("addedOn23").matches(methodSubject))
+                        .findFirst();
                 assertTrue(synthesizedAddedOn23.isPresent());
-                assertEquals(4, inspector.allClasses().size());
-                ClassSubject testClass = inspector.clazz(TestClass.class);
-                assertThat(testClass, isPresent());
-                MethodSubject testMethod = testClass.uniqueMethodWithName("test");
-                assertThat(testMethod, isPresent());
+                MethodSubject testMethodSubject = inspector.method(testMethod);
+                assertThat(testMethodSubject, isPresent());
                 assertEquals(
                     2,
-                    testMethod
+                    testMethodSubject
                         .streamInstructions()
                         .filter(
                             instructionSubject -> {
@@ -114,10 +116,6 @@
                                   .equals(synthesizedAddedOn23.get().asMethodReference());
                             })
                         .count());
-              } else {
-                // No outlining on this api level.
-                assertFalse(synthesizedAddedOn23.isPresent());
-                assertEquals(3, inspector.allClasses().size());
               }
             });
   }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineHorizontalMergingTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineHorizontalMergingTest.java
index 436d2fc..2426afb 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineHorizontalMergingTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineHorizontalMergingTest.java
@@ -122,8 +122,7 @@
                               methodSubject.isSynthetic()
                                   && invokesMethodWithName("addedOn27").matches(methodSubject))
                       .collect(Collectors.toList());
-              if (parameters.isCfRuntime()
-                  || parameters.getApiLevel().isLessThan(libraryClassApiLevel)) {
+              if (parameters.isCfRuntime()) {
                 assertTrue(outlinedAddedOn23.isEmpty());
                 assertTrue(outlinedAddedOn27.isEmpty());
                 assertEquals(3, inspector.allClasses().size());
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodAndStubClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodAndStubClassTest.java
new file mode 100644
index 0000000..051f793
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodAndStubClassTest.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2021, 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.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.apimodel.ApiModelMockClassTest.TestClass;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Method;
+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 ApiModelOutlineMethodAndStubClassTest extends TestBase {
+
+  private final AndroidApiLevel libraryClassLevel = AndroidApiLevel.M;
+  private final AndroidApiLevel libraryMethodLevel = AndroidApiLevel.Q;
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/197078995): Make this work on 12.
+    assumeFalse(
+        parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isEqualTo(Version.V12_0_0));
+    boolean libraryClassNotStubbed =
+        parameters.isDexRuntime()
+            && parameters.getApiLevel().isGreaterThanOrEqualTo(libraryClassLevel);
+    Method apiMethod = LibraryClass.class.getDeclaredMethod("foo");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, TestClass.class)
+        .addLibraryClasses(LibraryClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addAndroidBuildVersion()
+        .apply(ApiModelingTestHelper::enableStubbingOfClasses)
+        .apply(ApiModelingTestHelper::enableOutliningOfMethods)
+        .apply(setMockApiLevelForClass(LibraryClass.class, libraryClassLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, libraryClassLevel))
+        .apply(setMockApiLevelForMethod(apiMethod, libraryMethodLevel))
+        .compile()
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters
+                    .getRuntime()
+                    .maxSupportedApiLevel()
+                    .isGreaterThanOrEqualTo(libraryClassLevel),
+            b -> b.addBootClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(libraryClassNotStubbed, "LibraryClass::foo")
+        .assertSuccessWithOutputLinesIf(!libraryClassNotStubbed, "Hello World")
+        .inspect(verifyThat(parameters, LibraryClass.class).stubbedUntil(libraryClassLevel))
+        .inspect(
+            verifyThat(parameters, apiMethod)
+                .isOutlinedFromUntil(
+                    Main.class.getDeclaredMethod("main", String[].class), libraryMethodLevel));
+  }
+
+  // Only present from api level 23.
+  public static class LibraryClass {
+
+    // Only present from api level 30
+    public void foo() {
+      System.out.println("LibraryClass::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 23) {
+        new LibraryClass().foo();
+      } else {
+        System.out.println("Hello World");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodMissingClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodMissingClassTest.java
index 4d6a526..9d0f7cf 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodMissingClassTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodMissingClassTest.java
@@ -7,12 +7,12 @@
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
 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 static org.junit.Assume.assumeFalse;
 
 import com.android.tools.r8.NeverInline;
@@ -20,11 +20,8 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.references.MethodReference;
-import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.lang.reflect.Method;
@@ -59,7 +56,7 @@
         !preMockApis && parameters.getApiLevel().isGreaterThanOrEqualTo(finalLibraryMethodLevel);
     boolean betweenMockApis = !preMockApis && !postMockApis;
     Method addedOn23 = LibraryClass.class.getMethod("addedOn23");
-    Method adeddOn27 = LibraryClass.class.getMethod("addedOn27");
+    Method addedOn27 = LibraryClass.class.getMethod("addedOn27");
     testForR8(parameters.getBackend())
         .addProgramClasses(Main.class, TestClass.class)
         .addLibraryClasses(LibraryClass.class)
@@ -72,7 +69,7 @@
             setMockApiLevelForDefaultInstanceInitializer(
                 LibraryClass.class, initialLibraryMockLevel))
         .apply(setMockApiLevelForMethod(addedOn23, initialLibraryMockLevel))
-        .apply(setMockApiLevelForMethod(adeddOn27, finalLibraryMethodLevel))
+        .apply(setMockApiLevelForMethod(addedOn27, finalLibraryMethodLevel))
         .apply(ApiModelingTestHelper::enableOutliningOfMethods)
         .enableInliningAnnotations()
         .compile()
@@ -103,27 +100,9 @@
                 assertEquals(3, inspector.allClasses().size());
                 return;
               }
-              ClassSubject testClass = inspector.clazz(TestClass.class);
-              assertThat(testClass, isPresent());
-              MethodSubject testMethod = testClass.uniqueMethodWithName("test");
-              assertThat(testMethod, isPresent());
-              Optional<FoundMethodSubject> synthesizedAddedOn27 =
-                  inspector.allClasses().stream()
-                      .flatMap(clazz -> clazz.allMethods().stream())
-                      .filter(
-                          methodSubject ->
-                              methodSubject.isSynthetic()
-                                  && invokesMethodWithName("addedOn27").matches(methodSubject))
-                      .findFirst();
-              Optional<FoundMethodSubject> synthesizedMissingAndReferenced =
-                  inspector.allClasses().stream()
-                      .flatMap(clazz -> clazz.allMethods().stream())
-                      .filter(
-                          methodSubject ->
-                              methodSubject.isSynthetic()
-                                  && invokesMethodWithName("missingAndReferenced")
-                                      .matches(methodSubject))
-                      .findFirst();
+              Method testMethod = TestClass.class.getDeclaredMethod("test");
+              MethodSubject testMethodSubject = inspector.method(testMethod);
+              assertThat(testMethodSubject, isPresent());
               Optional<FoundMethodSubject> synthesizedMissingNotReferenced =
                   inspector.allClasses().stream()
                       .flatMap(clazz -> clazz.allMethods().stream())
@@ -134,64 +113,19 @@
                                       .matches(methodSubject))
                       .findFirst();
               assertFalse(synthesizedMissingNotReferenced.isPresent());
-              if (parameters.getApiLevel().isLessThan(AndroidApiLevel.M)) {
-                assertEquals(3, inspector.allClasses().size());
-                assertFalse(synthesizedAddedOn27.isPresent());
-                assertFalse(synthesizedMissingAndReferenced.isPresent());
-              } else if (parameters.getApiLevel().isLessThan(AndroidApiLevel.O_MR1)) {
+              verifyThat(parameters, addedOn23).isOutlinedFromUntil(testMethod, AndroidApiLevel.M);
+              verifyThat(parameters, addedOn27)
+                  .isOutlinedFromUntil(testMethod, AndroidApiLevel.O_MR1);
+              verifyThat(parameters, LibraryClass.class.getDeclaredMethod("missingAndReferenced"))
+                  .isOutlinedFrom(testMethod);
+              if (parameters.getApiLevel().isLessThan(AndroidApiLevel.O_MR1)) {
                 assertEquals(5, inspector.allClasses().size());
-                assertTrue(synthesizedAddedOn27.isPresent());
-                inspectRewrittenToOutline(testMethod, synthesizedAddedOn27.get(), "addedOn27");
-                assertTrue(synthesizedMissingAndReferenced.isPresent());
-                inspectRewrittenToOutline(
-                    testMethod, synthesizedMissingAndReferenced.get(), "missingAndReferenced");
               } else {
                 assertEquals(4, inspector.allClasses().size());
-                assertFalse(synthesizedAddedOn27.isPresent());
-                assertTrue(synthesizedMissingAndReferenced.isPresent());
-                inspectRewrittenToOutline(
-                    testMethod, synthesizedMissingAndReferenced.get(), "missingAndReferenced");
               }
             });
   }
 
-  private void inspectRewrittenToOutline(
-      MethodSubject callerSubject, FoundMethodSubject outline, String apiMethodName)
-      throws Exception {
-    // Check that the library reference is no longer present.
-    MethodReference libraryMethodReference =
-        Reference.methodFromMethod(LibraryClass.class.getDeclaredMethod(apiMethodName));
-    assertFalse(
-        callerSubject
-            .streamInstructions()
-            .anyMatch(
-                instructionSubject -> {
-                  if (!instructionSubject.isInvoke()) {
-                    return false;
-                  }
-                  return instructionSubject
-                      .getMethod()
-                      .asMethodReference()
-                      .equals(libraryMethodReference);
-                }));
-    MethodReference outlineReference = outline.getMethod().getReference().asMethodReference();
-    assertEquals(
-        1,
-        callerSubject
-            .streamInstructions()
-            .filter(
-                instructionSubject -> {
-                  if (!instructionSubject.isInvoke()) {
-                    return false;
-                  }
-                  return instructionSubject
-                      .getMethod()
-                      .asMethodReference()
-                      .equals(outlineReference);
-                })
-            .count());
-  }
-
   // Only present from api level 23.
   public static class LibraryClass {
 
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
index c109def..70d4f36 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
@@ -4,10 +4,13 @@
 
 package com.android.tools.r8.apimodel;
 
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestCompilerBuilder;
@@ -20,12 +23,15 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
 
 public abstract class ApiModelingTestHelper {
 
@@ -223,5 +229,40 @@
         assertThat(target, not(CodeMatchers.invokesMethod(candidate)));
       };
     }
+
+    ThrowingConsumer<CodeInspector, Exception> isOutlinedFromUntil(
+        Method method, AndroidApiLevel apiLevel) {
+      return parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(apiLevel)
+          ? isOutlinedFrom(method)
+          : isNotOutlinedFrom(method);
+    }
+
+    ThrowingConsumer<CodeInspector, Exception> isOutlinedFrom(Method method) {
+      return inspector -> {
+        // Check that the call is in a synthetic class.
+        List<FoundMethodSubject> outlinedMethod =
+            inspector.allClasses().stream()
+                .flatMap(clazz -> clazz.allMethods().stream())
+                .filter(
+                    methodSubject ->
+                        methodSubject.isSynthetic()
+                            && invokesMethodWithName(methodOfInterest.getMethodName())
+                                .matches(methodSubject))
+                .collect(Collectors.toList());
+        assertEquals(1, outlinedMethod.size());
+        // Assert that method invokes the outline
+        MethodSubject caller = inspector.method(method);
+        assertThat(caller, isPresent());
+        assertThat(caller, invokesMethod(outlinedMethod.get(0)));
+      };
+    }
+
+    ThrowingConsumer<CodeInspector, Exception> isNotOutlinedFrom(Method method) {
+      return inspector -> {
+        MethodSubject caller = inspector.method(method);
+        assertThat(caller, isPresent());
+        assertThat(caller, invokesMethodWithName(methodOfInterest.getMethodName()));
+      };
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTest.java b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTest.java
index 7e969c8..596a3f1 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTest.java
@@ -4,8 +4,11 @@
 
 package com.android.tools.r8.debuginfo;
 
+import com.android.tools.r8.AlwaysInline;
+
 public class DexPcWithDebugInfoForOverloadedMethodsTest {
 
+  @AlwaysInline
   private static void inlinee(String message) {
     if (System.currentTimeMillis() > 0) {
       throw new RuntimeException(message);
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
index b3c1c08..a2f71b4 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.retrace.RetraceFrameResult;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -67,6 +68,8 @@
         .addKeepMainRule(MAIN)
         .addKeepMethodRules(MAIN, "void overloaded(...)")
         .addKeepAttributeLineNumberTable()
+        .addKeepAttributes(ProguardKeepAttributes.SOURCE_FILE)
+        .enableAlwaysInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
         .assertFailureWithErrorThatMatches(containsString(EXPECTED))
@@ -88,13 +91,13 @@
                           Reference.methodFromMethod(
                               MAIN.getDeclaredMethod("inlinee", String.class)),
                           MINIFIED_LINE_POSITION,
-                          11,
+                          14,
                           FILENAME_INLINE),
                       LinePosition.create(
                           Reference.methodFromMethod(
                               MAIN.getDeclaredMethod("overloaded", String.class)),
                           MINIFIED_LINE_POSITION,
-                          20,
+                          23,
                           FILENAME_INLINE));
               RetraceFrameResult retraceResult =
                   throwingSubject
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
new file mode 100644
index 0000000..4d861a0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
@@ -0,0 +1,133 @@
+// Copyright (c) 2021, 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.desugar.desugaredlibrary.specification;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.StringResource;
+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.ir.desugar.desugaredlibrary.humanspecification.HumanRewritingFlags;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanTopLevelFlags;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.MultiAPILevelHumanDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.MultiAPILevelHumanDesugaredLibrarySpecificationParser;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.MultiAPILevelLegacyDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.MultiAPILevelLegacyDesugaredLibrarySpecificationParser;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.specificationconversion.LegacyToHumanSpecificationConverter;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.InternalOptions;
+import java.io.IOException;
+import java.util.Map;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ConvertExportReadTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public ConvertExportReadTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testMultiLevel() throws IOException {
+    Assume.assumeTrue(ToolHelper.isLocalDevelopment());
+
+    LegacyToHumanSpecificationConverter converter = new LegacyToHumanSpecificationConverter();
+
+    InternalOptions options = new InternalOptions();
+
+    MultiAPILevelLegacyDesugaredLibrarySpecification spec =
+        new MultiAPILevelLegacyDesugaredLibrarySpecificationParser(
+                options.dexItemFactory(), options.reporter)
+            .parseMultiLevelConfiguration(
+                StringResource.fromFile(ToolHelper.getDesugarLibJsonForTesting()));
+
+    MultiAPILevelHumanDesugaredLibrarySpecification humanSpec1 =
+        converter.convertAllAPILevels(spec, ToolHelper.getAndroidJar(31), options);
+
+    Box<String> json = new Box<>();
+    MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.export(
+        humanSpec1, (string, handler) -> json.set(string));
+    MultiAPILevelHumanDesugaredLibrarySpecification humanSpec2 =
+        new MultiAPILevelHumanDesugaredLibrarySpecificationParser(
+                options.dexItemFactory(), options.reporter)
+            .parseMultiLevelConfiguration(StringResource.fromString(json.get(), Origin.unknown()));
+
+    assertSpecEquals(humanSpec1, humanSpec2);
+  }
+
+  private void assertSpecEquals(
+      MultiAPILevelHumanDesugaredLibrarySpecification humanSpec1,
+      MultiAPILevelHumanDesugaredLibrarySpecification humanSpec2) {
+    assertTopLevelFlagsEquals(humanSpec1.getTopLevelFlags(), humanSpec2.getTopLevelFlags());
+    assertFlagMapEquals(
+        humanSpec1.getCommonFlagsForTesting(), humanSpec2.getCommonFlagsForTesting());
+    assertFlagMapEquals(
+        humanSpec1.getLibraryFlagsForTesting(), humanSpec2.getLibraryFlagsForTesting());
+    assertFlagMapEquals(
+        humanSpec1.getProgramFlagsForTesting(), humanSpec2.getProgramFlagsForTesting());
+  }
+
+  private void assertFlagMapEquals(
+      Map<Integer, HumanRewritingFlags> commonFlags1,
+      Map<Integer, HumanRewritingFlags> commonFlags2) {
+    assertEquals(commonFlags1.size(), commonFlags2.size());
+    for (int integer : commonFlags1.keySet()) {
+      assertTrue(commonFlags2.containsKey(integer));
+      assertFlagsEquals(commonFlags1.get(integer), commonFlags2.get(integer));
+    }
+  }
+
+  private void assertFlagsEquals(
+      HumanRewritingFlags humanRewritingFlags1, HumanRewritingFlags humanRewritingFlags2) {
+    assertEquals(humanRewritingFlags1.getRewritePrefix(), humanRewritingFlags2.getRewritePrefix());
+    assertEquals(
+        humanRewritingFlags1.getBackportCoreLibraryMember(),
+        humanRewritingFlags2.getBackportCoreLibraryMember());
+    assertEquals(
+        humanRewritingFlags1.getCustomConversions(), humanRewritingFlags2.getCustomConversions());
+    assertEquals(
+        humanRewritingFlags1.getEmulateLibraryInterface(),
+        humanRewritingFlags2.getEmulateLibraryInterface());
+    assertEquals(
+        humanRewritingFlags1.getRetargetCoreLibMember(),
+        humanRewritingFlags2.getRetargetCoreLibMember());
+
+    assertEquals(
+        humanRewritingFlags1.getDontRetargetLibMember(),
+        humanRewritingFlags2.getDontRetargetLibMember());
+    assertEquals(
+        humanRewritingFlags1.getDontRewriteInvocation(),
+        humanRewritingFlags2.getDontRewriteInvocation());
+    assertEquals(
+        humanRewritingFlags1.getWrapperConversions(), humanRewritingFlags2.getWrapperConversions());
+  }
+
+  private void assertTopLevelFlagsEquals(
+      HumanTopLevelFlags topLevelFlags1, HumanTopLevelFlags topLevelFlags2) {
+    assertEquals(topLevelFlags1.getExtraKeepRules(), topLevelFlags2.getExtraKeepRules());
+    assertEquals(topLevelFlags1.getIdentifier(), topLevelFlags2.getIdentifier());
+    assertEquals(
+        topLevelFlags1.getRequiredCompilationAPILevel().getLevel(),
+        topLevelFlags2.getRequiredCompilationAPILevel().getLevel());
+    assertEquals(
+        topLevelFlags1.getSynthesizedLibraryClassesPackagePrefix(),
+        topLevelFlags2.getSynthesizedLibraryClassesPackagePrefix());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java b/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
new file mode 100644
index 0000000..6b593fd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/opensourceapps/TiviTest.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2021, 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.internal.opensourceapps;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+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.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.BeforeClass;
+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 TiviTest extends TestBase {
+
+  private static Path outDirectory;
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @BeforeClass
+  public static void setup() throws IOException {
+    assumeTrue(ToolHelper.isLocalDevelopment());
+    outDirectory = getStaticTemp().newFolder().toPath();
+    ZipUtils.unzip(Paths.get("third_party/opensource-apps/tivi/dump_app.zip"), outDirectory);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(Backend.DEX)
+        .addProgramFiles(outDirectory.resolve("program.jar"))
+        .apply(this::configure)
+        .compile();
+  }
+
+  @Test
+  public void testR8Recompilation() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(Backend.CF)
+            .addProgramFiles(outDirectory.resolve("program.jar"))
+            .apply(this::configure)
+            .compile();
+    testForR8(Backend.DEX)
+        .addProgramFiles(compileResult.writeToZip())
+        .apply(this::configure)
+        .compile();
+  }
+
+  @Test
+  public void testR8Compat() throws Exception {
+    testForR8Compat(Backend.DEX)
+        .addProgramFiles(outDirectory.resolve("program.jar"))
+        .apply(this::configure)
+        .compile();
+  }
+
+  @Test
+  public void testR8CompatRecompilation() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8Compat(Backend.CF)
+            .addProgramFiles(outDirectory.resolve("program.jar"))
+            .apply(this::configure)
+            .compile();
+    testForR8Compat(Backend.DEX)
+        .addProgramFiles(compileResult.writeToZip())
+        .apply(this::configure)
+        .compile();
+  }
+
+  private void configure(R8TestBuilder<?> testBuilder) {
+    testBuilder
+        .addClasspathFiles(outDirectory.resolve("classpath.jar"))
+        .addLibraryFiles(outDirectory.resolve("library.jar"))
+        .addKeepRuleFiles(outDirectory.resolve("proguard.config"))
+        .setMinApi(AndroidApiLevel.M)
+        .allowDiagnosticMessages()
+        .allowUnusedDontWarnPatterns()
+        .allowUnusedProguardConfigurationRules();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java b/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java
index a24f51f..35762f4 100644
--- a/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java
@@ -15,7 +15,7 @@
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.ir.conversion.callgraph.Node;
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import java.util.Collections;
 
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java b/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java
index 2611dcd..5a6c914 100644
--- a/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.conversion;
 
+import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -12,8 +13,8 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
-import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator;
+import com.android.tools.r8.ir.conversion.callgraph.CycleEliminator;
+import com.android.tools.r8.ir.conversion.callgraph.Node;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
@@ -163,7 +164,7 @@
         if (configuration.forceInline.contains(node)) {
           node.getMethod().getMutableOptimizationInfo().markForceInline();
         } else {
-          node.getMethod().getMutableOptimizationInfo().unsetForceInline();
+          getSimpleFeedback().unsetForceInline(node.getProgramMethod());
         }
       }
 
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java b/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java
index 1459b20..030465c 100644
--- a/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java
@@ -9,8 +9,9 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
-import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraph;
+import com.android.tools.r8.ir.conversion.callgraph.CycleEliminator;
+import com.android.tools.r8.ir.conversion.callgraph.Node;
 import java.util.Set;
 import java.util.TreeSet;
 import org.junit.Test;
@@ -46,7 +47,7 @@
     nodes.add(n5);
     nodes.add(n6);
 
-    CallGraph cg = new CallGraph(nodes);
+    CallGraph cg = CallGraph.createForTesting(nodes);
     Set<DexEncodedMethod> wave = cg.extractLeaves().toDefinitionSet();
     assertEquals(3, wave.size());
     assertThat(wave, hasItem(n3.getMethod()));
@@ -61,7 +62,7 @@
     wave = cg.extractLeaves().toDefinitionSet();
     assertEquals(1, wave.size());
     assertThat(wave, hasItem(n1.getMethod()));
-    assertTrue(nodes.isEmpty());
+    assertTrue(cg.isEmpty());
   }
 
   @Test
@@ -95,7 +96,7 @@
     CycleEliminator cycleEliminator = new CycleEliminator();
     assertEquals(1, cycleEliminator.breakCycles(nodes).numberOfRemovedCallEdges());
 
-    CallGraph cg = new CallGraph(nodes);
+    CallGraph cg = CallGraph.createForTesting(nodes);
     Set<DexEncodedMethod> wave = cg.extractLeaves().toDefinitionSet();
     assertEquals(3, wave.size());
     assertThat(wave, hasItem(n3.getMethod()));
@@ -112,7 +113,7 @@
     wave = cg.extractLeaves().toDefinitionSet();
     assertEquals(1, wave.size());
     assertThat(wave, hasItem(n1.getMethod()));
-    assertTrue(nodes.isEmpty());
+    assertTrue(cg.isEmpty());
   }
 
   @Test
@@ -141,7 +142,7 @@
     nodes.add(n5);
     nodes.add(n6);
 
-    CallGraph callGraph = new CallGraph(nodes, null);
+    CallGraph callGraph = CallGraph.createForTesting(nodes);
     Set<DexEncodedMethod> wave = callGraph.extractRoots().toDefinitionSet();
     assertEquals(2, wave.size());
     assertThat(wave, hasItem(n1.getMethod()));
@@ -156,7 +157,7 @@
     assertEquals(2, wave.size());
     assertThat(wave, hasItem(n3.getMethod()));
     assertThat(wave, hasItem(n4.getMethod()));
-    assertTrue(nodes.isEmpty());
+    assertTrue(callGraph.isEmpty());
   }
 
   @Test
@@ -190,7 +191,7 @@
     CycleEliminator cycleEliminator = new CycleEliminator();
     assertEquals(1, cycleEliminator.breakCycles(nodes).numberOfRemovedCallEdges());
 
-    CallGraph callGraph = new CallGraph(nodes, null);
+    CallGraph callGraph = CallGraph.createForTesting(nodes);
     Set<DexEncodedMethod> wave = callGraph.extractRoots().toDefinitionSet();
     assertEquals(2, wave.size());
     assertThat(wave, hasItem(n1.getMethod()));
@@ -205,6 +206,6 @@
     assertEquals(2, wave.size());
     assertThat(wave, hasItem(n3.getMethod()));
     assertThat(wave, hasItem(n4.getMethod()));
-    assertTrue(nodes.isEmpty());
+    assertTrue(callGraph.isEmpty());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
index 36acfe9..f5b16f4 100644
--- a/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
+++ b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
@@ -15,7 +15,10 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraph;
+import com.android.tools.r8.ir.conversion.callgraph.CallGraphBuilder;
+import com.android.tools.r8.ir.conversion.callgraph.Node;
+import com.android.tools.r8.ir.conversion.callgraph.PartialCallGraphBuilder;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
 import com.android.tools.r8.utils.AndroidApp;
@@ -58,12 +61,12 @@
   @Test
   public void testFullGraph() throws Exception {
     CallGraph cg = new CallGraphBuilder(appView).build(executorService, Timing.empty());
-    Node m1 = findNode(cg.nodes, "m1");
-    Node m2 = findNode(cg.nodes, "m2");
-    Node m3 = findNode(cg.nodes, "m3");
-    Node m4 = findNode(cg.nodes, "m4");
-    Node m5 = findNode(cg.nodes, "m5");
-    Node m6 = findNode(cg.nodes, "m6");
+    Node m1 = findNode(cg.getNodes(), "m1");
+    Node m2 = findNode(cg.getNodes(), "m2");
+    Node m3 = findNode(cg.getNodes(), "m3");
+    Node m4 = findNode(cg.getNodes(), "m4");
+    Node m5 = findNode(cg.getNodes(), "m5");
+    Node m6 = findNode(cg.getNodes(), "m6");
     assertNotNull(m1);
     assertNotNull(m2);
     assertNotNull(m3);
@@ -85,7 +88,7 @@
     wave = cg.extractLeaves().toDefinitionSet();
     assertEquals(1, wave.size());
     assertThat(wave, hasItem(m1.getMethod()));
-    assertTrue(cg.nodes.isEmpty());
+    assertTrue(cg.isEmpty());
   }
 
   @Test
@@ -107,10 +110,10 @@
     CallGraph pg =
         new PartialCallGraphBuilder(appView, seeds).build(executorService, Timing.empty());
 
-    Node m1 = findNode(pg.nodes, "m1");
-    Node m2 = findNode(pg.nodes, "m2");
-    Node m4 = findNode(pg.nodes, "m4");
-    Node m5 = findNode(pg.nodes, "m5");
+    Node m1 = findNode(pg.getNodes(), "m1");
+    Node m2 = findNode(pg.getNodes(), "m2");
+    Node m4 = findNode(pg.getNodes(), "m4");
+    Node m5 = findNode(pg.getNodes(), "m5");
     assertNotNull(m1);
     assertNotNull(m2);
     assertNotNull(m4);
@@ -132,7 +135,7 @@
     wave.addAll(pg.extractRoots().toDefinitionSet());
     assertEquals(1, wave.size());
     assertThat(wave, hasItem(m4.getMethod()));
-    assertTrue(pg.nodes.isEmpty());
+    assertTrue(pg.isEmpty());
   }
 
   private Node findNode(Iterable<Node> nodes, String name) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java b/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java
index 15d6a42..9b0bb90 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java
@@ -97,19 +97,19 @@
 
     MethodSubject checkNull = mainSubject.uniqueMethodWithName("checkNull");
     assertThat(checkNull, isPresent());
-    assertEquals(1, countCallToParamNullCheck(checkNull));
+    assertEquals(0, countCallToParamNullCheck(checkNull));
     assertEquals(1, countPrintCall(checkNull));
-    assertEquals(0, countThrow(checkNull));
+    assertEquals(1, countThrow(checkNull));
 
     MethodSubject paramCheck = mainSubject.uniqueMethodWithName("nonNullAfterParamCheck");
     assertThat(paramCheck, isPresent());
     assertEquals(1, countPrintCall(paramCheck));
-    assertEquals(0, countThrow(paramCheck));
+    assertEquals(1, countThrow(paramCheck));
 
     paramCheck = mainSubject.uniqueMethodWithName("nonNullAfterParamCheckDifferently");
     assertThat(paramCheck, isPresent());
     assertEquals(1, countPrintCall(paramCheck));
-    assertEquals(0, countThrow(paramCheck));
+    assertEquals(1, countThrow(paramCheck));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index 42bb70f..fba166a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -333,7 +333,10 @@
     assertCounters(INLINABLE, ALWAYS_INLINABLE, countInvokes(inspector, m));
 
     m = clazz.method("int", "notInlinableDueToSideEffect", ImmutableList.of("inlining.A"));
-    assertCounters(INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
+    assertCounters(
+        parameters.isCfRuntime() ? INLINABLE : NEVER_INLINABLE,
+        NEVER_INLINABLE,
+        countInvokes(inspector, m));
 
     m = clazz.method("int", "notInlinableOnThrow", ImmutableList.of("java.lang.Throwable"));
     assertCounters(ALWAYS_INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index 52fcf43..6ea5d98 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -272,7 +273,9 @@
         Sets.newHashSet("java.lang.StringBuilder", "java.lang.RuntimeException"),
         collectTypes(clazz.uniqueMethodWithName("testInitNeverReturnsNormally")));
 
-    assertThat(inspector.clazz(InvalidRootsTestClass.NeverReturnsNormally.class), isPresent());
+    assertThat(
+        inspector.clazz(InvalidRootsTestClass.NeverReturnsNormally.class),
+        notIf(isPresent(), parameters.isCfRuntime()));
     assertThat(
         inspector.clazz(InvalidRootsTestClass.InitNeverReturnsNormally.class), not(isPresent()));
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnConstantClassIdAfterBranchPruningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnConstantClassIdAfterBranchPruningTest.java
index 9ddf887..f3d6a4d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnConstantClassIdAfterBranchPruningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnConstantClassIdAfterBranchPruningTest.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -42,6 +43,7 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(A.class, B.class, C.class))
+        .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .noMinification()
@@ -80,6 +82,7 @@
   @NeverClassInline
   static class A {
 
+    @NeverInline
     void m() {
       System.out.println("A");
     }
@@ -88,6 +91,7 @@
   @NeverClassInline
   static class B {
 
+    @NeverInline
     void m() {
       System.out.println("B");
     }
@@ -96,6 +100,7 @@
   @NeverClassInline
   static class C {
 
+    @NeverInline
     void m() {
       System.out.println("C");
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnNonConstantClassIdAfterBranchPruningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnNonConstantClassIdAfterBranchPruningTest.java
index a2bfb2c..1c2705e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnNonConstantClassIdAfterBranchPruningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/fields/SwitchOnNonConstantClassIdAfterBranchPruningTest.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -42,6 +43,7 @@
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(A.class, B.class, C.class))
+        .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .noMinification()
@@ -82,6 +84,7 @@
   @NeverClassInline
   static class A {
 
+    @NeverInline
     void m() {
       System.out.println("A");
     }
@@ -90,6 +93,7 @@
   @NeverClassInline
   static class B {
 
+    @NeverInline
     void m() {
       System.out.println("B");
     }
@@ -98,6 +102,7 @@
   @NeverClassInline
   static class C {
 
+    @NeverInline
     void m() {
       System.out.println("C");
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/AvoidInliningRecursiveMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/AvoidInliningRecursiveMethodTest.java
index 3a501a6..d231fdf 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/AvoidInliningRecursiveMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/AvoidInliningRecursiveMethodTest.java
@@ -53,9 +53,7 @@
 
     MethodSubject mainMethodSubject = classSubject.mainMethod();
     assertThat(mainMethodSubject, isPresent());
-
-    // TODO(b/145276800): Should not inline recursive methods.
-    assertTrue(mainMethodSubject.streamInstructions().anyMatch(InstructionSubject::isIf));
+    assertTrue(mainMethodSubject.streamInstructions().noneMatch(InstructionSubject::isIf));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/DoubleInliningNullCheckTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/DoubleInliningNullCheckTest.java
index 0edb4cf..94fba01 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/DoubleInliningNullCheckTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/DoubleInliningNullCheckTest.java
@@ -22,7 +22,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection params() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   private final TestParameters parameters;
@@ -37,16 +37,17 @@
         .addInnerClasses(DoubleInliningNullCheckTest.class)
         .addKeepMainRule(TestClass.class)
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("true")
-        .inspect(codeInspector -> {
-          ClassSubject main = codeInspector.clazz(TestClass.class);
-          assertThat(main, isPresent());
-          MethodSubject mainMethod = main.mainMethod();
-          assertThat(mainMethod, isPresent());
-          assertEquals(0, countCall(mainMethod, "checkParameterIsNotNull"));
-        });
+        .inspect(
+            codeInspector -> {
+              ClassSubject main = codeInspector.clazz(TestClass.class);
+              assertThat(main, isPresent());
+              MethodSubject mainMethod = main.mainMethod();
+              assertThat(mainMethod, isPresent());
+              assertEquals(0, countCall(mainMethod, "checkParameterIsNotNull"));
+            });
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineFunctionalInterfaceMethodImplementedByLambdasTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineFunctionalInterfaceMethodImplementedByLambdasTest.java
index 8a999ff..587795f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineFunctionalInterfaceMethodImplementedByLambdasTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineFunctionalInterfaceMethodImplementedByLambdasTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.inliner;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -52,7 +53,8 @@
       assertThat(inspector.clazz(I.class), isPresent());
     }
 
-    assertThat(inspector.clazz(A.class), not(isPresent()));
+    // When compiling to DEX, A.m() will be single caller inlined in the second optimization pass.
+    assertThat(inspector.clazz(A.class), notIf(isPresent(), parameters.isDexRuntime()));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java
index dc74e0f..841c369 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningIntoVisibilityBridgeTest.java
@@ -47,7 +47,7 @@
 
   @Test
   public void test() throws Exception {
-    String expectedOutput = StringUtils.lines("Hello world", "Hello world", "Hello world");
+    String expectedOutput = StringUtils.times(StringUtils.lines("Hello world"), 6);
 
     R8TestRunResult result =
         testForR8(parameters.getBackend())
@@ -91,8 +91,11 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      // Invoke method three times to prevent the synthetic bridge on InliningIntoVisibilityBridge-
-      // TestClassB from being inlined.
+      // Invoke method multiple times to prevent the synthetic bridge on
+      // InliningIntoVisibilityBridgeTestClassB from being inlined.
+      InliningIntoVisibilityBridgeTestClassC.method();
+      InliningIntoVisibilityBridgeTestClassC.method();
+      InliningIntoVisibilityBridgeTestClassC.method();
       InliningIntoVisibilityBridgeTestClassC.method();
       InliningIntoVisibilityBridgeTestClassC.method();
       InliningIntoVisibilityBridgeTestClassC.method();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetFromExactReceiverTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetFromExactReceiverTypeTest.java
index 7c9b7b7..e7b6931 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetFromExactReceiverTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetFromExactReceiverTypeTest.java
@@ -10,6 +10,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.AlwaysInline;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
@@ -32,7 +33,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public SingleTargetFromExactReceiverTypeTest(TestParameters parameters) {
@@ -48,9 +49,10 @@
             "-keepclassmembers class " + A.class.getTypeName() + " {",
             "  void cannotBeInlinedDueToKeepRule();",
             "}")
+        .enableAlwaysInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyOnlyCanBeInlinedHasBeenInlined)
         .run(parameters.getRuntime(), TestClass.class)
@@ -135,6 +137,7 @@
       System.out.println("A.canBeInlined()");
     }
 
+    @AlwaysInline
     public void canBeInlinedDueToAssume() {
       System.out.println("A.canBeInlinedDueToAssume()");
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index 615aa20..f3a61c4 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -205,8 +205,6 @@
     assertEquals(
         Lists.newArrayList(
             "STATIC: String TrivialTestClass.next()",
-            "STATIC: void SimpleWithThrowingGetter.getInstance()",
-            "STATIC: void SimpleWithThrowingGetter.getInstance()",
             "SimpleWithThrowingGetter SimpleWithThrowingGetter.INSTANCE",
             "VIRTUAL: String SimpleWithThrowingGetter.bar(String)",
             "VIRTUAL: String SimpleWithThrowingGetter.foo()"),
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineChainTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineChainTest.java
index da2f568..f72637c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineChainTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineChainTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
@@ -77,9 +78,13 @@
               MethodSubject main = mainClass.mainMethod();
               long checkParameterIsNotNull = countCall(main, "checkParameterIsNotNull");
               long checkNotNullParameter = countCall(main, "checkNotNullParameter");
-              if (kotlinParameters.is(KotlinCompilerVersion.KOTLINC_1_3_72)) {
-                assertEquals(1, checkParameterIsNotNull);
+              if (parameters.isDexRuntime()
+                  && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.I)) {
                 assertEquals(0, checkNotNullParameter);
+                assertEquals(0, checkParameterIsNotNull);
+              } else if (kotlinc.is(KotlinCompilerVersion.KOTLINC_1_3_72)) {
+                assertEquals(0, checkNotNullParameter);
+                assertEquals(1, checkParameterIsNotNull);
               } else {
                 assertEquals(1, checkNotNullParameter);
                 assertEquals(0, checkParameterIsNotNull);
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java
index a03ebb2..6d64e37 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -90,7 +91,10 @@
   @Test
   public void b139432507_isSupported() throws Exception {
     assumeTrue("Different inlining behavior on CF backend", parameters.isDexRuntime());
-    testSingle("isSupported", kotlinc.is(KotlinCompilerVersion.KOTLINC_1_3_72));
+    testSingle(
+        "isSupported",
+        kotlinc.is(KotlinCompilerVersion.KOTLINC_1_3_72)
+            && parameters.getApiLevel().isLessThan(AndroidApiLevel.I));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index 8544744..3486422 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -4,6 +4,10 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_3_72;
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLIN_DEV;
+
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
@@ -212,8 +216,16 @@
                     .addOptionsModification(disableClassInliner))
         .inspect(
             inspector -> {
-              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
-              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              // TODO(b/210828502): Investigate why Person is not removed with kotlin dev.
+              if (allowAccessModification
+                  && !(kotlinc.isOneOf(KOTLINC_1_3_72, KOTLINC_1_4_20, KOTLIN_DEV)
+                      && testParameters.isCfRuntime())) {
+                checkClassIsRemoved(inspector, TEST_DATA_CLASS.getClassName());
+              } else {
+                ClassSubject dataClass =
+                    checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
+                checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              }
             });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 5a60483..c979be9 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -556,11 +556,7 @@
             PACKAGE_NAME,
             mainClass,
             testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
-        .inspect(
-            inspector -> {
-              checkClassIsRemoved(inspector, testedClass.getClassName());
-              checkClassIsRemoved(inspector, testedClass.getClassName());
-            });
+        .inspect(inspector -> checkClassIsRemoved(inspector, testedClass.getClassName()));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/naming/adaptclassstrings/AdaptClassStringKeepTest.java b/src/test/java/com/android/tools/r8/naming/adaptclassstrings/AdaptClassStringKeepTest.java
new file mode 100644
index 0000000..fd9cce7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/adaptclassstrings/AdaptClassStringKeepTest.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2021, 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.naming.adaptclassstrings;
+
+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.Assume.assumeTrue;
+
+import com.android.tools.r8.ProguardVersion;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import 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 AdaptClassStringKeepTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean isCompat;
+
+  @Parameter(2)
+  public ProguardVersion proguardVersion;
+
+  @Parameters(name = "{0}, isCompat: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withSystemRuntime().build(),
+        BooleanUtils.values(),
+        ProguardVersion.values());
+  }
+
+  @Test
+  public void testProguard() throws Exception {
+    assumeTrue(isCompat);
+    testForProguard(proguardVersion)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-adaptclassstrings")
+        .addDontWarn(AdaptClassStringKeepTest.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("com.android.tools.r8.naming.adaptclassstrings.a")
+        .inspect(inspector -> assertThat(inspector.clazz(Foo.class), isPresent()));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(proguardVersion == ProguardVersion.getLatest());
+    (isCompat ? testForR8Compat(parameters.getBackend()) : testForR8(parameters.getBackend()))
+        .addInnerClasses(getClass())
+        .setMinApi(AndroidApiLevel.B)
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-adaptclassstrings")
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(Foo.class.getName())
+        // TODO(b/210825389): We currently interpret -adaptclasstrings without pinning the class
+        .inspect(inspector -> assertThat(inspector.clazz(Foo.class), isAbsent()));
+  }
+
+  public static class Foo {}
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(
+          "com.android.tools.r8.naming.adaptclassstrings.AdaptClassStringKeepTest$Foo");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/adaptclassstrings/AdaptClassStringWithRepackagingTest.java b/src/test/java/com/android/tools/r8/naming/adaptclassstrings/AdaptClassStringWithRepackagingTest.java
new file mode 100644
index 0000000..06557fa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/adaptclassstrings/AdaptClassStringWithRepackagingTest.java
@@ -0,0 +1,52 @@
+// Copyright (c) 2021, 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.naming.adaptclassstrings;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class AdaptClassStringWithRepackagingTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-repackageclasses ''")
+        .addKeepRules("-adaptclassstrings")
+        .addKeepClassRulesWithAllowObfuscation(Foo.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("a")
+        .inspect(inspector -> assertThat(inspector.clazz(Foo.class), isPresentAndRenamed()));
+  }
+
+  public static class Foo {}
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(
+          "com.android.tools.r8.naming.adaptclassstrings.AdaptClassStringWithRepackagingTest$Foo");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/adaptclassstrings/RepackageMinificationNameClashTest.java b/src/test/java/com/android/tools/r8/naming/adaptclassstrings/RepackageMinificationNameClashTest.java
new file mode 100644
index 0000000..1f23283
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/adaptclassstrings/RepackageMinificationNameClashTest.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2021, 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.naming.adaptclassstrings;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+/** This is a regression test for b/210699098 */
+public class RepackageMinificationNameClashTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules("-repackageclasses ''")
+        .addKeepRules("-adaptclassstrings")
+        .addKeepClassRulesWithAllowObfuscation(Foo.class)
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("RepackageMinificationNameClashTest$Foo");
+  }
+
+  public static class Foo {}
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println("RepackageMinificationNameClashTest$Foo");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
index 3cb240b..57b70d2 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
-import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -80,9 +79,7 @@
     }
   }
 
-  private static final Class<?>[] LIBRARY_CLASSES = {
-    NoVerticalClassMerging.class, LibraryBase.class, LibrarySubclass.class
-  };
+  private static final Class<?>[] LIBRARY_CLASSES = {LibraryBase.class, LibrarySubclass.class};
 
   private static final Class<?>[] PROGRAM_CLASSES = {
       ProgramClass.class
@@ -107,13 +104,15 @@
   }
 
   private static R8TestCompileResult compileLibrary(Backend backend)
-      throws CompilationFailedException, IOException, ExecutionException {
+      throws CompilationFailedException, IOException {
     return testForR8(staticTemp, backend)
         .enableInliningAnnotations()
         .enableMemberValuePropagationAnnotations()
         .addProgramClasses(LIBRARY_CLASSES)
         .addKeepMainRule(LibrarySubclass.class)
         .addKeepClassAndDefaultConstructor(LibrarySubclass.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(LibraryBase.class))
         .setMinApi(AndroidApiLevel.B)
         .compile()
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/resolution/InvokeSuperCallInStaticTest.java b/src/test/java/com/android/tools/r8/resolution/InvokeSuperCallInStaticTest.java
index 09e9882..8d5a997 100644
--- a/src/test/java/com/android/tools/r8/resolution/InvokeSuperCallInStaticTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/InvokeSuperCallInStaticTest.java
@@ -84,6 +84,7 @@
         .addProgramClasses(Base.class, Main.class)
         .addProgramClassFileData(getAWithRewrittenInvokeSpecialToBase())
         .addKeepMainRule(Main.class)
+        .allowAccessModification()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines(EXPECTED)
diff --git a/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java b/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java
index c6efef7..4726fe4 100644
--- a/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java
@@ -3,9 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.resolution;
 
+import static org.junit.Assume.assumeTrue;
+
 import com.android.tools.r8.AsmTestBase;
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.resolution.singletarget.Main;
 import com.android.tools.r8.resolution.singletarget.one.AbstractSubClass;
 import com.android.tools.r8.resolution.singletarget.one.AbstractTopClass;
@@ -14,12 +17,14 @@
 import com.android.tools.r8.resolution.singletarget.one.SubSubClassOne;
 import com.android.tools.r8.resolution.singletarget.one.SubSubClassThree;
 import com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 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)
@@ -39,57 +44,26 @@
       getBytesFromAsmClass(IrrelevantInterfaceWithDefaultDump::dump)
   );
 
-  public static final String EXPECTED =
-      StringUtils.lines(
-          "SubSubClassOne",
-          "SubSubClassOne",
-          "AbstractTopClass",
-          "SubSubClassOne",
-          "AbstractTopClass",
-          "com.android.tools.r8.resolution.singletarget.one.AbstractSubClass",
-          "InterfaceWithDefault",
-          "InterfaceWithDefault",
-          "ICCE",
-          "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
-          "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
-          "AbstractTopClass",
-          "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
-          "AbstractTopClass",
-          "com.android.tools.r8.resolution.singletarget.one.AbstractSubClass",
-          "InterfaceWithDefault",
-          "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
-          "InterfaceWithDefault",
-          "InterfaceWithDefault",
-          "InterfaceWithDefault",
-          "ICCE",
-          "InterfaceWithDefault",
-          "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
-          "InterfaceWithDefault",
-          "InterfaceWithDefault",
-          "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
-          "InterfaceWithDefault",
-          "InterfaceWithDefault",
-          "InterfaceWithDefault",
-          "ICCE");
+  @Parameter(0)
+  public boolean enableInliningAnnotations;
 
-  public final TestParameters parameters;
+  @Parameter(1)
+  public TestParameters parameters;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
-
-  public SingleTargetExecutionTest(TestParameters parameters) {
-    this.parameters = parameters;
+  @Parameters(name = "{1}, enable inlining annotations: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   @Test
   public void testReference() throws Exception {
+    assumeTrue(enableInliningAnnotations);
     testForRuntime(parameters)
         .addProgramClasses(CLASSES)
         .addProgramClassFileData(ASM_CLASSES)
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutput(EXPECTED);
+        .assertSuccessWithOutput(getExpectedOutput());
   }
 
   @Test
@@ -99,9 +73,51 @@
         .addProgramClasses(CLASSES)
         .addProgramClassFileData(ASM_CLASSES)
         .addKeepMainRule(Main.class)
+        .applyIf(
+            enableInliningAnnotations,
+            R8TestBuilder::enableInliningAnnotations,
+            TestShrinkerBuilder::addInliningAnnotations)
         .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutput(EXPECTED);
+        .assertSuccessWithOutput(getExpectedOutput());
+  }
+
+  private String getExpectedOutput() {
+    String icceOrNot =
+        enableInliningAnnotations || !parameters.canUseDefaultAndStaticInterfaceMethods()
+            ? "ICCE"
+            : "InterfaceWithDefault";
+    return StringUtils.lines(
+        "SubSubClassOne",
+        "SubSubClassOne",
+        "AbstractTopClass",
+        "SubSubClassOne",
+        "AbstractTopClass",
+        "com.android.tools.r8.resolution.singletarget.one.AbstractSubClass",
+        "InterfaceWithDefault",
+        "InterfaceWithDefault",
+        icceOrNot,
+        "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
+        "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
+        "AbstractTopClass",
+        "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
+        "AbstractTopClass",
+        "com.android.tools.r8.resolution.singletarget.one.AbstractSubClass",
+        "InterfaceWithDefault",
+        "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
+        "InterfaceWithDefault",
+        "InterfaceWithDefault",
+        "InterfaceWithDefault",
+        icceOrNot,
+        "InterfaceWithDefault",
+        "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
+        "InterfaceWithDefault",
+        "InterfaceWithDefault",
+        "com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo",
+        "InterfaceWithDefault",
+        "InterfaceWithDefault",
+        "InterfaceWithDefault",
+        "ICCE");
   }
 }
diff --git a/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java b/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java
index f0faedd..b25ece9 100644
--- a/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java
+++ b/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.resolution.singletarget.one;
 
+import com.android.tools.r8.NeverInline;
+
 public interface InterfaceWithDefault {
 
   // Avoid InterfaceWithDefault.class.getCanonicalName() as it may change during shrinking.
@@ -16,6 +18,7 @@
     System.out.println(TAG);
   }
 
+  @NeverInline
   default void overriddenInOtherInterface() {
     System.out.println(TAG);
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/LambdaCaptureShrinkingTest.java b/src/test/java/com/android/tools/r8/shaking/LambdaCaptureShrinkingTest.java
new file mode 100644
index 0000000..4efaeb4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/LambdaCaptureShrinkingTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2021, 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 junit.framework.TestCase.assertTrue;
+
+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.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Arrays;
+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 LambdaCaptureShrinkingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withCfRuntimes()
+        .withDexRuntimes()
+        .withApiLevelsStartingAtIncluding(AndroidApiLevel.N)
+        .build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .applyIf(
+            parameters.isCfRuntime(),
+            compileResult ->
+                compileResult.inspect(
+                    inspector -> {
+                      MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+                      assertTrue(
+                          mainMethodSubject
+                              .streamInstructions()
+                              .anyMatch(InstructionSubject::isInvokeDynamic));
+                    }))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("true");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      PredicateInterface f = new Predicate();
+      System.out.println(Arrays.asList(args).stream().noneMatch(f::m));
+    }
+  }
+
+  @NoVerticalClassMerging
+  interface PredicateInterfaceBase {
+    boolean m(String item);
+  }
+
+  @NoVerticalClassMerging
+  interface PredicateInterface extends PredicateInterfaceBase {}
+
+  static class Predicate implements PredicateInterface {
+
+    @Override
+    public boolean m(String item) {
+      return System.currentTimeMillis() > 0;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
index 6b01b51..12c0643 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
@@ -21,7 +21,7 @@
 
   static class TestClass {
 
-    private static A field = new B();
+    private static A field = System.currentTimeMillis() >= 0 ? new B() : null;
 
     public static void main(String[] args) {
       System.out.print(field.getClass().getName());
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
index c0ab377..a482eee 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
@@ -64,7 +64,7 @@
   public static Collection<Object[]> data() {
     // We don't run this on Proguard, as Proguard does not merge A into B.
     return buildParameters(
-        getTestParameters().withAllRuntimes().build(), BooleanUtils.values());
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
   }
 
   public void configure(R8FullTestBuilder builder) {
@@ -105,7 +105,7 @@
             "-keep class " + Unused.class.getTypeName(),
             getAdditionalKeepRules())
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .apply(this::configure)
         .run(parameters.getRuntime(), getTestClass())
         .assertSuccessWithOutput(expected)
diff --git a/third_party/opensource-apps/empty-activity.tar.gz.sha1 b/third_party/opensource-apps/empty-activity.tar.gz.sha1
new file mode 100644
index 0000000..4244c67
--- /dev/null
+++ b/third_party/opensource-apps/empty-activity.tar.gz.sha1
@@ -0,0 +1 @@
+ff20836a8bc101c9ec5ded3fe025013605453919
\ No newline at end of file
diff --git a/third_party/opensource-apps/empty-compose-activity.tar.gz.sha1 b/third_party/opensource-apps/empty-compose-activity.tar.gz.sha1
new file mode 100644
index 0000000..2f0c18e
--- /dev/null
+++ b/third_party/opensource-apps/empty-compose-activity.tar.gz.sha1
@@ -0,0 +1 @@
+a9ee18fef196b70fe4d5ca7e6a754383df0d1a59
\ No newline at end of file
diff --git a/third_party/protobuf-lite.tar.gz.sha1 b/third_party/protobuf-lite.tar.gz.sha1
index 6c757bb..77f4e56 100644
--- a/third_party/protobuf-lite.tar.gz.sha1
+++ b/third_party/protobuf-lite.tar.gz.sha1
@@ -1 +1 @@
-f5f3295899f7eecd937830fc8a0a35a4ef5b4083
\ No newline at end of file
+047e2914d6898dc764803e7709eda60f7de1ecfa
\ No newline at end of file
diff --git a/tools/build_sample_apk.py b/tools/build_sample_apk.py
index a08a4ec..376c5c0 100755
--- a/tools/build_sample_apk.py
+++ b/tools/build_sample_apk.py
@@ -159,7 +159,7 @@
   for root, dirnames, filenames in os.walk(get_bin_path(app)):
     for filename in fnmatch.filter(filenames, '*.class'):
         files.append(os.path.join(root, filename))
-  command = [DEFAULT_D8,
+  command = [DEFAULT_D8, '--',
              '--output', get_bin_path(app),
              '--classpath', utils.get_android_jar(api),
              '--min-api', str(api)]
diff --git a/tools/internal_test.py b/tools/internal_test.py
index 47d236e..dae6247 100755
--- a/tools/internal_test.py
+++ b/tools/internal_test.py
@@ -109,6 +109,7 @@
       '--max-memory=%s' % int(record['oom-threshold'] * 0.85)
   ]
 
+# TODO(b/210982978): Enable testing of min xmx again
 TEST_COMMANDS = [
     # Run test.py internal testing.
     ['tools/test.py', '--only_internal', '--slow_tests',
@@ -117,9 +118,7 @@
     ['tools/run_on_app.py', '--run-all', '--out=out'],
     # Find min xmx for selected benchmark apps
     ['tools/gradle.py', 'r8lib'],
-] + (map(find_min_xmx_command, BENCHMARK_APPS)
-     + map(compile_with_memory_max_command, BENCHMARK_APPS)
-     + map(compile_with_memory_min_command, BENCHMARK_APPS))
+]
 
 # Command timeout, in seconds.
 RUN_TIMEOUT = 3600 * 6
diff --git a/tools/run_on_app_dump.py b/tools/run_on_app_dump.py
index 750a877..c55a6f7 100755
--- a/tools/run_on_app_dump.py
+++ b/tools/run_on_app_dump.py
@@ -131,6 +131,24 @@
     # not abstract
     'compiler_properties': ['-Dcom.android.tools.r8.allowInvalidCfAccessFlags=true']
   }),
+  App({
+    'id': 'com.example.myapplication',
+    'name': 'empty-activity',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release.apk',
+    'url': 'https://github.com/christofferqa/empty_android_activity.git',
+    'revision': '2d297ec3373dadb03cbae916b9feba4792563156',
+    'folder': 'empty-activity',
+  }),
+  App({
+    'id': 'com.example.emptycomposeactivity',
+    'name': 'empty-compose-activity',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release.apk',
+    'url': 'https://github.com/christofferqa/empty_android_compose_activity.git',
+    'revision': '3c8111b8b7d6e9184049a07e2b96702d7b33d03e',
+    'folder': 'empty-compose-activity',
+  }),
   # TODO(b/172539375): Monkey runner fails on recompilation.
   App({
     'id': 'com.google.firebase.example.fireeats',