Merge commit '82beb3099fab513a2da769a449a61f0d27e6c3ad' into dev-release
diff --git a/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java b/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java
index 137ac12..2b46e36 100644
--- a/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java
+++ b/buildSrc/src/main/java/desugaredlibrary/CustomConversionAsmRewriteDescription.java
@@ -17,8 +17,6 @@
 
   private static final Set<String> ENUM_WRAP_CONVERT_OWNER =
       ImmutableSet.of(
-          "j$/nio/file/StandardOpenOption",
-          "j$/nio/file/LinkOption",
           "j$/nio/file/attribute/PosixFilePermission",
           "j$/util/stream/Collector$Characteristics");
   private static final Set<String> WRAP_CONVERT_OWNER =
@@ -27,18 +25,14 @@
           "j$/nio/file/spi/FileTypeDetector",
           "j$/nio/file/Path",
           "j$/nio/file/WatchEvent",
-          "j$/nio/file/attribute/BasicFileAttributes",
-          "j$/nio/file/attribute/BasicFileAttributeView",
-          "j$/nio/file/attribute/FileOwnerAttributeView",
-          "j$/nio/file/attribute/PosixFileAttributes",
-          "j$/nio/file/attribute/PosixFileAttributeView");
+          "j$/nio/file/OpenOption");
 
   static Map<String, String> getJavaWrapConvertOwnerMap() {
-    return computeConvertOwnerMap("$VivifiedWrapper");
+    return computeConvertOwnerMap("$Wrapper");
   }
 
   static Map<String, String> getJ$WrapConvertOwnerMap() {
-    return computeConvertOwnerMap("$Wrapper");
+    return computeConvertOwnerMap("$VivifiedWrapper");
   }
 
   private static HashMap<String, String> computeConvertOwnerMap(String suffix) {
diff --git a/src/library_desugar/desugar_jdk_libs.json b/src/library_desugar/desugar_jdk_libs.json
index 59ffcc0..1b2e6bf 100644
--- a/src/library_desugar/desugar_jdk_libs.json
+++ b/src/library_desugar/desugar_jdk_libs.json
@@ -2,7 +2,7 @@
   "configuration_format_version": 3,
   "group_id" : "com.tools.android",
   "artifact_id" : "desugar_jdk_libs",
-  "version": "1.1.6",
+  "version": "1.1.7",
   "required_compilation_api_level": 26,
   "synthesized_library_classes_package_prefix": "j$.",
   "support_all_callbacks_from_library": true,
@@ -83,12 +83,14 @@
         "java.util.TimeZone#toZoneId": "java.util.DesugarTimeZone"
       },
       "custom_conversion": {
-        "java.time.ZonedDateTime": "java.time.TimeConversions",
-        "java.time.LocalDate": "java.time.TimeConversions",
         "java.time.Duration": "java.time.TimeConversions",
-        "java.time.ZoneId": "java.time.TimeConversions",
+        "java.time.Instant": "java.time.TimeConversions",
+        "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.LocalTime": "java.time.TimeConversions",
         "java.time.MonthDay": "java.time.TimeConversions",
-        "java.time.Instant": "java.time.TimeConversions"
+        "java.time.Period": "java.time.TimeConversions",
+        "java.time.ZoneId": "java.time.TimeConversions",
+        "java.time.ZonedDateTime": "java.time.TimeConversions"
       }
     },
     {
@@ -164,12 +166,14 @@
         "java.util.TimeZone#toZoneId": "java.util.DesugarTimeZone"
       },
       "custom_conversion": {
-        "java.time.ZonedDateTime": "java.time.TimeConversions",
-        "java.time.LocalDate": "java.time.TimeConversions",
         "java.time.Duration": "java.time.TimeConversions",
-        "java.time.ZoneId": "java.time.TimeConversions",
+        "java.time.Instant": "java.time.TimeConversions",
+        "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.LocalTime": "java.time.TimeConversions",
         "java.time.MonthDay": "java.time.TimeConversions",
-        "java.time.Instant": "java.time.TimeConversions"
+        "java.time.Period": "java.time.TimeConversions",
+        "java.time.ZoneId": "java.time.TimeConversions",
+        "java.time.ZonedDateTime": "java.time.TimeConversions"
       }
     },
     {
diff --git a/src/library_desugar/java/j$/nio/file/OpenOption.java b/src/library_desugar/java/j$/nio/file/OpenOption.java
index 8b063d1..a22eb7c 100644
--- a/src/library_desugar/java/j$/nio/file/OpenOption.java
+++ b/src/library_desugar/java/j$/nio/file/OpenOption.java
@@ -4,4 +4,12 @@
 
 package j$.nio.file;
 
-public class OpenOption {}
+public class OpenOption {
+  public static java.nio.file.OpenOption wrap_convert(j$.nio.file.OpenOption option) {
+    return null;
+  }
+
+  public static j$.nio.file.OpenOption wrap_convert(java.nio.file.OpenOption option) {
+    return null;
+  }
+}
diff --git a/src/library_desugar/java/j$/nio/file/Path.java b/src/library_desugar/java/j$/nio/file/Path.java
index cb01ec0..781ab5f 100644
--- a/src/library_desugar/java/j$/nio/file/Path.java
+++ b/src/library_desugar/java/j$/nio/file/Path.java
@@ -6,7 +6,7 @@
 
 public class Path {
 
-  public static j$.nio.file.Path inverted_wrap_convert(java.nio.file.Path path) {
+  public static j$.nio.file.Path wrap_convert(java.nio.file.Path path) {
     return null;
   }
 }
diff --git a/src/library_desugar/java/j$/nio/file/StandardOpenOption.java b/src/library_desugar/java/j$/nio/file/StandardOpenOption.java
deleted file mode 100644
index a65da37..0000000
--- a/src/library_desugar/java/j$/nio/file/StandardOpenOption.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package j$.nio.file;
-
-public class StandardOpenOption extends OpenOption {
-
-  public static java.nio.file.StandardOpenOption wrap_convert(
-      j$.nio.file.StandardOpenOption option) {
-    return null;
-  }
-
-  public static j$.nio.file.StandardOpenOption wrap_convert(
-      java.nio.file.StandardOpenOption option) {
-    return null;
-  }
-}
diff --git a/src/library_desugar/java/j$/nio/file/spi/FileSystemProvider.java b/src/library_desugar/java/j$/nio/file/spi/FileSystemProvider.java
index 38152b3..8db22b3 100644
--- a/src/library_desugar/java/j$/nio/file/spi/FileSystemProvider.java
+++ b/src/library_desugar/java/j$/nio/file/spi/FileSystemProvider.java
@@ -5,7 +5,8 @@
 package j$.nio.file.spi;
 
 public class FileSystemProvider {
-  public static java.nio.file.spi.FileSystemProvider wrap_convert(FileSystemProvider provider) {
+  public static java.nio.file.spi.FileSystemProvider inverted_wrap_convert(
+      j$.nio.file.spi.FileSystemProvider provider) {
     // Rewritten in ASM to the wrapper method.
     return null;
   }
diff --git a/src/library_desugar/java/j$/time/LocalTime.java b/src/library_desugar/java/j$/time/LocalTime.java
new file mode 100644
index 0000000..3bfd2e2
--- /dev/null
+++ b/src/library_desugar/java/j$/time/LocalTime.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package j$.time;
+
+public class LocalTime {
+
+  public static LocalTime ofNanoOfDay(long toNanoOfDay) {
+    return null;
+  }
+
+  public long toNanoOfDay() {
+    return 0;
+  }
+}
diff --git a/src/library_desugar/java/j$/time/Period.java b/src/library_desugar/java/j$/time/Period.java
new file mode 100644
index 0000000..b7857c1
--- /dev/null
+++ b/src/library_desugar/java/j$/time/Period.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package j$.time;
+
+public class Period {
+
+  public static Period of(int years, int months, int days) {
+    return null;
+  }
+
+  public int getYears() {
+    return 0;
+  }
+
+  public int getMonths() {
+    return 0;
+  }
+
+  public int getDays() {
+    return 0;
+  }
+}
diff --git a/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java b/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java
index d02344d..5be05a8 100644
--- a/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java
+++ b/src/library_desugar/java/java/adapter/HybridFileSystemProvider.java
@@ -17,17 +17,19 @@
  * runtime environment.
  */
 public final class HybridFileSystemProvider {
+
   private static final FileSystemProvider INSTANCE = getFileSystemProvider();
   private static final FileSystem FILE_SYSTEM_INSTANCE =
       INSTANCE.getFileSystem(URI.create("file:///"));
 
   private static FileSystemProvider getFileSystemProvider() {
+    // Note: this fails on non Android devices.
     try {
       // On API 26 and above, FileSystems is present.
       Class.forName("java.nio.file.FileSystems");
       j$.nio.file.FileSystem fileSystem = FileSystems.getDefault();
       j$.nio.file.spi.FileSystemProvider provider = fileSystem.provider();
-      return j$.nio.file.spi.FileSystemProvider.wrap_convert(provider);
+      return j$.nio.file.spi.FileSystemProvider.inverted_wrap_convert(provider);
     } catch (ClassNotFoundException ignored) {
       // We reach this path is API < 26.
     }
diff --git a/src/library_desugar/java/java/adapter/HybridFileTypeDetector.java b/src/library_desugar/java/java/adapter/HybridFileTypeDetector.java
index af2c8f7..da93736 100644
--- a/src/library_desugar/java/java/adapter/HybridFileTypeDetector.java
+++ b/src/library_desugar/java/java/adapter/HybridFileTypeDetector.java
@@ -29,7 +29,7 @@
   static class PlatformFileTypeDetector extends FileTypeDetector {
     @Override
     public String probeContentType(Path path) throws IOException {
-      return j$.nio.file.Files.probeContentType(j$.nio.file.Path.inverted_wrap_convert(path));
+      return j$.nio.file.Files.probeContentType(j$.nio.file.Path.wrap_convert(path));
     }
   }
 }
diff --git a/src/library_desugar/java/java/nio/file/FileApiFlips.java b/src/library_desugar/java/java/nio/file/FileApiFlips.java
index 71b172a..b68aeb2 100644
--- a/src/library_desugar/java/java/nio/file/FileApiFlips.java
+++ b/src/library_desugar/java/java/nio/file/FileApiFlips.java
@@ -78,7 +78,7 @@
         } catch (ClassCastException cce) {
           throw exceptionOpenOption(cce);
         }
-        convertedSet.add(OpenOptionConversions.convert(option));
+        convertedSet.add(j$.nio.file.OpenOption.wrap_convert(option));
       }
       return convertedSet;
     }
@@ -90,7 +90,7 @@
         } catch (ClassCastException cce) {
           throw exceptionOpenOption(cce);
         }
-        convertedSet.add(OpenOptionConversions.convert(option));
+        convertedSet.add(j$.nio.file.OpenOption.wrap_convert(option));
       }
       return convertedSet;
     }
diff --git a/src/library_desugar/java/java/nio/file/OpenOptionConversions.java b/src/library_desugar/java/java/nio/file/OpenOptionConversions.java
deleted file mode 100644
index bfe4968..0000000
--- a/src/library_desugar/java/java/nio/file/OpenOptionConversions.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package java.nio.file;
-
-import static java.util.ConversionRuntimeException.exception;
-
-public class OpenOptionConversions {
-  public static java.nio.file.OpenOption convert(j$.nio.file.OpenOption option) {
-    if (option == null) {
-      return null;
-    }
-    if (option instanceof j$.nio.file.StandardOpenOption) {
-      return j$.nio.file.StandardOpenOption.wrap_convert((j$.nio.file.StandardOpenOption) option);
-    }
-    if (option instanceof j$.nio.file.LinkOption) {
-      return j$.nio.file.LinkOption.wrap_convert((j$.nio.file.LinkOption) option);
-    }
-    throw exception("java.nio.file.OpenOption", option);
-  }
-
-  public static j$.nio.file.OpenOption convert(java.nio.file.OpenOption option) {
-    if (option == null) {
-      return null;
-    }
-    if (option instanceof java.nio.file.StandardOpenOption) {
-      return j$.nio.file.StandardOpenOption.wrap_convert((java.nio.file.StandardOpenOption) option);
-    }
-    if (option instanceof java.nio.file.LinkOption) {
-      return j$.nio.file.LinkOption.wrap_convert((java.nio.file.LinkOption) option);
-    }
-    throw exception("java.nio.file.OpenOption", option);
-  }
-}
diff --git a/src/library_desugar/java/java/nio/file/attribute/FileAttributeConversions.java b/src/library_desugar/java/java/nio/file/attribute/FileAttributeConversions.java
index 332c89b..c895d52 100644
--- a/src/library_desugar/java/java/nio/file/attribute/FileAttributeConversions.java
+++ b/src/library_desugar/java/java/nio/file/attribute/FileAttributeConversions.java
@@ -4,8 +4,6 @@
 
 package java.nio.file.attribute;
 
-import static java.util.ConversionRuntimeException.exception;
-
 public class FileAttributeConversions {
 
   public static java.nio.file.attribute.FileTime convert(j$.nio.file.attribute.FileTime fileTime) {
@@ -22,67 +20,4 @@
     return j$.nio.file.attribute.FileTime.fromMillis(fileTime.toMillis());
   }
 
-  public static java.nio.file.attribute.FileAttributeView convert(
-      j$.nio.file.attribute.FileAttributeView fileAttributeView) {
-    if (fileAttributeView == null) {
-      return null;
-    }
-    if (fileAttributeView instanceof j$.nio.file.attribute.PosixFileAttributeView) {
-      return j$.nio.file.attribute.PosixFileAttributeView.wrap_convert(
-          (j$.nio.file.attribute.PosixFileAttributeView) fileAttributeView);
-    }
-    if (fileAttributeView instanceof j$.nio.file.attribute.FileOwnerAttributeView) {
-      return j$.nio.file.attribute.FileOwnerAttributeView.wrap_convert(
-          (j$.nio.file.attribute.FileOwnerAttributeView) fileAttributeView);
-    }
-    if (fileAttributeView instanceof j$.nio.file.attribute.BasicFileAttributeView) {
-      return j$.nio.file.attribute.BasicFileAttributeView.wrap_convert(
-          (j$.nio.file.attribute.BasicFileAttributeView) fileAttributeView);
-    }
-    throw exception("java.nio.file.attribute.FileAttributeView", fileAttributeView);
-  }
-
-  public static j$.nio.file.attribute.FileAttributeView convert(
-      java.nio.file.attribute.FileAttributeView fileAttributeView) {
-    if (fileAttributeView == null) {
-      return null;
-    }
-    if (fileAttributeView instanceof java.nio.file.attribute.PosixFileAttributeView) {
-      return j$.nio.file.attribute.PosixFileAttributeView.wrap_convert(
-          (java.nio.file.attribute.PosixFileAttributeView) fileAttributeView);
-    }
-    if (fileAttributeView instanceof java.nio.file.attribute.FileOwnerAttributeView) {
-      return j$.nio.file.attribute.FileOwnerAttributeView.wrap_convert(
-          (java.nio.file.attribute.FileOwnerAttributeView) fileAttributeView);
-    }
-    if (fileAttributeView instanceof java.nio.file.attribute.BasicFileAttributeView) {
-      return j$.nio.file.attribute.BasicFileAttributeView.wrap_convert(
-          (java.nio.file.attribute.BasicFileAttributeView) fileAttributeView);
-    }
-    throw exception("java.nio.file.attribute.FileAttributeView", fileAttributeView);
-  }
-
-  public static java.nio.file.attribute.BasicFileAttributes convert(
-      j$.nio.file.attribute.BasicFileAttributes fileAttributes) {
-    if (fileAttributes == null) {
-      return null;
-    }
-    if (fileAttributes instanceof j$.nio.file.attribute.PosixFileAttributes) {
-      return j$.nio.file.attribute.PosixFileAttributes.wrap_convert(
-          (j$.nio.file.attribute.PosixFileAttributes) fileAttributes);
-    }
-    return j$.nio.file.attribute.BasicFileAttributes.wrap_convert(fileAttributes);
-  }
-
-  public static j$.nio.file.attribute.BasicFileAttributes convert(
-      java.nio.file.attribute.BasicFileAttributes fileAttributes) {
-    if (fileAttributes == null) {
-      return null;
-    }
-    if (fileAttributes instanceof java.nio.file.attribute.PosixFileAttributes) {
-      return j$.nio.file.attribute.PosixFileAttributes.wrap_convert(
-          (java.nio.file.attribute.PosixFileAttributes) fileAttributes);
-    }
-    return j$.nio.file.attribute.BasicFileAttributes.wrap_convert(fileAttributes);
-  }
 }
diff --git a/src/library_desugar/java/java/time/TimeConversions.java b/src/library_desugar/java/java/time/TimeConversions.java
index 1320546..80824df 100644
--- a/src/library_desugar/java/java/time/TimeConversions.java
+++ b/src/library_desugar/java/java/time/TimeConversions.java
@@ -8,6 +8,34 @@
 
   private TimeConversions() {}
 
+  public static j$.time.Period convert(java.time.Period period) {
+    if (period == null) {
+      return null;
+    }
+    return j$.time.Period.of(period.getYears(), period.getMonths(), period.getDays());
+  }
+
+  public static java.time.Period convert(j$.time.Period period) {
+    if (period == null) {
+      return null;
+    }
+    return java.time.Period.of(period.getYears(), period.getMonths(), period.getDays());
+  }
+
+  public static j$.time.LocalTime convert(java.time.LocalTime localTime) {
+    if (localTime == null) {
+      return null;
+    }
+    return j$.time.LocalTime.ofNanoOfDay(localTime.toNanoOfDay());
+  }
+
+  public static java.time.LocalTime convert(j$.time.LocalTime localTime) {
+    if (localTime == null) {
+      return null;
+    }
+    return java.time.LocalTime.ofNanoOfDay(localTime.toNanoOfDay());
+  }
+
   public static j$.time.ZonedDateTime convert(java.time.ZonedDateTime dateTime) {
     if (dateTime == null) {
       return null;
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index 45d529c..e3caa6e 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -26,7 +26,9 @@
         "java.time.Duration": "java.time.TimeConversions",
         "java.time.Instant": "java.time.TimeConversions",
         "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.LocalTime": "java.time.TimeConversions",
         "java.time.MonthDay": "java.time.TimeConversions",
+        "java.time.Period": "java.time.TimeConversions",
         "java.time.ZoneId": "java.time.TimeConversions",
         "java.time.ZonedDateTime": "java.time.TimeConversions"
       }
@@ -102,6 +104,7 @@
         "java.util.PrimitiveIterator$OfDouble",
         "java.util.PrimitiveIterator$OfInt",
         "java.util.PrimitiveIterator$OfLong",
+        "java.util.Spliterator",
         "java.util.Spliterator$OfDouble",
         "java.util.Spliterator$OfInt",
         "java.util.Spliterator$OfLong",
@@ -114,12 +117,6 @@
         "java.util.stream.LongStream",
         "java.util.stream.Stream"
       ],
-      "wrapper_conversion_excluding": {
-        "java.util.Spliterator": [
-          "boolean java.util.Spliterator#hasCharacteristics(int)",
-          "long java.util.Spliterator#getExactSizeIfKnown()"
-        ]
-      },
       "custom_conversion": {
         "java.util.DoubleSummaryStatistics": "java.util.DoubleSummaryStatisticsConversions",
         "java.util.IntSummaryStatistics": "java.util.IntSummaryStatisticsConversions",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_legacy.json b/src/library_desugar/jdk11/desugar_jdk_libs_legacy.json
index 222feda..a4ddd2ba 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_legacy.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_legacy.json
@@ -2,7 +2,7 @@
   "configuration_format_version": 5,
   "group_id" : "com.tools.android",
   "artifact_id" : "desugar_jdk_libs",
-  "version": "1.2.0",
+  "version": "1.2.1",
   "required_compilation_api_level": 30,
   "synthesized_library_classes_package_prefix": "j$.",
   "support_all_callbacks_from_library": true,
@@ -25,7 +25,9 @@
         "java.time.Duration": "java.time.TimeConversions",
         "java.time.Instant": "java.time.TimeConversions",
         "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.LocalTime": "java.time.TimeConversions",
         "java.time.MonthDay": "java.time.TimeConversions",
+        "java.time.Period": "java.time.TimeConversions",
         "java.time.ZoneId": "java.time.TimeConversions",
         "java.time.ZonedDateTime": "java.time.TimeConversions"
       }
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_path.json b/src/library_desugar/jdk11/desugar_jdk_libs_path.json
index 7b767e1..a00bb68 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_path.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_path.json
@@ -40,7 +40,9 @@
         "java.time.Duration": "java.time.TimeConversions",
         "java.time.Instant": "java.time.TimeConversions",
         "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.LocalTime": "java.time.TimeConversions",
         "java.time.MonthDay": "java.time.TimeConversions",
+        "java.time.Period": "java.time.TimeConversions",
         "java.time.ZoneId": "java.time.TimeConversions",
         "java.time.ZonedDateTime": "java.time.TimeConversions"
       }
@@ -61,6 +63,7 @@
         "java.nio.file.": "j$.nio.file."
       },
       "dont_rewrite_prefix": [
+        "java.nio.channels.AsynchronousChannelGroup",
         "java.nio.file.AccessDeniedException",
         "java.nio.file.AtomicMoveNotSupportedException",
         "java.nio.file.ClosedDirectoryStreamException",
@@ -110,6 +113,7 @@
         "java.nio.channels.SeekableByteChannel java.nio.file.spi.FileSystemProvider#newByteChannel(java.nio.file.Path, java.util.Set, java.nio.file.attribute.FileAttribute[])" : [1, "java.util.Set java.nio.file.FileApiFlips#flipOpenOptionSet(java.util.Set)"],
         "java.nio.channels.FileChannel java.nio.file.spi.FileSystemProvider#newFileChannel(java.nio.file.Path, java.util.Set, java.nio.file.attribute.FileAttribute[])" : [1, "java.util.Set java.nio.file.FileApiFlips#flipOpenOptionSet(java.util.Set)"],
         "java.util.List java.nio.file.WatchKey#pollEvents()": [-1, "java.util.List java.nio.file.FileApiFlips#flipWatchEventList(java.util.List)"],
+        "boolean java.nio.file.FileStore#supportsFileAttributeView(java.lang.Class)": [0, "java.lang.Class java.nio.file.FileApiFlips#flipFileAttributeView(java.lang.Class)"],
         "java.nio.file.attribute.FileAttributeView java.nio.file.spi.FileSystemProvider#getFileAttributeView(java.nio.file.Path, java.lang.Class, java.nio.file.LinkOption[])": [1, "java.lang.Class java.nio.file.FileApiFlips#flipFileAttributeView(java.lang.Class)"],
         "java.nio.file.attribute.BasicFileAttributes java.nio.file.spi.FileSystemProvider#readAttributes(java.nio.file.Path, java.lang.Class, java.nio.file.LinkOption[])": [1, "java.lang.Class java.nio.file.FileApiFlips#flipFileAttributes(java.lang.Class)"],
         "java.util.Set java.nio.file.attribute.PosixFileAttributes#permissions()": [-1, "java.util.Set java.nio.file.FileApiFlips#flipPosixPermissionSet(java.util.Set)"],
@@ -117,7 +121,6 @@
         "java.util.Map java.nio.file.spi.FileSystemProvider#readAttributes(java.nio.file.Path, java.lang.String, java.nio.file.LinkOption[])" : [-1, "java.util.Map java.nio.file.FileApiFlips#flipMapWithMaybeFileTimeValues(java.util.Map)"]
       },
       "wrapper_conversion": [
-        "java.nio.channels.AsynchronousChannel",
         "java.nio.channels.CompletionHandler",
         "java.nio.file.Path",
         "java.nio.file.FileSystem",
@@ -130,8 +133,8 @@
         "java.nio.file.WatchEvent$Modifier",
         "java.nio.file.attribute.UserPrincipalLookupService",
         "java.nio.file.spi.FileSystemProvider",
-        "java.nio.file.spi.FileTypeDetector",
         "java.nio.file.AccessMode",
+        "java.nio.file.OpenOption",
         "java.nio.file.StandardOpenOption",
         "java.nio.file.LinkOption",
         "java.nio.file.CopyOption",
@@ -145,6 +148,7 @@
         "java.nio.file.attribute.PosixFilePermission",
         "java.nio.file.attribute.BasicFileAttributes",
         "java.nio.file.attribute.PosixFileAttributes",
+        "java.nio.file.attribute.FileAttributeView",
         "java.nio.file.attribute.FileOwnerAttributeView",
         "java.nio.file.attribute.PosixFileAttributeView",
         "java.nio.file.attribute.BasicFileAttributeView"
@@ -157,10 +161,7 @@
         ]
       },
       "custom_conversion": {
-        "java.nio.file.OpenOption": "java.nio.file.OpenOptionConversions",
-        "java.nio.file.attribute.FileTime": "java.nio.file.attribute.FileAttributeConversions",
-        "java.nio.file.attribute.BasicFileAttributes": "java.nio.file.attribute.FileAttributeConversions",
-        "java.nio.file.attribute.FileAttributeView": "java.nio.file.attribute.FileAttributeConversions"
+        "java.nio.file.attribute.FileTime": "java.nio.file.attribute.FileAttributeConversions"
       }
     },
     {
@@ -233,7 +234,7 @@
         "java.util.stream.Stream java.io.BufferedReader#lines()": "java.io.DesugarBufferedReader",
         "java.util.Spliterator java.util.LinkedHashSet#spliterator()": "java.util.DesugarLinkedHashSet"
       },
-      "api_generic_conversion": {
+      "api_generic_types_conversion": {
         "java.util.Set java.util.stream.Collector#characteristics()" : [-1, "java.util.Set java.util.stream.StreamApiFlips#flipCharacteristicSet(java.util.Set)"]
       },
       "wrapper_conversion": [
@@ -241,6 +242,7 @@
         "java.util.PrimitiveIterator$OfDouble",
         "java.util.PrimitiveIterator$OfInt",
         "java.util.PrimitiveIterator$OfLong",
+        "java.util.Spliterator",
         "java.util.Spliterator$OfDouble",
         "java.util.Spliterator$OfInt",
         "java.util.Spliterator$OfLong",
@@ -254,10 +256,6 @@
         "java.util.stream.Stream"
       ],
       "wrapper_conversion_excluding": {
-        "java.util.Spliterator": [
-          "boolean java.util.Spliterator#hasCharacteristics(int)",
-          "long java.util.Spliterator#getExactSizeIfKnown()"
-        ],
         "java.nio.channels.FileChannel": [
           "long java.nio.channels.FileChannel#read(java.nio.ByteBuffer[])",
           "long java.nio.channels.FileChannel#write(java.nio.ByteBuffer[])",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_path_alternative_3.json b/src/library_desugar/jdk11/desugar_jdk_libs_path_alternative_3.json
index a784fd1..3c503e9 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_path_alternative_3.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_path_alternative_3.json
@@ -40,7 +40,9 @@
         "java.time.Duration": "java.time.TimeConversions",
         "java.time.Instant": "java.time.TimeConversions",
         "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.LocalTime": "java.time.TimeConversions",
         "java.time.MonthDay": "java.time.TimeConversions",
+        "java.time.Period": "java.time.TimeConversions",
         "java.time.ZoneId": "java.time.TimeConversions",
         "java.time.ZonedDateTime": "java.time.TimeConversions"
       }
@@ -158,14 +160,9 @@
         "java.util.function.DoubleToLongFunction",
         "java.util.function.LongUnaryOperator",
         "java.util.stream.BaseStream",
-        "java.util.function.DoublePredicate"
+        "java.util.function.DoublePredicate",
+        "java.util.Spliterator"
       ],
-      "wrapper_conversion_excluding": {
-        "java.util.Spliterator": [
-          "boolean java.util.Spliterator#hasCharacteristics(int)",
-          "long java.util.Spliterator#getExactSizeIfKnown()"
-        ]
-      },
       "custom_conversion": {
         "java.util.DoubleSummaryStatistics": "java.util.DoubleSummaryStatisticsConversions",
         "java.util.IntSummaryStatistics": "java.util.IntSummaryStatisticsConversions",
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 3887ee7..03ec1dd 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
+import com.android.tools.r8.experimental.startup.StartupInstrumentation;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
@@ -400,6 +401,9 @@
         () -> new KotlinMetadataRewriter(appView).runForD8(executorService));
 
     timing.time(
+        "Startup instrumentation", () -> StartupInstrumentation.run(appView, executorService));
+
+    timing.time(
         "Api reference stubber", () -> new ApiReferenceStubber(appView).run(executorService));
   }
 
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index b781d3a..5855643 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -16,7 +16,6 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CheckDiscardDiagnostic;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
-import com.android.tools.r8.experimental.startup.StartupInstrumentation;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
@@ -444,8 +443,6 @@
 
       new CfOpenClosedInterfacesAnalysis(appViewWithLiveness).run(executorService);
 
-      new StartupInstrumentation(appView).instrumentAllClasses(executorService);
-
       assert verifyNoJarApplicationReaders(appView.appInfo().classes());
       assert appView.checkForTesting(() -> allReferencesAssignedApiLevel(appViewWithLiveness));
       // Build conservative main dex content after first round of tree shaking. This is used
diff --git a/src/main/java/com/android/tools/r8/dex/StartupMixedSectionLayoutStrategy.java b/src/main/java/com/android/tools/r8/dex/StartupMixedSectionLayoutStrategy.java
index eb03451..c71d0d3 100644
--- a/src/main/java/com/android/tools/r8/dex/StartupMixedSectionLayoutStrategy.java
+++ b/src/main/java/com/android/tools/r8/dex/StartupMixedSectionLayoutStrategy.java
@@ -79,7 +79,7 @@
             virtualFile.classes().size());
     LensCodeRewriterUtils rewriter = new LensCodeRewriterUtils(appView, true);
     StartupIndexedItemCollection indexedItemCollection = new StartupIndexedItemCollection();
-    for (StartupClass<DexType> startupClass : startupOrderForWriting.getClasses()) {
+    for (StartupClass<DexType, DexMethod> startupClass : startupOrderForWriting.getClasses()) {
       assert !startupClass.isSynthetic();
       DexProgramClass definition = virtualFileDefinitions.get(startupClass.getReference());
       if (definition != null) {
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/EmptyStartupOrder.java b/src/main/java/com/android/tools/r8/experimental/startup/EmptyStartupOrder.java
index 3566ad1..7bbaba0 100644
--- a/src/main/java/com/android/tools/r8/experimental/startup/EmptyStartupOrder.java
+++ b/src/main/java/com/android/tools/r8/experimental/startup/EmptyStartupOrder.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.experimental.startup;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.PrunedItems;
@@ -22,7 +23,7 @@
   }
 
   @Override
-  public Collection<StartupClass<DexType>> getClasses() {
+  public Collection<StartupClass<DexType, DexMethod>> getClasses() {
     return Collections.emptyList();
   }
 
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/NonEmptyStartupOrder.java b/src/main/java/com/android/tools/r8/experimental/startup/NonEmptyStartupOrder.java
index 673e0de..4122aa0 100644
--- a/src/main/java/com/android/tools/r8/experimental/startup/NonEmptyStartupOrder.java
+++ b/src/main/java/com/android/tools/r8/experimental/startup/NonEmptyStartupOrder.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
+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.GraphLens;
@@ -25,16 +26,16 @@
 
 public class NonEmptyStartupOrder extends StartupOrder {
 
-  private final LinkedHashSet<StartupClass<DexType>> startupClasses;
+  private final LinkedHashSet<StartupClass<DexType, DexMethod>> startupClasses;
 
   // Redundant sets to allow efficient querying without boxing.
   private final Set<DexType> nonSyntheticStartupClasses = Sets.newIdentityHashSet();
   private final Set<DexType> syntheticStartupClasses = Sets.newIdentityHashSet();
 
-  NonEmptyStartupOrder(LinkedHashSet<StartupClass<DexType>> startupClasses) {
+  NonEmptyStartupOrder(LinkedHashSet<StartupClass<DexType, DexMethod>> startupClasses) {
     assert !startupClasses.isEmpty();
     this.startupClasses = startupClasses;
-    for (StartupClass<DexType> startupClass : startupClasses) {
+    for (StartupClass<DexType, DexMethod> startupClass : startupClasses) {
       if (startupClass.isSynthetic()) {
         syntheticStartupClasses.add(startupClass.getReference());
       } else {
@@ -66,7 +67,7 @@
   }
 
   @Override
-  public Collection<StartupClass<DexType>> getClasses() {
+  public Collection<StartupClass<DexType, DexMethod>> getClasses() {
     return startupClasses;
   }
 
@@ -77,13 +78,13 @@
 
   @Override
   public StartupOrder rewrittenWithLens(GraphLens graphLens) {
-    LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses =
+    LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses =
         new LinkedHashSet<>(startupClasses.size());
-    for (StartupClass<DexType> startupClass : startupClasses) {
+    for (StartupClass<DexType, DexMethod> startupClass : startupClasses) {
       rewrittenStartupClasses.add(
-          StartupClass.<DexType>builder()
+          StartupClass.dexBuilder()
               .setFlags(startupClass.getFlags())
-              .setReference(graphLens.lookupType(startupClass.getReference()))
+              .setClassReference(graphLens.lookupType(startupClass.getReference()))
               .build());
     }
     return createNonEmpty(rewrittenStartupClasses);
@@ -91,7 +92,7 @@
 
   @Override
   public StartupOrder toStartupOrderForWriting(AppView<?> appView) {
-    LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses =
+    LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses =
         new LinkedHashSet<>(startupClasses.size());
     Map<DexType, List<DexProgramClass>> syntheticContextsToSyntheticClasses =
         new IdentityHashMap<>();
@@ -105,7 +106,7 @@
         }
       }
     }
-    for (StartupClass<DexType> startupClass : startupClasses) {
+    for (StartupClass<DexType, DexMethod> startupClass : startupClasses) {
       addStartupClass(
           startupClass, rewrittenStartupClasses, syntheticContextsToSyntheticClasses, appView);
     }
@@ -114,8 +115,8 @@
   }
 
   private static void addStartupClass(
-      StartupClass<DexType> startupClass,
-      LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses,
+      StartupClass<DexType, DexMethod> startupClass,
+      LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses,
       Map<DexType, List<DexProgramClass>> syntheticContextsToSyntheticClasses,
       AppView<?> appView) {
     if (startupClass.isSynthetic()) {
@@ -131,14 +132,15 @@
   }
 
   private static boolean addClass(
-      DexProgramClass clazz, LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses) {
+      DexProgramClass clazz,
+      LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses) {
     return rewrittenStartupClasses.add(
-        StartupClass.<DexType>builder().setReference(clazz.getType()).build());
+        StartupClass.dexBuilder().setClassReference(clazz.getType()).build());
   }
 
   private static void addClassAndParentClasses(
       DexType type,
-      LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses,
+      LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses,
       AppView<?> appView) {
     DexProgramClass definition = appView.app().programDefinitionFor(type);
     if (definition != null) {
@@ -148,7 +150,7 @@
 
   private static void addClassAndParentClasses(
       DexProgramClass clazz,
-      LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses,
+      LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses,
       AppView<?> appView) {
     if (addClass(clazz, rewrittenStartupClasses)) {
       addParentClasses(clazz, rewrittenStartupClasses, appView);
@@ -157,7 +159,7 @@
 
   private static void addParentClasses(
       DexProgramClass clazz,
-      LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses,
+      LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses,
       AppView<?> appView) {
     clazz.forEachImmediateSupertype(
         supertype -> addClassAndParentClasses(supertype, rewrittenStartupClasses, appView));
@@ -165,12 +167,12 @@
 
   @Override
   public StartupOrder withoutPrunedItems(PrunedItems prunedItems, SyntheticItems syntheticItems) {
-    LinkedHashSet<StartupClass<DexType>> rewrittenStartupClasses =
+    LinkedHashSet<StartupClass<DexType, DexMethod>> rewrittenStartupClasses =
         new LinkedHashSet<>(startupClasses.size());
     LazyBox<Set<DexType>> contextsOfLiveSynthetics =
         new LazyBox<>(
             () -> computeContextsOfLiveSynthetics(prunedItems.getPrunedApp(), syntheticItems));
-    for (StartupClass<DexType> startupClass : startupClasses) {
+    for (StartupClass<DexType, DexMethod> startupClass : startupClasses) {
       // Only prune non-synthetic classes, since the pruning of a class does not imply that all
       // classes synthesized from it have been pruned.
       if (startupClass.isSynthetic()) {
@@ -196,7 +198,8 @@
     return contextsOfLiveSynthetics;
   }
 
-  private StartupOrder createNonEmpty(LinkedHashSet<StartupClass<DexType>> startupClasses) {
+  private StartupOrder createNonEmpty(
+      LinkedHashSet<StartupClass<DexType, DexMethod>> startupClasses) {
     if (startupClasses.isEmpty()) {
       assert false;
       return empty();
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupClass.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupClass.java
index 9338d5f..0f76036 100644
--- a/src/main/java/com/android/tools/r8/experimental/startup/StartupClass.java
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupClass.java
@@ -4,84 +4,47 @@
 
 package com.android.tools.r8.experimental.startup;
 
-public class StartupClass<T> {
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
 
-  private static final int FLAG_SYNTHETIC = 1;
+// TODO(b/238173796): When updating the compiler to have support for taking a list of startup
+//  methods, this class may likely be removed along with the StartupItem class, so that only
+//  StartupMethod remains.
+public class StartupClass<C, M> extends StartupItem<C, M, C> {
 
-  private final int flags;
-  private final T reference;
-
-  public StartupClass(int flags, T reference) {
-    this.flags = flags;
-    this.reference = reference;
+  public StartupClass(int flags, C reference) {
+    super(flags, reference);
   }
 
-  public static <T> Builder<T> builder() {
+  public static <C, M> Builder<C, M> builder() {
     return new Builder<>();
   }
 
-  public int getFlags() {
-    return flags;
-  }
-
-  public T getReference() {
-    return reference;
-  }
-
-  public boolean isSynthetic() {
-    return (flags & FLAG_SYNTHETIC) != 0;
+  public static Builder<DexType, DexMethod> dexBuilder() {
+    return new Builder<>();
   }
 
   @Override
-  public boolean equals(Object obj) {
-    if (this == obj) {
-      return true;
-    }
-    if (obj == null || getClass() != obj.getClass()) {
-      return false;
-    }
-    StartupClass<?> startupClass = (StartupClass<?>) obj;
-    return flags == startupClass.flags && reference.equals(startupClass.reference);
+  public boolean isStartupClass() {
+    return true;
   }
 
   @Override
-  public int hashCode() {
-    assert flags <= 1;
-    return (reference.hashCode() << 1) | flags;
+  public StartupClass<C, M> asStartupClass() {
+    return this;
   }
 
-  @Override
-  public String toString() {
-    StringBuilder builder = new StringBuilder();
-    if (isSynthetic()) {
-      builder.append('S');
-    }
-    builder.append(reference);
-    return builder.toString();
-  }
+  public static class Builder<C, M> extends StartupItem.Builder<C, M, Builder<C, M>> {
 
-  public static class Builder<T> {
-
-    private int flags;
-    private T reference;
-
-    public Builder<T> setFlags(int flags) {
-      this.flags = flags;
-      return this;
+    @Override
+    public Builder<C, M> setMethodReference(M reference) {
+      throw new Unreachable();
     }
 
-    public Builder<T> setReference(T reference) {
-      this.reference = reference;
-      return this;
-    }
-
-    public Builder<T> setSynthetic() {
-      this.flags |= FLAG_SYNTHETIC;
-      return this;
-    }
-
-    public StartupClass<T> build() {
-      return new StartupClass<>(flags, reference);
+    @Override
+    public StartupClass<C, M> build() {
+      return new StartupClass<>(flags, classReference);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupConfiguration.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupConfiguration.java
index 9f16dce..9731423 100644
--- a/src/main/java/com/android/tools/r8/experimental/startup/StartupConfiguration.java
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupConfiguration.java
@@ -6,9 +6,7 @@
 
 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.DexType;
-import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.Reporter;
@@ -23,13 +21,10 @@
 
 public class StartupConfiguration {
 
-  private final List<StartupClass<DexType>> startupClasses;
-  private final List<DexMethod> startupMethods;
+  private final List<StartupClass<DexType, DexMethod>> startupClasses;
 
-  public StartupConfiguration(
-      List<StartupClass<DexType>> startupClasses, List<DexMethod> startupMethods) {
+  public StartupConfiguration(List<StartupClass<DexType, DexMethod>> startupClasses) {
     this.startupClasses = startupClasses;
-    this.startupMethods = startupMethods;
   }
 
   public static Builder builder() {
@@ -79,120 +74,68 @@
 
   public static StartupConfiguration createStartupConfigurationFromLines(
       DexItemFactory dexItemFactory, Reporter reporter, List<String> startupDescriptors) {
-    List<StartupClass<DexType>> startupClasses = new ArrayList<>();
-    List<DexMethod> startupMethods = new ArrayList<>();
-    for (String startupDescriptor : startupDescriptors) {
-      if (startupDescriptor.isEmpty()) {
-        continue;
-      }
-      StartupClass.Builder<DexType> startupClassBuilder = StartupClass.builder();
-      startupDescriptor = parseSyntheticFlag(startupDescriptor, startupClassBuilder);
-      int methodNameStartIndex = getMethodNameStartIndex(startupDescriptor);
-      if (methodNameStartIndex >= 0) {
-        DexMethod startupMethod =
-            parseStartupMethodDescriptor(startupDescriptor, methodNameStartIndex, dexItemFactory);
-        if (startupMethod != null) {
-          startupClasses.add(
-              startupClassBuilder.setReference(startupMethod.getHolderType()).build());
-          startupMethods.add(startupMethod);
-        } else {
-          reporter.warning(
-              new StringDiagnostic("Invalid descriptor for startup method: " + startupDescriptor));
-        }
-      } else {
-        DexType startupClass = parseStartupClassDescriptor(startupDescriptor, dexItemFactory);
-        if (startupClass != null) {
-          startupClasses.add(startupClassBuilder.setReference(startupClass).build());
-        } else {
-          reporter.warning(
-              new StringDiagnostic("Invalid descriptor for startup class: " + startupDescriptor));
-        }
-      }
-    }
-    return new StartupConfiguration(startupClasses, startupMethods);
-  }
-
-  public static String parseSyntheticFlag(
-      String startupDescriptor, StartupClass.Builder<?> startupClassBuilder) {
-    if (!startupDescriptor.isEmpty() && startupDescriptor.charAt(0) == 'S') {
-      startupClassBuilder.setSynthetic();
-      return startupDescriptor.substring(1);
-    }
-    return startupDescriptor;
-  }
-
-  private static int getMethodNameStartIndex(String startupDescriptor) {
-    int arrowIndex = startupDescriptor.indexOf("->");
-    return arrowIndex >= 0 ? arrowIndex + 2 : arrowIndex;
-  }
-
-  private static DexType parseStartupClassDescriptor(
-      String startupClassDescriptor, DexItemFactory dexItemFactory) {
-    if (DescriptorUtils.isClassDescriptor(startupClassDescriptor)) {
-      return dexItemFactory.createType(startupClassDescriptor);
-    } else {
-      return null;
-    }
-  }
-
-  private static DexMethod parseStartupMethodDescriptor(
-      String startupMethodDescriptor, int methodNameStartIndex, DexItemFactory dexItemFactory) {
-    String classDescriptor = startupMethodDescriptor.substring(0, methodNameStartIndex - 2);
-    DexType classType = parseStartupClassDescriptor(classDescriptor, dexItemFactory);
-    if (classType == null) {
-      return null;
-    }
-
-    String protoWithNameDescriptor = startupMethodDescriptor.substring(methodNameStartIndex);
-    int methodNameEndIndex = protoWithNameDescriptor.indexOf('(');
-    if (methodNameEndIndex <= 0) {
-      return null;
-    }
-    String methodName = protoWithNameDescriptor.substring(0, methodNameEndIndex);
-
-    String protoDescriptor = protoWithNameDescriptor.substring(methodNameEndIndex);
-    DexProto proto = parseStartupMethodProto(protoDescriptor, dexItemFactory);
-    return dexItemFactory.createMethod(classType, proto, methodName);
-  }
-
-  private static DexProto parseStartupMethodProto(
-      String protoDescriptor, DexItemFactory dexItemFactory) {
-    List<DexType> parameterTypes = new ArrayList<>();
-    for (String parameterTypeDescriptor :
-        DescriptorUtils.getArgumentTypeDescriptors(protoDescriptor)) {
-      parameterTypes.add(dexItemFactory.createType(parameterTypeDescriptor));
-    }
-    String returnTypeDescriptor = DescriptorUtils.getReturnTypeDescriptor(protoDescriptor);
-    DexType returnType = dexItemFactory.createType(returnTypeDescriptor);
-    return dexItemFactory.createProto(returnType, parameterTypes);
+    List<StartupClass<DexType, DexMethod>> startupClasses = new ArrayList<>();
+    StartupConfigurationParser.createDexParser(dexItemFactory)
+        .parseLines(
+            startupDescriptors,
+            startupClasses::add,
+            // TODO(b/238173796): Startup methods should be added as startup methods.
+            startupMethod ->
+                startupClasses.add(
+                    StartupClass.dexBuilder()
+                        .setClassReference(startupMethod.getReference().getHolderType())
+                        .setFlags(startupMethod.getFlags())
+                        .build()),
+            error ->
+                reporter.warning(
+                    new StringDiagnostic(
+                        "Invalid descriptor for startup class or method: " + error)));
+    return new StartupConfiguration(startupClasses);
   }
 
   public boolean hasStartupClasses() {
     return !startupClasses.isEmpty();
   }
 
-  public List<StartupClass<DexType>> getStartupClasses() {
+  public List<StartupClass<DexType, DexMethod>> getStartupClasses() {
     return startupClasses;
   }
 
   public static class Builder {
 
-    private final ImmutableList.Builder<StartupClass<DexType>> startupClassesBuilder =
+    private final ImmutableList.Builder<StartupClass<DexType, DexMethod>> startupClassesBuilder =
         ImmutableList.builder();
-    private final ImmutableList.Builder<DexMethod> startupMethodsBuilder = ImmutableList.builder();
 
-    public Builder addStartupClass(StartupClass<DexType> startupClass) {
+    public Builder addStartupItem(StartupItem<DexType, DexMethod, ?> startupItem) {
+      if (startupItem.isStartupClass()) {
+        return addStartupClass(startupItem.asStartupClass());
+      } else {
+        assert startupItem.isStartupMethod();
+        return addStartupMethod(startupItem.asStartupMethod());
+      }
+    }
+
+    public Builder addStartupClass(StartupClass<DexType, DexMethod> startupClass) {
       this.startupClassesBuilder.add(startupClass);
       return this;
     }
 
+    public Builder addStartupMethod(StartupMethod<DexType, DexMethod> startupMethod) {
+      // TODO(b/238173796): Startup methods should be added as startup methods.
+      return addStartupClass(
+          StartupClass.dexBuilder()
+              .setFlags(startupMethod.getFlags())
+              .setClassReference(startupMethod.getReference().getHolderType())
+              .build());
+    }
+
     public Builder apply(Consumer<Builder> consumer) {
       consumer.accept(this);
       return this;
     }
 
     public StartupConfiguration build() {
-      return new StartupConfiguration(startupClassesBuilder.build(), startupMethodsBuilder.build());
+      return new StartupConfiguration(startupClassesBuilder.build());
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupConfigurationParser.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupConfigurationParser.java
new file mode 100644
index 0000000..79ebc1e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupConfigurationParser.java
@@ -0,0 +1,163 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.experimental.startup;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class StartupConfigurationParser<C, M, T> {
+
+  interface MethodFactory<C, M, T> {
+
+    M createMethod(
+        C methodHolder, String methodName, List<T> methodParameterTypes, T methodReturnType);
+  }
+
+  private final Function<String, C> classFactory;
+  private final MethodFactory<C, M, T> methodFactory;
+  private final Function<String, T> typeFactory;
+
+  StartupConfigurationParser(
+      Function<String, C> classFactory,
+      MethodFactory<C, M, T> methodFactory,
+      Function<String, T> typeFactory) {
+    this.classFactory = classFactory;
+    this.methodFactory = methodFactory;
+    this.typeFactory = typeFactory;
+  }
+
+  public static StartupConfigurationParser<DexType, DexMethod, DexType> createDexParser(
+      DexItemFactory dexItemFactory) {
+    return new StartupConfigurationParser<>(
+        dexItemFactory::createType,
+        (methodHolder, methodName, methodParameters, methodReturnType) ->
+            dexItemFactory.createMethod(
+                methodHolder,
+                dexItemFactory.createProto(methodReturnType, methodParameters),
+                dexItemFactory.createString(methodName)),
+        dexItemFactory::createType);
+  }
+
+  public static StartupConfigurationParser<ClassReference, MethodReference, TypeReference>
+      createReferenceParser() {
+    return new StartupConfigurationParser<>(
+        Reference::classFromDescriptor, Reference::method, Reference::returnTypeFromDescriptor);
+  }
+
+  public void parseLines(
+      List<String> startupDescriptors,
+      Consumer<? super StartupClass<C, M>> startupClassConsumer,
+      Consumer<? super StartupMethod<C, M>> startupMethodConsumer,
+      Consumer<String> parseErrorHandler) {
+    for (String startupDescriptor : startupDescriptors) {
+      if (!startupDescriptor.isEmpty()) {
+        parseLine(
+            startupDescriptor, startupClassConsumer, startupMethodConsumer, parseErrorHandler);
+      }
+    }
+  }
+
+  public void parseLine(
+      String startupDescriptor,
+      Consumer<? super StartupClass<C, M>> startupClassConsumer,
+      Consumer<? super StartupMethod<C, M>> startupMethodConsumer,
+      Consumer<String> parseErrorHandler) {
+    StartupItem.Builder<C, M, ?> startupItemBuilder = StartupItem.builder();
+    startupDescriptor = parseSyntheticFlag(startupDescriptor, startupItemBuilder);
+    parseStartupClassOrMethod(
+        startupDescriptor,
+        startupItemBuilder,
+        startupClassConsumer,
+        startupMethodConsumer,
+        parseErrorHandler);
+  }
+
+  private static String parseSyntheticFlag(
+      String startupDescriptor, StartupItem.Builder<?, ?, ?> startupItemBuilder) {
+    if (!startupDescriptor.isEmpty() && startupDescriptor.charAt(0) == 'S') {
+      startupItemBuilder.setSynthetic();
+      return startupDescriptor.substring(1);
+    }
+    return startupDescriptor;
+  }
+
+  private void parseStartupClassOrMethod(
+      String startupDescriptor,
+      StartupItem.Builder<C, M, ?> startupItemBuilder,
+      Consumer<? super StartupClass<C, M>> startupClassConsumer,
+      Consumer<? super StartupMethod<C, M>> startupMethodConsumer,
+      Consumer<String> parseErrorHandler) {
+    int arrowStartIndex = getArrowStartIndex(startupDescriptor);
+    if (arrowStartIndex >= 0) {
+      M startupMethod = parseStartupMethodDescriptor(startupDescriptor, arrowStartIndex);
+      if (startupMethod != null) {
+        startupMethodConsumer.accept(
+            startupItemBuilder.setMethodReference(startupMethod).buildStartupMethod());
+      } else {
+        parseErrorHandler.accept(startupDescriptor);
+      }
+    } else {
+      C startupClass = parseStartupClassDescriptor(startupDescriptor);
+      if (startupClass != null) {
+        startupClassConsumer.accept(
+            startupItemBuilder.setClassReference(startupClass).buildStartupClass());
+      } else {
+        parseErrorHandler.accept(startupDescriptor);
+      }
+    }
+  }
+
+  private static int getArrowStartIndex(String startupDescriptor) {
+    return startupDescriptor.indexOf("->");
+  }
+
+  private C parseStartupClassDescriptor(String startupClassDescriptor) {
+    if (DescriptorUtils.isClassDescriptor(startupClassDescriptor)) {
+      return classFactory.apply(startupClassDescriptor);
+    } else {
+      return null;
+    }
+  }
+
+  private M parseStartupMethodDescriptor(String startupMethodDescriptor, int arrowStartIndex) {
+    String classDescriptor = startupMethodDescriptor.substring(0, arrowStartIndex);
+    C methodHolder = parseStartupClassDescriptor(classDescriptor);
+    if (methodHolder == null) {
+      return null;
+    }
+
+    int methodNameStartIndex = arrowStartIndex + 2;
+    String protoWithNameDescriptor = startupMethodDescriptor.substring(methodNameStartIndex);
+    int methodNameEndIndex = protoWithNameDescriptor.indexOf('(');
+    if (methodNameEndIndex <= 0) {
+      return null;
+    }
+    String methodName = protoWithNameDescriptor.substring(0, methodNameEndIndex);
+
+    String protoDescriptor = protoWithNameDescriptor.substring(methodNameEndIndex);
+    return parseStartupMethodProto(methodHolder, methodName, protoDescriptor);
+  }
+
+  private M parseStartupMethodProto(C methodHolder, String methodName, String protoDescriptor) {
+    List<T> parameterTypes = new ArrayList<>();
+    for (String parameterTypeDescriptor :
+        DescriptorUtils.getArgumentTypeDescriptors(protoDescriptor)) {
+      parameterTypes.add(typeFactory.apply(parameterTypeDescriptor));
+    }
+    String returnTypeDescriptor = DescriptorUtils.getReturnTypeDescriptor(protoDescriptor);
+    T returnType = typeFactory.apply(returnTypeDescriptor);
+    return methodFactory.createMethod(methodHolder, methodName, parameterTypes, returnType);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupInstrumentation.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupInstrumentation.java
index aa97fa6..0678602 100644
--- a/src/main/java/com/android/tools/r8/experimental/startup/StartupInstrumentation.java
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupInstrumentation.java
@@ -4,144 +4,160 @@
 
 package com.android.tools.r8.experimental.startup;
 
+import static com.android.tools.r8.utils.PredicateUtils.not;
+
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.cf.code.CfConstString;
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfInvoke;
-import com.android.tools.r8.cf.code.CfReturnVoid;
-import com.android.tools.r8.cf.code.CfStackInstruction;
-import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
-import com.android.tools.r8.cf.code.CfStaticFieldRead;
+import com.android.tools.r8.dex.code.DexInstruction;
+import com.android.tools.r8.dex.code.DexReturnVoid;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue.DexValueBoolean;
+import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.IRToDexFinalizer;
+import com.android.tools.r8.startup.generated.InstrumentationServerFactory;
+import com.android.tools.r8.startup.generated.InstrumentationServerImplFactory;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import org.objectweb.asm.Opcodes;
 
 public class StartupInstrumentation {
 
-  private final AppView<?> appView;
+  private final AppView<AppInfo> appView;
+  private final IRConverter converter;
   private final DexItemFactory dexItemFactory;
   private final StartupOptions options;
+  private final StartupReferences references;
 
-  public StartupInstrumentation(AppView<?> appView) {
+  private StartupInstrumentation(AppView<AppInfo> appView) {
     this.appView = appView;
+    this.converter = new IRConverter(appView, Timing.empty());
     this.dexItemFactory = appView.dexItemFactory();
     this.options = appView.options().getStartupOptions();
+    this.references = new StartupReferences(dexItemFactory);
   }
 
-  public void instrumentAllClasses(ExecutorService executorService) throws ExecutionException {
-    instrumentClasses(appView.appInfo().classes(), executorService);
-  }
-
-  public boolean instrumentClasses(
-      Collection<DexProgramClass> classes, ExecutorService executorService)
+  public static void run(AppView<AppInfo> appView, ExecutorService executorService)
       throws ExecutionException {
-    if (!appView.options().getStartupOptions().isStartupInstrumentationEnabled()) {
-      return false;
+    if (appView.options().getStartupOptions().isStartupInstrumentationEnabled()) {
+      StartupInstrumentation startupInstrumentation = new StartupInstrumentation(appView);
+      startupInstrumentation.instrumentAllClasses(executorService);
+      startupInstrumentation.injectStartupRuntimeLibrary(executorService);
     }
-    ThreadUtils.processItems(classes, this::internalInstrumentClass, executorService);
-    return true;
   }
 
-  public void instrumentClass(DexProgramClass clazz) {
-    if (!appView.options().getStartupOptions().isStartupInstrumentationEnabled()) {
-      return;
+  private void instrumentAllClasses(ExecutorService executorService) throws ExecutionException {
+    ThreadUtils.processItems(appView.appInfo().classes(), this::instrumentClass, executorService);
+  }
+
+  private void injectStartupRuntimeLibrary(ExecutorService executorService)
+      throws ExecutionException {
+    List<DexProgramClass> extraProgramClasses = createStartupRuntimeLibraryClasses();
+    converter.processClassesConcurrently(extraProgramClasses, executorService);
+
+    DexApplication newApplication =
+        appView.app().builder().addProgramClasses(extraProgramClasses).build();
+    appView.setAppInfo(
+        new AppInfo(
+            appView.appInfo().getSyntheticItems().commit(newApplication),
+            appView.appInfo().getMainDexInfo()));
+  }
+
+  private List<DexProgramClass> createStartupRuntimeLibraryClasses() {
+    DexProgramClass instrumentationServerImplClass =
+        InstrumentationServerImplFactory.createClass(dexItemFactory);
+    if (options.hasStartupInstrumentationTag()) {
+      instrumentationServerImplClass
+          .lookupUniqueStaticFieldWithName(dexItemFactory.createString("writeToLogcat"))
+          .setStaticValue(DexValueBoolean.create(true));
+      instrumentationServerImplClass
+          .lookupUniqueStaticFieldWithName(dexItemFactory.createString("logcatTag"))
+          .setStaticValue(
+              new DexValueString(
+                  dexItemFactory.createString(options.getStartupInstrumentationTag())));
     }
-    internalInstrumentClass(clazz);
+
+    return ImmutableList.of(
+        InstrumentationServerFactory.createClass(dexItemFactory), instrumentationServerImplClass);
   }
 
-  private void internalInstrumentClass(DexProgramClass clazz) {
-    ProgramMethod classInitializer = ensureClassInitializer(clazz);
-    instrumentClassInitializer(classInitializer);
+  private void instrumentClass(DexProgramClass clazz) {
+    ensureClassInitializer(clazz);
+    clazz.forEachProgramMethod(this::instrumentMethod);
   }
 
-  private ProgramMethod ensureClassInitializer(DexProgramClass clazz) {
+  private void ensureClassInitializer(DexProgramClass clazz) {
     if (!clazz.hasClassInitializer()) {
-      int maxLocals = 0;
-      int maxStack = 0;
       ComputedApiLevel computedApiLevel =
           appView.apiLevelCompute().computeInitialMinApiLevel(appView.options());
+      DexReturnVoid returnInstruction = new DexReturnVoid();
+      returnInstruction.setOffset(0);
       clazz.addDirectMethod(
           DexEncodedMethod.syntheticBuilder()
               .setAccessFlags(MethodAccessFlags.createForClassInitializer())
               .setApiLevelForCode(computedApiLevel)
               .setApiLevelForDefinition(computedApiLevel)
               .setClassFileVersion(CfVersion.V1_6)
-              .setCode(
-                  new CfCode(
-                      clazz.getType(), maxStack, maxLocals, ImmutableList.of(new CfReturnVoid())))
+              .setCode(new DexCode(0, 0, 0, new DexInstruction[] {returnInstruction}))
               .setMethod(dexItemFactory.createClassInitializer(clazz.getType()))
               .build());
     }
-    return clazz.getProgramClassInitializer();
   }
 
-  private void instrumentClassInitializer(ProgramMethod classInitializer) {
-    Code code = classInitializer.getDefinition().getCode();
-    if (!code.isCfCode()) {
-      // Should generally not happen.
-      assert false;
-      return;
-    }
-
+  private void instrumentMethod(ProgramMethod method) {
+    DexMethod methodToInvoke;
+    DexMethod methodToPrint;
     SyntheticItems syntheticItems = appView.getSyntheticItems();
-    DexString message;
-    if (syntheticItems.isSyntheticClass(classInitializer.getHolder())) {
+    if (syntheticItems.isSyntheticClass(method.getHolder())) {
       Collection<DexType> synthesizingContexts =
-          syntheticItems.getSynthesizingContextTypes(classInitializer.getHolderType());
+          syntheticItems.getSynthesizingContextTypes(method.getHolderType());
       assert synthesizingContexts.size() == 1;
-      message = synthesizingContexts.iterator().next().getDescriptor().prepend("S", dexItemFactory);
+      DexType synthesizingContext = synthesizingContexts.iterator().next();
+      methodToInvoke = references.addSyntheticMethod;
+      methodToPrint = method.getReference().withHolder(synthesizingContext, dexItemFactory);
     } else {
-      message = classInitializer.getHolderType().getDescriptor();
+      methodToInvoke = references.addNonSyntheticMethod;
+      methodToPrint = method.getReference();
     }
 
-    CfCode cfCode = code.asCfCode();
-    List<CfInstruction> instructions;
-    if (options.hasStartupInstrumentationTag()) {
-      instructions = new ArrayList<>(4 + cfCode.getInstructions().size());
-      instructions.add(
-          new CfConstString(dexItemFactory.createString(options.getStartupInstrumentationTag())));
-      instructions.add(new CfConstString(message));
-      instructions.add(
-          new CfInvoke(Opcodes.INVOKESTATIC, dexItemFactory.androidUtilLogMembers.i, false));
-      instructions.add(new CfStackInstruction(Opcode.Pop));
-    } else {
-      instructions = new ArrayList<>(3 + cfCode.getInstructions().size());
-      instructions.add(new CfStaticFieldRead(dexItemFactory.javaLangSystemMembers.out));
-      instructions.add(new CfConstString(message));
-      instructions.add(
-          new CfInvoke(
-              Opcodes.INVOKEVIRTUAL,
-              dexItemFactory.javaIoPrintStreamMembers.printlnWithString,
-              false));
-    }
-    instructions.addAll(cfCode.getInstructions());
-    classInitializer.setCode(
-        new CfCode(
-            cfCode.getOriginalHolder(),
-            Math.max(cfCode.getMaxStack(), 2),
-            cfCode.getMaxLocals(),
-            instructions,
-            cfCode.getTryCatchRanges(),
-            cfCode.getLocalVariables(),
-            cfCode.getDiagnosticPosition(),
-            cfCode.getMetadata()),
-        appView);
+    IRCode code = method.buildIR(appView);
+    InstructionListIterator instructionIterator = code.entryBlock().listIterator(code);
+    instructionIterator.positionBeforeNextInstructionThatMatches(not(Instruction::isArgument));
+
+    Value descriptorValue =
+        instructionIterator.insertConstStringInstruction(
+            appView, code, dexItemFactory.createString(methodToPrint.toSmaliString()));
+    instructionIterator.add(
+        InvokeStatic.builder()
+            .setMethod(methodToInvoke)
+            .setSingleArgument(descriptorValue)
+            .setPosition(Position.syntheticNone())
+            .build());
+    DexCode instrumentedCode =
+        new IRToDexFinalizer(appView, converter.deadCodeRemover)
+            .finalizeCode(code, BytecodeMetadataProvider.empty(), Timing.empty());
+    method.setCode(instrumentedCode, appView);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupItem.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupItem.java
new file mode 100644
index 0000000..c745c90
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupItem.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.experimental.startup;
+
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import java.util.function.Consumer;
+
+public abstract class StartupItem<C, M, R> {
+
+  private static final int FLAG_SYNTHETIC = 1;
+
+  protected final int flags;
+  protected final R reference;
+
+  public StartupItem(int flags, R reference) {
+    this.flags = flags;
+    this.reference = reference;
+  }
+
+  public boolean isStartupClass() {
+    return false;
+  }
+
+  public StartupClass<C, M> asStartupClass() {
+    return null;
+  }
+
+  public boolean isStartupMethod() {
+    return false;
+  }
+
+  public StartupMethod<C, M> asStartupMethod() {
+    return null;
+  }
+
+  public static <C, M> Builder<C, M, ?> builder() {
+    return new Builder<>();
+  }
+
+  public static Builder<DexType, DexMethod, ?> dexBuilder() {
+    return new Builder<>();
+  }
+
+  public int getFlags() {
+    return flags;
+  }
+
+  public R getReference() {
+    return reference;
+  }
+
+  public boolean isSynthetic() {
+    return (flags & FLAG_SYNTHETIC) != 0;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    StartupItem<?, ?, ?> startupItem = (StartupItem<?, ?, ?>) obj;
+    return flags == startupItem.flags && reference.equals(startupItem.reference);
+  }
+
+  @Override
+  public int hashCode() {
+    return (reference.hashCode() << 1) | flags;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    if (isSynthetic()) {
+      builder.append('S');
+    }
+    builder.append(reference);
+    return builder.toString();
+  }
+
+  public static class Builder<C, M, B extends Builder<C, M, B>> {
+
+    protected int flags;
+    protected C classReference;
+    protected M methodReference;
+
+    public B applyIf(boolean condition, Consumer<B> thenConsumer, Consumer<B> elseConsumer) {
+      if (condition) {
+        thenConsumer.accept(self());
+      } else {
+        elseConsumer.accept(self());
+      }
+      return self();
+    }
+
+    public B setFlags(int flags) {
+      this.flags = flags;
+      return self();
+    }
+
+    public B setClassReference(C reference) {
+      this.classReference = reference;
+      return self();
+    }
+
+    public B setMethodReference(M reference) {
+      this.methodReference = reference;
+      return self();
+    }
+
+    public B setSynthetic() {
+      this.flags |= FLAG_SYNTHETIC;
+      return self();
+    }
+
+    public StartupItem<C, M, ?> build() {
+      if (classReference != null) {
+        return buildStartupClass();
+      } else {
+        return buildStartupMethod();
+      }
+    }
+
+    public StartupClass<C, M> buildStartupClass() {
+      assert classReference != null;
+      return new StartupClass<>(flags, classReference);
+    }
+
+    public StartupMethod<C, M> buildStartupMethod() {
+      assert methodReference != null;
+      return new StartupMethod<>(flags, methodReference);
+    }
+
+    @SuppressWarnings("unchecked")
+    public B self() {
+      return (B) this;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupMethod.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupMethod.java
new file mode 100644
index 0000000..3109611
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupMethod.java
@@ -0,0 +1,43 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.experimental.startup;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+
+public class StartupMethod<C, M> extends StartupItem<C, M, M> {
+
+  public StartupMethod(int flags, M reference) {
+    super(flags, reference);
+  }
+
+  public static Builder<ClassReference, MethodReference> referenceBuilder() {
+    return new Builder<>();
+  }
+
+  @Override
+  public boolean isStartupMethod() {
+    return true;
+  }
+
+  @Override
+  public StartupMethod<C, M> asStartupMethod() {
+    return this;
+  }
+
+  public static class Builder<C, M> extends StartupItem.Builder<C, M, Builder<C, M>> {
+
+    @Override
+    public Builder<C, M> setClassReference(C reference) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public StartupMethod<C, M> build() {
+      return new StartupMethod<>(flags, methodReference);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupOrder.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupOrder.java
index 5d67d8c..1676991 100644
--- a/src/main/java/com/android/tools/r8/experimental/startup/StartupOrder.java
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupOrder.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.experimental.startup;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.PrunedItems;
@@ -35,7 +36,7 @@
 
   public abstract boolean contains(DexType type, SyntheticItems syntheticItems);
 
-  public abstract Collection<StartupClass<DexType>> getClasses();
+  public abstract Collection<StartupClass<DexType, DexMethod>> getClasses();
 
   public abstract boolean isEmpty();
 
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupReferences.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupReferences.java
new file mode 100644
index 0000000..0c2de77
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupReferences.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.experimental.startup;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+
+public class StartupReferences {
+
+  final DexType instrumentationServerImplType;
+  final DexMethod addNonSyntheticMethod;
+  final DexMethod addSyntheticMethod;
+
+  StartupReferences(DexItemFactory dexItemFactory) {
+    instrumentationServerImplType =
+        dexItemFactory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;");
+    addNonSyntheticMethod =
+        dexItemFactory.createMethod(
+            instrumentationServerImplType,
+            dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.stringType),
+            "addNonSyntheticMethod");
+    addSyntheticMethod =
+        dexItemFactory.createMethod(
+            instrumentationServerImplType,
+            dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.stringType),
+            "addSyntheticMethod");
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 0a485e7..b8613b8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -78,6 +78,17 @@
         .withItemArray(c -> c.instructions);
   }
 
+  public DexCode(int registerSize, int insSize, int outsSize, DexInstruction[] instructions) {
+    this(
+        registerSize,
+        insSize,
+        outsSize,
+        instructions,
+        Try.EMPTY_ARRAY,
+        TryHandler.EMPTY_ARRAY,
+        null);
+  }
+
   public DexCode(
       int registerSize,
       int insSize,
@@ -714,6 +725,8 @@
 
   public static class Try extends DexItem implements StructuralItem<Try> {
 
+    public static final Try[] EMPTY_ARRAY = new Try[0];
+
     public static final int NO_INDEX = -1;
 
     public final int handlerOffset;
@@ -779,6 +792,8 @@
 
   public static class TryHandler extends DexItem implements StructuralItem<TryHandler> {
 
+    public static final TryHandler[] EMPTY_ARRAY = new TryHandler[0];
+
     public static final int NO_HANDLER = -1;
 
     public final TypeAddrPair[] pairs;
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 8fe1c7e..44506fd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2118,7 +2118,7 @@
     }
 
     public boolean isAppendCharSequenceMethod(DexMethod method) {
-      return method == appendCharSequence;
+      return method == appendCharSequence || method == appendSubCharSequence;
     }
 
     public boolean isAppendObjectOrCharSequenceMethod(DexMethod method) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
index ef0e76e..0efe7d0 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
@@ -407,6 +407,11 @@
         .asArrayType();
   }
 
+  public static ClassTypeElement stringClassType(AppView<?> appView) {
+    return fromDexType(appView.dexItemFactory().stringType, Nullability.maybeNull(), appView)
+        .asClassType();
+  }
+
   public static ClassTypeElement classClassType(AppView<?> appView, Nullability nullability) {
     return fromDexType(appView.dexItemFactory().classType, nullability, appView).asClassType();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 0cb7228..3dcfa35 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -125,6 +125,11 @@
     return next();
   }
 
+  default Instruction positionBeforeNextInstructionThatMatches(Predicate<Instruction> predicate) {
+    nextUntil(predicate);
+    return previous();
+  }
+
   boolean replaceCurrentInstructionByNullCheckIfPossible(AppView<?> appView, ProgramMethod context);
 
   default boolean removeOrReplaceCurrentInstructionByInitClassIfPossible(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
index 36f717a..71d88d5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
@@ -158,8 +158,7 @@
       // Finalize the desugaring of the processed classes. This may require processing (and
       // reprocessing) of some methods.
       List<ProgramMethod> needsProcessing =
-          instructionDesugaringEventConsumer.finalizeDesugaring(
-              appView, executorService, resultBuilder);
+          instructionDesugaringEventConsumer.finalizeDesugaring(appView, resultBuilder);
       if (!needsProcessing.isEmpty()) {
         // Create a new processor context to ensure unique method processing contexts.
         methodProcessor.newWave();
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 52dc8b3..33f3f28 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
@@ -9,7 +9,6 @@
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.experimental.startup.StartupInstrumentation;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Code;
@@ -104,6 +103,7 @@
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -466,8 +466,6 @@
       D8CfInstructionDesugaringEventConsumer desugaringEventConsumer,
       D8MethodProcessor methodProcessor,
       InterfaceProcessor interfaceProcessor) {
-    new StartupInstrumentation(appView).instrumentClass(clazz);
-
     // When converting all methods on a class always convert <clinit> first.
     ProgramMethod classInitializer = clazz.getProgramClassInitializer();
 
@@ -931,6 +929,16 @@
     }
   }
 
+  public void processClassesConcurrently(
+      Collection<DexProgramClass> classes, ExecutorService executorService)
+      throws ExecutionException {
+    ProgramMethodSet wave = ProgramMethodSet.create();
+    for (DexProgramClass clazz : classes) {
+      clazz.forEachProgramMethod(wave::add);
+    }
+    processMethodsConcurrently(wave, executorService);
+  }
+
   public void processMethodsConcurrently(ProgramMethodSet wave, ExecutorService executorService)
       throws ExecutionException {
     if (!wave.isEmpty()) {
@@ -1112,8 +1120,7 @@
       codeRewriter.simplifyDebugLocals(code);
     }
 
-    if (appView.graphLens().hasCodeRewritings()) {
-      assert lensCodeRewriter != null;
+    if (lensCodeRewriter != null) {
       timing.begin("Lens rewrite");
       lensCodeRewriter.rewrite(code, context, methodProcessor);
       timing.end();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 15c214a..fcf37fb 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.ir.desugar;
 
-import com.android.tools.r8.experimental.startup.StartupInstrumentation;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClasspathClass;
@@ -41,8 +40,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
@@ -90,7 +87,6 @@
 
     private final D8MethodProcessor methodProcessor;
 
-    private final Set<DexProgramClass> synthesizedClasses = Sets.newConcurrentHashSet();
     private final Map<DexReference, InvokeSpecialBridgeInfo> pendingInvokeSpecialBridges =
         new LinkedHashMap<>();
     private final List<LambdaClass> synthesizedLambdaClasses = new ArrayList<>();
@@ -100,18 +96,9 @@
       this.methodProcessor = methodProcessor;
     }
 
-    private void acceptClass(DexProgramClass clazz) {
-      synthesizedClasses.add(clazz);
-    }
-
-    private void acceptMethod(ProgramMethod method) {
-      acceptClass(method.getHolder());
-    }
-
     @Override
     public void acceptCompanionMethod(ProgramMethod method, ProgramMethod companionMethod) {
       // Intentionally empty. Methods are moved when processing the interface definition.
-      acceptMethod(method);
     }
 
     @Override
@@ -126,25 +113,21 @@
 
     @Override
     public void acceptCollectionConversion(ProgramMethod arrayConversion) {
-      acceptMethod(arrayConversion);
       methodProcessor.scheduleMethodForProcessing(arrayConversion, this);
     }
 
     @Override
     public void acceptCovariantRetargetMethod(ProgramMethod method) {
-      acceptMethod(method);
       methodProcessor.scheduleMethodForProcessing(method, this);
     }
 
     @Override
     public void acceptBackportedMethod(ProgramMethod backportedMethod, ProgramMethod context) {
-      acceptMethod(backportedMethod);
       methodProcessor.scheduleMethodForProcessing(backportedMethod, this);
     }
 
     @Override
     public void acceptRecordMethod(ProgramMethod method) {
-      acceptMethod(method);
       methodProcessor.scheduleDesugaredMethodForProcessing(method);
     }
 
@@ -158,13 +141,11 @@
 
     @Override
     public void acceptRecordClass(DexProgramClass recordClass) {
-      acceptClass(recordClass);
       methodProcessor.scheduleDesugaredMethodsForProcessing(recordClass.programMethods());
     }
 
     @Override
     public void acceptLambdaClass(LambdaClass lambdaClass, ProgramMethod context) {
-      acceptClass(lambdaClass.getLambdaProgramClass());
       synchronized (synthesizedLambdaClasses) {
         synthesizedLambdaClasses.add(lambdaClass);
       }
@@ -173,7 +154,6 @@
     @Override
     public void acceptConstantDynamicClass(
         ConstantDynamicClass constantDynamicClass, ProgramMethod context) {
-      acceptClass(constantDynamicClass.getConstantDynamicProgramClass());
       synchronized (synthesizedConstantDynamicClasses) {
         synthesizedConstantDynamicClasses.add(constantDynamicClass);
       }
@@ -196,20 +176,17 @@
 
     @Override
     public void acceptTwrCloseResourceMethod(ProgramMethod closeMethod, ProgramMethod context) {
-      acceptMethod(closeMethod);
       methodProcessor.scheduleMethodForProcessing(closeMethod, this);
     }
 
     @Override
     public void acceptThrowMethod(ProgramMethod method, ProgramMethod context) {
-      acceptMethod(method);
       methodProcessor.scheduleDesugaredMethodForProcessing(method);
     }
 
     @Override
     public void acceptInvokeStaticInterfaceOutliningMethod(
         ProgramMethod method, ProgramMethod context) {
-      acceptMethod(method);
       methodProcessor.scheduleDesugaredMethodForProcessing(method);
     }
 
@@ -225,31 +202,20 @@
 
     @Override
     public void acceptAPIConversion(ProgramMethod method) {
-      acceptMethod(method);
       methodProcessor.scheduleDesugaredMethodForProcessing(method);
     }
 
     @Override
     public void acceptCompanionClassClinit(ProgramMethod method) {
-      acceptMethod(method);
       methodProcessor.scheduleDesugaredMethodForProcessing(method);
     }
 
     public List<ProgramMethod> finalizeDesugaring(
-        AppView<?> appView,
-        ExecutorService executorService,
-        ClassConverterResult.Builder classConverterResultBuilder)
-        throws ExecutionException {
+        AppView<?> appView, ClassConverterResult.Builder classConverterResultBuilder) {
       List<ProgramMethod> needsProcessing = new ArrayList<>();
       finalizeInvokeSpecialDesugaring(appView, needsProcessing::add);
       finalizeLambdaDesugaring(classConverterResultBuilder, needsProcessing::add);
       finalizeConstantDynamicDesugaring(needsProcessing::add);
-      if (new StartupInstrumentation(appView)
-          .instrumentClasses(synthesizedClasses, executorService)) {
-        for (DexProgramClass synthesizedClass : synthesizedClasses) {
-          needsProcessing.add(synthesizedClass.getProgramClassInitializer());
-        }
-      }
       return needsProcessing;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryWrapperSynthesizer.java
index d28ccaf..2dcf7a4 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/apiconversion/DesugaredLibraryWrapperSynthesizer.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.apiconversion.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.CustomConversionDescriptor;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MachineDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.WrapperDescriptor;
 import com.android.tools.r8.ir.synthetic.apiconverter.NullableConversionCfCodeProvider;
 import com.android.tools.r8.ir.synthetic.apiconverter.NullableConversionCfCodeProvider.ArrayConversionCfCodeProvider;
 import com.android.tools.r8.ir.synthetic.apiconverter.WrapperConstructorCfCodeProvider;
@@ -46,6 +47,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.function.BiFunction;
@@ -108,6 +110,35 @@
     this.conversionCfProvider = new DesugaredLibraryConversionCfProvider(appView, this);
   }
 
+  enum WrapperKind {
+    WRAPPER,
+    VIVIFIED_WRAPPER;
+
+    SyntheticKindSelector getKindSelector() {
+      if (this == WrapperKind.WRAPPER) {
+        return kinds1 -> kinds1.WRAPPER;
+      } else {
+        return kinds -> kinds.VIVIFIED_WRAPPER;
+      }
+    }
+
+    DexType getWrappingType(DexType type, DexType vivifiedType) {
+      if (this == WrapperKind.WRAPPER) {
+        return vivifiedType;
+      } else {
+        return type;
+      }
+    }
+
+    DexType getWrappedType(DexType type, DexType vivifiedType) {
+      if (this == WrapperKind.WRAPPER) {
+        return type;
+      } else {
+        return vivifiedType;
+      }
+    }
+  }
+
   public DesugaredLibraryConversionCfProvider getConversionCfProvider() {
     return conversionCfProvider;
   }
@@ -363,14 +394,19 @@
     }
     assert context.isNotProgramClass();
     Iterable<DexMethod> methods =
-        appView.options().machineDesugaredLibrarySpecification.getWrappers().get(context.type);
+        appView
+            .options()
+            .machineDesugaredLibrarySpecification
+            .getWrappers()
+            .get(context.type)
+            .getMethods();
     assert methods != null;
     ClasspathOrLibraryClass classpathOrLibraryContext = context.asClasspathOrLibraryClass();
     DexType type = context.type;
     DexType vivifiedType = vivifiedTypeFor(type);
     DexClass wrapper =
         ensureClasspathWrapper(
-            kinds -> kinds.WRAPPER,
+            WrapperKind.WRAPPER,
             vivifiedType,
             type,
             classpathOrLibraryContext,
@@ -379,7 +415,7 @@
             conversionCfProvider::generateWrapperConversionWithoutCode);
     DexClass vivifiedWrapper =
         ensureClasspathWrapper(
-            kinds -> kinds.VIVIFIED_WRAPPER,
+            WrapperKind.VIVIFIED_WRAPPER,
             type,
             vivifiedType,
             classpathOrLibraryContext,
@@ -395,8 +431,8 @@
     DexClass vivifiedWrapper;
     DexClass wrapper;
     assert appView.options().isDesugaredLibraryCompilation();
-    wrapper = getExistingProgramWrapper(context, kinds -> kinds.WRAPPER);
-    vivifiedWrapper = getExistingProgramWrapper(context, kinds -> kinds.VIVIFIED_WRAPPER);
+    wrapper = getExistingProgramWrapper(context, WrapperKind.WRAPPER);
+    vivifiedWrapper = getExistingProgramWrapper(context, WrapperKind.VIVIFIED_WRAPPER);
     DexField wrapperField = getWrapperUniqueField(wrapper);
     DexField vivifiedWrapperField = getWrapperUniqueField(vivifiedWrapper);
     return new WrapperConversions(
@@ -404,9 +440,15 @@
         getConversion(vivifiedWrapper, wrapperField.type, vivifiedWrapperField.type));
   }
 
-  private DexProgramClass getExistingProgramWrapper(
-      DexClass context, SyntheticKindSelector kindSelector) {
-    return appView.getSyntheticItems().getExistingFixedClass(kindSelector, context, appView);
+  private DexProgramClass getExistingProgramWrapper(DexClass context, WrapperKind kind) {
+    if (context.isEnum()) {
+      return appView
+          .getSyntheticItems()
+          .getExistingFixedClass(kinds -> kinds.ENUM_CONVERSION, context, appView);
+    }
+    return appView
+        .getSyntheticItems()
+        .getExistingFixedClass(kind.getKindSelector(), getWrapperContext(context, kind), appView);
   }
 
   private DexMethod getConversion(DexClass wrapper, DexType returnType, DexType argType) {
@@ -426,27 +468,32 @@
   }
 
   private DexProgramClass ensureProgramWrapper(
-      SyntheticKindSelector kindSelector,
-      DexType wrappingType,
-      DexType wrappedType,
-      DexProgramClass programContext,
+      DexType type,
+      DexProgramClass context,
+      WrapperKind kind,
       DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer eventConsumer) {
     assert appView.options().isDesugaredLibraryCompilation();
     assert eventConsumer != null;
+    DexType vivifiedType = vivifiedTypeFor(type);
     return appView
         .getSyntheticItems()
         .ensureFixedClass(
-            kindSelector,
-            programContext,
+            kind.getKindSelector(),
+            getWrapperContext(context, kind),
             appView,
-            builder -> buildWrapper(wrappingType, wrappedType, programContext, builder),
+            builder ->
+                buildWrapper(
+                    kind.getWrappingType(type, vivifiedType),
+                    kind.getWrappedType(type, vivifiedType),
+                    context.isInterface(),
+                    builder),
             // The creation of virtual methods may require new wrappers, this needs to happen
             // once the wrapper is created to avoid infinite recursion.
             eventConsumer::acceptWrapperProgramClass);
   }
 
   private DexClasspathClass ensureClasspathWrapper(
-      SyntheticKindSelector kindSelector,
+      WrapperKind kind,
       DexType wrappingType,
       DexType wrappedType,
       ClasspathOrLibraryClass classpathOrLibraryContext,
@@ -457,13 +504,14 @@
     return appView
         .getSyntheticItems()
         .ensureFixedClasspathClass(
-            kindSelector,
-            classpathOrLibraryContext,
+            kind.getKindSelector(),
+            getWrapperContext(classpathOrLibraryContext.asDexClass(), kind)
+                .asClasspathOrLibraryClass(),
             appView,
             builder -> {
               DexEncodedField wrapperField =
                   buildWrapper(
-                      wrappingType, wrappedType, classpathOrLibraryContext.asDexClass(), builder);
+                      wrappingType, wrappedType, classpathOrLibraryContext.isInterface(), builder);
               builder.addMethod(
                   methodBuilder ->
                       buildConversionMethod(
@@ -474,21 +522,29 @@
             eventConsumer::acceptWrapperClasspathClass);
   }
 
-  private void getExistingProgramConversionMethod(
-      SyntheticKindSelector kindSelector,
+  private void synthesizeProgramConversionMethod(
+      WrapperKind kind,
       DexProgramClass context,
+      List<DexType> subwrappers,
       DexClass wrapper,
       DexClass reverseWrapper) {
     DexField wrapperField = getWrapperUniqueField(wrapper);
     DexField reverseWrapperField = getWrapperUniqueField(reverseWrapper);
+    List<DexMethod> subwrapperConvertList = new ArrayList<>();
+    for (DexType subwrapper : subwrappers) {
+      DexClass subwrapperClass = appView.definitionFor(subwrapper);
+      assert subwrapperClass != null;
+      DexProgramClass subwrapperWrapper = getExistingProgramWrapper(subwrapperClass, kind);
+      subwrapperConvertList.add(getConversion(subwrapperWrapper));
+    }
     DexProto proto = factory.createProto(reverseWrapperField.type, wrapperField.type);
     appView
         .getSyntheticItems()
         .ensureFixedClassMethod(
             factory.convertMethodName,
             proto,
-            kindSelector,
-            context,
+            kind.getKindSelector(),
+            getWrapperContext(context, kind).asProgramOrClasspathDefinition(),
             appView,
             ignored -> {},
             methodBuilder ->
@@ -496,14 +552,27 @@
                     methodBuilder,
                     proto,
                     computeProgramConversionMethodCode(
-                        wrapperField, reverseWrapperField, context)));
+                        wrapperField, reverseWrapperField, context, subwrapperConvertList)));
+  }
+
+  private DexMethod getConversion(DexProgramClass subwrapperWrapper) {
+    Iterator<DexEncodedMethod> iterator = subwrapperWrapper.directMethods().iterator();
+    DexEncodedMethod method;
+    do {
+      method = iterator.next();
+    } while (!method.isStatic());
+    assert method.getName() == factory.convertMethodName;
+    return method.getReference();
   }
 
   private CfCode computeProgramConversionMethodCode(
-      DexField wrapperField, DexField reverseWrapperField, DexClass context) {
+      DexField wrapperField,
+      DexField reverseWrapperField,
+      DexClass context,
+      List<DexMethod> subwrapperConvertList) {
     assert context.isProgramClass();
     return new NullableConversionCfCodeProvider.WrapperConversionCfCodeProvider(
-            appView, reverseWrapperField, wrapperField)
+            appView, reverseWrapperField, wrapperField, subwrapperConvertList)
         .generateCfCode();
   }
 
@@ -523,9 +592,8 @@
   private DexEncodedField buildWrapper(
       DexType wrappingType,
       DexType wrappedType,
-      DexClass clazz,
+      boolean isItf,
       SyntheticClassBuilder<?, ?> builder) {
-    boolean isItf = clazz.isInterface();
     DexType superType = isItf ? factory.objectType : wrappingType;
     List<DexType> interfaces =
         isItf ? Collections.singletonList(wrappingType) : Collections.emptyList();
@@ -607,7 +675,8 @@
     DexField field = wrappedValueField(holder, fieldType);
     // Field is package private to be accessible from convert methods without a getter.
     FieldAccessFlags fieldAccessFlags =
-        FieldAccessFlags.fromCfAccessFlags(Constants.ACC_FINAL | Constants.ACC_SYNTHETIC);
+        FieldAccessFlags.fromCfAccessFlags(
+            Constants.ACC_PUBLIC | Constants.ACC_FINAL | Constants.ACC_SYNTHETIC);
     return DexEncodedField.syntheticBuilder()
         .setField(field)
         .setAccessFlags(fieldAccessFlags)
@@ -635,7 +704,7 @@
     librarySpecification
         .getWrappers()
         .forEach(
-            (type, methods) -> {
+            (type, descriptor) -> {
               DexClass validClassToWrap = getValidClassToWrap(type);
               // In broken set-ups we can end up having a json files containing wrappers of non
               // desugared classes. Such wrappers are not required since the class won't be
@@ -644,47 +713,65 @@
                 if (validClassToWrap.isEnum()) {
                   enumConverter.ensureProgramEnumConversionClass(validClassToWrap, eventConsumer);
                 } else {
-                  validClassesToWrap.put(validClassToWrap.asProgramClass(), methods);
-                  ensureProgramWrappersWithoutVirtualMethods(validClassToWrap, eventConsumer);
+                  validClassesToWrap.put(
+                      validClassToWrap.asProgramClass(), descriptor.getMethods());
+                  synthesizeProgramWrappersWithoutVirtualMethods(
+                      validClassToWrap, descriptor.getSubwrappers(), eventConsumer);
                 }
               }
             });
     validClassesToWrap.forEach(
         (clazz, methods) ->
-            ensureProgramWrappersVirtualMethods(clazz, methods, eventConsumer, processingContext));
+            synthesizeProgramWrappersVirtualMethods(
+                clazz, methods, eventConsumer, processingContext));
+  }
+
+  private DexClass getWrapperContext(DexClass context, WrapperKind kind) {
+    if (kind != WrapperKind.VIVIFIED_WRAPPER) {
+      return context;
+    }
+    WrapperDescriptor descriptor =
+        appView.options().machineDesugaredLibrarySpecification.getWrappers().get(context.type);
+    assert descriptor != null;
+    if (descriptor.hasNonPublicAccess()) {
+      return appView
+          .getSyntheticItems()
+          .ensureFixedClasspathClassFromType(
+              kinds -> kinds.VIVIFIED,
+              vivifiedTypeFor(context.type),
+              appView,
+              ignored -> {},
+              ignored -> {});
+    }
+    return context;
   }
 
   // We generate first the two wrappers with the constructor method and the fields, then we
   // the two conversion methods which requires the wrappers to know both fields.
-  private void ensureProgramWrappersWithoutVirtualMethods(
-      DexClass context, DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer eventConsumer) {
+  private void synthesizeProgramWrappersWithoutVirtualMethods(
+      DexClass context,
+      List<DexType> subwrappers,
+      DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer eventConsumer) {
     assert eventConsumer != null;
     assert context.isProgramClass();
     DexType type = context.type;
-    assert appView.options().isDesugaredLibraryCompilation();
     DexProgramClass programContext = context.asProgramClass();
     DexClass wrapper =
-        ensureProgramWrapper(
-            kinds -> kinds.WRAPPER, vivifiedTypeFor(type), type, programContext, eventConsumer);
+        ensureProgramWrapper(type, programContext, WrapperKind.WRAPPER, eventConsumer);
     DexClass vivifiedWrapper =
-        ensureProgramWrapper(
-            kinds -> kinds.VIVIFIED_WRAPPER,
-            type,
-            vivifiedTypeFor(type),
-            programContext,
-            eventConsumer);
-    getExistingProgramConversionMethod(
-        kinds -> kinds.WRAPPER, programContext, wrapper, vivifiedWrapper);
-    getExistingProgramConversionMethod(
-        kinds -> kinds.VIVIFIED_WRAPPER, programContext, vivifiedWrapper, wrapper);
+        ensureProgramWrapper(type, programContext, WrapperKind.VIVIFIED_WRAPPER, eventConsumer);
+    synthesizeProgramConversionMethod(
+        WrapperKind.WRAPPER, programContext, subwrappers, wrapper, vivifiedWrapper);
+    synthesizeProgramConversionMethod(
+        WrapperKind.VIVIFIED_WRAPPER, programContext, subwrappers, vivifiedWrapper, wrapper);
   }
 
-  private void ensureProgramWrappersVirtualMethods(
+  private void synthesizeProgramWrappersVirtualMethods(
       DexProgramClass context,
       Iterable<DexMethod> methods,
       CfClassSynthesizerDesugaringEventConsumer eventConsumer,
       ClassSynthesisDesugaringContext processingContext) {
-    DexProgramClass wrapper = getExistingProgramWrapper(context, kinds -> kinds.WRAPPER);
+    DexProgramClass wrapper = getExistingProgramWrapper(context, WrapperKind.WRAPPER);
     DexEncodedField wrapperField = getWrapperUniqueEncodedField(wrapper);
     wrapper.addVirtualMethods(
         synthesizeVirtualMethodsForWrapper(
@@ -698,7 +785,7 @@
                     () -> processingContext.createUniqueContext(wrapper))));
     wrapper.addVirtualMethods(synthesizeHashCodeAndEquals(wrapper, wrapperField));
     DexProgramClass vivifiedWrapper =
-        getExistingProgramWrapper(context, kinds -> kinds.VIVIFIED_WRAPPER);
+        getExistingProgramWrapper(context, WrapperKind.VIVIFIED_WRAPPER);
     DexEncodedField vivifiedWrapperField = getWrapperUniqueEncodedField(vivifiedWrapper);
     vivifiedWrapper.addVirtualMethods(
         synthesizeVirtualMethodsForWrapper(
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
index f17235f..5c70c34 100644
--- 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
@@ -15,11 +15,13 @@
 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.Collections;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 public class HumanRewritingFlags {
 
@@ -443,6 +445,7 @@
     }
 
     public HumanRewritingFlags build() {
+      validate();
       return new HumanRewritingFlags(
           ImmutableMap.copyOf(rewritePrefix),
           ImmutableSet.copyOf(dontRewritePrefix),
@@ -462,5 +465,19 @@
           ImmutableMap.copyOf(amendLibraryMethod),
           ImmutableMap.copyOf(amendLibraryField));
     }
+
+    private void validate() {
+      SetView<DexType> dups =
+          Sets.intersection(customConversions.keySet(), wrapperConversions.keySet());
+      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/machinespecification/MachineDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
index 16b0b9f..4aa99b6 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
@@ -145,7 +145,7 @@
     return rewritingFlags.isEmulatedInterfaceRewrittenType(type);
   }
 
-  public Map<DexType, List<DexMethod>> getWrappers() {
+  public Map<DexType, WrapperDescriptor> getWrappers() {
     return rewritingFlags.getWrappers();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
index e125c53..5ad734b 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineRewritingFlags.java
@@ -10,13 +10,12 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.MethodAccessFlags;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
-import java.util.List;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -39,7 +38,7 @@
       Map<DexMethod, DexMethod> emulatedVirtualRetargetThroughEmulatedInterface,
       Map<DexMethod, DexMethod[]> apiGenericTypesConversion,
       Map<DexType, EmulatedInterfaceDescriptor> emulatedInterfaces,
-      Map<DexType, List<DexMethod>> wrappers,
+      LinkedHashMap<DexType, WrapperDescriptor> wrappers,
       Map<DexType, DexType> legacyBackport,
       Set<DexType> dontRetarget,
       Map<DexType, CustomConversionDescriptor> customConversions,
@@ -101,8 +100,8 @@
   // Emulated interface descriptors.
   private final Map<DexType, EmulatedInterfaceDescriptor> emulatedInterfaces;
 
-  // Wrappers and the list of methods they implement.
-  private final Map<DexType, List<DexMethod>> wrappers;
+  // Wrapper descriptors.
+  private final LinkedHashMap<DexType, WrapperDescriptor> wrappers;
 
   private final Map<DexType, DexType> legacyBackport;
   private final Set<DexType> dontRetarget;
@@ -160,7 +159,7 @@
     return emulatedInterfaces;
   }
 
-  public Map<DexType, List<DexMethod>> getWrappers() {
+  public Map<DexType, WrapperDescriptor> getWrappers() {
     return wrappers;
   }
 
@@ -249,7 +248,7 @@
         ImmutableMap.builder();
     private final ImmutableMap.Builder<DexType, EmulatedInterfaceDescriptor> emulatedInterfaces =
         ImmutableMap.builder();
-    private final ImmutableMap.Builder<DexType, List<DexMethod>> wrappers = ImmutableMap.builder();
+    private final LinkedHashMap<DexType, WrapperDescriptor> wrappers = new LinkedHashMap<>();
     private final ImmutableMap.Builder<DexType, DexType> legacyBackport = ImmutableMap.builder();
     private final ImmutableSet.Builder<DexType> dontRetarget = ImmutableSet.builder();
     private final ImmutableMap.Builder<DexType, CustomConversionDescriptor> customConversions =
@@ -308,8 +307,8 @@
       apiGenericTypesConversion.put(method, conversions);
     }
 
-    public void addWrapper(DexType wrapperConversion, List<DexMethod> methods) {
-      wrappers.put(wrapperConversion, ImmutableList.copyOf(methods));
+    public void addWrapper(DexType type, WrapperDescriptor descriptor) {
+      this.wrappers.put(type, descriptor);
     }
 
     public void putLegacyBackport(DexType src, DexType target) {
@@ -367,7 +366,7 @@
           emulatedVirtualRetargetThroughEmulatedInterface.build(),
           apiGenericTypesConversion.build(),
           emulatedInterfaces.build(),
-          wrappers.build(),
+          wrappers,
           legacyBackport.build(),
           dontRetarget.build(),
           customConversions.build(),
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/WrapperDescriptor.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/WrapperDescriptor.java
new file mode 100644
index 0000000..2403b90
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/WrapperDescriptor.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification;
+
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import java.util.List;
+
+public class WrapperDescriptor {
+  private final List<DexMethod> methods;
+  private final List<DexType> subwrappers;
+  private final boolean nonPublicAccess;
+
+  public WrapperDescriptor(
+      List<DexMethod> methods, List<DexType> directSubtypes, boolean nonPublicAccess) {
+    this.methods = methods;
+    this.subwrappers = directSubtypes;
+    this.nonPublicAccess = nonPublicAccess;
+  }
+
+  public List<DexMethod> getMethods() {
+    return methods;
+  }
+
+  public List<DexType> getSubwrappers() {
+    return subwrappers;
+  }
+
+  public boolean hasNonPublicAccess() {
+    return nonPublicAccess;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineWrapperConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineWrapperConverter.java
index c0d9157..e471367 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineWrapperConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineWrapperConverter.java
@@ -12,14 +12,19 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.humanspecification.HumanRewritingFlags;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MachineRewritingFlags;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.WrapperDescriptor;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -28,6 +33,7 @@
   private final MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get();
   private final AppInfoWithClassHierarchy appInfo;
   private final Set<DexType> missingClasses = Sets.newIdentityHashSet();
+  private final Set<DexMethod> invalidMethods = Sets.newIdentityHashSet();
 
   public HumanToMachineWrapperConverter(AppInfoWithClassHierarchy appInfo) {
     this.appInfo = appInfo;
@@ -37,6 +43,84 @@
       HumanRewritingFlags rewritingFlags,
       MachineRewritingFlags.Builder builder,
       BiConsumer<String, Set<? extends DexReference>> warnConsumer) {
+    Map<DexType, WrapperDescriptorBuilder> descriptors = initializeDescriptors(rewritingFlags);
+    fillDescriptors(rewritingFlags, descriptors);
+    // The descriptors have to be ordered so that when processing a type, subtypes have been
+    // processed before.
+    LinkedHashMap<DexType, WrapperDescriptorBuilder> orderedDescriptors =
+        orderDescriptors(descriptors);
+    clearIncompleteSubwrappers(orderedDescriptors, rewritingFlags.getWrapperConversions());
+    finalizeWrapperDescriptors(orderedDescriptors, builder);
+    warnConsumer.accept("The following types to wrap are missing: ", missingClasses);
+    warnConsumer.accept(
+        "The following methods cannot be handled by the wrappers due to their flags: ",
+        invalidMethods);
+  }
+
+  private void clearIncompleteSubwrappers(
+      LinkedHashMap<DexType, WrapperDescriptorBuilder> orderedDescriptors,
+      Map<DexType, Set<DexMethod>> wrapperConversions) {
+    // If the wrapper is incomplete, it may lead to runtime errors.
+    // We never try to specialize the wrapper to an incomplete wrapper for this reason.
+    for (WrapperDescriptorBuilder descriptor : orderedDescriptors.values()) {
+      List<DexType> toRemove = new ArrayList<>();
+      for (DexType subwrapper : descriptor.getSubwrappers()) {
+        if (!wrapperConversions.get(subwrapper).isEmpty()) {
+          toRemove.add(subwrapper);
+        }
+      }
+      descriptor.removeSubwrappers(toRemove);
+    }
+  }
+
+  private static class WrapperDescriptorBuilder {
+    private final List<DexMethod> methods = new ArrayList<>();
+    private final List<DexType> subwrappers = new ArrayList<>();
+    private boolean nonPublicAccess = false;
+
+    public WrapperDescriptorBuilder() {}
+
+    public List<DexMethod> getMethods() {
+      return methods;
+    }
+
+    public List<DexType> getSubwrappers() {
+      return subwrappers;
+    }
+
+    public void addSubwrapper(DexType type) {
+      subwrappers.add(type);
+    }
+
+    public void setNonPublicAccess() {
+      nonPublicAccess = true;
+    }
+
+    public WrapperDescriptor toWrapperDescriptor() {
+      methods.sort(DexMethod::compareTo);
+      subwrappers.sort(DexType::compareTo);
+      return new WrapperDescriptor(
+          ImmutableList.copyOf(methods), ImmutableList.copyOf(subwrappers), nonPublicAccess);
+    }
+
+    public void removeSubwrappers(List<DexType> toRemove) {
+      if (!toRemove.isEmpty()) {
+        subwrappers.removeAll(toRemove);
+      }
+    }
+  }
+
+  private Map<DexType, WrapperDescriptorBuilder> initializeDescriptors(
+      HumanRewritingFlags rewritingFlags) {
+    Map<DexType, WrapperDescriptorBuilder> descriptors = new IdentityHashMap<>();
+    for (DexType wrapperType : rewritingFlags.getWrapperConversions().keySet()) {
+      descriptors.put(wrapperType, new WrapperDescriptorBuilder());
+    }
+    return descriptors;
+  }
+
+  private void fillDescriptors(
+      HumanRewritingFlags rewritingFlags, Map<DexType, WrapperDescriptorBuilder> descriptors) {
     rewritingFlags
         .getWrapperConversions()
         .forEach(
@@ -44,53 +128,96 @@
               DexClass wrapperClass = appInfo.definitionFor(wrapperType);
               if (wrapperClass == null) {
                 missingClasses.add(wrapperType);
+                descriptors.remove(wrapperType);
                 return;
               }
-              List<DexMethod> methods;
-              if (wrapperClass.isEnum()) {
-                methods = ImmutableList.of();
-              } else {
-                methods = allImplementedMethods(wrapperClass, excludedMethods);
-                methods.sort(DexMethod::compareTo);
-              }
-              builder.addWrapper(wrapperType, methods);
+              WrapperDescriptorBuilder descriptor = descriptors.get(wrapperType);
+              fillDescriptors(wrapperClass, excludedMethods, descriptor, descriptors);
             });
-    warnConsumer.accept("The following types to wrap are missing: ", missingClasses);
   }
 
-  private List<DexMethod> allImplementedMethods(
-      DexClass wrapperClass, Set<DexMethod> excludedMethods) {
+  private LinkedHashMap<DexType, WrapperDescriptorBuilder> orderDescriptors(
+      Map<DexType, WrapperDescriptorBuilder> descriptors) {
+    LinkedHashMap<DexType, WrapperDescriptorBuilder> orderedDescriptors = new LinkedHashMap<>();
+    List<DexType> preOrdered = new ArrayList<>(descriptors.keySet());
+    preOrdered.sort(DexType::compareTo);
+    LinkedList<DexType> workList = new LinkedList<>(preOrdered);
+    while (!workList.isEmpty()) {
+      DexType dexType = workList.removeFirst();
+      WrapperDescriptorBuilder descriptor = descriptors.get(dexType);
+      List<DexType> subwrappers = descriptor.getSubwrappers();
+      if (Iterables.all(subwrappers, orderedDescriptors::containsKey)) {
+        orderedDescriptors.put(dexType, descriptor);
+      } else {
+        workList.addLast(dexType);
+      }
+    }
+    return orderedDescriptors;
+  }
+
+  private void finalizeWrapperDescriptors(
+      LinkedHashMap<DexType, WrapperDescriptorBuilder> descriptors,
+      MachineRewritingFlags.Builder builder) {
+    descriptors.forEach(
+        (type, descriptor) -> {
+          LinkedList<DexType> workList = new LinkedList<>(descriptor.getSubwrappers());
+          while (!workList.isEmpty()) {
+            DexType dexType = workList.removeFirst();
+            List<DexType> subwrappers = descriptors.get(dexType).getSubwrappers();
+            descriptor.getSubwrappers().removeAll(subwrappers);
+            workList.addAll(subwrappers);
+          }
+          builder.addWrapper(type, descriptor.toWrapperDescriptor());
+        });
+  }
+
+  private void fillDescriptors(
+      DexClass wrapperClass,
+      Set<DexMethod> excludedMethods,
+      WrapperDescriptorBuilder descriptor,
+      Map<DexType, WrapperDescriptorBuilder> descriptors) {
     HashSet<Wrapper<DexMethod>> wrappers = new HashSet<>();
     for (DexMethod excludedMethod : excludedMethods) {
       wrappers.add(equivalence.wrap(excludedMethod));
     }
     LinkedList<DexClass> workList = new LinkedList<>();
-    List<DexMethod> implementedMethods = new ArrayList<>();
+    List<DexMethod> implementedMethods = descriptor.getMethods();
     workList.add(wrapperClass);
     while (!workList.isEmpty()) {
       DexClass dexClass = workList.removeFirst();
-      for (DexEncodedMethod virtualMethod : dexClass.virtualMethods()) {
-        if (!virtualMethod.isPrivateMethod()
-            // Don't include hashCode and equals overrides, as hashCode and equals are added to
-            // all wrappers regardless.
-            && (!appInfo.dexItemFactory().objectMembers.hashCode.match(virtualMethod))
-            && (!appInfo.dexItemFactory().objectMembers.equals.match(virtualMethod))) {
-          assert virtualMethod.isProtectedMethod() || virtualMethod.isPublicMethod();
-          boolean alreadyAdded = wrappers.contains(equivalence.wrap(virtualMethod.getReference()));
-          // This looks quadratic but given the size of the collections met in practice for
-          // desugared libraries (Max ~15) it does not matter.
-          if (!alreadyAdded) {
-            for (DexMethod alreadyImplementedMethod : implementedMethods) {
-              if (alreadyImplementedMethod.match(virtualMethod.getReference())) {
-                alreadyAdded = true;
-                break;
+      if (dexClass != wrapperClass && descriptors.containsKey(dexClass.type)) {
+        descriptors.get(dexClass.type).addSubwrapper(wrapperClass.type);
+      }
+      if (!wrapperClass.isEnum()) {
+        for (DexEncodedMethod virtualMethod : dexClass.virtualMethods()) {
+          if (!virtualMethod.isPrivateMethod()
+              // Don't include hashCode and equals overrides, as hashCode and equals are added to
+              // all wrappers regardless.
+              && (!appInfo.dexItemFactory().objectMembers.hashCode.match(virtualMethod))
+              && (!appInfo.dexItemFactory().objectMembers.equals.match(virtualMethod))) {
+            assert virtualMethod.isProtectedMethod() || virtualMethod.isPublicMethod();
+            boolean alreadyAdded =
+                wrappers.contains(equivalence.wrap(virtualMethod.getReference()));
+            // This looks quadratic but given the size of the collections met in practice for
+            // desugared libraries (Max ~15) it does not matter.
+            if (!alreadyAdded) {
+              for (DexMethod alreadyImplementedMethod : implementedMethods) {
+                if (alreadyImplementedMethod.match(virtualMethod.getReference())) {
+                  alreadyAdded = true;
+                  break;
+                }
               }
             }
-          }
-          if (!alreadyAdded) {
-            assert !virtualMethod.isFinal()
-                : "Cannot wrap final method " + virtualMethod + " while wrapping " + wrapperClass;
-            implementedMethods.add(virtualMethod.getReference());
+            if (!alreadyAdded) {
+              if (virtualMethod.isFinal() || virtualMethod.isPrivateMethod()) {
+                invalidMethods.add(virtualMethod.getReference());
+              } else {
+                if (!virtualMethod.isPublic()) {
+                  descriptor.setNonPublicAccess();
+                }
+                implementedMethods.add(virtualMethod.getReference());
+              }
+            }
           }
         }
       }
@@ -107,6 +234,5 @@
         workList.add(superClass);
       }
     }
-    return implementedMethods;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
index d5168da..92467a3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/ClassProcessor.java
@@ -723,6 +723,30 @@
     assert resolutionResult.isSuccessfulMemberResolutionResult();
     LookupMethodTarget lookupMethodTarget =
         resolutionResult.lookupVirtualDispatchTarget(clazz, appInfo);
+    if (lookupMethodTarget == null) {
+      // This should not happen, but is does in b/237507594.
+      // We have no reproduction for the issue.
+      // We try to raise a warning here to help investigating:
+      // It could be an issue related to array#clone, or this can be due to a missing/invalid class.
+      assert false;
+      appView
+          .options()
+          .reporter
+          .warning(
+              "The class processor was not able to look-up the default method "
+                  + method
+                  + " in the class "
+                  + clazz
+                  + " (Single resolution: "
+                  + resolutionResult.isSingleResolution()
+                  + "; resolution pair: "
+                  + resolutionResult.getResolutionPair()
+                  + "). Please report this issue in the D8/R8 bug tracker at"
+                  + " https://issuetracker.google.com/issues/237507594.");
+      // To be able to resume compilation we add a NoSuchMethodErrorThrowingMethod.
+      addNoSuchMethodErrorThrowingMethod(method, clazz);
+      return;
+    }
     DexClassAndMethod virtualDispatchTarget = lookupMethodTarget.getTarget();
     assert virtualDispatchTarget != null;
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
index 05a91e5..fd6ae9b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -26,6 +27,18 @@
       Instruction instruction,
       StringBuilderOracle oracle);
 
+  default boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() {
+    return false;
+  }
+
+  default boolean isReplaceArgumentByStringConcat() {
+    return false;
+  }
+
+  default ReplaceArgumentByStringConcat asReplaceArgumentByStringConcat() {
+    return null;
+  }
+
   /** The RemoveStringBuilderAction will simply remove the instruction completely. */
   class RemoveStringBuilderAction implements StringBuilderAction {
 
@@ -38,22 +51,17 @@
         InstructionListIterator iterator,
         Instruction instruction,
         StringBuilderOracle oracle) {
-      assert oracle.isModeledStringBuilderInstruction(
-          instruction,
-          value ->
-              value.getType().isClassType()
-                  && oracle.isStringBuilderType(value.getType().asClassType().getClassType()));
-      if (oracle.isAppend(instruction) && instruction.outValue() != null) {
-        // Append will return the string builder instance. Before removing, ensure that
-        // all users of the output values uses the receiver.
-        instruction.outValue().replaceUsers(instruction.getFirstOperand());
-      }
-      iterator.removeOrReplaceByDebugLocalRead();
+      removeStringBuilderInstruction(iterator, instruction, oracle);
     }
 
     static RemoveStringBuilderAction getInstance() {
       return INSTANCE;
     }
+
+    @Override
+    public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() {
+      return true;
+    }
   }
 
   /**
@@ -102,28 +110,7 @@
       Instruction previous = iterator.previous();
       InvokeMethodWithReceiver invoke = previous.asInvokeMethodWithReceiver();
       assert invoke != null;
-      // If the block has catch handlers, inserting a constant string in the same block as the
-      // append violates our block representation in DEX since constant string is throwing. If the
-      // append is in a block with catch handlers, we simply insert a new constant string in the
-      // entry block after all arguments.
-      Value value;
-      if (!invoke.getBlock().hasCatchHandlers()) {
-        value =
-            iterator.insertConstStringInstruction(
-                appView, code, appView.dexItemFactory().createString(replacement));
-      } else {
-        InstructionListIterator stringInsertIterator = code.entryBlock().listIterator(code);
-        while (stringInsertIterator.hasNext()) {
-          Instruction next = stringInsertIterator.next();
-          if (!next.isArgument()) {
-            stringInsertIterator.previous();
-            break;
-          }
-        }
-        value =
-            stringInsertIterator.insertConstStringInstruction(
-                appView, code, appView.dexItemFactory().createString(replacement));
-      }
+      Value value = insertStringConstantInstruction(appView, code, iterator, invoke, replacement);
       iterator.next();
       DexMethod invokedMethod = invoke.getInvokedMethod();
       if (invoke.isInvokeConstructor(appView.dexItemFactory())) {
@@ -146,21 +133,16 @@
       }
     }
 
+    @Override
+    public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() {
+      return true;
+    }
+
     private boolean isAppendWithString(DexMethod method, DexItemFactory factory) {
       return factory.stringBufferMethods.isAppendStringMethod(method)
           || factory.stringBuilderMethods.isAppendStringMethod(method);
     }
 
-    private DexMethod getConstructorWithStringParameter(
-        DexMethod invokedMethod, DexItemFactory factory) {
-      if (invokedMethod.getHolderType() == factory.stringBufferType) {
-        return factory.stringBufferMethods.stringConstructor;
-      } else {
-        assert invokedMethod.getHolderType() == factory.stringBuilderType;
-        return factory.stringBuilderMethods.stringConstructor;
-      }
-    }
-
     private DexMethod getAppendWithStringParameter(
         DexMethod invokedMethod, DexItemFactory factory) {
       if (invokedMethod.getHolderType() == factory.stringBufferType) {
@@ -171,4 +153,253 @@
       }
     }
   }
+
+  class ReplaceByExistingString implements StringBuilderAction {
+
+    private final Value existingString;
+
+    public ReplaceByExistingString(Value existingString) {
+      this.existingString = existingString;
+    }
+
+    @Override
+    public void perform(
+        AppView<?> appView,
+        IRCode code,
+        InstructionListIterator iterator,
+        Instruction instruction,
+        StringBuilderOracle oracle) {
+      instruction.outValue().replaceUsers(existingString);
+      iterator.removeOrReplaceByDebugLocalRead();
+    }
+
+    @Override
+    public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() {
+      return true;
+    }
+  }
+
+  class ReplaceByStringConcat implements StringBuilderAction {
+
+    private final Value first;
+    private final Value second;
+
+    private final String newConstant;
+
+    private ReplaceByStringConcat(Value first, Value second, String newConstant) {
+      assert first != null || newConstant != null;
+      assert second != null || newConstant != null;
+      this.first = first;
+      this.second = second;
+      this.newConstant = newConstant;
+    }
+
+    public static ReplaceByStringConcat replaceByValues(Value first, Value second) {
+      return new ReplaceByStringConcat(first, second, null);
+    }
+
+    public static ReplaceByStringConcat replaceByNewConstantConcatValue(
+        String newConstant, Value second) {
+      return new ReplaceByStringConcat(null, second, newConstant);
+    }
+
+    public static ReplaceByStringConcat replaceByValueConcatNewConstant(
+        Value first, String newConstant) {
+      return new ReplaceByStringConcat(first, null, newConstant);
+    }
+
+    @Override
+    public void perform(
+        AppView<?> appView,
+        IRCode code,
+        InstructionListIterator iterator,
+        Instruction instruction,
+        StringBuilderOracle oracle) {
+      Value constString = null;
+      if (newConstant != null) {
+        Instruction previous = iterator.previous();
+        constString =
+            insertStringConstantInstruction(appView, code, iterator, previous, newConstant);
+        iterator.next();
+      }
+      assert first != null || constString != null;
+      assert second != null || constString != null;
+      // To ensure that we do not fail narrowing when evaluating String.concat, we mark the type
+      // as maybe null.
+      iterator.replaceCurrentInstruction(
+          InvokeVirtual.builder()
+              .setFreshOutValue(
+                  code, TypeElement.stringClassType(appView), instruction.getLocalInfo())
+              .setMethod(appView.dexItemFactory().stringMembers.concat)
+              .setArguments(
+                  ImmutableList.of(
+                      first != null ? first : constString, second != null ? second : constString))
+              .build());
+    }
+  }
+
+  class ReplaceArgumentByExistingString implements StringBuilderAction {
+
+    private final Value string;
+
+    public ReplaceArgumentByExistingString(Value string) {
+      this.string = string;
+    }
+
+    @Override
+    public void perform(
+        AppView<?> appView,
+        IRCode code,
+        InstructionListIterator iterator,
+        Instruction instruction,
+        StringBuilderOracle oracle) {
+      instruction.replaceValue(1, string);
+    }
+
+    @Override
+    public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() {
+      return true;
+    }
+  }
+
+  class ReplaceArgumentByStringConcat implements StringBuilderAction {
+
+    private final Value first;
+    private final Value second;
+    private final String newConstant;
+    private final Value outValue;
+    private boolean removeInstruction;
+
+    private ReplaceArgumentByStringConcat(
+        Value first, Value second, String newConstant, Value outValue) {
+      assert first != null || newConstant != null;
+      assert second != null || newConstant != null;
+      this.first = first;
+      this.second = second;
+      this.newConstant = newConstant;
+      this.outValue = outValue;
+    }
+
+    public static ReplaceArgumentByStringConcat replaceByValues(
+        Value first, Value second, Value outValue) {
+      return new ReplaceArgumentByStringConcat(first, second, null, outValue);
+    }
+
+    public static ReplaceArgumentByStringConcat replaceByNewConstantConcatValue(
+        String newConstant, Value second, Value outValue) {
+      return new ReplaceArgumentByStringConcat(null, second, newConstant, outValue);
+    }
+
+    public static ReplaceArgumentByStringConcat replaceByValueConcatNewConstant(
+        Value first, String newConstant, Value outValue) {
+      return new ReplaceArgumentByStringConcat(first, null, newConstant, outValue);
+    }
+
+    public void setRemoveInstruction() {
+      removeInstruction = true;
+    }
+
+    @Override
+    public void perform(
+        AppView<?> appView,
+        IRCode code,
+        InstructionListIterator iterator,
+        Instruction instruction,
+        StringBuilderOracle oracle) {
+      assert instruction.isInvokeMethod();
+      assert instruction.inValues().size() == 2;
+      Instruction previous = iterator.previous();
+      assert previous == instruction;
+      Value constString = null;
+      if (newConstant != null) {
+        constString =
+            insertStringConstantInstruction(appView, code, iterator, previous, newConstant);
+      }
+      assert first != null || constString != null;
+      assert second != null || constString != null;
+      InvokeVirtual stringConcat =
+          InvokeVirtual.builder()
+              .setMethod(appView.dexItemFactory().stringMembers.concat)
+              .setOutValue(outValue)
+              .setArguments(
+                  ImmutableList.of(
+                      first != null ? first : constString, second != null ? second : constString))
+              .setPosition(instruction.getPosition())
+              .build();
+      iterator.add(stringConcat);
+      Instruction next = iterator.next();
+      assert next == instruction;
+      if (removeInstruction) {
+        removeStringBuilderInstruction(iterator, instruction, oracle);
+      } else {
+        instruction.replaceValue(1, outValue);
+      }
+    }
+
+    @Override
+    public boolean isReplaceArgumentByStringConcat() {
+      return true;
+    }
+
+    @Override
+    public ReplaceArgumentByStringConcat asReplaceArgumentByStringConcat() {
+      return this;
+    }
+  }
+
+  static DexMethod getConstructorWithStringParameter(
+      DexMethod invokedMethod, DexItemFactory factory) {
+    if (invokedMethod.getHolderType() == factory.stringBufferType) {
+      return factory.stringBufferMethods.stringConstructor;
+    } else {
+      assert invokedMethod.getHolderType() == factory.stringBuilderType;
+      return factory.stringBuilderMethods.stringConstructor;
+    }
+  }
+
+  static Value insertStringConstantInstruction(
+      AppView<?> appView,
+      IRCode code,
+      InstructionListIterator iterator,
+      Instruction instruction,
+      String newString) {
+    // If the block has catch handlers, inserting a constant string in the same block as the
+    // append violates our block representation in DEX since constant string is throwing. If the
+    // append is in a block with catch handlers, we simply insert a new constant string in the
+    // entry block after all arguments.
+    Value value;
+    if (!instruction.getBlock().hasCatchHandlers()) {
+      value =
+          iterator.insertConstStringInstruction(
+              appView, code, appView.dexItemFactory().createString(newString));
+    } else {
+      InstructionListIterator stringInsertIterator = code.entryBlock().listIterator(code);
+      while (stringInsertIterator.hasNext()) {
+        Instruction next = stringInsertIterator.next();
+        if (!next.isArgument()) {
+          stringInsertIterator.previous();
+          break;
+        }
+      }
+      value =
+          stringInsertIterator.insertConstStringInstruction(
+              appView, code, appView.dexItemFactory().createString(newString));
+    }
+    return value;
+  }
+
+  static void removeStringBuilderInstruction(
+      InstructionListIterator iterator, Instruction instruction, StringBuilderOracle oracle) {
+    assert oracle.isModeledStringBuilderInstruction(
+        instruction,
+        value ->
+            value.getType().isClassType()
+                && oracle.isStringBuilderType(value.getType().asClassType().getClassType()));
+    if (oracle.isAppend(instruction) && instruction.outValue() != null) {
+      // Append will return the string builder instance. Before removing, ensure that
+      // all users of the output values uses the receiver.
+      instruction.outValue().replaceUsers(instruction.getFirstOperand());
+    }
+    iterator.removeOrReplaceByDebugLocalRead();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
index ee85c13..407717c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.IntraProceduralDataflowAnalysisOptions;
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.IntraproceduralDataflowAnalysis;
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.TransferFunctionResult;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -33,7 +34,7 @@
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.EscapeNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
-import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppend;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.LoopNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.NewInstanceNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.SplitReferenceNode;
@@ -107,6 +108,7 @@
         stringBuilderAction.perform(appView, code, it, instruction, oracle);
       }
     }
+    code.removeAllDeadAndTrivialPhis();
   }
 
   private static class StringBuilderGraphState {
@@ -273,7 +275,13 @@
               Value receiver = invoke.getReceiver();
               if (oracle.isInit(instruction)) {
                 InitNode initNode = createInitNode(instruction.asInvokeDirect());
-                initNode.setConstantArgument(oracle.getConstantArgument(instruction));
+                String constantArgument = oracle.getConstantArgument(instruction);
+                initNode.setConstantArgument(constantArgument);
+                if (constantArgument == null
+                    && oracle.isStringConstructor(instruction)
+                    && invoke.getFirstNonReceiverArgument().isNeverNull()) {
+                  initNode.setNonConstantArgument(invoke.getFirstNonReceiverArgument());
+                }
                 if (invoke.arguments().size() == 2) {
                   Value arg = invoke.getOperand(1);
                   if (oracle.hasStringBuilderType(arg)) {
@@ -288,9 +296,15 @@
                     escaped -> nodeConsumer.accept(escaped, createInspectionNode(instruction)));
               } else if (oracle.isAppend(instruction)) {
                 AppendNode appendNode = createAppendNode(instruction.asInvokeVirtual());
-                appendNode.setConstantArgument(oracle.getConstantArgument(instruction));
-                Value arg = invoke.getFirstNonReceiverArgument().getAliasedValue();
-                if (oracle.hasStringBuilderType(arg)) {
+                String constantArgument = oracle.getConstantArgument(instruction);
+                appendNode.setConstantArgument(constantArgument);
+                Value arg = invoke.getFirstNonReceiverArgument();
+                if (constantArgument == null
+                    && oracle.isAppendString(instruction)
+                    && arg.isNeverNull()) {
+                  appendNode.setNonConstantArgument(arg);
+                }
+                if (oracle.hasStringBuilderType(arg.getAliasedValue())) {
                   insertImplicitToStringNode(
                       arg, instruction, appendNode, escapeState, nodeConsumer);
                 }
@@ -341,7 +355,7 @@
           private void insertImplicitToStringNode(
               Value value,
               Instruction instruction,
-              InitOrAppend node,
+              InitOrAppendNode node,
               StringBuilderEscapeState escapeState,
               BiConsumer<Value, StringBuilderNode> nodeConsumer) {
             assert escapeState.isLiveStringBuilder(value);
@@ -544,7 +558,14 @@
 
     MunchingState munchingState =
         new MunchingState(
-            actions, escaping, inspectingCapacity, looping, materializing, newInstances, oracle);
+            actions,
+            escaping,
+            inspectingCapacity,
+            looping,
+            materializing,
+            newInstances,
+            oracle,
+            () -> code.createValue(TypeElement.stringClassType(appView)));
 
     boolean keepMunching = true;
     for (int i = 0; i < NUMBER_OF_MUNCHING_PASSES && keepMunching; i++) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
index c189299..033a17c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Value;
 import com.google.common.collect.Sets;
 import java.util.Set;
 
@@ -27,7 +28,15 @@
     StringBuilderInstruction asStringBuilderInstructionNode();
   }
 
-  interface InitOrAppend extends StringBuilderInstruction {
+  interface InitOrAppendNode extends StringBuilderInstruction {
+
+    boolean isInitNode();
+
+    boolean isAppendNode();
+
+    InitNode asInitNode();
+
+    AppendNode asAppendNode();
 
     boolean hasConstantArgument();
 
@@ -38,6 +47,24 @@
     void setImplicitToStringNode(ImplicitToStringNode node);
 
     ImplicitToStringNode getImplicitToStringNode();
+
+    boolean hasNonConstantArgument();
+
+    void setNonConstantArgument(Value value);
+
+    Value getNonConstantArgument();
+
+    default boolean hasConstantOrNonConstantArgument() {
+      return hasConstantArgument() || hasNonConstantArgument();
+    }
+
+    boolean hasSingleSuccessor();
+
+    boolean hasSinglePredecessor();
+
+    StringBuilderNode getSingleSuccessor();
+
+    StringBuilderNode getSinglePredecessor();
   }
 
   private final Set<StringBuilderNode> successors = Sets.newLinkedHashSet();
@@ -69,11 +96,11 @@
     return false;
   }
 
-  boolean isInitNode() {
+  public boolean isInitNode() {
     return false;
   }
 
-  boolean isAppendNode() {
+  public boolean isAppendNode() {
     return false;
   }
 
@@ -105,15 +132,15 @@
     return null;
   }
 
-  InitNode asInitNode() {
+  public InitNode asInitNode() {
     return null;
   }
 
-  AppendNode asAppendNode() {
+  public AppendNode asAppendNode() {
     return null;
   }
 
-  InitOrAppend asInitOrAppend() {
+  InitOrAppendNode asInitOrAppend() {
     return null;
   }
 
@@ -141,11 +168,11 @@
     return isDead;
   }
 
-  boolean hasSingleSuccessor() {
+  public boolean hasSingleSuccessor() {
     return successors.size() == 1;
   }
 
-  StringBuilderNode getSingleSuccessor() {
+  public StringBuilderNode getSingleSuccessor() {
     assert hasSingleSuccessor();
     return successors.iterator().next();
   }
@@ -159,11 +186,11 @@
     return successors;
   }
 
-  boolean hasSinglePredecessor() {
+  public boolean hasSinglePredecessor() {
     return predecessors.size() == 1;
   }
 
-  StringBuilderNode getSinglePredecessor() {
+  public StringBuilderNode getSinglePredecessor() {
     assert hasSinglePredecessor();
     return predecessors.iterator().next();
   }
@@ -278,18 +305,19 @@
 
   /** An initNode is where a StringBuilder/StringBuffer is initialized. */
   static class InitNode extends StringBuilderNode
-      implements InitOrAppend, StringBuilderInstruction {
+      implements InitOrAppendNode, StringBuilderInstruction {
 
     private final InvokeDirect instruction;
     private ImplicitToStringNode implicitToStringNode;
     private String constantArgument;
+    private Value nonConstantArgument;
 
     private InitNode(InvokeDirect instruction) {
       this.instruction = instruction;
     }
 
     @Override
-    boolean isInitNode() {
+    public boolean isInitNode() {
       return true;
     }
 
@@ -299,12 +327,12 @@
     }
 
     @Override
-    InitNode asInitNode() {
+    public InitNode asInitNode() {
       return this;
     }
 
     @Override
-    InitOrAppend asInitOrAppend() {
+    InitOrAppendNode asInitOrAppend() {
       return this;
     }
 
@@ -329,16 +357,6 @@
     }
 
     @Override
-    public void setImplicitToStringNode(ImplicitToStringNode node) {
-      implicitToStringNode = node;
-    }
-
-    @Override
-    public ImplicitToStringNode getImplicitToStringNode() {
-      return implicitToStringNode;
-    }
-
-    @Override
     public String getConstantArgument() {
       return constantArgument;
     }
@@ -347,22 +365,53 @@
     public boolean hasConstantArgument() {
       return constantArgument != null;
     }
+
+    @Override
+    public void setNonConstantArgument(Value value) {
+      this.nonConstantArgument = value;
+    }
+
+    @Override
+    public void setImplicitToStringNode(ImplicitToStringNode node) {
+      implicitToStringNode = node;
+    }
+
+    @Override
+    public ImplicitToStringNode getImplicitToStringNode() {
+      return implicitToStringNode;
+    }
+
+    @Override
+    public boolean hasNonConstantArgument() {
+      return nonConstantArgument != null;
+    }
+
+    @Override
+    public Value getNonConstantArgument() {
+      assert nonConstantArgument != null;
+      return nonConstantArgument;
+    }
+
+    boolean isConstructorInvokeSideEffectFree(StringBuilderOracle oracle) {
+      return oracle.isConstructorInvokeSideEffectFree(instruction);
+    }
   }
 
   /** AppendNodes are StringBuilder.append or StringBuffer.append. */
   static class AppendNode extends StringBuilderNode
-      implements InitOrAppend, StringBuilderInstruction {
+      implements InitOrAppendNode, StringBuilderInstruction {
 
     private final InvokeVirtual instruction;
     private ImplicitToStringNode implicitToStringNode;
     private String constantArgument;
+    private Value nonConstantArgument;
 
     private AppendNode(InvokeVirtual instruction) {
       this.instruction = instruction;
     }
 
     @Override
-    boolean isAppendNode() {
+    public boolean isAppendNode() {
       return true;
     }
 
@@ -372,12 +421,12 @@
     }
 
     @Override
-    AppendNode asAppendNode() {
+    public AppendNode asAppendNode() {
       return this;
     }
 
     @Override
-    InitOrAppend asInitOrAppend() {
+    InitOrAppendNode asInitOrAppend() {
       return this;
     }
 
@@ -412,6 +461,21 @@
     }
 
     @Override
+    public boolean hasNonConstantArgument() {
+      return nonConstantArgument != null;
+    }
+
+    @Override
+    public void setNonConstantArgument(Value value) {
+      this.nonConstantArgument = value;
+    }
+
+    @Override
+    public Value getNonConstantArgument() {
+      return nonConstantArgument;
+    }
+
+    @Override
     public String getConstantArgument() {
       return constantArgument;
     }
@@ -540,13 +604,13 @@
    */
   static class ImplicitToStringNode extends StringBuilderNode {
 
-    private final InitOrAppend initOrAppend;
+    private final InitOrAppendNode initOrAppend;
 
-    ImplicitToStringNode(InitOrAppend initOrAppend) {
+    ImplicitToStringNode(InitOrAppendNode initOrAppend) {
       this.initOrAppend = initOrAppend;
     }
 
-    public InitOrAppend getInitOrAppend() {
+    public InitOrAppendNode getInitOrAppend() {
       return initOrAppend;
     }
 
@@ -601,7 +665,7 @@
     return new OtherStringBuilderNode(instruction);
   }
 
-  static ImplicitToStringNode createImplicitToStringNode(InitOrAppend otherNode) {
+  static ImplicitToStringNode createImplicitToStringNode(InitOrAppendNode otherNode) {
     return new ImplicitToStringNode(otherNode);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java
index 0659a8a..5cdcc6b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java
@@ -8,19 +8,26 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.string.StringBuilderAction.AppendWithNewConstantString;
 import com.android.tools.r8.ir.optimize.string.StringBuilderAction.RemoveStringBuilderAction;
+import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceArgumentByExistingString;
+import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceArgumentByStringConcat;
 import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByConstantString;
+import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByExistingString;
+import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByStringConcat;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.AppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
-import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppend;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.NewInstanceNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.StringBuilderInstruction;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ToStringNode;
 import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 
 /**
  * StringBuilderNodeMuncher is a classic munching algorithm that will try to remove nodes on string
@@ -42,6 +49,7 @@
     private final Map<StringBuilderNode, Set<StringBuilderNode>> materializingInstructions;
     private final Map<StringBuilderNode, NewInstanceNode> newInstances;
     private final Map<Value, String> optimizedStrings = new IdentityHashMap<>();
+    private final Supplier<Value> newValueSupplier;
 
     MunchingState(
         Map<Instruction, StringBuilderAction> actions,
@@ -50,7 +58,8 @@
         Set<StringBuilderNode> looping,
         Map<StringBuilderNode, Set<StringBuilderNode>> materializingInstructions,
         Map<StringBuilderNode, NewInstanceNode> newInstances,
-        StringBuilderOracle oracle) {
+        StringBuilderOracle oracle,
+        Supplier<Value> newValueSupplier) {
       this.actions = actions;
       this.escaping = escaping;
       this.inspectingCapacity = inspectingCapacity;
@@ -58,6 +67,7 @@
       this.materializingInstructions = materializingInstructions;
       this.newInstances = newInstances;
       this.oracle = oracle;
+      this.newValueSupplier = newValueSupplier;
     }
 
     public NewInstanceNode getNewInstanceNode(StringBuilderNode root) {
@@ -67,6 +77,18 @@
     public boolean isLooping(StringBuilderNode root) {
       return looping.contains(root);
     }
+
+    public boolean isEscaping(StringBuilderNode root) {
+      return escaping.contains(root);
+    }
+
+    public boolean isInspecting(StringBuilderNode root) {
+      return inspectingCapacity.contains(root);
+    }
+
+    public Value getNewOutValue() {
+      return newValueSupplier.get();
+    }
   }
 
   private interface PeepholePattern {
@@ -93,30 +115,32 @@
     @Override
     public boolean optimize(
         StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
-      if (!currentNode.isAppendNode()) {
+      AppendNode appendNode = currentNode.asAppendNode();
+      if (appendNode == null || !appendNode.hasSinglePredecessor()) {
         return false;
       }
-      String currentConstantArgument = getConstantArgumentForNode(currentNode, munchingState);
-      if (currentConstantArgument == null || !currentNode.hasSinglePredecessor()) {
-        return false;
-      }
-      StringBuilderNode previous = currentNode.getSinglePredecessor();
-      String previousConstantArgument = getConstantArgumentForNode(previous, munchingState);
-      if (previousConstantArgument == null || !previous.hasSingleSuccessor()) {
+      InitOrAppendNode previous = currentNode.getSinglePredecessor().asInitOrAppend();
+      if (previous == null || !previous.hasSingleSuccessor()) {
         return false;
       }
       // The capacity changes based on the init call (on JVM it adds 16 to length of input).
       if (previous.isInitNode() && munchingState.inspectingCapacity.contains(root)) {
         return false;
       }
-      assert previous.isInitOrAppend();
+      String currentConstantArgument = getConstantArgumentForNode(appendNode, munchingState);
+      if (currentConstantArgument == null) {
+        return false;
+      }
+      String previousConstantArgument = getConstantArgumentForNode(previous, munchingState);
+      if (previousConstantArgument == null) {
+        return false;
+      }
       String newConstant = previousConstantArgument + currentConstantArgument;
-      InitOrAppend initOrAppend = previous.asInitOrAppend();
-      initOrAppend.setConstantArgument(newConstant);
+      previous.setConstantArgument(newConstant);
       munchingState.actions.put(
-          initOrAppend.getInstruction(), new AppendWithNewConstantString(newConstant));
+          previous.getInstruction(), new AppendWithNewConstantString(newConstant));
       munchingState.actions.put(
-          currentNode.asAppendNode().getInstruction(), RemoveStringBuilderAction.getInstance());
+          appendNode.getInstruction(), RemoveStringBuilderAction.getInstance());
       currentNode.removeNode();
       return true;
     }
@@ -126,13 +150,13 @@
    * This peephole will try to remove toString nodes and replace by a constant string:
    *
    * <pre>
-   * newInstance -> init("foo") -> append("bar") -> toString() =>
-   * newInstance -> init("foo") -> append("bar")
+   * newInstance -> init("foo") -> toString() => newInstance -> init("foo") -> append("bar")
+   * actions: [toString() => ReplaceByConstantString("foo")]
    * </pre>
    *
    * <p>If the node is an implicitToString, we update the append of another builder to have the new
-   * constant value directly. If not, we keep track of the outValue toString() had is replaced by a
-   * constant, by updating {@code MunchingState.optimizedStrings}
+   * constant value directly. If not, we keep track of the outValue toString() being replaced by a
+   * constant by updating {@code MunchingState.optimizedStrings}
    */
   private static class MunchToString implements PeepholePattern {
 
@@ -141,6 +165,9 @@
         StringBuilderNode originalRoot,
         StringBuilderNode currentNode,
         MunchingState munchingState) {
+      if (munchingState.isEscaping(originalRoot) || munchingState.isInspecting(originalRoot)) {
+        return false;
+      }
       if (!currentNode.isToStringNode() && !currentNode.isImplicitToStringNode()) {
         return false;
       }
@@ -148,48 +175,163 @@
       if (newInstanceNode == null || !newInstanceNode.hasSingleSuccessor()) {
         return false;
       }
-      StringBuilderNode init = newInstanceNode.getSingleSuccessor();
-      String rootConstantArgument = getConstantArgumentForNode(init, munchingState);
-      if (rootConstantArgument == null || !init.isInitNode()) {
+      InitNode init = newInstanceNode.getSingleSuccessor().asInitNode();
+      if (init == null || !init.hasSingleSuccessor()) {
         return false;
       }
-      // This is either <init>(str) -> toString() or <init>(str) -> append(str) -> toString()
+      if (!currentNode.hasSinglePredecessor() || currentNode.getSinglePredecessor() != init) {
+        return false;
+      }
+      String initConstantArgument = getConstantArgumentForNode(init, munchingState);
+      if (initConstantArgument == null) {
+        return false;
+      }
       // If the string builder dependency is directly given to another string builder, there is
       // no toString() but an append with this string builder as argument.
-      if (!currentNode.hasSinglePredecessor() || !init.hasSingleSuccessor()) {
-        return false;
-      }
-      String constantArgument = null;
-      if (currentNode.getSinglePredecessor() == init) {
-        constantArgument = rootConstantArgument;
-      } else {
-        StringBuilderNode expectedAppend = init.getSingleSuccessor();
-        StringBuilderNode expectedSameAppend = currentNode.getSinglePredecessor();
-        String appendConstantArgument = getConstantArgumentForNode(expectedAppend, munchingState);
-        if (expectedAppend == expectedSameAppend && appendConstantArgument != null) {
-          // TODO(b/190489514): See if this larger pattern is necessary.
-          assert false : "See why this larger pattern is necessary";
-          constantArgument = rootConstantArgument + appendConstantArgument;
-        }
-      }
-      if (constantArgument == null) {
-        return false;
-      }
       if (currentNode.isToStringNode()) {
         ToStringNode toStringNode = currentNode.asToStringNode();
         munchingState.actions.put(
-            toStringNode.getInstruction(), new ReplaceByConstantString(constantArgument));
+            toStringNode.getInstruction(), new ReplaceByConstantString(initConstantArgument));
         String oldValue =
             munchingState.optimizedStrings.put(
-                toStringNode.getInstruction().outValue(), constantArgument);
+                toStringNode.getInstruction().outValue(), initConstantArgument);
         assert oldValue == null;
       } else {
         assert currentNode.isImplicitToStringNode();
         ImplicitToStringNode implicitToStringNode = currentNode.asImplicitToStringNode();
-        InitOrAppend initOrAppend = implicitToStringNode.getInitOrAppend();
-        initOrAppend.setConstantArgument(constantArgument);
+        InitOrAppendNode initOrAppend = implicitToStringNode.getInitOrAppend();
+        initOrAppend.setConstantArgument(initConstantArgument);
         munchingState.actions.put(
-            initOrAppend.getInstruction(), new AppendWithNewConstantString(constantArgument));
+            initOrAppend.getInstruction(), new AppendWithNewConstantString(initConstantArgument));
+      }
+      munchingState.materializingInstructions.get(originalRoot).remove(currentNode);
+      currentNode.removeNode();
+      return true;
+    }
+  }
+
+  /**
+   * This peephole will try to remove toString nodes and replace by an invoke to String.concat:
+   *
+   * <pre>
+   * newInstance -> init(notNull(string)) -> append(notNull(otherString)) -> toString() =>
+   * newInstance -> init(notNull(string)) -> append(otherString)
+   * actions: [toString() => string.concat(otherString)]
+   * </pre>
+   *
+   * <p>This pattern only triggers when a constant munching of toString could happen.
+   */
+  private static class MunchToStringIntoStringConcat implements PeepholePattern {
+
+    @Override
+    public boolean optimize(
+        StringBuilderNode originalRoot,
+        StringBuilderNode currentNode,
+        MunchingState munchingState) {
+      if (munchingState.isEscaping(originalRoot)
+          || munchingState.isInspecting(originalRoot)
+          || !currentNode.hasSinglePredecessor()) {
+        return false;
+      }
+      if (!currentNode.isToStringNode() && !currentNode.isImplicitToStringNode()) {
+        return false;
+      }
+      NewInstanceNode newInstanceNode = munchingState.getNewInstanceNode(originalRoot);
+      if (newInstanceNode == null || !newInstanceNode.hasSingleSuccessor()) {
+        return false;
+      }
+      InitOrAppendNode firstNode = newInstanceNode.getSingleSuccessor().asInitNode();
+      if (firstNode == null || !firstNode.hasSingleSuccessor()) {
+        return false;
+      }
+      if (firstNode.asInitNode().isConstructorInvokeSideEffectFree(munchingState.oracle)
+          && "".equals(firstNode.getConstantArgument())
+          && firstNode.hasSingleSuccessor()) {
+        firstNode = firstNode.getSingleSuccessor().asAppendNode();
+        if (firstNode == null
+            || !firstNode.hasSinglePredecessor()
+            || !firstNode.hasSingleSuccessor()) {
+          return false;
+        }
+      }
+      // We cannot String.concat or return the string safely when it is not constant and maybe null.
+      if (!firstNode.hasConstantOrNonConstantArgument()) {
+        return false;
+      }
+      List<InitOrAppendNode> initOrAppends = Lists.newArrayList(firstNode);
+      if (currentNode.getSinglePredecessor() != firstNode) {
+        AppendNode appendAfterFirstNode = firstNode.getSingleSuccessor().asAppendNode();
+        AppendNode appendBeforeToString = currentNode.getSinglePredecessor().asAppendNode();
+        if (appendAfterFirstNode == null
+            || appendAfterFirstNode != appendBeforeToString
+            || !appendAfterFirstNode.hasConstantOrNonConstantArgument()) {
+          return false;
+        }
+        initOrAppends.add(appendAfterFirstNode);
+      }
+      // Check that all values are not constant otherwise we can compute the constant value and
+      // replace all entirely.
+      if (Iterables.all(initOrAppends, InitOrAppendNode::hasConstantArgument)) {
+        return false;
+      }
+      InitOrAppendNode first = initOrAppends.get(0);
+      // If the string builder dependency is directly given to another string builder, there is
+      // no toString() but an append with this string builder as argument.
+      if (currentNode.isToStringNode()) {
+        Instruction currentInstruction = currentNode.asToStringNode().getInstruction();
+        if (initOrAppends.size() == 1) {
+          // Replace with the string itself.
+          munchingState.actions.put(
+              currentInstruction, new ReplaceByExistingString(first.getNonConstantArgument()));
+        } else {
+          InitOrAppendNode second = initOrAppends.get(1);
+          ReplaceByStringConcat concatAction;
+          if (first.hasConstantArgument()) {
+            concatAction =
+                ReplaceByStringConcat.replaceByNewConstantConcatValue(
+                    first.getConstantArgument(), second.getNonConstantArgument());
+          } else if (second.hasConstantArgument()) {
+            concatAction =
+                ReplaceByStringConcat.replaceByValueConcatNewConstant(
+                    first.getNonConstantArgument(), second.getConstantArgument());
+          } else {
+            concatAction =
+                ReplaceByStringConcat.replaceByValues(
+                    first.getNonConstantArgument(), second.getNonConstantArgument());
+          }
+          munchingState.actions.put(currentInstruction, concatAction);
+        }
+      } else {
+        assert currentNode.isImplicitToStringNode();
+        ImplicitToStringNode implicitToStringNode = currentNode.asImplicitToStringNode();
+        InitOrAppendNode initOrAppend = implicitToStringNode.getInitOrAppend();
+        if (initOrAppends.size() == 1) {
+          initOrAppend.setNonConstantArgument(first.getNonConstantArgument());
+          munchingState.actions.put(
+              initOrAppend.getInstruction(),
+              new ReplaceArgumentByExistingString(first.getNonConstantArgument()));
+        } else {
+          // Changing append to String.concat require us to calculate a new string value that will
+          // be the result. We allocate it here such that we can use it repeatedly in munching.
+          InitOrAppendNode second = initOrAppends.get(1);
+          Value newOutValue = munchingState.getNewOutValue();
+          initOrAppend.setNonConstantArgument(newOutValue);
+          ReplaceArgumentByStringConcat concatAction;
+          if (first.hasConstantArgument()) {
+            concatAction =
+                ReplaceArgumentByStringConcat.replaceByNewConstantConcatValue(
+                    first.getConstantArgument(), second.getNonConstantArgument(), newOutValue);
+          } else if (second.hasConstantArgument()) {
+            concatAction =
+                ReplaceArgumentByStringConcat.replaceByValueConcatNewConstant(
+                    first.getNonConstantArgument(), second.getConstantArgument(), newOutValue);
+          } else {
+            concatAction =
+                ReplaceArgumentByStringConcat.replaceByValues(
+                    first.getNonConstantArgument(), second.getNonConstantArgument(), newOutValue);
+          }
+          munchingState.actions.put(initOrAppend.getInstruction(), concatAction);
+        }
       }
       munchingState.materializingInstructions.get(originalRoot).remove(currentNode);
       currentNode.removeNode();
@@ -198,21 +340,11 @@
   }
 
   private static String getConstantArgumentForNode(
-      StringBuilderNode node, MunchingState munchingState) {
-    if (node.isAppendNode()) {
-      AppendNode appendNode = node.asAppendNode();
-      if (appendNode.hasConstantArgument()) {
-        return appendNode.getConstantArgument();
-      }
-      return getOptimizedConstantArgument(appendNode, munchingState);
-    } else if (node.isInitNode()) {
-      InitNode initNode = node.asInitNode();
-      if (initNode.hasConstantArgument()) {
-        return initNode.getConstantArgument();
-      }
-      return getOptimizedConstantArgument(initNode, munchingState);
+      InitOrAppendNode node, MunchingState munchingState) {
+    if (node.hasConstantArgument()) {
+      return node.getConstantArgument();
     }
-    return null;
+    return getOptimizedConstantArgument(node, munchingState);
   }
 
   private static String getOptimizedConstantArgument(
@@ -253,16 +385,16 @@
           boolean canRemoveIfLastAndNoLoop =
               !isLoopingOnPath(root, currentNode, munchingState)
                   && currentNode.getSuccessors().isEmpty();
+          boolean hasKnownArgumentOrCannotBeObserved =
+              appendNode.hasConstantOrNonConstantArgument()
+                  || !munchingState.oracle.canObserveStringBuilderCall(
+                      currentNode.asAppendNode().getInstruction());
           if (canRemoveIfNoInspectionOrMaterializing
               && canRemoveIfLastAndNoLoop
-              && !munchingState.oracle.canObserveStringBuilderCall(
-                  currentNode.asAppendNode().getInstruction())) {
-            munchingState.actions.put(
-                appendNode.getInstruction(), RemoveStringBuilderAction.getInstance());
+              && hasKnownArgumentOrCannotBeObserved) {
             removeNode = true;
           }
         } else if (currentNode.isInitNode()
-            && currentNode.asInitNode().hasConstantArgument()
             && currentNode.hasSinglePredecessor()
             && currentNode.getSinglePredecessor().isNewInstanceNode()
             && currentNode.getSuccessors().isEmpty()
@@ -277,9 +409,17 @@
           removedAnyNodes = true;
           currentNode.removeNode();
           if (currentNode.isStringBuilderInstructionNode()) {
-            munchingState.actions.put(
-                currentNode.asStringBuilderInstructionNode().getInstruction(),
-                RemoveStringBuilderAction.getInstance());
+            Instruction currentInstruction =
+                currentNode.asStringBuilderInstructionNode().getInstruction();
+            StringBuilderAction currentAction = munchingState.actions.get(currentInstruction);
+            if (currentAction != null
+                && !currentAction.isAllowedToBeOverwrittenByRemoveStringBuilderAction()) {
+              assert currentAction.isReplaceArgumentByStringConcat();
+              currentAction.asReplaceArgumentByStringConcat().setRemoveInstruction();
+            } else {
+              munchingState.actions.put(
+                  currentInstruction, RemoveStringBuilderAction.getInstance());
+            }
           }
           currentNode =
               currentNode.hasSinglePredecessor() ? currentNode.getSinglePredecessor() : null;
@@ -312,7 +452,12 @@
   }
 
   private static final PeepholePattern[] peepholePatterns =
-      new PeepholePattern[] {new MunchAppends(), new MunchToString(), new MunchNonMaterializing()};
+      new PeepholePattern[] {
+        new MunchAppends(),
+        new MunchToString(),
+        new MunchToStringIntoStringConcat(),
+        new MunchNonMaterializing()
+      };
 
   static boolean optimize(
       StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
index 063f791..8b59c2f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
@@ -43,6 +43,12 @@
 
   boolean isInit(Instruction instruction);
 
+  boolean isAppendString(Instruction instruction);
+
+  boolean isStringConstructor(Instruction instruction);
+
+  boolean isConstructorInvokeSideEffectFree(Instruction instruction);
+
   class DefaultStringBuilderOracle implements StringBuilderOracle {
 
     private final DexItemFactory factory;
@@ -185,12 +191,12 @@
       }
       DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
       if (factory.stringBuilderMethods.isAppendObjectOrCharSequenceMethod(invokedMethod)
-          || factory.stringBufferMethods.isAppendObjectOrCharSequenceMethod(invokedMethod)
-          || factory.stringBuilderMethods.charSequenceConstructor == invokedMethod
-          || factory.stringBufferMethods.charSequenceConstructor == invokedMethod) {
-        assert instruction.inValues().size() == 2;
+          || factory.stringBufferMethods.isAppendObjectOrCharSequenceMethod(invokedMethod)) {
         return !instruction.inValues().get(1).getType().isStringType(factory);
       }
+      if (invokedMethod.isInstanceInitializer(factory)) {
+        return !isConstructorInvokeSideEffectFree(instruction);
+      }
       if (factory.stringBuilderMethods.isAppendCharArrayMethod(invokedMethod)
           || factory.stringBufferMethods.isAppendCharArrayMethod(invokedMethod)) {
         return instruction.asInvokeVirtual().getFirstNonReceiverArgument().isMaybeNull();
@@ -207,5 +213,41 @@
       return factory.stringBuilderMethods.isConstructorMethod(invokedMethod)
           || factory.stringBufferMethods.isConstructorMethod(invokedMethod);
     }
+
+    @Override
+    public boolean isAppendString(Instruction instruction) {
+      if (!instruction.isInvokeMethod()) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
+      return factory.stringBuilderMethods.isAppendStringMethod(invokedMethod)
+          || factory.stringBufferMethods.isAppendStringMethod(invokedMethod);
+    }
+
+    @Override
+    public boolean isStringConstructor(Instruction instruction) {
+      if (!instruction.isInvokeMethod()) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
+      return invokedMethod == factory.stringBuilderMethods.stringConstructor
+          || invokedMethod == factory.stringBufferMethods.stringConstructor;
+    }
+
+    @Override
+    public boolean isConstructorInvokeSideEffectFree(Instruction instruction) {
+      if (!instruction.isInvokeConstructor(factory)) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
+      if (invokedMethod.getHolderType() == factory.stringBuilderType) {
+        return factory.stringBuilderMethods.constructorInvokeIsSideEffectFree(
+            invokedMethod, instruction.inValues());
+      } else {
+        assert invokedMethod.getHolderType() == factory.stringBufferType;
+        return factory.stringBufferMethods.constructorInvokeIsSideEffectFree(
+            invokedMethod, instruction.inValues());
+      }
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/EqualsCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/EqualsCfCodeProvider.java
index d1a4bf3..febbc58 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/EqualsCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/EqualsCfCodeProvider.java
@@ -43,15 +43,23 @@
   public CfCode generateCfCode() {
     // return wrapperField.equals(
     //     other instanceof WrapperType ? ((WrapperType) other).wrapperField : other);
+    DexType wrapperType = wrapperField.getHolderType();
+    FrameType[] locals = {
+      FrameType.initialized(wrapperType), FrameType.initialized(appView.dexItemFactory().objectType)
+    };
     CfLabel label1 = new CfLabel();
     CfLabel label2 = new CfLabel();
+    // this.wrapperField
     List<CfInstruction> instructions = new ArrayList<>();
     instructions.add(new CfLoad(ValueType.OBJECT, 0));
     instructions.add(new CfInstanceFieldRead(wrapperField));
+
+    // other instanceof WrapperType
     instructions.add(new CfLoad(ValueType.OBJECT, 1));
-    DexType wrapperType = wrapperField.getHolderType();
     instructions.add(new CfInstanceOf(wrapperType));
     instructions.add(new CfIf(If.Type.EQ, ValueType.INT, label1));
+
+    // ((WrapperType) other).wrapperField
     instructions.add(new CfLoad(ValueType.OBJECT, 1));
     instructions.add(new CfCheckCast(wrapperType));
     instructions.add(new CfInstanceFieldRead(wrapperField));
@@ -59,27 +67,21 @@
     instructions.add(label1);
     instructions.add(
         new CfFrame(
-            new Int2ObjectAVLTreeMap<>(
-                new int[] {0, 1},
-                new FrameType[] {
-                  FrameType.initialized(wrapperType),
-                  FrameType.initialized(appView.dexItemFactory().objectType)
-                }),
-            new ArrayDeque<>(Arrays.asList(FrameType.initialized(wrapperType)))));
+            new Int2ObjectAVLTreeMap<>(new int[] {0, 1}, locals),
+            new ArrayDeque<>(Arrays.asList(FrameType.initialized(wrapperField.type)))));
+
+    // other
     instructions.add(new CfLoad(ValueType.OBJECT, 1));
     instructions.add(label2);
     instructions.add(
         new CfFrame(
-            new Int2ObjectAVLTreeMap<>(
-                new int[] {0, 1},
-                new FrameType[] {
-                  FrameType.initialized(wrapperType),
-                  FrameType.initialized(appView.dexItemFactory().objectType)
-                }),
+            new Int2ObjectAVLTreeMap<>(new int[] {0, 1}, locals),
             new ArrayDeque<>(
                 Arrays.asList(
-                    FrameType.initialized(wrapperType),
+                    FrameType.initialized(wrapperField.type),
                     FrameType.initialized(appView.dexItemFactory().objectType)))));
+
+    // equals.
     instructions.add(
         new CfInvoke(Opcodes.INVOKEVIRTUAL, appView.dexItemFactory().objectMembers.equals, false));
     instructions.add(new CfReturn(ValueType.INT));
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/NullableConversionCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/NullableConversionCfCodeProvider.java
index a32d51e..8f0f2f4 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/NullableConversionCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/apiconverter/NullableConversionCfCodeProvider.java
@@ -193,14 +193,19 @@
 
   public static class WrapperConversionCfCodeProvider extends NullableConversionCfCodeProvider {
 
-    DexField reverseWrapperField;
-    DexField wrapperField;
+    private final DexField reverseWrapperField;
+    private final DexField wrapperField;
+    private final List<DexMethod> subwrapperConvertList;
 
     public WrapperConversionCfCodeProvider(
-        AppView<?> appView, DexField reverseWrapperField, DexField wrapperField) {
+        AppView<?> appView,
+        DexField reverseWrapperField,
+        DexField wrapperField,
+        List<DexMethod> subwrapperConvertList) {
       super(appView, wrapperField.holder);
       this.reverseWrapperField = reverseWrapperField;
       this.wrapperField = wrapperField;
+      this.subwrapperConvertList = subwrapperConvertList;
     }
 
     @Override
@@ -228,6 +233,23 @@
       instructions.add(unwrapDest);
       instructions.add(frame.clone());
 
+      // if (arg instanceOf Subtype) {
+      //     return SubtypeWrapper.convert((Subtype) arg)
+      // };
+      for (DexMethod convert : subwrapperConvertList) {
+        CfLabel dest = new CfLabel();
+        DexType convertArgType = convert.getArgumentType(0, true);
+        instructions.add(new CfLoad(ValueType.fromDexType(argType), 0));
+        instructions.add(new CfInstanceOf(convertArgType));
+        instructions.add(new CfIf(If.Type.EQ, ValueType.INT, dest));
+        instructions.add(new CfLoad(ValueType.fromDexType(argType), 0));
+        instructions.add(new CfCheckCast(convertArgType));
+        instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, convert, false));
+        instructions.add(new CfReturn(ValueType.fromDexType(reverseWrapperField.type)));
+        instructions.add(dest);
+        instructions.add(frame.clone());
+      }
+
       // return new Wrapper(wrappedValue);
       instructions.add(new CfNew(wrapperField.holder));
       instructions.add(CfStackInstruction.fromAsm(Opcodes.DUP));
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceRegularExpressionParser.java b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceRegularExpressionParser.java
index 973a89d..9a74b4e 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceRegularExpressionParser.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceRegularExpressionParser.java
@@ -159,7 +159,7 @@
 
   // TODO(b/145731185): Extend support for identifiers with strings inside back ticks.
   private static final String javaIdentifierSegment =
-      "\\p{javaJavaIdentifierStart}(?:-|\\p{javaJavaIdentifierPart})*";
+      "\\p{javaJavaIdentifierStart}[-\\p{javaJavaIdentifierPart}]*";
 
   private static final String METHOD_NAME_REGULAR_EXPRESSION =
       "(?:(" + javaIdentifierSegment + "|\\<init\\>|\\<clinit\\>))";
diff --git a/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerFactory.java b/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerFactory.java
index cbbfc33..1c5dd2b 100644
--- a/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerFactory.java
+++ b/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerFactory.java
@@ -38,14 +38,13 @@
 public final class InstrumentationServerFactory {
   public static DexProgramClass createClass(DexItemFactory dexItemFactory) {
     return new DexProgramClass(
-        dexItemFactory.createType(
-            "Lcom/android/tools/r8/startup/generated/InstrumentationServerFactory;"),
+        dexItemFactory.createType("Lcom/android/tools/r8/startup/InstrumentationServer;"),
         Kind.CF,
         Origin.unknown(),
-        ClassAccessFlags.fromCfAccessFlags(1025),
-        null,
+        ClassAccessFlags.fromCfAccessFlags(1057),
+        dexItemFactory.createType("Ljava/lang/Object;"),
         DexTypeList.empty(),
-        dexItemFactory.createString("InstrumentationServerFactory"),
+        dexItemFactory.createString("InstrumentationServer.java"),
         NestHostClassAttribute.none(),
         Collections.emptyList(),
         Collections.emptyList(),
@@ -72,6 +71,18 @@
   private static DexEncodedMethod[] createDirectMethods(DexItemFactory dexItemFactory) {
     return new DexEncodedMethod[] {
       DexEncodedMethod.syntheticBuilder()
+          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(1, true))
+          .setApiLevelForCode(ComputedApiLevel.unknown())
+          .setApiLevelForDefinition(ComputedApiLevel.unknown())
+          .setClassFileVersion(CfVersion.V1_8)
+          .setMethod(
+              dexItemFactory.createMethod(
+                  dexItemFactory.createType("Lcom/android/tools/r8/startup/InstrumentationServer;"),
+                  dexItemFactory.createProto(dexItemFactory.createType("V")),
+                  dexItemFactory.createString("<init>")))
+          .setCode(method -> createInstanceInitializerCfCode0(dexItemFactory, method))
+          .build(),
+      DexEncodedMethod.syntheticBuilder()
           .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(9, false))
           .setApiLevelForCode(ComputedApiLevel.unknown())
           .setApiLevelForDefinition(ComputedApiLevel.unknown())
@@ -91,17 +102,6 @@
   private static DexEncodedMethod[] createVirtualMethods(DexItemFactory dexItemFactory) {
     return new DexEncodedMethod[] {
       DexEncodedMethod.syntheticBuilder()
-          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(1, false))
-          .setApiLevelForCode(ComputedApiLevel.unknown())
-          .setApiLevelForDefinition(ComputedApiLevel.unknown())
-          .setClassFileVersion(CfVersion.V1_8)
-          .setMethod(
-              dexItemFactory.createInstanceInitializer(
-                  dexItemFactory.createType(
-                      "Lcom/android/tools/r8/startup/InstrumentationServer;")))
-          .setCode(method -> createInstanceInitializerCfCode0(dexItemFactory, method))
-          .build(),
-      DexEncodedMethod.syntheticBuilder()
           .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(1025, false))
           .setApiLevelForCode(ComputedApiLevel.unknown())
           .setApiLevelForDefinition(ComputedApiLevel.unknown())
diff --git a/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerImplFactory.java b/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerImplFactory.java
index f0c8f9b..e5af39a 100644
--- a/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerImplFactory.java
+++ b/src/main/java/com/android/tools/r8/startup/generated/InstrumentationServerImplFactory.java
@@ -11,15 +11,18 @@
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.cf.code.CfCheckCast;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfConstString;
 import com.android.tools.r8.cf.code.CfFrame;
 import com.android.tools.r8.cf.code.CfGoto;
+import com.android.tools.r8.cf.code.CfIf;
 import com.android.tools.r8.cf.code.CfInstanceFieldRead;
 import com.android.tools.r8.cf.code.CfInstanceFieldWrite;
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.cf.code.CfLabel;
 import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfMonitor;
 import com.android.tools.r8.cf.code.CfNew;
 import com.android.tools.r8.cf.code.CfReturn;
 import com.android.tools.r8.cf.code.CfReturnVoid;
@@ -45,6 +48,8 @@
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.MethodCollection.MethodCollectionFactory;
 import com.android.tools.r8.graph.NestHostClassAttribute;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.Monitor;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.origin.Origin;
 import com.google.common.collect.ImmutableList;
@@ -56,14 +61,13 @@
 public final class InstrumentationServerImplFactory {
   public static DexProgramClass createClass(DexItemFactory dexItemFactory) {
     return new DexProgramClass(
-        dexItemFactory.createType(
-            "Lcom/android/tools/r8/startup/generated/InstrumentationServerImplFactory;"),
+        dexItemFactory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
         Kind.CF,
         Origin.unknown(),
-        ClassAccessFlags.fromCfAccessFlags(1),
-        null,
+        ClassAccessFlags.fromCfAccessFlags(33),
+        dexItemFactory.createType("Lcom/android/tools/r8/startup/InstrumentationServer;"),
         DexTypeList.empty(),
-        dexItemFactory.createString("InstrumentationServerImplFactory"),
+        dexItemFactory.createString("InstrumentationServerImpl.java"),
         NestHostClassAttribute.none(),
         Collections.emptyList(),
         Collections.emptyList(),
@@ -86,28 +90,8 @@
               dexItemFactory.createField(
                   dexItemFactory.createType(
                       "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                  dexItemFactory.createType("Ljava/lang/StringBuilder;"),
-                  dexItemFactory.createString("builder")))
-          .setAccessFlags(FieldAccessFlags.fromCfAccessFlags(18))
-          .setApiLevel(ComputedApiLevel.unknown())
-          .build(),
-      DexEncodedField.syntheticBuilder()
-          .setField(
-              dexItemFactory.createField(
-                  dexItemFactory.createType(
-                      "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                  dexItemFactory.createType("Ljava/lang/String;"),
-                  dexItemFactory.createString("logcatTag")))
-          .setAccessFlags(FieldAccessFlags.fromCfAccessFlags(18))
-          .setApiLevel(ComputedApiLevel.unknown())
-          .build(),
-      DexEncodedField.syntheticBuilder()
-          .setField(
-              dexItemFactory.createField(
-                  dexItemFactory.createType(
-                      "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                  dexItemFactory.createType("Z"),
-                  dexItemFactory.createString("writeToLogcat")))
+                  dexItemFactory.createType("Ljava/util/LinkedHashSet;"),
+                  dexItemFactory.createString("lines")))
           .setAccessFlags(FieldAccessFlags.fromCfAccessFlags(18))
           .setApiLevel(ComputedApiLevel.unknown())
           .build()
@@ -126,6 +110,26 @@
                   dexItemFactory.createString("INSTANCE")))
           .setAccessFlags(FieldAccessFlags.fromCfAccessFlags(26))
           .setApiLevel(ComputedApiLevel.unknown())
+          .build(),
+      DexEncodedField.syntheticBuilder()
+          .setField(
+              dexItemFactory.createField(
+                  dexItemFactory.createType(
+                      "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                  dexItemFactory.createType("Z"),
+                  dexItemFactory.createString("writeToLogcat")))
+          .setAccessFlags(FieldAccessFlags.fromCfAccessFlags(10))
+          .setApiLevel(ComputedApiLevel.unknown())
+          .build(),
+      DexEncodedField.syntheticBuilder()
+          .setField(
+              dexItemFactory.createField(
+                  dexItemFactory.createType(
+                      "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                  dexItemFactory.createType("Ljava/lang/String;"),
+                  dexItemFactory.createString("logcatTag")))
+          .setAccessFlags(FieldAccessFlags.fromCfAccessFlags(10))
+          .setApiLevel(ComputedApiLevel.unknown())
           .build()
     };
   }
@@ -133,18 +137,20 @@
   private static DexEncodedMethod[] createDirectMethods(DexItemFactory dexItemFactory) {
     return new DexEncodedMethod[] {
       DexEncodedMethod.syntheticBuilder()
-          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(2, false))
+          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(2, true))
           .setApiLevelForCode(ComputedApiLevel.unknown())
           .setApiLevelForDefinition(ComputedApiLevel.unknown())
           .setClassFileVersion(CfVersion.V1_8)
           .setMethod(
-              dexItemFactory.createInstanceInitializer(
+              dexItemFactory.createMethod(
                   dexItemFactory.createType(
-                      "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")))
+                      "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                  dexItemFactory.createProto(dexItemFactory.createType("V")),
+                  dexItemFactory.createString("<init>")))
           .setCode(method -> createInstanceInitializerCfCode1(dexItemFactory, method))
           .build(),
       DexEncodedMethod.syntheticBuilder()
-          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(34, false))
+          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(9, false))
           .setApiLevelForCode(ComputedApiLevel.unknown())
           .setApiLevelForDefinition(ComputedApiLevel.unknown())
           .setClassFileVersion(CfVersion.V1_8)
@@ -153,10 +159,10 @@
                   dexItemFactory.createType(
                       "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
                   dexItemFactory.createProto(
-                      dexItemFactory.createType("V"),
-                      dexItemFactory.createType("Ljava/lang/String;")),
-                  dexItemFactory.createString("addLine")))
-          .setCode(method -> createCfCode2_addLine(dexItemFactory, method))
+                      dexItemFactory.createType(
+                          "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                  dexItemFactory.createString("getInstance")))
+          .setCode(method -> createCfCode5_getInstance(dexItemFactory, method))
           .build(),
       DexEncodedMethod.syntheticBuilder()
           .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(9, false))
@@ -189,7 +195,7 @@
           .setCode(method -> createCfCode4_addSyntheticMethod(dexItemFactory, method))
           .build(),
       DexEncodedMethod.syntheticBuilder()
-          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(9, false))
+          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(2, false))
           .setApiLevelForCode(ComputedApiLevel.unknown())
           .setApiLevelForDefinition(ComputedApiLevel.unknown())
           .setClassFileVersion(CfVersion.V1_8)
@@ -198,10 +204,10 @@
                   dexItemFactory.createType(
                       "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
                   dexItemFactory.createProto(
-                      dexItemFactory.createType(
-                          "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
-                  dexItemFactory.createString("getInstance")))
-          .setCode(method -> createCfCode5_getInstance(dexItemFactory, method))
+                      dexItemFactory.createType("V"),
+                      dexItemFactory.createType("Ljava/lang/String;")),
+                  dexItemFactory.createString("addLine")))
+          .setCode(method -> createCfCode2_addLine(dexItemFactory, method))
           .build(),
       DexEncodedMethod.syntheticBuilder()
           .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(2, false))
@@ -217,6 +223,19 @@
                       dexItemFactory.createType("Ljava/lang/String;")),
                   dexItemFactory.createString("writeToLogcat")))
           .setCode(method -> createCfCode7_writeToLogcat(dexItemFactory, method))
+          .build(),
+      DexEncodedMethod.syntheticBuilder()
+          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(8, true))
+          .setApiLevelForCode(ComputedApiLevel.unknown())
+          .setApiLevelForDefinition(ComputedApiLevel.unknown())
+          .setClassFileVersion(CfVersion.V1_8)
+          .setMethod(
+              dexItemFactory.createMethod(
+                  dexItemFactory.createType(
+                      "Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                  dexItemFactory.createProto(dexItemFactory.createType("V")),
+                  dexItemFactory.createString("<clinit>")))
+          .setCode(method -> createClassInitializerCfCode(dexItemFactory, method))
           .build()
     };
   }
@@ -224,7 +243,7 @@
   private static DexEncodedMethod[] createVirtualMethods(DexItemFactory dexItemFactory) {
     return new DexEncodedMethod[] {
       DexEncodedMethod.syntheticBuilder()
-          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(33, false))
+          .setAccessFlags(MethodAccessFlags.fromCfAccessFlags(1, false))
           .setApiLevelForCode(ComputedApiLevel.unknown())
           .setApiLevelForDefinition(ComputedApiLevel.unknown())
           .setClassFileVersion(CfVersion.V1_8)
@@ -273,8 +292,6 @@
     CfLabel label1 = new CfLabel();
     CfLabel label2 = new CfLabel();
     CfLabel label3 = new CfLabel();
-    CfLabel label4 = new CfLabel();
-    CfLabel label5 = new CfLabel();
     return new CfCode(
         method.holder,
         3,
@@ -291,39 +308,23 @@
                 false),
             label1,
             new CfLoad(ValueType.OBJECT, 0),
-            new CfNew(factory.stringBuilderType),
+            new CfNew(factory.createType("Ljava/util/LinkedHashSet;")),
             new CfStackInstruction(CfStackInstruction.Opcode.Dup),
             new CfInvoke(
                 183,
                 factory.createMethod(
-                    factory.stringBuilderType,
+                    factory.createType("Ljava/util/LinkedHashSet;"),
                     factory.createProto(factory.voidType),
                     factory.createString("<init>")),
                 false),
             new CfInstanceFieldWrite(
                 factory.createField(
                     factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                    factory.stringBuilderType,
-                    factory.createString("builder"))),
+                    factory.createType("Ljava/util/LinkedHashSet;"),
+                    factory.createString("lines"))),
             label2,
-            new CfLoad(ValueType.OBJECT, 0),
-            new CfConstNumber(0, ValueType.INT),
-            new CfInstanceFieldWrite(
-                factory.createField(
-                    factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                    factory.booleanType,
-                    factory.createString("writeToLogcat"))),
-            label3,
-            new CfLoad(ValueType.OBJECT, 0),
-            new CfConstString(factory.createString("r8")),
-            new CfInstanceFieldWrite(
-                factory.createField(
-                    factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                    factory.stringType,
-                    factory.createString("logcatTag"))),
-            label4,
             new CfReturnVoid(),
-            label5),
+            label3),
         ImmutableList.of(),
         ImmutableList.of());
   }
@@ -332,39 +333,130 @@
     CfLabel label0 = new CfLabel();
     CfLabel label1 = new CfLabel();
     CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    CfLabel label4 = new CfLabel();
+    CfLabel label5 = new CfLabel();
+    CfLabel label6 = new CfLabel();
+    CfLabel label7 = new CfLabel();
+    CfLabel label8 = new CfLabel();
+    CfLabel label9 = new CfLabel();
+    CfLabel label10 = new CfLabel();
+    CfLabel label11 = new CfLabel();
     return new CfCode(
         method.holder,
         2,
-        2,
+        4,
         ImmutableList.of(
             label0,
             new CfLoad(ValueType.OBJECT, 0),
             new CfInstanceFieldRead(
                 factory.createField(
                     factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                    factory.stringBuilderType,
-                    factory.createString("builder"))),
+                    factory.createType("Ljava/util/LinkedHashSet;"),
+                    factory.createString("lines"))),
+            new CfStackInstruction(CfStackInstruction.Opcode.Dup),
+            new CfStore(ValueType.OBJECT, 2),
+            new CfMonitor(Monitor.Type.ENTER),
+            label1,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInstanceFieldRead(
+                factory.createField(
+                    factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                    factory.createType("Ljava/util/LinkedHashSet;"),
+                    factory.createString("lines"))),
             new CfLoad(ValueType.OBJECT, 1),
             new CfInvoke(
                 182,
                 factory.createMethod(
-                    factory.stringBuilderType,
-                    factory.createProto(factory.stringBuilderType, factory.stringType),
-                    factory.createString("append")),
+                    factory.createType("Ljava/util/LinkedHashSet;"),
+                    factory.createProto(factory.booleanType, factory.objectType),
+                    factory.createString("add")),
                 false),
-            new CfConstNumber(10, ValueType.INT),
-            new CfInvoke(
-                182,
-                factory.createMethod(
-                    factory.stringBuilderType,
-                    factory.createProto(factory.stringBuilderType, factory.charType),
-                    factory.createString("append")),
-                false),
-            new CfStackInstruction(CfStackInstruction.Opcode.Pop),
-            label1,
+            new CfIf(If.Type.NE, ValueType.INT, label4),
+            label2,
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfMonitor(Monitor.Type.EXIT),
+            label3,
             new CfReturnVoid(),
-            label2),
-        ImmutableList.of(),
+            label4,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.stringType),
+                      FrameType.initializedNonNullReference(factory.objectType)
+                    })),
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfMonitor(Monitor.Type.EXIT),
+            label5,
+            new CfGoto(label8),
+            label6,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.stringType),
+                      FrameType.initializedNonNullReference(factory.objectType)
+                    }),
+                new ArrayDeque<>(
+                    Arrays.asList(FrameType.initializedNonNullReference(factory.throwableType)))),
+            new CfStore(ValueType.OBJECT, 3),
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfMonitor(Monitor.Type.EXIT),
+            label7,
+            new CfLoad(ValueType.OBJECT, 3),
+            new CfThrow(),
+            label8,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.stringType)
+                    })),
+            new CfStaticFieldRead(
+                factory.createField(
+                    factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                    factory.booleanType,
+                    factory.createString("writeToLogcat"))),
+            new CfIf(If.Type.EQ, ValueType.INT, label10),
+            label9,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfInvoke(
+                183,
+                factory.createMethod(
+                    factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                    factory.createProto(factory.voidType, factory.stringType),
+                    factory.createString("writeToLogcat")),
+                false),
+            label10,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.stringType)
+                    })),
+            new CfReturnVoid(),
+            label11),
+        ImmutableList.of(
+            new CfTryCatch(
+                label1, label3, ImmutableList.of(factory.throwableType), ImmutableList.of(label6)),
+            new CfTryCatch(
+                label4, label5, ImmutableList.of(factory.throwableType), ImmutableList.of(label6)),
+            new CfTryCatch(
+                label6, label7, ImmutableList.of(factory.throwableType), ImmutableList.of(label6))),
         ImmutableList.of());
   }
 
@@ -495,102 +587,146 @@
     CfLabel label5 = new CfLabel();
     CfLabel label6 = new CfLabel();
     CfLabel label7 = new CfLabel();
+    CfLabel label8 = new CfLabel();
+    CfLabel label9 = new CfLabel();
+    CfLabel label10 = new CfLabel();
+    CfLabel label11 = new CfLabel();
+    CfLabel label12 = new CfLabel();
+    CfLabel label13 = new CfLabel();
+    CfLabel label14 = new CfLabel();
+    CfLabel label15 = new CfLabel();
+    CfLabel label16 = new CfLabel();
     return new CfCode(
         method.holder,
-        3,
         4,
+        8,
         ImmutableList.of(
             label0,
-            new CfNew(factory.createType("Ljava/io/FileOutputStream;")),
+            new CfNew(factory.createType("Ljava/io/PrintWriter;")),
             new CfStackInstruction(CfStackInstruction.Opcode.Dup),
             new CfLoad(ValueType.OBJECT, 1),
+            new CfConstString(factory.createString("UTF-8")),
             new CfInvoke(
                 183,
                 factory.createMethod(
-                    factory.createType("Ljava/io/FileOutputStream;"),
-                    factory.createProto(factory.voidType, factory.createType("Ljava/io/File;")),
+                    factory.createType("Ljava/io/PrintWriter;"),
+                    factory.createProto(
+                        factory.voidType, factory.createType("Ljava/io/File;"), factory.stringType),
                     factory.createString("<init>")),
                 false),
             new CfStore(ValueType.OBJECT, 2),
             label1,
-            new CfLoad(ValueType.OBJECT, 2),
             new CfLoad(ValueType.OBJECT, 0),
             new CfInstanceFieldRead(
                 factory.createField(
                     factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
-                    factory.stringBuilderType,
-                    factory.createString("builder"))),
-            new CfInvoke(
-                182,
-                factory.createMethod(
-                    factory.stringBuilderType,
-                    factory.createProto(factory.stringType),
-                    factory.createString("toString")),
-                false),
-            new CfConstString(factory.createString("UTF-8")),
-            new CfInvoke(
-                184,
-                factory.createMethod(
-                    factory.createType("Ljava/nio/charset/Charset;"),
-                    factory.createProto(
-                        factory.createType("Ljava/nio/charset/Charset;"), factory.stringType),
-                    factory.createString("forName")),
-                false),
-            new CfInvoke(
-                182,
-                factory.createMethod(
-                    factory.stringType,
-                    factory.createProto(
-                        factory.byteArrayType, factory.createType("Ljava/nio/charset/Charset;")),
-                    factory.createString("getBytes")),
-                false),
-            new CfInvoke(
-                182,
-                factory.createMethod(
-                    factory.createType("Ljava/io/FileOutputStream;"),
-                    factory.createProto(factory.voidType, factory.byteArrayType),
-                    factory.createString("write")),
-                false),
+                    factory.createType("Ljava/util/LinkedHashSet;"),
+                    factory.createString("lines"))),
+            new CfStackInstruction(CfStackInstruction.Opcode.Dup),
+            new CfStore(ValueType.OBJECT, 3),
+            new CfMonitor(Monitor.Type.ENTER),
             label2,
-            new CfLoad(ValueType.OBJECT, 2),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInstanceFieldRead(
+                factory.createField(
+                    factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                    factory.createType("Ljava/util/LinkedHashSet;"),
+                    factory.createString("lines"))),
             new CfInvoke(
                 182,
                 factory.createMethod(
-                    factory.createType("Ljava/io/FileOutputStream;"),
-                    factory.createProto(factory.voidType),
-                    factory.createString("close")),
+                    factory.createType("Ljava/util/LinkedHashSet;"),
+                    factory.createProto(factory.createType("Ljava/util/Iterator;")),
+                    factory.createString("iterator")),
                 false),
+            new CfStore(ValueType.OBJECT, 4),
             label3,
-            new CfGoto(label6),
-            label4,
             new CfFrame(
                 new Int2ObjectAVLTreeMap<>(
-                    new int[] {0, 1, 2},
+                    new int[] {0, 1, 2, 3, 4},
                     new FrameType[] {
                       FrameType.initializedNonNullReference(
                           factory.createType(
                               "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
                       FrameType.initializedNonNullReference(factory.createType("Ljava/io/File;")),
                       FrameType.initializedNonNullReference(
-                          factory.createType("Ljava/io/FileOutputStream;"))
-                    }),
-                new ArrayDeque<>(
-                    Arrays.asList(FrameType.initializedNonNullReference(factory.throwableType)))),
-            new CfStore(ValueType.OBJECT, 3),
+                          factory.createType("Ljava/io/PrintWriter;")),
+                      FrameType.initializedNonNullReference(factory.objectType),
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/util/Iterator;"))
+                    })),
+            new CfLoad(ValueType.OBJECT, 4),
+            new CfInvoke(
+                185,
+                factory.createMethod(
+                    factory.createType("Ljava/util/Iterator;"),
+                    factory.createProto(factory.booleanType),
+                    factory.createString("hasNext")),
+                true),
+            new CfIf(If.Type.EQ, ValueType.INT, label6),
+            new CfLoad(ValueType.OBJECT, 4),
+            new CfInvoke(
+                185,
+                factory.createMethod(
+                    factory.createType("Ljava/util/Iterator;"),
+                    factory.createProto(factory.objectType),
+                    factory.createString("next")),
+                true),
+            new CfCheckCast(factory.stringType),
+            new CfStore(ValueType.OBJECT, 5),
+            label4,
             new CfLoad(ValueType.OBJECT, 2),
+            new CfLoad(ValueType.OBJECT, 5),
             new CfInvoke(
                 182,
                 factory.createMethod(
-                    factory.createType("Ljava/io/FileOutputStream;"),
-                    factory.createProto(factory.voidType),
-                    factory.createString("close")),
+                    factory.createType("Ljava/io/PrintWriter;"),
+                    factory.createProto(factory.voidType, factory.stringType),
+                    factory.createString("println")),
                 false),
             label5,
-            new CfLoad(ValueType.OBJECT, 3),
-            new CfThrow(),
+            new CfGoto(label3),
             label6,
             new CfFrame(
                 new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2, 3},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.createType("Ljava/io/File;")),
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/io/PrintWriter;")),
+                      FrameType.initializedNonNullReference(factory.objectType)
+                    })),
+            new CfLoad(ValueType.OBJECT, 3),
+            new CfMonitor(Monitor.Type.EXIT),
+            label7,
+            new CfGoto(label10),
+            label8,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2, 3},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.createType("Ljava/io/File;")),
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/io/PrintWriter;")),
+                      FrameType.initializedNonNullReference(factory.objectType)
+                    }),
+                new ArrayDeque<>(
+                    Arrays.asList(FrameType.initializedNonNullReference(factory.throwableType)))),
+            new CfStore(ValueType.OBJECT, 6),
+            new CfLoad(ValueType.OBJECT, 3),
+            new CfMonitor(Monitor.Type.EXIT),
+            label9,
+            new CfLoad(ValueType.OBJECT, 6),
+            new CfThrow(),
+            label10,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
                     new int[] {0, 1, 2},
                     new FrameType[] {
                       FrameType.initializedNonNullReference(
@@ -598,13 +734,74 @@
                               "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
                       FrameType.initializedNonNullReference(factory.createType("Ljava/io/File;")),
                       FrameType.initializedNonNullReference(
-                          factory.createType("Ljava/io/FileOutputStream;"))
+                          factory.createType("Ljava/io/PrintWriter;"))
+                    })),
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Ljava/io/PrintWriter;"),
+                    factory.createProto(factory.voidType),
+                    factory.createString("close")),
+                false),
+            label11,
+            new CfGoto(label15),
+            label12,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.createType("Ljava/io/File;")),
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/io/PrintWriter;"))
+                    }),
+                new ArrayDeque<>(
+                    Arrays.asList(FrameType.initializedNonNullReference(factory.throwableType)))),
+            new CfStore(ValueType.OBJECT, 7),
+            label13,
+            new CfLoad(ValueType.OBJECT, 2),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Ljava/io/PrintWriter;"),
+                    factory.createProto(factory.voidType),
+                    factory.createString("close")),
+                false),
+            label14,
+            new CfLoad(ValueType.OBJECT, 7),
+            new CfThrow(),
+            label15,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0, 1, 2},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType(
+                              "Lcom/android/tools/r8/startup/InstrumentationServerImpl;")),
+                      FrameType.initializedNonNullReference(factory.createType("Ljava/io/File;")),
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/io/PrintWriter;"))
                     })),
             new CfReturnVoid(),
-            label7),
+            label16),
         ImmutableList.of(
             new CfTryCatch(
-                label1, label2, ImmutableList.of(factory.throwableType), ImmutableList.of(label4))),
+                label2, label7, ImmutableList.of(factory.throwableType), ImmutableList.of(label8)),
+            new CfTryCatch(
+                label8, label9, ImmutableList.of(factory.throwableType), ImmutableList.of(label8)),
+            new CfTryCatch(
+                label1,
+                label10,
+                ImmutableList.of(factory.throwableType),
+                ImmutableList.of(label12)),
+            new CfTryCatch(
+                label12,
+                label13,
+                ImmutableList.of(factory.throwableType),
+                ImmutableList.of(label12))),
         ImmutableList.of());
   }
 
@@ -618,14 +815,18 @@
         2,
         ImmutableList.of(
             label0,
-            new CfConstString(factory.createString("r8")),
+            new CfStaticFieldRead(
+                factory.createField(
+                    factory.createType("Lcom/android/tools/r8/startup/InstrumentationServerImpl;"),
+                    factory.stringType,
+                    factory.createString("logcatTag"))),
             new CfLoad(ValueType.OBJECT, 1),
             new CfInvoke(
                 184,
                 factory.createMethod(
                     factory.createType("Landroid/util/Log;"),
                     factory.createProto(factory.intType, factory.stringType, factory.stringType),
-                    factory.createString("v")),
+                    factory.createString("i")),
                 false),
             new CfStackInstruction(CfStackInstruction.Opcode.Pop),
             label1,
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 044b77f..843a0f3 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.graph.MethodCollection;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.ProgramOrClasspathDefinition;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.naming.NamingLens;
@@ -728,7 +729,7 @@
       DexString name,
       DexProto proto,
       SyntheticKindSelector kindSelector,
-      ProgramDefinition context,
+      ProgramOrClasspathDefinition context,
       AppView<?> appView,
       Consumer<SyntheticProgramClassBuilder> buildClassCallback,
       Consumer<SyntheticMethodBuilder> buildMethodCallback) {
@@ -747,7 +748,7 @@
       DexString name,
       DexProto proto,
       SyntheticKindSelector kindSelector,
-      ProgramDefinition context,
+      ProgramOrClasspathDefinition context,
       AppView<?> appView,
       Consumer<SyntheticProgramClassBuilder> buildClassCallback,
       Consumer<SyntheticMethodBuilder> buildMethodCallback,
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 631115f..9aed303 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -43,6 +43,7 @@
   public final SyntheticKind RETARGET_CLASS = generator.forFixedClass("RetargetClass");
   public final SyntheticKind RETARGET_INTERFACE = generator.forFixedClass("RetargetInterface");
   public final SyntheticKind WRAPPER = generator.forFixedClass("$Wrapper");
+  public final SyntheticKind VIVIFIED = generator.forFixedClass("");
   public final SyntheticKind VIVIFIED_WRAPPER = generator.forFixedClass("$VivifiedWrapper");
   public final SyntheticKind INIT_TYPE_ARGUMENT = generator.forFixedClass("-IA");
   public final SyntheticKind HORIZONTAL_INIT_TYPE_ARGUMENT_1 =
diff --git a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
index 9818d3e..037fafb 100644
--- a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
 import com.google.common.collect.ImmutableList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 
@@ -55,6 +56,22 @@
         return method.getFormalTypes().size() - other.getFormalTypes().size();
       };
 
+  public static MethodReference classConstructor(Class<?> clazz) {
+    return classConstructor(Reference.classFromClass(clazz));
+  }
+
+  public static MethodReference classConstructor(ClassReference type) {
+    return Reference.classConstructor(type);
+  }
+
+  public static MethodReference instanceConstructor(Class<?> clazz) {
+    return instanceConstructor(Reference.classFromClass(clazz));
+  }
+
+  public static MethodReference instanceConstructor(ClassReference type) {
+    return Reference.method(type, "<init>", Collections.emptyList(), null);
+  }
+
   public static int compare(MethodReference methodReference, ClassReference other) {
     return ClassReferenceUtils.compare(other, methodReference) * -1;
   }
diff --git a/src/test/java/android/util/Log.java b/src/test/java/android/util/Log.java
index 2ca06ef..67faa9e 100644
--- a/src/test/java/android/util/Log.java
+++ b/src/test/java/android/util/Log.java
@@ -8,6 +8,10 @@
 
 public class Log {
 
+  public static int i(String tag, String message) {
+    throw new RuntimeException();
+  }
+
   public static int v(String tag, String message) {
     throw new RuntimeException();
   }
diff --git a/src/test/java/com/android/tools/r8/KotlinTestParameters.java b/src/test/java/com/android/tools/r8/KotlinTestParameters.java
index d5a943f..abce6c3 100644
--- a/src/test/java/com/android/tools/r8/KotlinTestParameters.java
+++ b/src/test/java/com/android/tools/r8/KotlinTestParameters.java
@@ -60,10 +60,6 @@
     return !isNewerThanOrEqualTo(otherVersion);
   }
 
-  public boolean isOlderThanMinSupported() {
-    return isOlderThan(KotlinCompilerVersion.MIN_SUPPORTED_VERSION);
-  }
-
   public boolean isFirst() {
     return index == 0;
   }
@@ -133,6 +129,11 @@
       return this;
     }
 
+    public Builder withOldCompiler(KotlinCompilerVersion oldVersion) {
+      oldCompilerFilter = oldCompilerFilter.and(v -> v.isEqualTo(oldVersion));
+      return this;
+    }
+
     public Builder withAllTargetVersions() {
       withTargetVersionFilter(t -> t != KotlinTargetVersion.NONE);
       return this;
diff --git a/src/test/java/com/android/tools/r8/TestParametersBuilder.java b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
index 59d2bc6..cd34cd5 100644
--- a/src/test/java/com/android/tools/r8/TestParametersBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
@@ -133,6 +133,10 @@
     return withDexRuntimeFilter(vm -> true);
   }
 
+  public TestParametersBuilder withDexRuntimesAndAllApiLevels() {
+    return withDexRuntimes().withAllApiLevels();
+  }
+
   /** Add specific runtime if available. */
   public TestParametersBuilder withDexRuntime(DexVm.Version runtime) {
     return withDexRuntimeFilter(vm -> vm == runtime);
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java
index ccb901d..962f964 100644
--- a/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/VarHandleTest.java
@@ -94,6 +94,9 @@
         .addProgramFiles(getProgramInputs())
         .setMinApi(parameters.getApiLevel())
         .mapUnsupportedFeaturesToWarnings()
+        // TODO(b/238175192): remove again when resolved
+        .addOptionsModification(
+            options -> options.enableUnrepresentableInDexInstructionRemoval = true)
         .compileWithExpectedDiagnostics(
             diagnostics -> {
               if (hasInvokePolymorphicCompileSupport()) {
diff --git a/src/test/java/com/android/tools/r8/cfmethodgeneration/CfClassGenerator.java b/src/test/java/com/android/tools/r8/cfmethodgeneration/CfClassGenerator.java
index 5435248..c706f50 100644
--- a/src/test/java/com/android/tools/r8/cfmethodgeneration/CfClassGenerator.java
+++ b/src/test/java/com/android/tools/r8/cfmethodgeneration/CfClassGenerator.java
@@ -4,37 +4,27 @@
 
 package com.android.tools.r8.cfmethodgeneration;
 
-import static com.android.tools.r8.utils.PredicateUtils.not;
-
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.cf.CfCodePrinter;
 import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.graph.JarClassFileReader;
-import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.references.MethodReference;
-import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.utils.FieldReferenceUtils;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
-import com.google.common.collect.Streams;
 import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Executable;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
-import java.util.function.Predicate;
 
 public abstract class CfClassGenerator extends CodeGenerationBase {
 
@@ -54,27 +44,33 @@
   private String generateClassDeclaration() throws IOException {
     JavaStringBuilder builder = new JavaStringBuilder();
     builder.append("public final class " + getGeneratedClassName() + " ").appendOpeningBrace();
-    generateCreateClassMethod(builder);
-    generateCreateFieldsMethod(builder, "createInstanceFields", not(FieldAccessFlags::isStatic));
-    generateCreateFieldsMethod(builder, "createStaticFields", FieldAccessFlags::isStatic);
+    DexProgramClass clazz = readImplementationClass();
+    generateCreateClassMethod(builder, clazz);
+    generateCreateFieldsMethod(builder, "createInstanceFields", clazz.instanceFields());
+    generateCreateFieldsMethod(builder, "createStaticFields", clazz.staticFields());
     CfCodePrinter codePrinter = new CfCodePrinter();
-    Map<MethodReference, String> createCfCodeMethodNames = generateCreateCfCodeMethods(codePrinter);
+    Map<MethodReference, String> createCfCodeMethodNames =
+        generateCreateCfCodeMethods(clazz, codePrinter);
     generateCreateMethodsMethod(
-        builder,
-        "createDirectMethods",
-        MethodAccessFlags::belongsToDirectPool,
-        createCfCodeMethodNames);
+        builder, "createDirectMethods", clazz.directMethods(), createCfCodeMethodNames);
     generateCreateMethodsMethod(
-        builder,
-        "createVirtualMethods",
-        MethodAccessFlags::belongsToVirtualPool,
-        createCfCodeMethodNames);
+        builder, "createVirtualMethods", clazz.virtualMethods(), createCfCodeMethodNames);
     codePrinter.getMethods().forEach(builder::appendLine);
     builder.appendClosingBrace();
     return builder.toString();
   }
 
-  private void generateCreateClassMethod(JavaStringBuilder builder) {
+  private DexProgramClass readImplementationClass() throws IOException {
+    InternalOptions options = new InternalOptions(factory, new Reporter());
+    options.testing.readInputStackMaps = true;
+    Box<DexProgramClass> result = new Box<>();
+    JarClassFileReader<DexProgramClass> reader =
+        new JarClassFileReader<>(new JarApplicationReader(options), result::set, ClassKind.PROGRAM);
+    reader.read(Origin.unknown(), ToolHelper.getClassAsBytes(getImplementation()));
+    return result.get();
+  }
+
+  private void generateCreateClassMethod(JavaStringBuilder builder, DexProgramClass clazz) {
     builder
         .startLine()
         .append("public static ")
@@ -93,7 +89,7 @@
     builder
         .startLine()
         .append("dexItemFactory.createType(\"")
-        .append(getGeneratedClassDescriptor())
+        .append(clazz.getType().toDescriptorString())
         .appendLine("\"),");
 
     builder.startLine().append(imports.getProgramResourceKind()).appendLine(".CF,");
@@ -104,17 +100,21 @@
         .startLine()
         .append(imports.getClassAccessFlags())
         .append(".fromCfAccessFlags(")
-        .append(getImplementation().getModifiers())
+        .append(clazz.getAccessFlags().getAsCfAccessFlags())
         .appendLine("),");
 
-    builder.startLine().appendLine("null,");
+    builder
+        .startLine()
+        .append("dexItemFactory.createType(\"")
+        .append(clazz.getSuperType().toDescriptorString())
+        .appendLine("\"),");
 
     builder.startLine().append(imports.getDexTypeList()).appendLine(".empty(),");
 
     builder
         .startLine()
         .append("dexItemFactory.createString(\"")
-        .append(getGeneratedClassName())
+        .append(clazz.getSourceFile().toString())
         .appendLine("\"),");
 
     builder.startLine().append(imports.getNestHostClassAttribute()).appendLine(".none(),");
@@ -151,7 +151,7 @@
   }
 
   private void generateCreateFieldsMethod(
-      JavaStringBuilder builder, String methodName, Predicate<FieldAccessFlags> predicate) {
+      JavaStringBuilder builder, String methodName, List<DexEncodedField> fields) {
     builder
         .startLine()
         .append("private static ")
@@ -170,73 +170,61 @@
         .append("[] ")
         .appendOpeningArrayBrace();
 
-    Iterator<Field> fieldIterator =
-        Arrays.stream(getImplementation().getDeclaredFields())
-            .filter(
-                field -> predicate.test(FieldAccessFlags.fromCfAccessFlags(field.getModifiers())))
-            .sorted(
-                (x, y) ->
-                    FieldReferenceUtils.compare(
-                        Reference.fieldFromField(x), Reference.fieldFromField(y)))
-            .iterator();
+    Iterator<DexEncodedField> fieldIterator = fields.iterator();
     while (fieldIterator.hasNext()) {
-      Field field = fieldIterator.next();
-      FieldAccessFlags flags = FieldAccessFlags.fromCfAccessFlags(field.getModifiers());
-      if (predicate.test(flags)) {
-        builder
-            .startLine()
-            .append(imports.getDexEncodedField())
-            .appendLine(".syntheticBuilder()")
-            .indent(4);
+      DexEncodedField field = fieldIterator.next();
+      assert !field.isStatic() || !field.hasExplicitStaticValue();
 
-        builder.startLine().append(".setField").appendOpeningMultiLineParenthesis();
+      builder
+          .startLine()
+          .append(imports.getDexEncodedField())
+          .appendLine(".syntheticBuilder()")
+          .indent(4);
 
-        builder
-            .startLine()
-            .append("dexItemFactory.createField")
-            .appendOpeningMultiLineParenthesis();
+      builder.startLine().append(".setField").appendOpeningMultiLineParenthesis();
 
-        builder
-            .startLine()
-            .append("dexItemFactory.createType(\"")
-            .append(descriptor(field.getDeclaringClass()))
-            .appendLine("\"),");
+      builder.startLine().append("dexItemFactory.createField").appendOpeningMultiLineParenthesis();
 
-        builder
-            .startLine()
-            .append("dexItemFactory.createType(\"")
-            .append(descriptor(field.getType()))
-            .appendLine("\"),");
+      builder
+          .startLine()
+          .append("dexItemFactory.createType(\"")
+          .append(field.getHolderType().toDescriptorString())
+          .appendLine("\"),");
 
-        builder
-            .startLine()
-            .append("dexItemFactory.createString(\"")
-            .append(field.getName())
-            .append("\")")
-            .appendClosingMultiLineParenthesis()
-            .appendClosingMultiLineParenthesis()
-            .appendLine();
+      builder
+          .startLine()
+          .append("dexItemFactory.createType(\"")
+          .append(field.getType().toDescriptorString())
+          .appendLine("\"),");
 
-        builder
-            .startLine()
-            .append(".setAccessFlags(")
-            .append(imports.getFieldAccessFlags())
-            .append(".fromCfAccessFlags(")
-            .append(field.getModifiers())
-            .appendLine("))");
+      builder
+          .startLine()
+          .append("dexItemFactory.createString(\"")
+          .append(field.getName().toString())
+          .append("\")")
+          .appendClosingMultiLineParenthesis()
+          .appendClosingMultiLineParenthesis()
+          .appendLine();
 
-        builder
-            .startLine()
-            .append(".setApiLevel(")
-            .append(imports.getComputedApiLevel())
-            .appendLine(".unknown())");
+      builder
+          .startLine()
+          .append(".setAccessFlags(")
+          .append(imports.getFieldAccessFlags())
+          .append(".fromCfAccessFlags(")
+          .append(field.getAccessFlags().getAsCfAccessFlags())
+          .appendLine("))");
 
-        builder.startLine().append(".build()").indent(-4);
-        if (fieldIterator.hasNext()) {
-          builder.appendLine(',');
-        } else {
-          builder.appendLine();
-        }
+      builder
+          .startLine()
+          .append(".setApiLevel(")
+          .append(imports.getComputedApiLevel())
+          .appendLine(".unknown())");
+
+      builder.startLine().append(".build()").indent(-4);
+      if (fieldIterator.hasNext()) {
+        builder.appendLine(',');
+      } else {
+        builder.appendLine();
       }
     }
 
@@ -247,7 +235,7 @@
   private void generateCreateMethodsMethod(
       JavaStringBuilder builder,
       String methodName,
-      Predicate<MethodAccessFlags> predicate,
+      Iterable<DexEncodedMethod> methods,
       Map<MethodReference, String> createCfCodeMethodNames) {
     builder
         .startLine()
@@ -267,23 +255,9 @@
         .append("[] ")
         .appendOpeningArrayBrace();
 
-    getImplementation().getDeclaredConstructors();
-
-    Iterator<Executable> executableIterator =
-        Streams.concat(
-                Arrays.stream(getImplementation().getDeclaredConstructors()),
-                Arrays.stream(getImplementation().getDeclaredMethods()))
-            .filter(
-                executable ->
-                    predicate.test(
-                        MethodAccessFlags.fromCfAccessFlags(executable.getModifiers(), false)))
-            .sorted(
-                (x, y) ->
-                    MethodReferenceUtils.compare(
-                        Reference.methodFromMethod(x), Reference.methodFromMethod(y)))
-            .iterator();
-    while (executableIterator.hasNext()) {
-      Executable executable = executableIterator.next();
+    Iterator<DexEncodedMethod> methodIterator = methods.iterator();
+    while (methodIterator.hasNext()) {
+      DexEncodedMethod method = methodIterator.next();
       builder
           .startLine()
           .append(imports.getDexEncodedMethod())
@@ -295,8 +269,9 @@
           .append(".setAccessFlags(")
           .append(imports.getMethodAccessFlags())
           .append(".fromCfAccessFlags(")
-          .append(executable.getModifiers())
-          .append(", false")
+          .append(method.getAccessFlags().getAsCfAccessFlags())
+          .append(", ")
+          .append(method.isInitializer())
           .appendLine("))");
 
       builder
@@ -319,74 +294,42 @@
 
       builder.startLine().append(".setMethod").appendOpeningMultiLineParenthesis();
 
-      if (executable instanceof Constructor<?>) {
-        Constructor<?> constructor = (Constructor<?>) executable;
-        builder
-            .startLine()
-            .append("dexItemFactory.createInstanceInitializer")
-            .appendOpeningMultiLineParenthesis();
+      builder.startLine().append("dexItemFactory.createMethod").appendOpeningMultiLineParenthesis();
 
+      builder
+          .startLine()
+          .append("dexItemFactory.createType(\"")
+          .append(method.getHolderType().toDescriptorString())
+          .appendLine("\"),");
+
+      builder.startLine().append("dexItemFactory.createProto").appendOpeningMultiLineParenthesis();
+
+      builder
+          .startLine()
+          .append("dexItemFactory.createType(\"")
+          .append(method.getReturnType().toDescriptorString())
+          .append("\")");
+
+      for (DexType parameter : method.getParameters()) {
         builder
+            .appendLine(",")
             .startLine()
             .append("dexItemFactory.createType(\"")
-            .append(descriptor(constructor.getDeclaringClass()))
-            .append("\")");
-
-        for (Class<?> parameter : constructor.getParameterTypes()) {
-          builder
-              .appendLine(",")
-              .startLine()
-              .append("dexItemFactory.createType(\"")
-              .append(descriptor(parameter))
-              .append("\")");
-        }
-      } else {
-        assert executable instanceof Method;
-        Method method = (Method) executable;
-
-        builder
-            .startLine()
-            .append("dexItemFactory.createMethod")
-            .appendOpeningMultiLineParenthesis();
-
-        builder
-            .startLine()
-            .append("dexItemFactory.createType(\"")
-            .append(descriptor(method.getDeclaringClass()))
-            .appendLine("\"),");
-
-        builder
-            .startLine()
-            .append("dexItemFactory.createProto")
-            .appendOpeningMultiLineParenthesis();
-
-        builder
-            .startLine()
-            .append("dexItemFactory.createType(\"")
-            .append(descriptor(method.getReturnType()))
-            .append("\")");
-
-        for (Class<?> parameter : method.getParameterTypes()) {
-          builder
-              .appendLine(",")
-              .startLine()
-              .append("dexItemFactory.createType(\"")
-              .append(descriptor(parameter))
-              .append("\")");
-        }
-        builder.appendClosingMultiLineParenthesis().appendLine(',');
-
-        builder
-            .startLine()
-            .append("dexItemFactory.createString(\"")
-            .append(method.getName())
+            .append(parameter.toDescriptorString())
             .append("\")");
       }
+      builder.appendClosingMultiLineParenthesis().appendLine(',');
+
+      builder
+          .startLine()
+          .append("dexItemFactory.createString(\"")
+          .append(method.getName().toString())
+          .append("\")");
 
       builder.appendClosingMultiLineParenthesis().appendClosingMultiLineParenthesis().appendLine();
 
       String createCfCodeMethodName =
-          createCfCodeMethodNames.get(Reference.methodFromMethod(executable));
+          createCfCodeMethodNames.get(method.getReference().asMethodReference());
       if (createCfCodeMethodName != null) {
         builder
             .startLine()
@@ -396,7 +339,7 @@
       }
 
       builder.startLine().append(".build()").indent(-4);
-      if (executableIterator.hasNext()) {
+      if (methodIterator.hasNext()) {
         builder.appendLine(',');
       } else {
         builder.appendLine();
@@ -407,29 +350,19 @@
     builder.appendClosingBrace();
   }
 
-  private Map<MethodReference, String> generateCreateCfCodeMethods(CfCodePrinter codePrinter)
-      throws IOException {
+  private Map<MethodReference, String> generateCreateCfCodeMethods(
+      DexProgramClass clazz, CfCodePrinter codePrinter) {
     Map<MethodReference, String> createCfCodeMethodNames = new HashMap<>();
-    InternalOptions options = new InternalOptions(factory, new Reporter());
-    options.testing.readInputStackMaps = true;
-    JarClassFileReader<DexProgramClass> reader =
-        new JarClassFileReader<>(
-            new JarApplicationReader(options),
-            clazz -> {
-              int index = 0;
-              for (DexEncodedMethod method : clazz.allMethodsSorted()) {
-                if (!method.hasCode()) {
-                  continue;
-                }
-                String generatedMethodName = getCreateCfCodeMethodName(method, index);
-                createCfCodeMethodNames.put(
-                    method.getReference().asMethodReference(), generatedMethodName);
-                codePrinter.visitMethod(generatedMethodName, method.getCode().asCfCode());
-                index++;
-              }
-            },
-            ClassKind.PROGRAM);
-    reader.read(Origin.unknown(), ToolHelper.getClassAsBytes(getImplementation()));
+    int index = 0;
+    for (DexEncodedMethod method : clazz.allMethodsSorted()) {
+      if (!method.hasCode()) {
+        continue;
+      }
+      String generatedMethodName = getCreateCfCodeMethodName(method, index);
+      createCfCodeMethodNames.put(method.getReference().asMethodReference(), generatedMethodName);
+      codePrinter.visitMethod(generatedMethodName, method.getCode().asCfCode());
+      index++;
+    }
     codePrinter.getImports().forEach(imports::addImport);
     return createCfCodeMethodNames;
   }
diff --git a/src/test/java/com/android/tools/r8/cfmethodgeneration/JavaStringBuilder.java b/src/test/java/com/android/tools/r8/cfmethodgeneration/JavaStringBuilder.java
index 7de114a..8d8d44a 100644
--- a/src/test/java/com/android/tools/r8/cfmethodgeneration/JavaStringBuilder.java
+++ b/src/test/java/com/android/tools/r8/cfmethodgeneration/JavaStringBuilder.java
@@ -9,6 +9,11 @@
   private final StringBuilder builder = new StringBuilder();
   private int indentation = 0;
 
+  public JavaStringBuilder append(boolean b) {
+    builder.append(b);
+    return this;
+  }
+
   public JavaStringBuilder append(char c) {
     builder.append(c);
     return this;
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingWithStartupClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingWithStartupClassesTest.java
index e77e3d9..2a325ad 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingWithStartupClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingWithStartupClassesTest.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.experimental.startup.StartupClass;
 import com.android.tools.r8.experimental.startup.StartupConfiguration;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexType;
 import com.google.common.collect.ImmutableList;
 import java.util.Collections;
 import java.util.List;
@@ -54,8 +53,8 @@
                                   startupClasses.forEach(
                                       startupClass ->
                                           builder.addStartupClass(
-                                              StartupClass.<DexType>builder()
-                                                  .setReference(
+                                              StartupClass.dexBuilder()
+                                                  .setClassReference(
                                                       toDexType(startupClass, dexItemFactory))
                                                   .build())))
                           .build());
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ExtractWrapperTypesTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ExtractWrapperTypesTest.java
index d90495c..8fc32b2 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ExtractWrapperTypesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ExtractWrapperTypesTest.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar.desugaredlibrary;
 
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK8;
 import static com.android.tools.r8.utils.DescriptorUtils.descriptorToJavaType;
 import static org.junit.Assert.assertEquals;
@@ -14,11 +16,17 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.TypeSignature;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecificationParser;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MachineDesugaredLibrarySpecification;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.WrapperDescriptor;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.MethodReference;
@@ -31,6 +39,7 @@
 import com.android.tools.r8.utils.WorkList;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -44,6 +53,7 @@
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,26 +63,25 @@
 @RunWith(Parameterized.class)
 public class ExtractWrapperTypesTest extends DesugaredLibraryTestBase {
 
-  // Filter on types that do not need to be considered for wrapping.
-  private static boolean doesNotNeedWrapper(String type, Set<String> customConversions) {
-    return excludePackage(type)
-        || NOT_NEEDED_NOT_IN_DOCS.contains(type)
-        || FINAL_CLASSES.contains(type)
-        || customConversions.contains(type);
-  }
-
-  private static boolean excludePackage(String type) {
-    return type.startsWith("java.lang.")
-        || type.startsWith("java.nio.")
-        || type.startsWith("java.security.")
-        || type.startsWith("java.net.")
-        || type.startsWith("java.awt.")
-        || type.startsWith("java.util.concurrent.");
-  }
-
   // Types not picked up by the android.jar scan but for which wrappers are needed.
   private static final Set<String> ADDITIONAL_WRAPPERS = ImmutableSet.of();
 
+  private static final Set<String> GENERIC_NOT_NEEDED =
+      ImmutableSet.of("java.util.String", "java.util.Locale$LanguageRange");
+
+  // We need wrappers for only a subset of java.nio.channels. The whole package is marked as
+  // needing wrappers and this is the exclusion set.
+  private static final Set<String> NOT_NEEDED =
+      ImmutableSet.of(
+          "java.nio.channels.AsynchronousByteChannel",
+          "java.nio.channels.AsynchronousChannelGroup",
+          "java.nio.channels.AsynchronousServerSocketChannel",
+          "java.nio.channels.AsynchronousSocketChannel",
+          "java.nio.channels.MembershipKey",
+          "java.nio.channels.MulticastChannel",
+          "java.nio.channels.NetworkChannel",
+          "java.nio.channels.spi.AsynchronousChannelProvider");
+
   // Types not in API docs, referenced in android.jar and must be wrapped.
   private static final Set<String> NEEDED_BUT_NOT_IN_DOCS = ImmutableSet.of();
 
@@ -90,18 +99,43 @@
           "java.util.Locale$FilteringMode",
           "java.util.SplittableRandom");
 
-  // List of referenced final classes (cannot be wrapper converted) with no custom conversions.
-  private static final Set<String> FINAL_CLASSES =
+  // TODO(b/238179854): Investigate how to fix these.
+  private static final Set<String> MISSING_GENERIC_TYPE_CONVERSION =
       ImmutableSet.of(
-          // TODO(b/159304624): Does this need custom conversion?
-          "java.time.Period");
+          "java.util.stream.Stream java.util.stream.Stream.flatMap(java.util.function.Function)",
+          "java.util.stream.DoubleStream"
+              + " java.util.stream.DoubleStream.flatMap(java.util.function.DoubleFunction)",
+          "java.util.stream.DoubleStream"
+              + " java.util.stream.Stream.flatMapToDouble(java.util.function.Function)",
+          "java.util.stream.IntStream"
+              + " java.util.stream.Stream.flatMapToInt(java.util.function.Function)",
+          "java.util.stream.IntStream"
+              + " java.util.stream.IntStream.flatMap(java.util.function.IntFunction)",
+          "java.util.stream.LongStream"
+              + " java.util.stream.Stream.flatMapToLong(java.util.function.Function)",
+          "java.util.stream.LongStream"
+              + " java.util.stream.LongStream.flatMap(java.util.function.LongFunction)");
+
+  private static final Set<String> MISSING_GENERIC_TYPE_CONVERSION_8 =
+      ImmutableSet.of("java.util.Set java.util.stream.Collector.characteristics()");
+
+  // TODO(b/238179854): Investigate how to fix these.
+  private static final Set<String> MISSING_GENERIC_TYPE_CONVERSION_PATH =
+      ImmutableSet.of(
+          "java.lang.Iterable java.nio.file.FileSystem.getFileStores()",
+          "java.lang.Iterable java.nio.file.FileSystem.getRootDirectories()",
+          "java.util.Iterator java.nio.file.Path.iterator()",
+          "java.nio.file.DirectoryStream"
+              + " java.nio.file.spi.FileSystemProvider.newDirectoryStream(java.nio.file.Path,"
+              + " java.nio.file.DirectoryStream$Filter)");
 
   private final LibraryDesugaringSpecification libraryDesugaringSpecification;
 
   @Parameters(name = "{0}, spec: {1}")
   public static List<Object[]> data() {
     // TODO(b/236356665): Support JDK11 desugared lib.
-    return buildParameters(getTestParameters().withNoneRuntime().build(), ImmutableList.of(JDK8));
+    return buildParameters(
+        getTestParameters().withNoneRuntime().build(), ImmutableList.of(JDK8, JDK11, JDK11_PATH));
   }
 
   public ExtractWrapperTypesTest(
@@ -112,13 +146,23 @@
 
   // TODO: parameterize to check both api<=23 as well as 23<api<26 for which the spec differs.
   private final AndroidApiLevel minApi = AndroidApiLevel.B;
-  private final AndroidApiLevel targetApi = AndroidApiLevel.Q;
+  private final AndroidApiLevel targetApi = AndroidApiLevel.S;
+
+  private Set<String> getMissingGenericTypeConversions() {
+    HashSet<String> missing = new HashSet<>(MISSING_GENERIC_TYPE_CONVERSION);
+    if (libraryDesugaringSpecification == JDK8) {
+      missing.addAll(MISSING_GENERIC_TYPE_CONVERSION_8);
+    }
+    if (libraryDesugaringSpecification == JDK11_PATH) {
+      missing.addAll(MISSING_GENERIC_TYPE_CONVERSION_PATH);
+    }
+    return missing;
+  }
 
   @Test
   public void checkConsistency() {
     List<Set<String>> sets =
-        ImmutableList.of(
-            ADDITIONAL_WRAPPERS, NEEDED_BUT_NOT_IN_DOCS, NOT_NEEDED_NOT_IN_DOCS, FINAL_CLASSES);
+        ImmutableList.of(ADDITIONAL_WRAPPERS, NEEDED_BUT_NOT_IN_DOCS, NOT_NEEDED_NOT_IN_DOCS);
     for (Set<String> set1 : sets) {
       for (Set<String> set2 : sets) {
         if (set1 != set2) {
@@ -133,6 +177,26 @@
     }
   }
 
+  // Filter on types that do not need to be considered for wrapping.
+  private boolean doesNotNeedWrapper(
+      String type, Set<String> customConversions, Set<String> maintainType) {
+    return excludePackage(type)
+        || NOT_NEEDED_NOT_IN_DOCS.contains(type)
+        || NOT_NEEDED.contains(type)
+        || customConversions.contains(type)
+        || maintainType.contains(type);
+  }
+
+  private boolean excludePackage(String type) {
+    return type.startsWith("java.lang.")
+        || type.startsWith("java.security.")
+        || type.startsWith("java.net.")
+        || type.startsWith("java.awt.")
+        || type.startsWith("java.util.concurrent.")
+        || (!libraryDesugaringSpecification.hasNioFileDesugaring(AndroidApiLevel.B)
+            && type.startsWith("java.nio."));
+  }
+
   @Test
   public void test() throws Exception {
     CodeInspector desugaredApiJar = getDesugaredApiJar();
@@ -159,17 +223,45 @@
         specification.getCustomConversions().keySet().stream()
             .map(DexType::toString)
             .collect(Collectors.toSet());
+    Set<String> maintainTypeInSet =
+        specification.getMaintainType().stream().map(DexType::toString).collect(Collectors.toSet());
     assertEquals(
         Collections.emptySet(), Sets.intersection(wrappersInSpec, customConversionsInSpec));
-    assertEquals(Collections.emptySet(), Sets.intersection(FINAL_CLASSES, customConversionsInSpec));
 
     CodeInspector nonDesugaredJar = new CodeInspector(ToolHelper.getAndroidJar(targetApi));
+    Set<DexEncodedMethod> genericDependencies = new HashSet<>();
     Map<ClassReference, Set<MethodReference>> directWrappers =
         getDirectlyReferencedWrapperTypes(
-            desugaredApiJar, preDesugarTypes, nonDesugaredJar, customConversionsInSpec);
+            desugaredApiJar,
+            preDesugarTypes,
+            nonDesugaredJar,
+            customConversionsInSpec,
+            maintainTypeInSet,
+            genericDependencies);
     Map<ClassReference, Set<ClassReference>> indirectWrappers =
         getIndirectlyReferencedWrapperTypes(
-            directWrappers, preDesugarTypes, nonDesugaredJar, customConversionsInSpec);
+            directWrappers,
+            preDesugarTypes,
+            nonDesugaredJar,
+            customConversionsInSpec,
+            maintainTypeInSet,
+            specification.getWrappers(),
+            genericDependencies);
+    {
+      Set<String> missingGenericDependency = new HashSet<>();
+      for (DexEncodedMethod genericDependency : genericDependencies) {
+        if (!specification
+            .getApiGenericConversion()
+            .containsKey(genericDependency.getReference())) {
+          missingGenericDependency.add(genericDependency.getReference().toString());
+        }
+      }
+      // TODO(b/236356665): There should be no missing conversion.
+      assertEquals(
+          "Missing generic type conversion:\n" + String.join("\n", missingGenericDependency),
+          getMissingGenericTypeConversions(),
+          missingGenericDependency);
+    }
 
     {
       Set<String> missingWrappers = getMissingWrappers(directWrappers, wrappersInSpec);
@@ -178,11 +270,17 @@
           missingWrappers.isEmpty());
     }
 
+    // java.util.stream.Collector$Characteristics is required for api generic type conversion
+    // on JDK8, but that is not supported on legacy specification used for JDK8 and on old
+    // R8 compiler versions.
+    int expectedMissingWrappers = libraryDesugaringSpecification == JDK8 ? 1 : 0;
+
     {
       Set<String> missingWrappers = getMissingWrappers(indirectWrappers, wrappersInSpec);
-      assertTrue(
+      assertEquals(
           "Missing indirect wrappers:\n" + String.join("\n", missingWrappers),
-          missingWrappers.isEmpty());
+          expectedMissingWrappers,
+          missingWrappers.size());
     }
 
     Set<String> additionalWrappers = new TreeSet<>();
@@ -200,7 +298,7 @@
 
     assertEquals(
         directWrappers.size() + indirectWrappers.size() + ADDITIONAL_WRAPPERS.size(),
-        wrappersInSpec.size());
+        wrappersInSpec.size() + expectedMissingWrappers);
   }
 
   private static <T> Set<String> getMissingWrappers(
@@ -219,7 +317,9 @@
       CodeInspector desugaredApiJar,
       Set<ClassReference> preDesugarTypes,
       CodeInspector nonDesugaredJar,
-      Set<String> customConversions) {
+      Set<String> customConversions,
+      Set<String> maintainType,
+      Set<DexEncodedMethod> genericDependencies) {
     Map<ClassReference, Set<MethodReference>> directWrappers = new HashMap<>();
     nonDesugaredJar.forAllClasses(
         clazz -> {
@@ -228,7 +328,12 @@
                 if (!method.isPublic() && !method.isProtected()) {
                   return;
                 }
-                if (desugaredApiJar.method(method.asMethodReference()).isPresent()) {
+                // We check the holder type to avoid dealing with methods on desugared types which
+                // are present in Android.jar and not in the desugared library, specifically on
+                // JDK 8 desugared library.
+                if (desugaredApiJar
+                    .clazz(method.getMethod().getHolderType().asClassReference())
+                    .isPresent()) {
                   return;
                 }
                 Consumer<ClassReference> adder =
@@ -236,41 +341,83 @@
                         directWrappers
                             .computeIfAbsent(t, k -> new HashSet<>())
                             .add(method.asMethodReference());
-                MethodSignature signature = method.getFinalSignature().asMethodSignature();
-                addType(adder, signature.type, preDesugarTypes, customConversions);
-                for (String parameter : signature.parameters) {
-                  addType(adder, parameter, preDesugarTypes, customConversions);
-                }
+                forEachType(
+                    method,
+                    t -> addType(adder, t, preDesugarTypes, customConversions, maintainType),
+                    genericDependencies);
               });
         });
     return directWrappers;
   }
 
+  private void forEachType(
+      FoundMethodSubject subject,
+      Function<String, Boolean> process,
+      Set<DexEncodedMethod> generics) {
+    MethodSignature signature = subject.getFinalSignature().asMethodSignature();
+    process.apply(signature.type);
+    for (String parameter : signature.parameters) {
+      process.apply(parameter);
+    }
+    MethodTypeSignature genericSignature = subject.getMethod().getGenericSignature();
+    if (genericSignature != null) {
+      TypeSignature[] typeSignatures = new TypeSignature[signature.parameters.length + 1];
+      for (int i = 0; i < signature.parameters.length; i++) {
+        typeSignatures[i] = genericSignature.getParameterTypeSignature(i);
+      }
+      typeSignatures[signature.parameters.length] = genericSignature.returnType().typeSignature();
+      for (TypeSignature typeSignature : typeSignatures) {
+        if ((typeSignature instanceof ClassTypeSignature)) {
+          for (FieldTypeSignature typeArgument :
+              ((ClassTypeSignature) typeSignature).typeArguments()) {
+            if (typeArgument instanceof ClassTypeSignature) {
+              String type = descriptorToJavaType(typeArgument.toString()).split("<")[0];
+              if (!GENERIC_NOT_NEEDED.contains(type)) {
+                boolean added = process.apply(type);
+                if (added) {
+                  generics.add(subject.getMethod());
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
   private Map<ClassReference, Set<ClassReference>> getIndirectlyReferencedWrapperTypes(
       Map<ClassReference, Set<MethodReference>> directWrappers,
       Set<ClassReference> existing,
       CodeInspector latest,
-      Set<String> customConversions) {
+      Set<String> customConversions,
+      Set<String> maintainType,
+      Map<DexType, WrapperDescriptor> wrapperDescriptorMap,
+      Set<DexEncodedMethod> genericDependencies) {
     Map<ClassReference, Set<ClassReference>> indirectWrappers = new HashMap<>();
     WorkList<ClassReference> worklist = WorkList.newEqualityWorkList(directWrappers.keySet());
     while (worklist.hasNext()) {
       ClassReference reference = worklist.next();
       ClassSubject clazz = latest.clazz(reference);
+      Consumer<ClassReference> adder =
+          t -> {
+            if (worklist.addIfNotSeen(t)) {
+              indirectWrappers.computeIfAbsent(t, k -> new HashSet<>()).add(reference);
+            }
+          };
       clazz.forAllVirtualMethods(
           method -> {
             assertTrue(method.toString(), method.isPublic() || method.isProtected());
-            MethodSignature signature = method.getFinalSignature().asMethodSignature();
-            Consumer<ClassReference> adder =
-                t -> {
-                  if (worklist.addIfNotSeen(t)) {
-                    indirectWrappers.computeIfAbsent(t, k -> new HashSet<>()).add(reference);
-                  }
-                };
-            addType(adder, signature.type, existing, customConversions);
-            for (String parameter : signature.parameters) {
-              addType(adder, parameter, existing, customConversions);
-            }
+            forEachType(
+                method,
+                t -> addType(adder, t, existing, customConversions, maintainType),
+                genericDependencies);
           });
+      WrapperDescriptor descriptor = wrapperDescriptorMap.get(clazz.getDexProgramClass().getType());
+      if (descriptor != null) {
+        for (DexType subwrapper : descriptor.getSubwrappers()) {
+          addType(adder, subwrapper.getTypeName(), existing, customConversions, maintainType);
+        }
+      }
     }
     return indirectWrappers;
   }
@@ -301,13 +448,14 @@
             .resolve("desugared_apis_" + targetApi.getLevel() + "_" + minApi.getLevel() + ".jar"));
   }
 
-  private void addType(
+  private boolean addType(
       Consumer<ClassReference> additions,
       String type,
       Set<ClassReference> preDesugarTypes,
-      Set<String> customConversions) {
+      Set<String> customConversions,
+      Set<String> maintainType) {
     if (type.equals("void")) {
-      return;
+      return false;
     }
     TypeReference typeReference = Reference.typeFromTypeName(type);
     if (typeReference.isArray()) {
@@ -317,10 +465,14 @@
       ClassReference clazz = typeReference.asClass();
       String clazzType = descriptorToJavaType(clazz.getDescriptor());
       if (clazzType.startsWith("java.")
-          && !doesNotNeedWrapper(clazzType, customConversions)
-          && !preDesugarTypes.contains(clazz)) {
+          && !doesNotNeedWrapper(clazzType, customConversions, maintainType)
+          // FileChannel is there since B but it needs wrapping due to recently added interfaces.
+          && (!preDesugarTypes.contains(clazz)
+              || clazzType.equals("java.nio.channels.FileChannel"))) {
         additions.accept(clazz);
+        return true;
       }
     }
+    return false;
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/LocalTimePeriodConversionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/LocalTimePeriodConversionTest.java
new file mode 100644
index 0000000..2dbc3d8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/LocalTimePeriodConversionTest.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.conversiontests;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CustomLibrarySpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.time.LocalTime;
+import java.time.Period;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class LocalTimePeriodConversionTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+  private final CompilationSpecification compilationSpecification;
+
+  private static final AndroidApiLevel MIN_SUPPORTED = AndroidApiLevel.O;
+  private static final String EXPECTED_RESULT = StringUtils.lines("00:00", "P0D");
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getConversionParametersUpToExcluding(MIN_SUPPORTED),
+        ImmutableList.of(JDK11_PATH),
+        DEFAULT_SPECIFICATIONS);
+  }
+
+  public LocalTimePeriodConversionTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+    this.compilationSpecification = compilationSpecification;
+  }
+
+  @Test
+  public void test() throws Throwable {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addProgramClasses(Executor.class)
+        .setCustomLibrarySpecification(
+            new CustomLibrarySpecification(CustomLibClass.class, MIN_SUPPORTED))
+        .addKeepMainRule(Executor.class)
+        .compile()
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  static class Executor {
+
+    public static void main(String[] args) {
+      System.out.println(CustomLibClass.get(LocalTime.MIDNIGHT));
+      System.out.println(CustomLibClass.get(Period.ZERO));
+    }
+  }
+
+  // This class will be put at compilation time as library and on the runtime class path.
+  // This class is convenient for easy testing. Each method plays the role of methods in the
+  // platform APIs for which argument/return values need conversion.
+  static class CustomLibClass {
+
+    public static LocalTime get(LocalTime localTime) {
+      return localTime;
+    }
+
+    public static Period get(Period period) {
+      return period;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/UnwrapConversionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/UnwrapConversionTest.java
index 278ec26..a61445b 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/UnwrapConversionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/UnwrapConversionTest.java
@@ -14,9 +14,12 @@
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.DoubleConsumer;
 import java.util.function.IntConsumer;
+import java.util.stream.BaseStream;
+import java.util.stream.IntStream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -30,7 +33,7 @@
   private final CompilationSpecification compilationSpecification;
 
   private static final AndroidApiLevel MIN_SUPPORTED = AndroidApiLevel.N;
-  private static final String EXPECTED_RESULT = StringUtils.lines("true", "true");
+  private static final String EXPECTED_RESULT = StringUtils.lines("true", "true", "true", "true");
 
   @Parameters(name = "{0}, spec: {1}, {2}")
   public static List<Object[]> data() {
@@ -64,6 +67,22 @@
 
     @SuppressWarnings("all")
     public static void main(String[] args) {
+      consumerTest();
+      streamTest();
+    }
+
+    private static void streamTest() {
+      // Type wrapper.
+      IntStream intStream = Arrays.stream(new int[] {1});
+      BaseStream<?, ?> unwrapped = CustomLibClass.identity(intStream);
+      System.out.println(unwrapped == intStream);
+
+      // Vivified wrapper.
+      IntStream consumer = CustomLibClass.getStream();
+      System.out.println(CustomLibClass.testStream(consumer));
+    }
+
+    private static void consumerTest() {
       // Type wrapper.
       IntConsumer intConsumer = i -> {};
       IntConsumer unwrappedIntConsumer = CustomLibClass.identity(intConsumer);
@@ -95,5 +114,21 @@
     public static boolean testConsumer(DoubleConsumer doubleConsumer) {
       return doubleConsumer == consumer;
     }
+
+    private static IntStream intStream = Arrays.stream(new int[] {0});
+
+    @SuppressWarnings("WeakerAccess")
+    public static BaseStream<Integer, IntStream> identity(IntStream arg) {
+      return arg;
+    }
+
+    public static IntStream getStream() {
+      return intStream;
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    public static boolean testStream(BaseStream<?, ?> stream) {
+      return stream == intStream;
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NameLimitsTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NameLimitsTest.java
new file mode 100644
index 0000000..44e6911
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NameLimitsTest.java
@@ -0,0 +1,92 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.jdktests;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11TestLibraryDesugaringSpecification.EXTENSION_PATH;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class Jdk11NameLimitsTest extends DesugaredLibraryTestBase {
+
+  private static final Path TEST_PATH =
+      Paths.get(ToolHelper.JDK_11_TESTS_DIR).resolve("java/nio/file/Files/NameLimits.java");
+  private static Path[] COMPILED_TEST_PATH;
+
+  private final TestParameters parameters;
+  private final CompilationSpecification compilationSpecification;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        ImmutableList.of(LibraryDesugaringSpecification.JDK11_PATH),
+        DEFAULT_SPECIFICATIONS);
+  }
+
+  public Jdk11NameLimitsTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.compilationSpecification = compilationSpecification;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+  }
+
+  @BeforeClass
+  public static void compileJdk11NioTests() throws Exception {
+    List<String> options =
+        Arrays.asList(
+            "--add-reads",
+            "java.base=ALL-UNNAMED",
+            "--patch-module",
+            "java.base=" + EXTENSION_PATH);
+    Path tmpDirectory = getStaticTemp().newFolder("cmp").toPath();
+    javac(TestRuntime.getCheckedInJdk11(), getStaticTemp())
+        .addOptions(options)
+        .addSourceFiles(TEST_PATH)
+        .setOutputPath(tmpDirectory)
+        .compile();
+    COMPILED_TEST_PATH = getAllFilesWithSuffixInDirectory(tmpDirectory, CLASS_EXTENSION);
+    assert COMPILED_TEST_PATH.length > 0;
+  }
+
+  @Test
+  public void test() throws Throwable {
+    Assume.assumeFalse(
+        "The behavior is invalid on 13 even without desugared library.",
+        parameters.isDexRuntime() && parameters.getDexRuntimeVersion().equals(Version.V13_0_0));
+    Assume.assumeFalse(
+        "Dead lock issue.",
+        parameters.isDexRuntime()
+            && libraryDesugaringSpecification.hasNioFileDesugaring(parameters)
+            && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V10_0_0));
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addProgramFiles(COMPILED_TEST_PATH)
+        .addKeepMainRule("NameLimits")
+        .run(parameters.getRuntime(), "NameLimits")
+        .assertSuccess();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
index 8168e33..8274628 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11NioFileTests.java
@@ -74,7 +74,7 @@
         // TODO(134732760): Support Dalvik VMs, currently fails because libjavacrypto is required
         // and present only in ART runtimes.
         getTestParameters()
-            .withDexRuntimesStartingFromIncluding(Version.V5_1_1)
+            .withDexRuntimesStartingFromIncluding(Version.V10_0_0)
             .withAllApiLevels()
             .build(),
         specs,
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
index b3cc9ab..70fbd07 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
@@ -267,7 +267,11 @@
   }
 
   public boolean hasNioFileDesugaring(TestParameters parameters) {
-    return parameters.getApiLevel().getLevel() < descriptor.getNioFileDesugaring();
+    return hasNioFileDesugaring(parameters.getApiLevel());
+  }
+
+  public boolean hasNioFileDesugaring(AndroidApiLevel apiLevel) {
+    return apiLevel.getLevel() < descriptor.getNioFileDesugaring();
   }
 
   public boolean hasNioChannelDesugaring(TestParameters parameters) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java
index d4ac494..4646691 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java
@@ -53,7 +53,6 @@
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
               assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              // TODO(b/129200243): Should only have two appends.
               assertEquals(3, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
             });
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java
index c28729a..cbe8fa7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +53,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): Should be String.concat.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(2, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, CodeMatchers.invokesMethodWithName("concat"));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java
index d2c95ab..8005ed5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java
@@ -7,12 +7,14 @@
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +54,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): This should just be arg.toString()
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(1, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, not(CodeMatchers.invokesMethodWithName("concat")));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java
index 9b9280f..0f7d286 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +53,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): Should be String.concat.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(1, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, CodeMatchers.invokesMethodWithName("concat"));
             });
   }
 
@@ -63,7 +64,7 @@
     public static void main(String[] args) {
       String arg = System.currentTimeMillis() > 0 ? "o" : null;
       if (arg != null) {
-        // The optimization should join "ok" into init and then append with arg.
+        // The optimization should join "ok" into init and then concat with arg.
         System.out.println(new StringBuilder("o").append("k").append(arg).toString());
       }
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatConstantTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatConstantTest.java
new file mode 100644
index 0000000..608c784
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatConstantTest.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.string;
+
+import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
+import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 StringConcatImplicitToStringConcatConstantTest extends TestBase {
+
+  private static final String EXPECTED = "oko";
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED)
+        .inspect(
+            inspect -> {
+              MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
+              assertThat(methodSubject, isPresent());
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+            });
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      String arg1 = System.currentTimeMillis() > 0 ? "o" : "";
+      System.out.println(
+          new StringBuilder(new StringBuilder(arg1).append("k").append("o")).toString());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatMultipleTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatMultipleTest.java
new file mode 100644
index 0000000..f05e84f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatMultipleTest.java
@@ -0,0 +1,75 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.string;
+
+import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
+import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 StringConcatImplicitToStringConcatMultipleTest extends TestBase {
+
+  private static final String EXPECTED = "okok";
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED)
+        .inspect(
+            inspect -> {
+              MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
+              assertThat(methodSubject, isPresent());
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+            });
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      String arg1 = System.currentTimeMillis() > 0 ? "o" : "";
+      String arg2 = System.currentTimeMillis() > 0 ? "k" : "";
+      System.out.println(
+          new StringBuilder()
+              .append(
+                  new StringBuilder()
+                      .append(new StringBuilder().append(arg1).append(arg2))
+                      .append(arg1))
+              .append(arg2)
+              .toString());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatTest.java
index a2eb814..4c9ec93 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringConcatTest.java
@@ -52,20 +52,17 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): This should be String.concat() as argument.
-              assertEquals(2, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(1, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
             });
   }
 
   public static class Main {
 
     public static void main(String[] args) {
-      String arg1 = System.currentTimeMillis() > 0 ? "o" : null;
-      String arg2 = System.currentTimeMillis() > 0 ? "k" : null;
-      if (arg1 != null && arg2 != null) {
-        System.out.println(new StringBuilder(new StringBuilder(arg1).append(arg2)).toString());
-      }
+      String arg1 = System.currentTimeMillis() > 0 ? "o" : "";
+      String arg2 = System.currentTimeMillis() > 0 ? "k" : "";
+      System.out.println(new StringBuilder(new StringBuilder(arg1).append(arg2)).toString());
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringSingleTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringSingleTest.java
index a3eb034..f42b6a5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringSingleTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatImplicitToStringSingleTest.java
@@ -52,8 +52,7 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): Use the arg string directly as input.
-              assertEquals(2, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
               assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
             });
   }
@@ -61,10 +60,8 @@
   public static class Main {
 
     public static void main(String[] args) {
-      String arg = System.currentTimeMillis() > 0 ? "o" : null;
-      if (arg != null) {
-        System.out.println(new StringBuilder(new StringBuilder(arg)).toString());
-      }
+      String arg = System.currentTimeMillis() > 0 ? "o" : "";
+      System.out.println(new StringBuilder(new StringBuilder(arg)).toString());
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java
index 4dce8fd..aca301c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +53,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): Should be concat.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(1, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, CodeMatchers.invokesMethodWithName("concat"));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java
index 690a604..4bb550e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java
@@ -7,12 +7,14 @@
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +54,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): This should just be the string as is.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
               assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, not(CodeMatchers.invokesMethodWithName("concat")));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java
index 73e61bc..33320dc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java
@@ -7,12 +7,14 @@
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,6 +56,7 @@
               assertThat(methodSubject, isPresent());
               assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
               assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, not(CodeMatchers.invokesMethodWithName("concat")));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
index 5f6030f..f2792a4 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
@@ -9,6 +9,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -42,7 +43,11 @@
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimesAndApiLevels().build(),
-        getKotlinTestParameters().withOldCompilersIfSet().withAllTargetVersions().build(),
+        getKotlinTestParameters()
+            .withOldCompilersIfSet()
+            .withOldCompiler(KotlinCompilerVersion.KOTLINC_1_3_72)
+            .withAllTargetVersions()
+            .build(),
         BooleanUtils.values());
   }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java
index f9381cb..de1e83c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataFirstToLatestTest.java
@@ -11,6 +11,7 @@
 
 import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
+import com.android.tools.r8.KotlinCompilerTool.KotlinTargetVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -44,7 +45,10 @@
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withCfRuntimes().build(),
-        getKotlinTestParameters().withOldCompilersIfSet().withAllTargetVersions().build());
+        getKotlinTestParameters()
+            .withOldCompilersIfSet()
+            .withTargetVersion(KotlinTargetVersion.JAVA_8)
+            .build());
   }
 
   public MetadataFirstToLatestTest(
@@ -86,15 +90,19 @@
             .assertAllWarningMessagesMatch(
                 equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
             .writeToZip();
-    AssertionError assertionError =
-        assertThrows(
-            AssertionError.class,
-            () -> {
-              runTest(kotlinParameters.getCompiler().getCompilerVersion(), libJar, stdLibJar);
-            });
-    assertThat(
-        assertionError.getMessage(),
-        containsString("compiled with an incompatible version of Kotlin"));
+    if (kotlinParameters.isOlderThan(KotlinCompilerVersion.KOTLINC_1_4_20)) {
+      AssertionError assertionError =
+          assertThrows(
+              AssertionError.class,
+              () -> {
+                runTest(kotlinParameters.getCompiler().getCompilerVersion(), libJar, stdLibJar);
+              });
+      assertThat(
+          assertionError.getMessage(),
+          containsString("compiled with an incompatible version of Kotlin"));
+    } else {
+      runTest(kotlinParameters.getCompiler().getCompilerVersion(), libJar, stdLibJar);
+    }
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
index 4cbb2e0..9406052 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.CoreMatchers.allOf;
@@ -100,7 +101,7 @@
             .assertAllWarningMessagesMatch(
                 equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
             .writeToZip();
-    boolean expectingCompilationError = kotlinParameters.isOlderThanMinSupported() && !keepUnit;
+    boolean expectingCompilationError = kotlinParameters.isOlderThan(KOTLINC_1_4_20) && !keepUnit;
     Path output =
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
index 5739e90..1306816 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -135,8 +136,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
     testForJvm()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
index b2e2631..a961f9e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -84,8 +85,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/anonymous_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
     testForJvm()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
index e45b633..8282926 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -94,8 +95,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/flexible_upper_bound_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
     testForJvm()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
index 2b316c9..ff9ff36 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -104,8 +105,8 @@
             .addClasspathFiles(baseLibJar, libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/classpath_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
index 472435a..eb588bb 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -147,8 +148,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/companion_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
index 19550d1..7a37728 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -109,8 +110,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(full || kotlinParameters.isOlderThanMinSupported());
-    if (full || kotlinParameters.isOlderThanMinSupported()) {
+            .compile(full || kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (full || kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
@@ -166,8 +167,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
index 3a8760a..02aa7a2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionProperty;
@@ -106,8 +107,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(full || kotlinParameters.isOlderThanMinSupported());
-    if (full || kotlinParameters.isOlderThanMinSupported()) {
+            .compile(full || kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (full || kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
@@ -172,8 +173,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
index 875b475..9ae2cb0 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -104,8 +105,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(full || kotlinParameters.isOlderThanMinSupported());
-    if (full || kotlinParameters.isOlderThanMinSupported()) {
+            .compile(full || kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (full || kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
@@ -168,8 +169,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
index fb2e952..435266f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -98,8 +99,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/vararg_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
index cbbbbb0..f5be48a 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.DescriptorUtils.descriptorToJavaType;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -93,8 +94,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/nested_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
index 557562c..86408fb 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -87,8 +88,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/parametertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
index ad4f8a2..f93a28d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_5_0;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
@@ -96,8 +97,8 @@
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_getter", "getter_user"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
@@ -196,8 +197,8 @@
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_setter", "setter_user"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
index 2a69c5a..c352c26 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -85,8 +86,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/propertytype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
index 84ee3ec..020c71d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -87,8 +88,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/returntype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
index afe617a..14fa2e1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -88,8 +89,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/supertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
@@ -138,8 +139,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/supertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
index cfc0ea8..79af7bd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isDexClass;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -138,8 +139,8 @@
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typealias_app", "main"))
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
index 880bb14..36d4580 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isDexClass;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
@@ -140,8 +141,8 @@
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typeargument_app", "main"))
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
     testForJvm()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
index b46dc08..8532cf9 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MIN_SUPPORTED_VERSION;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static junit.framework.TestCase.assertEquals;
@@ -89,8 +90,8 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
-            .compile(kotlinParameters.isOlderThanMinSupported());
-    if (kotlinParameters.isOlderThanMinSupported()) {
+            .compile(kotlinParameters.isOlderThan(KOTLINC_1_4_20));
+    if (kotlinParameters.isOlderThan(KOTLINC_1_4_20)) {
       return;
     }
     testForJvm()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
index 664db57..3766300 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
@@ -98,7 +98,7 @@
     ZipUtils.iter(
         kotlinc.getKotlinStdlibJar().toString(),
         ((entry, input) -> {
-          if (!entry.getName().endsWith(".class")) {
+          if (!entry.getName().endsWith(".class") || entry.getName().contains("module-info")) {
             return;
           }
           final byte[] bytes = StreamUtils.streamToByteArrayClose(input);
diff --git a/src/test/java/com/android/tools/r8/startup/InstrumentationServerImpl.java b/src/test/java/com/android/tools/r8/startup/InstrumentationServerImpl.java
index 9893c44..328b485 100644
--- a/src/test/java/com/android/tools/r8/startup/InstrumentationServerImpl.java
+++ b/src/test/java/com/android/tools/r8/startup/InstrumentationServerImpl.java
@@ -5,19 +5,19 @@
 package com.android.tools.r8.startup;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.nio.charset.Charset;
+import java.io.PrintWriter;
+import java.util.LinkedHashSet;
 
 public class InstrumentationServerImpl extends InstrumentationServer {
 
   private static final InstrumentationServerImpl INSTANCE = new InstrumentationServerImpl();
 
-  private final StringBuilder builder = new StringBuilder();
-
   // May be set to true by the instrumentation.
-  private final boolean writeToLogcat = false;
-  private final String logcatTag = "r8";
+  private static boolean writeToLogcat;
+  private static String logcatTag;
+
+  private final LinkedHashSet<String> lines = new LinkedHashSet<>();
 
   private InstrumentationServerImpl() {}
 
@@ -33,25 +33,32 @@
     getInstance().addLine('S' + descriptor);
   }
 
-  private synchronized void addLine(String line) {
+  private void addLine(String line) {
+    synchronized (lines) {
+      if (!lines.add(line)) {
+        return;
+      }
+    }
     if (writeToLogcat) {
       writeToLogcat(line);
-    } else {
-      builder.append(line).append('\n');
     }
   }
 
   @Override
-  public synchronized void writeToFile(File file) throws IOException {
-    FileOutputStream stream = new FileOutputStream(file);
+  public void writeToFile(File file) throws IOException {
+    PrintWriter writer = new PrintWriter(file, "UTF-8");
     try {
-      stream.write(builder.toString().getBytes(Charset.forName("UTF-8")));
+      synchronized (lines) {
+        for (String line : lines) {
+          writer.println(line);
+        }
+      }
     } finally {
-      stream.close();
+      writer.close();
     }
   }
 
   private void writeToLogcat(String line) {
-    android.util.Log.v(logcatTag, line);
+    android.util.Log.i(logcatTag, line);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/startup/MinimalStartupDexTest.java b/src/test/java/com/android/tools/r8/startup/MinimalStartupDexTest.java
index 4419055..d57a5ab 100644
--- a/src/test/java/com/android/tools/r8/startup/MinimalStartupDexTest.java
+++ b/src/test/java/com/android/tools/r8/startup/MinimalStartupDexTest.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.experimental.startup.StartupClass;
 import com.android.tools.r8.experimental.startup.StartupConfiguration;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -53,12 +52,13 @@
                     .setStartupConfiguration(
                         StartupConfiguration.builder()
                             .addStartupClass(
-                                StartupClass.<DexType>builder()
-                                    .setReference(toDexType(Main.class, options.dexItemFactory()))
+                                StartupClass.dexBuilder()
+                                    .setClassReference(
+                                        toDexType(Main.class, options.dexItemFactory()))
                                     .build())
                             .addStartupClass(
-                                StartupClass.<DexType>builder()
-                                    .setReference(
+                                StartupClass.dexBuilder()
+                                    .setClassReference(
                                         toDexType(AStartupClass.class, options.dexItemFactory()))
                                     .build())
                             .build()))
diff --git a/src/test/java/com/android/tools/r8/startup/StartupInstrumentationTest.java b/src/test/java/com/android/tools/r8/startup/StartupInstrumentationTest.java
index 6364cb0..6047b63 100644
--- a/src/test/java/com/android/tools/r8/startup/StartupInstrumentationTest.java
+++ b/src/test/java/com/android/tools/r8/startup/StartupInstrumentationTest.java
@@ -4,13 +4,24 @@
 
 package com.android.tools.r8.startup;
 
-import static org.junit.Assume.assumeTrue;
+import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.experimental.startup.StartupItem;
+import com.android.tools.r8.experimental.startup.StartupMethod;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.startup.utils.StartupTestingUtils;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -22,48 +33,73 @@
 public class StartupInstrumentationTest extends TestBase {
 
   @Parameter(0)
+  public boolean logcat;
+
+  @Parameter(1)
   public TestParameters parameters;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameters(name = "{1}, logcat: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimesAndAllApiLevels().build());
   }
 
   @Test
-  public void testD8() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
+  public void test() throws Exception {
+    Path out = temp.newFolder().toPath().resolve("out.txt").toAbsolutePath();
+    List<StartupItem<ClassReference, MethodReference, ?>> startupList = new ArrayList<>();
     testForD8(parameters.getBackend())
         .addInnerClasses(getClass())
-        .addOptionsModification(
-            options -> options.getStartupOptions().setEnableStartupInstrumentation())
+        .applyIf(
+            logcat,
+            StartupTestingUtils.enableStartupInstrumentationUsingLogcat(parameters),
+            StartupTestingUtils.enableStartupInstrumentationUsingFile(parameters))
+        .release()
         .setMinApi(parameters.getApiLevel())
         .compile()
-        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            logcat,
+            compileResult ->
+                compileResult.addRunClasspathFiles(StartupTestingUtils.getAndroidUtilLog(temp)))
+        .run(parameters.getRuntime(), Main.class, Boolean.toString(logcat), out.toString())
+        .applyIf(
+            logcat,
+            StartupTestingUtils.removeStartupListFromStdout(startupList::add),
+            runResult -> StartupTestingUtils.readStartupListFromFile(out, startupList::add))
         .assertSuccessWithOutputLines(getExpectedOutput());
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    testForR8(parameters.getBackend())
-        .addInnerClasses(getClass())
-        .addKeepClassAndMembersRules(Main.class)
-        .addOptionsModification(
-            options -> options.getStartupOptions().setEnableStartupInstrumentation())
-        .enableInliningAnnotations()
-        .setMinApi(parameters.getApiLevel())
-        .compile()
-        .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines(getExpectedOutput());
+    assertEquals(getExpectedStartupList(), startupList);
   }
 
   private static List<String> getExpectedOutput() {
-    return ImmutableList.of(descriptor(Main.class), descriptor(AStartupClass.class), "foo");
+    return ImmutableList.of("foo");
+  }
+
+  private List<StartupMethod<ClassReference, MethodReference>> getExpectedStartupList()
+      throws NoSuchMethodException {
+    return ImmutableList.of(
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(Main.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.mainMethod(Main.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(AStartupClass.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(
+                Reference.methodFromMethod(AStartupClass.class.getDeclaredMethod("foo")))
+            .build());
   }
 
   static class Main {
 
-    public static void main(String[] args) {
+    public static void main(String[] args) throws IOException {
+      boolean logcat = Boolean.parseBoolean(args[0]);
       AStartupClass.foo();
+      if (!logcat) {
+        InstrumentationServer.getInstance().writeToFile(new File(args[1]));
+      }
     }
 
     // @Keep
diff --git a/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java b/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java
index 884c1bf..5e25d36 100644
--- a/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java
+++ b/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java
@@ -13,15 +13,18 @@
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.experimental.startup.StartupClass;
+import com.android.tools.r8.experimental.startup.StartupItem;
+import com.android.tools.r8.experimental.startup.StartupMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.startup.utils.MixedSectionLayoutInspector;
 import com.android.tools.r8.startup.utils.StartupTestingUtils;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
@@ -57,15 +60,16 @@
 
   @Test
   public void test() throws Exception {
-    List<StartupClass<ClassReference>> startupList = new ArrayList<>();
+    List<StartupItem<ClassReference, MethodReference, ?>> startupList = new ArrayList<>();
     testForD8(parameters.getBackend())
         .addInnerClasses(getClass())
-        .apply(StartupTestingUtils.enableStartupInstrumentation(parameters))
+        .apply(StartupTestingUtils.enableStartupInstrumentationUsingLogcat(parameters))
+        .release()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .addRunClasspathFiles(StartupTestingUtils.getAndroidUtilLog(temp))
         .run(parameters.getRuntime(), Main.class, Boolean.toString(useLambda))
-        .apply(StartupTestingUtils.removeStartupClassesFromStdout(startupList::add))
+        .apply(StartupTestingUtils.removeStartupListFromStdout(startupList::add))
         .assertSuccessWithOutputLines(getExpectedOutput());
     assertEquals(getExpectedStartupList(), startupList);
 
@@ -92,30 +96,60 @@
     return ImmutableList.of("A", "B", "C");
   }
 
-  private List<StartupClass<ClassReference>> getExpectedStartupList() {
-    ImmutableList.Builder<StartupClass<ClassReference>> builder = ImmutableList.builder();
+  private List<StartupMethod<ClassReference, MethodReference>> getExpectedStartupList()
+      throws NoSuchMethodException {
+    ImmutableList.Builder<StartupMethod<ClassReference, MethodReference>> builder =
+        ImmutableList.builder();
     builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(Main.class))
-            .build());
-    builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(A.class))
-            .build());
-    builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(B.class))
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(Main.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.mainMethod(Main.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(A.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(Reference.methodFromMethod(A.class.getDeclaredMethod("a")))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(B.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(
+                Reference.methodFromMethod(B.class.getDeclaredMethod("b", boolean.class)))
             .build());
     if (useLambda) {
       builder.add(
-          StartupClass.<ClassReference>builder()
-              .setReference(Reference.classFromClass(B.class))
+          StartupMethod.referenceBuilder()
+              .setMethodReference(MethodReferenceUtils.classConstructor(B.class))
               .setSynthetic()
+              .build(),
+          StartupMethod.referenceBuilder()
+              .setMethodReference(MethodReferenceUtils.instanceConstructor(B.class))
+              .setSynthetic()
+              .build(),
+          StartupMethod.referenceBuilder()
+              .setMethodReference(
+                  Reference.method(
+                      Reference.classFromClass(B.class),
+                      "accept",
+                      ImmutableList.of(Reference.classFromClass(Object.class)),
+                      null))
+              .setSynthetic()
+              .build(),
+          StartupMethod.referenceBuilder()
+              .setMethodReference(
+                  Reference.methodFromMethod(B.class.getDeclaredMethod("lambda$b$0", Object.class)))
               .build());
     }
     builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(C.class))
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(C.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(Reference.methodFromMethod(C.class.getDeclaredMethod("c")))
             .build());
     return builder.build();
   }
diff --git a/src/test/java/com/android/tools/r8/startup/StartupSyntheticWithoutContextTest.java b/src/test/java/com/android/tools/r8/startup/StartupSyntheticWithoutContextTest.java
index 18ed52b..e1e3149 100644
--- a/src/test/java/com/android/tools/r8/startup/StartupSyntheticWithoutContextTest.java
+++ b/src/test/java/com/android/tools/r8/startup/StartupSyntheticWithoutContextTest.java
@@ -14,19 +14,23 @@
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.experimental.startup.StartupClass;
+import com.android.tools.r8.experimental.startup.StartupItem;
+import com.android.tools.r8.experimental.startup.StartupMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.startup.utils.MixedSectionLayoutInspector;
 import com.android.tools.r8.startup.utils.StartupTestingUtils;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,15 +57,16 @@
 
   @Test
   public void test() throws Exception {
-    List<StartupClass<ClassReference>> startupList = new ArrayList<>();
+    List<StartupItem<ClassReference, MethodReference, ?>> startupList = new ArrayList<>();
     testForD8(parameters.getBackend())
         .addInnerClasses(getClass())
-        .apply(StartupTestingUtils.enableStartupInstrumentation(parameters))
+        .apply(StartupTestingUtils.enableStartupInstrumentationUsingLogcat(parameters))
+        .release()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .addRunClasspathFiles(StartupTestingUtils.getAndroidUtilLog(temp))
         .run(parameters.getRuntime(), Main.class)
-        .apply(StartupTestingUtils.removeStartupClassesFromStdout(startupList::add))
+        .apply(StartupTestingUtils.removeStartupListFromStdout(startupList::add))
         .assertSuccessWithOutputLines(getExpectedOutput());
     assertEquals(getExpectedStartupList(), startupList);
 
@@ -88,30 +93,50 @@
     return ImmutableList.of("A", "B", "C");
   }
 
-  private List<StartupClass<ClassReference>> getExpectedStartupList() {
-    ImmutableList.Builder<StartupClass<ClassReference>> builder = ImmutableList.builder();
-    builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(Main.class))
-            .build());
-    builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(A.class))
-            .build());
-    builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(B.class))
-            .build());
-    builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(B.class))
+  private List<StartupMethod<ClassReference, MethodReference>> getExpectedStartupList()
+      throws NoSuchMethodException {
+    return ImmutableList.of(
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(Main.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.mainMethod(Main.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(A.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(Reference.methodFromMethod(A.class.getDeclaredMethod("a")))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(B.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(Reference.methodFromMethod(B.class.getDeclaredMethod("b")))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(B.class))
             .setSynthetic()
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.instanceConstructor(B.class))
+            .setSynthetic()
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(
+                Reference.method(
+                    Reference.classFromClass(B.class), "run", Collections.emptyList(), null))
+            .setSynthetic()
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(Reference.methodFromMethod(B.class.getDeclaredMethod("lambda$b$0")))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(MethodReferenceUtils.classConstructor(C.class))
+            .build(),
+        StartupMethod.referenceBuilder()
+            .setMethodReference(Reference.methodFromMethod(C.class.getDeclaredMethod("c")))
             .build());
-    builder.add(
-        StartupClass.<ClassReference>builder()
-            .setReference(Reference.classFromClass(C.class))
-            .build());
-    return builder.build();
   }
 
   private List<ClassReference> getExpectedClassDataLayout(int virtualFile) {
diff --git a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
index dad4d44..ae31fe8 100644
--- a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.startup.utils;
 
 import static com.android.tools.r8.TestBase.transformer;
+import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8TestBuilder;
@@ -13,16 +14,23 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ThrowableConsumer;
-import com.android.tools.r8.experimental.startup.StartupClass;
 import com.android.tools.r8.experimental.startup.StartupConfiguration;
+import com.android.tools.r8.experimental.startup.StartupConfigurationParser;
+import com.android.tools.r8.experimental.startup.StartupItem;
+import com.android.tools.r8.experimental.startup.StartupOptions;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ClassReferenceUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.function.Consumer;
@@ -32,20 +40,27 @@
 
   private static String startupInstrumentationTag = "startup";
 
-  public static ThrowableConsumer<D8TestBuilder> enableStartupInstrumentation(
+  public static ThrowableConsumer<D8TestBuilder> enableStartupInstrumentationUsingFile(
       TestParameters parameters) {
-    return testBuilder -> enableStartupInstrumentation(testBuilder, parameters);
+    return testBuilder -> enableStartupInstrumentation(testBuilder, parameters, false);
   }
 
-  public static void enableStartupInstrumentation(
-      D8TestBuilder testBuilder, TestParameters parameters) throws IOException {
+  public static ThrowableConsumer<D8TestBuilder> enableStartupInstrumentationUsingLogcat(
+      TestParameters parameters) {
+    return testBuilder -> enableStartupInstrumentation(testBuilder, parameters, true);
+  }
+
+  private static void enableStartupInstrumentation(
+      D8TestBuilder testBuilder, TestParameters parameters, boolean logcat) throws IOException {
     testBuilder
         .addOptionsModification(
-            options ->
-                options
-                    .getStartupOptions()
-                    .setEnableStartupInstrumentation()
-                    .setStartupInstrumentationTag(startupInstrumentationTag))
+            options -> {
+              StartupOptions startupOptions =
+                  options.getStartupOptions().setEnableStartupInstrumentation();
+              if (logcat) {
+                startupOptions.setStartupInstrumentationTag(startupInstrumentationTag);
+              }
+            })
         .addLibraryFiles(parameters.getDefaultRuntimeLibrary())
         .addLibraryClassFileData(getTransformedAndroidUtilLog());
   }
@@ -59,22 +74,37 @@
         .writeToZip();
   }
 
-  public static ThrowingConsumer<D8TestRunResult, RuntimeException> removeStartupClassesFromStdout(
-      Consumer<StartupClass<ClassReference>> startupClassConsumer) {
-    return runResult -> removeStartupClassesFromStdout(runResult, startupClassConsumer);
+  public static void readStartupListFromFile(
+      Path path, Consumer<StartupItem<ClassReference, MethodReference, ?>> startupItemConsumer)
+      throws IOException {
+    StartupConfigurationParser.createReferenceParser()
+        .parseLines(
+            Files.readAllLines(path),
+            startupItemConsumer,
+            startupItemConsumer,
+            error -> fail("Unexpected parse error: " + error));
   }
 
-  public static void removeStartupClassesFromStdout(
-      D8TestRunResult runResult, Consumer<StartupClass<ClassReference>> startupClassConsumer) {
+  public static ThrowingConsumer<D8TestRunResult, RuntimeException> removeStartupListFromStdout(
+      Consumer<StartupItem<ClassReference, MethodReference, ?>> startupItemConsumer) {
+    return runResult -> removeStartupListFromStdout(runResult, startupItemConsumer);
+  }
+
+  public static void removeStartupListFromStdout(
+      D8TestRunResult runResult,
+      Consumer<StartupItem<ClassReference, MethodReference, ?>> startupItemConsumer) {
+    StartupConfigurationParser<ClassReference, MethodReference, TypeReference> parser =
+        StartupConfigurationParser.createReferenceParser();
     StringBuilder stdoutBuilder = new StringBuilder();
     String startupDescriptorPrefix = "[" + startupInstrumentationTag + "] ";
     for (String line : StringUtils.splitLines(runResult.getStdOut(), true)) {
       if (line.startsWith(startupDescriptorPrefix)) {
-        StartupClass.Builder<ClassReference> startupClassBuilder = StartupClass.builder();
         String message = line.substring(startupDescriptorPrefix.length());
-        message = StartupConfiguration.parseSyntheticFlag(message, startupClassBuilder);
-        startupClassBuilder.setReference(Reference.classFromDescriptor(message));
-        startupClassConsumer.accept(startupClassBuilder.build());
+        parser.parseLine(
+            message,
+            startupItemConsumer,
+            startupItemConsumer,
+            error -> fail("Unexpected parse error: " + error));
       } else {
         stdoutBuilder.append(line).append(System.lineSeparator());
       }
@@ -83,29 +113,41 @@
   }
 
   public static void setStartupConfiguration(
-      R8TestBuilder<?> testBuilder, List<StartupClass<ClassReference>> startupClasses) {
+      R8TestBuilder<?> testBuilder,
+      List<StartupItem<ClassReference, MethodReference, ?>> startupItems) {
     testBuilder.addOptionsModification(
         options -> {
           DexItemFactory dexItemFactory = options.dexItemFactory();
-          options
-              .getStartupOptions()
-              .setStartupConfiguration(
-                  StartupConfiguration.builder()
-                      .apply(
-                          builder ->
-                              startupClasses.forEach(
-                                  startupClass ->
-                                      builder.addStartupClass(
-                                          StartupClass.<DexType>builder()
-                                              .setFlags(startupClass.getFlags())
-                                              .setReference(
-                                                  dexItemFactory.createType(
-                                                      startupClass.getReference().getDescriptor()))
-                                              .build())))
-                      .build());
+          StartupConfiguration startupConfiguration =
+              StartupConfiguration.builder()
+                  .apply(
+                      builder ->
+                          startupItems.forEach(
+                              startupItem ->
+                                  builder.addStartupItem(
+                                      convertStartupItemToDex(startupItem, dexItemFactory))))
+                  .build();
+          options.getStartupOptions().setStartupConfiguration(startupConfiguration);
         });
   }
 
+  private static StartupItem<DexType, DexMethod, ?> convertStartupItemToDex(
+      StartupItem<ClassReference, MethodReference, ?> startupItem, DexItemFactory dexItemFactory) {
+    return StartupItem.dexBuilder()
+        .applyIf(
+            startupItem.isStartupClass(),
+            builder ->
+                builder.setClassReference(
+                    ClassReferenceUtils.toDexType(
+                        startupItem.asStartupClass().getReference(), dexItemFactory)),
+            builder ->
+                builder.setMethodReference(
+                    MethodReferenceUtils.toDexMethod(
+                        startupItem.asStartupMethod().getReference(), dexItemFactory)))
+        .setFlags(startupItem.getFlags())
+        .build();
+  }
+
   private static byte[] getTransformedAndroidUtilLog() throws IOException {
     return transformer(Log.class).setClassDescriptor("Landroid/util/Log;").transform();
   }