Enable new stringbuilder optimizer

Bug: 190489514
Bug: 219455761
Bug: 113859361
Bug: 114002137
Bug: 222437581
Bug: 154899065
Change-Id: I5d87e7ebf64ad615cd1e57571180b6fd60efc155
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 5c8046f..fbd4b5a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2127,6 +2127,10 @@
       return appendPrimitiveMethods.contains(method);
     }
 
+    public boolean isAppendSubArrayMethod(DexMethod method) {
+      return appendSubCharArray == method || appendSubCharSequence == method;
+    }
+
     public boolean isAppendStringMethod(DexMethod method) {
       return method == appendString;
     }
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 226cb11..2a97c16 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
@@ -80,6 +80,7 @@
 import com.android.tools.r8.ir.optimize.membervaluepropagation.MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.membervaluepropagation.R8MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.outliner.Outliner;
+import com.android.tools.r8.ir.optimize.string.StringBuilderAppendOptimizer;
 import com.android.tools.r8.ir.optimize.string.StringBuilderOptimizer;
 import com.android.tools.r8.ir.optimize.string.StringOptimizer;
 import com.android.tools.r8.logging.Log;
@@ -1315,15 +1316,14 @@
     timing.begin("Rewrite move result");
     codeRewriter.rewriteMoveResult(code);
     timing.end();
-    // TODO(b/114002137): for now, string concatenation depends on rewriteMoveResult.
+    // TODO(b/114002137): Also run for CF
     if (options.enableStringConcatenationOptimization
         && !isDebugMode
         && options.isGeneratingDex()) {
       timing.begin("Rewrite string concat");
-      stringBuilderOptimizer.computeTrivialStringConcatenation(code);
+      StringBuilderAppendOptimizer.run(appView, code);
       timing.end();
     }
-
     timing.begin("Split range invokes");
     codeRewriter.splitRangeInvokeConstants(code);
     timing.end();
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 5ecb6bb..05a91e5 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
@@ -38,7 +38,11 @@
         InstructionListIterator iterator,
         Instruction instruction,
         StringBuilderOracle oracle) {
-      assert oracle.isModeledStringBuilderInstruction(instruction);
+      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.
@@ -70,7 +74,7 @@
         InstructionListIterator iterator,
         Instruction instruction,
         StringBuilderOracle oracle) {
-      assert oracle.isToString(instruction);
+      assert oracle.isToString(instruction, instruction.getFirstOperand());
       iterator.replaceCurrentInstructionWithConstString(appView, code, replacement);
     }
   }
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 caf8432..702cf76 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
@@ -229,7 +229,8 @@
             if (instruction.isAssume()) {
               return;
             }
-            if (oracle.isModeledStringBuilderInstruction(instruction)) {
+            if (oracle.isModeledStringBuilderInstruction(
+                instruction, escapeState::isLiveStringBuilder)) {
               createNodesForStringBuilderInstruction(instruction, escapeState, nodeConsumer);
             } else {
               for (Value newEscapedValue : escapeState.getNewlyEscaped()) {
@@ -291,7 +292,7 @@
                     escapeState,
                     actual -> nodeConsumer.accept(actual, appendNode),
                     escaped -> nodeConsumer.accept(escaped, createMutateNode()));
-              } else if (oracle.isToString(instruction)) {
+              } else if (oracle.isToString(instruction, receiver)) {
                 visitStringBuilderValues(
                     receiver,
                     escapeState,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
index ff4a62a..2ea5a08 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
@@ -53,16 +53,30 @@
   public TransferFunctionResult<StringBuilderEscapeState> apply(
       Instruction instruction, StringBuilderEscapeState state) {
     StringBuilderEscapeState.Builder builder = state.builder();
-    boolean isStringBuilderInstruction = oracle.isModeledStringBuilderInstruction(instruction);
+    boolean isStringBuilderInstruction =
+        oracle.isModeledStringBuilderInstruction(instruction, state::isLiveStringBuilder);
     if (!isStringBuilderInstruction && isEscapingInstructionForInValues(instruction)) {
       for (Value inValue : instruction.inValues()) {
         if (isLiveStringBuilder(builder, inValue)) {
-          // TODO(b/232377424): Account for calls to Object (such as Object.toString()).
           builder.addEscaping(inValue);
         }
       }
     }
+    if (isStringBuilderInstruction) {
+      if (instruction.isInvokeMethodWithReceiver()) {
+        Value firstOperand = instruction.getFirstOperand();
+        if (!builder.getLiveStringBuilders().contains(firstOperand)) {
+          // We can have constant NULL being the first operand, which we have not marked as
+          // a live string builder.
+          assert firstOperand.isConstZero();
+          builder.addLiveStringBuilder(firstOperand);
+        }
+      } else {
+        assert instruction.isNewInstance();
+      }
+    }
     assert !isStringBuilderInstruction
+        || instruction.isNewInstance()
         || builder.getLiveStringBuilders().contains(instruction.getFirstOperand());
     Value outValue = instruction.outValue();
     if (outValue != null) {
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 000ca52..01be10c 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
@@ -246,16 +246,12 @@
       boolean removeNode;
       boolean isEscaping = munchingState.escaping.contains(root);
       while (currentNode != null) {
-        removeNode =
-            currentNode.isSplitReferenceNode()
-                && (currentNode.getSuccessors().isEmpty() || currentNode.hasSinglePredecessor());
         // Remove appends if the string builder do not escape, is not inspected or materialized
-        if (removeNode) {
-          // TODO(b/190489514): See if this larger pattern is necessary.
-          assert false : "Check when this happens";
-        }
         // and if it is not part of a loop.
-        if (currentNode.isAppendNode() && !isEscaping) {
+        removeNode = false;
+        if (currentNode.isSplitReferenceNode()) {
+          removeNode = currentNode.getSuccessors().isEmpty() || currentNode.hasSinglePredecessor();
+        } else if (currentNode.isAppendNode() && !isEscaping) {
           AppendNode appendNode = currentNode.asAppendNode();
           boolean canRemoveIfNoInspectionOrMaterializing =
               !munchingState.inspectingCapacity.contains(root)
@@ -271,8 +267,7 @@
                 appendNode.getInstruction(), RemoveStringBuilderAction.getInstance());
             removeNode = true;
           }
-        }
-        if (currentNode.isInitNode()
+        } else if (currentNode.isInitNode()
             && currentNode.asInitNode().hasConstantArgument()
             && currentNode.hasSinglePredecessor()
             && currentNode.getSinglePredecessor().isNewInstanceNode()
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 b6c0b4f..dd49ae4 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
@@ -12,10 +12,10 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
 import java.util.List;
+import java.util.function.Predicate;
 
 /**
  * The {@link StringBuilderOracle} can answer if an instruction is of particular interest to the
@@ -23,13 +23,14 @@
  */
 interface StringBuilderOracle {
 
-  boolean isModeledStringBuilderInstruction(Instruction instruction);
+  boolean isModeledStringBuilderInstruction(
+      Instruction instruction, Predicate<Value> isLiveStringBuilder);
 
   boolean hasStringBuilderType(Value value);
 
   boolean isStringBuilderType(DexType type);
 
-  boolean isToString(Instruction instruction);
+  boolean isToString(Instruction instruction, Value value);
 
   String getConstantArgument(Instruction instruction);
 
@@ -50,13 +51,18 @@
     }
 
     @Override
-    public boolean isModeledStringBuilderInstruction(Instruction instruction) {
+    public boolean isModeledStringBuilderInstruction(
+        Instruction instruction, Predicate<Value> isLiveStringBuilder) {
       if (instruction.isNewInstance()) {
         return isStringBuilderType(instruction.asNewInstance().getType());
       } else if (instruction.isInvokeMethod()) {
         DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
-        return isStringBuildingMethod(factory.stringBuilderMethods, invokedMethod)
-            || isStringBuildingMethod(factory.stringBufferMethods, invokedMethod);
+        if (isStringBuildingMethod(factory.stringBuilderMethods, invokedMethod)
+            || isStringBuildingMethod(factory.stringBufferMethods, invokedMethod)) {
+          return true;
+        }
+        return invokedMethod == factory.objectMembers.toString
+            && isLiveStringBuilder.test(instruction.getFirstOperand());
       }
       return false;
     }
@@ -80,14 +86,18 @@
     }
 
     @Override
-    public boolean isToString(Instruction instruction) {
-      if (!instruction.isInvokeMethodWithReceiver()) {
+    public boolean isToString(Instruction instruction, Value value) {
+      if (!instruction.isInvokeVirtual()) {
         return false;
       }
-      InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
+      InvokeVirtual invoke = instruction.asInvokeVirtual();
+      if (invoke.getReceiver() != value) {
+        return false;
+      }
       DexMethod invokedMethod = invoke.getInvokedMethod();
       return factory.stringBuilderMethods.toString == invokedMethod
-          || factory.stringBufferMethods.toString == invokedMethod;
+          || factory.stringBufferMethods.toString == invokedMethod
+          || factory.objectMembers.toString == invokedMethod;
     }
 
     @Override
@@ -151,13 +161,16 @@
         return false;
       }
       DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
+      if (factory.stringBuilderMethods.isAppendSubArrayMethod(invokedMethod)
+          || factory.stringBufferMethods.isAppendSubArrayMethod(invokedMethod)) {
+        return false;
+      }
       return factory.stringBuilderMethods.isAppendMethod(invokedMethod)
           || factory.stringBufferMethods.isAppendMethod(invokedMethod);
     }
 
     @Override
     public boolean canObserveStringBuilderCall(Instruction instruction) {
-      assert isModeledStringBuilderInstruction(instruction);
       if (!instruction.isInvokeMethod()) {
         assert false : "Expecting a call to string builder";
         return true;
diff --git a/src/test/examples/shaking1/print-mapping-dex.ref b/src/test/examples/shaking1/print-mapping-dex.ref
index 5a9504a..3635115 100644
--- a/src/test/examples/shaking1/print-mapping-dex.ref
+++ b/src/test/examples/shaking1/print-mapping-dex.ref
@@ -4,5 +4,5 @@
     3:5:void <init>(java.lang.String):13:13 -> <init>
     0:6:void main(java.lang.String[]):8:8 -> main
     7:21:void main(java.lang.String[]):9:9 -> main
-    0:19:java.lang.String method():17:17 -> a
+    0:16:java.lang.String method():17:17 -> a
     java.lang.String name -> a
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index a7759e3..7b5d60b 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -131,7 +131,7 @@
         .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 4, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaring"))
         .run();
   }
 
@@ -170,7 +170,7 @@
         .withMinApiLevel(AndroidApiLevel.N)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 4, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaring"))
         .run();
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringCanonicalizationTest.java
index a8b1557..3aa8509 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringCanonicalizationTest.java
@@ -6,7 +6,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.NeverInline;
@@ -159,7 +158,7 @@
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
     // CF should not canonicalize strings or lower them. See (r8g/30163) and (r8g/30320).
-    return getTestParameters().withDexRuntimes().build();
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
   }
 
   private final TestParameters parameters;
@@ -202,24 +201,25 @@
   }
 
   @Test
-  public void testD8() throws Exception {
-    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
+  public void testR8Debug() throws Exception {
+    D8TestCompileResult result =
+        testForD8()
+            .debug()
+            .addProgramClassesAndInnerClasses(MAIN)
+            .setMinApi(parameters.getApiLevel())
+            .compile();
+    test(result, 2, 1, 1, 1, 1);
+  }
 
+  @Test
+  public void testD8Release() throws Exception {
     D8TestCompileResult result =
         testForD8()
             .release()
             .addProgramClassesAndInnerClasses(MAIN)
-            .setMinApi(parameters.getRuntime())
+            .setMinApi(parameters.getApiLevel())
             .compile();
-    test(result, 1, 1, 1, 1, 1);
-
-    result =
-        testForD8()
-            .debug()
-            .addProgramClassesAndInnerClasses(MAIN)
-            .setMinApi(parameters.getRuntime())
-            .compile();
-    test(result, 2, 1, 1, 1, 1);
+    test(result, 1, 1, 1, 1, 0);
   }
 
   @Test
@@ -230,9 +230,8 @@
             .enableProguardTestOptions()
             .enableInliningAnnotations()
             .addKeepMainRule(MessageLoader.class)
-            .setMinApi(parameters.getRuntime())
+            .setMinApi(parameters.getApiLevel())
             .compile();
-    test(result, 1, 1, 1, 1, 1);
+    test(result, 1, 1, 1, 1, 0);
   }
-
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b111893131/B111893131.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b111893131/B111893131.java
index 7ba5906..7873c68 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b111893131/B111893131.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b111893131/B111893131.java
@@ -81,6 +81,7 @@
               // To trigger outliner, set # of expected outline candidate as threshold.
               options.outline.threshold = 2;
               options.inlinerOptions().enableInlining = false;
+              options.enableStringConcatenationOptimization = false;
             });
     ProcessResult result = runOnArtRaw(app, TestClass.class);
     assertEquals(result.toString(), 0, result.exitCode);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisSmaliTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisSmaliTest.java
deleted file mode 100644
index cc582ea..0000000
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisSmaliTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.ir.optimize.string;
-
-import static com.android.tools.r8.ir.optimize.string.StringBuilderOptimizerAnalysisTest.checkBuilderState;
-import static com.android.tools.r8.ir.optimize.string.StringBuilderOptimizerAnalysisTest.checkOptimizerStates;
-import static org.junit.Assert.assertEquals;
-
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ir.analysis.AnalysisTestBase;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.string.StringBuilderOptimizer.BuilderState;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.smali.SmaliBuilder;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.StringUtils;
-import com.google.common.collect.ImmutableList;
-import java.util.Map;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class StringBuilderOptimizerAnalysisSmaliTest extends AnalysisTestBase {
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
-
-  public StringBuilderOptimizerAnalysisSmaliTest(TestParameters parameters) throws Exception {
-    super(parameters, buildApp(), "TestClass");
-  }
-
-  private static AndroidApp buildApp() throws Exception {
-    SmaliBuilder smaliBuilder = new SmaliBuilder("TestClass");
-
-    {
-      // IR of StringConcatenationTestClass#phiAtInit at DVM older than or equal to 5.1.1
-      String code =
-          StringUtils.lines(
-              "invoke-static {}, Ljava/lang/System;->currentTimeMillis()J",
-              "move-result-wide v0",
-              "const-wide/16 v2, 0",
-              "cmp-long v4, v0, v2",
-              // new-instance is hoisted, but <init> calls are still at each branch
-              "new-instance v0, Ljava/lang/StringBuilder;",
-              "if-lez v4, :cond",
-              "const-string v1, \"Hello\"",
-              "invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V",
-              "goto :merge",
-              ":cond",
-              "const-string v1, \"Hi\"",
-              "invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V",
-              "goto :merge",
-              ":merge",
-              "const-string v1, \", R8\"",
-              "invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append"
-                  + "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
-              "sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream;",
-              "invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;",
-              "move-result-object v1",
-              "invoke-virtual {v2, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
-              "return-void");
-      smaliBuilder.addStaticMethod(
-          "void", "phiAtInit_5_1_1", ImmutableList.of(), 5, code);
-    }
-
-    {
-      // Compiled StringConcatenationTestClass#phiAtInit and modified two new-instance instructions
-      // are flown to the same <init> call.
-      String code =
-          StringUtils.lines(
-              "invoke-static {}, Ljava/lang/System;->currentTimeMillis()J",
-              "move-result-wide v0",
-              "const-wide/16 v2, 0",
-              "cmp-long v4, v0, v2",
-              "if-lez v4, :cond",
-              "new-instance v0, Ljava/lang/StringBuilder;",
-              "const-string v1, \"Hello\"",
-              "goto :merge",
-              ":cond",
-              "new-instance v0, Ljava/lang/StringBuilder;",
-              "const-string v1, \"Hi\"",
-              "goto :merge",
-              ":merge",
-              // Two separate new-instance instructions are flown to the same <init>.
-              "invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V",
-              "const-string v1, \", R8\"",
-              "invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append"
-                  + "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
-              "sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream;",
-              "invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;",
-              "move-result-object v1",
-              "invoke-virtual {v2, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
-              "return-void");
-      smaliBuilder.addStaticMethod(
-          "void", "phiWithDifferentNewInstance", ImmutableList.of(), 5, code);
-    }
-
-    {
-      // IR of StringConcatenationTestClass#phiAtInit at DVM/ART newer than 5.1.1
-      String code =
-          StringUtils.lines(
-              "invoke-static {}, Ljava/lang/System;->currentTimeMillis()J",
-              "move-result-wide v0",
-              "const-wide/16 v2, 0",
-              "cmp-long v4, v0, v2",
-              // new-instance and <init> are in each branch.
-              "if-lez v4, :cond",
-              "new-instance v0, Ljava/lang/StringBuilder;",
-              "const-string v1, \"Hello\"",
-              "invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V",
-              "goto :merge",
-              ":cond",
-              "new-instance v0, Ljava/lang/StringBuilder;",
-              "const-string v1, \"Hi\"",
-              "invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V",
-              "goto :merge",
-              ":merge",
-              "const-string v1, \", R8\"",
-              "invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append"
-                  + "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
-              "sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream;",
-              "invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;",
-              "move-result-object v1",
-              "invoke-virtual {v2, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
-              "return-void");
-      smaliBuilder.addStaticMethod(
-          "void", "phiAtInit", ImmutableList.of(), 5, code);
-    }
-
-    return AndroidApp.builder()
-        .addDexProgramData(smaliBuilder.compile(), Origin.unknown())
-        .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
-        .build();
-  }
-
-  @Test
-  public void testPhiAtInit_5_1_1() {
-    buildAndCheckIR(
-        "phiAtInit_5_1_1",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, null, true);
-          }
-          assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testPhiWithDifferentNewInstance() {
-    buildAndCheckIR(
-        "phiWithDifferentNewInstance",
-        checkOptimizerStates(
-            appView,
-            optimizer -> {
-              assertEquals(0, optimizer.analysis.builderStates.size());
-              for (Value builder : optimizer.analysis.builderStates.keySet()) {
-                Map<Instruction, BuilderState> perBuilderState =
-                    optimizer.analysis.builderStates.get(builder);
-                checkBuilderState(optimizer, perBuilderState, null, false);
-              }
-              assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-            }));
-  }
-
-  @Test
-  public void testPhiAtInit() {
-    buildAndCheckIR(
-        "phiAtInit",
-        checkOptimizerStates(
-            appView,
-            optimizer -> {
-              assertEquals(0, optimizer.analysis.builderStates.size());
-              for (Value builder : optimizer.analysis.builderStates.keySet()) {
-                Map<Instruction, BuilderState> perBuilderState =
-                    optimizer.analysis.builderStates.get(builder);
-                checkBuilderState(optimizer, perBuilderState, null, false);
-              }
-              assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-            }));
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisTest.java
deleted file mode 100644
index 2a81ef0..0000000
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisTest.java
+++ /dev/null
@@ -1,401 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.ir.optimize.string;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.ir.analysis.AnalysisTestBase;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.string.StringBuilderOptimizer.BuilderState;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.InternalOptions;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Consumer;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class StringBuilderOptimizerAnalysisTest extends AnalysisTestBase {
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
-  }
-
-  public StringBuilderOptimizerAnalysisTest(TestParameters parameters) throws Exception {
-    super(
-        parameters,
-        StringConcatenationTestClass.class.getTypeName(),
-        StringConcatenationTestClass.class);
-  }
-
-  @Override
-  public void configure(InternalOptions options) {}
-
-  @Test
-  public void testUnusedBuilder() {
-    buildAndCheckIR(
-        "unusedBuilder",
-        code -> assertTrue(code.streamInstructions().allMatch(Instruction::isReturn)));
-  }
-
-  @Test
-  public void testTrivialSequence() {
-    buildAndCheckIR(
-        "trivialSequence",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "xyz", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testBuilderWithInitialValue() {
-    buildAndCheckIR(
-        "builderWithInitialValue",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "Hello,R8", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testBuilderWithCapacity() {
-    buildAndCheckIR(
-        "builderWithCapacity",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "42", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testNonStringArgs() {
-    buildAndCheckIR(
-        "nonStringArgs",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "42", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testTypeConversion() {
-    buildAndCheckIR(
-        "typeConversion",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "0.14 0 false null", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testTypeConversion_withPhis() {
-    buildAndCheckIR(
-        "typeConversion_withPhis",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, null, true);
-          }
-          assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Ignore("TODO(b/113859361): passed to another builder should be an eligible case.")
-  @Test
-  public void testNestedBuilders_appendBuilderItself() {
-    buildAndCheckIR(
-        "nestedBuilders_appendBuilderItself",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "Hello,R8", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-          // TODO(b/113859361): check # of merged builders.
-          // assertEquals(2, optimizer.analysis.mergedBuilders.size());
-        }));
-  }
-
-  @Ignore("TODO(b/113859361): merge builder.")
-  @Test
-  public void testNestedBuilders_appendBuilderResult() {
-    buildAndCheckIR(
-        "nestedBuilders_appendBuilderResult",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "Hello,R8", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-          // TODO(b/113859361): check # of merged builders.
-          // assertEquals(2, optimizer.analysis.mergedBuilders.size());
-        }));
-  }
-
-  @Ignore("TODO(b/113859361): merge builder.")
-  @Test
-  public void testNestedBuilders_conditional() {
-    buildAndCheckIR(
-        "nestedBuilders_conditional",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, null, true);
-          }
-          assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-          // TODO(b/113859361): check # of merged builders.
-          // assertEquals(3, optimizer.analysis.mergedBuilders.size());
-        }));
-  }
-
-  @Ignore("TODO(b/113859361): merge builder.")
-  @Test
-  public void testConcatenatedBuilders_init() {
-    buildAndCheckIR(
-        "concatenatedBuilders_init",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "Hello,R8", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-          // TODO(b/113859361): check # of merged builders.
-          // assertEquals(2, optimizer.analysis.mergedBuilders.size());
-        }));
-  }
-
-  @Ignore("TODO(b/113859361): merge builder.")
-  @Test
-  public void testConcatenatedBuilders_append() {
-    buildAndCheckIR(
-        "concatenatedBuilders_append",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, "Hello,R8", true);
-          }
-          assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-          // TODO(b/113859361): check # of merged builders.
-          // assertEquals(2, optimizer.analysis.mergedBuilders.size());
-        }));
-  }
-
-  @Ignore("TODO(b/113859361): merge builder.")
-  @Test
-  public void testConcatenatedBuilders_conditional() {
-    final Set<String> expectedConstStrings = new HashSet<>();
-    expectedConstStrings.add("Hello,R8");
-    expectedConstStrings.add("D8");
-    buildAndCheckIR(
-        "concatenatedBuilders_conditional",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(2, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderStates(optimizer, perBuilderState, expectedConstStrings, true);
-          }
-          assertEquals(2, optimizer.analysis.simplifiedBuilders.size());
-          // TODO(b/113859361): check # of merged builders.
-          // assertEquals(2, optimizer.analysis.mergedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testSimplePhi() {
-    buildAndCheckIR(
-        "simplePhi",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(0, optimizer.analysis.builderStates.size());
-          assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testPhiAtInit() {
-    int expectedNumOfNewBuilder = 0;
-    boolean expectToMeetToString = false;
-    if (parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.M)) {
-      expectedNumOfNewBuilder = 1;
-      expectToMeetToString = true;
-    }
-    final int finalExpectedNumOfNewBuilder = expectedNumOfNewBuilder;
-    final boolean finalExpectToMeetToString = expectToMeetToString;
-    buildAndCheckIR(
-        "phiAtInit",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(finalExpectedNumOfNewBuilder, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, null, finalExpectToMeetToString);
-          }
-          assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testPhiWithDifferentInits() {
-    buildAndCheckIR(
-        "phiWithDifferentInits",
-        checkOptimizerStates(
-            appView,
-            optimizer -> {
-              assertEquals(0, optimizer.analysis.builderStates.size());
-              for (Value builder : optimizer.analysis.builderStates.keySet()) {
-                Map<Instruction, BuilderState> perBuilderState =
-                    optimizer.analysis.builderStates.get(builder);
-                checkBuilderState(optimizer, perBuilderState, null, false);
-              }
-              assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-            }));
-  }
-
-  @Test
-  public void testConditionalPhiWithoutAppend() {
-    buildAndCheckIR(
-        "conditionalPhiWithoutAppend",
-        checkOptimizerStates(
-            appView,
-            optimizer -> {
-              assertEquals(1, optimizer.analysis.builderStates.size());
-              for (Value builder : optimizer.analysis.builderStates.keySet()) {
-                Map<Instruction, BuilderState> perBuilderState =
-                    optimizer.analysis.builderStates.get(builder);
-                checkBuilderState(optimizer, perBuilderState, "initial:suffix", true);
-              }
-              assertEquals(1, optimizer.analysis.simplifiedBuilders.size());
-            }));
-  }
-
-  @Test
-  public void testLoop() {
-    buildAndCheckIR(
-        "loop",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(2, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, null, true);
-          }
-          assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  @Test
-  public void testLoopWithBuilder() {
-    buildAndCheckIR(
-        "loopWithBuilder",
-        checkOptimizerStates(appView, optimizer -> {
-          assertEquals(1, optimizer.analysis.builderStates.size());
-          for (Value builder : optimizer.analysis.builderStates.keySet()) {
-            Map<Instruction, BuilderState> perBuilderState =
-                optimizer.analysis.builderStates.get(builder);
-            checkBuilderState(optimizer, perBuilderState, null, true);
-          }
-          assertEquals(0, optimizer.analysis.simplifiedBuilders.size());
-        }));
-  }
-
-  static Consumer<IRCode> checkOptimizerStates(
-      AppView<?> appView, Consumer<StringBuilderOptimizer> optimizerConsumer) {
-    return code -> {
-      StringBuilderOptimizer optimizer = new StringBuilderOptimizer(appView);
-      optimizer.computeTrivialStringConcatenation(code);
-      optimizerConsumer.accept(optimizer);
-    };
-  }
-
-  static void checkBuilderState(
-      StringBuilderOptimizer optimizer,
-      Map<Instruction, BuilderState> perBuilderState,
-      String expectedConstString,
-      boolean expectToSeeMethodToString) {
-    boolean seenMethodToString = false;
-    for (Map.Entry<Instruction, BuilderState> entry : perBuilderState.entrySet()) {
-      InvokeMethod invoke = entry.getKey().asInvokeMethod();
-      if (invoke != null
-          && optimizer.optimizationConfiguration.isToStringMethod(invoke.getInvokedMethod())) {
-        seenMethodToString = true;
-        Value builder = invoke.getArgument(0).getAliasedValue();
-        assertEquals(
-            expectedConstString, optimizer.analysis.toCompileTimeString(builder, entry.getValue()));
-      }
-    }
-    assertEquals(expectToSeeMethodToString, seenMethodToString);
-  }
-
-  static void checkBuilderStates(
-      StringBuilderOptimizer optimizer,
-      Map<Instruction, BuilderState> perBuilderState,
-      Set<String> expectedConstStrings,
-      boolean expectToSeeMethodToString) {
-    boolean seenMethodToString = false;
-    for (Map.Entry<Instruction, BuilderState> entry : perBuilderState.entrySet()) {
-      InvokeMethod invoke = entry.getKey().asInvokeMethod();
-      if (invoke != null
-          && optimizer.optimizationConfiguration.isToStringMethod(invoke.getInvokedMethod())) {
-        seenMethodToString = true;
-        Value builder = invoke.getArgument(0).getAliasedValue();
-        String computedString = optimizer.analysis.toCompileTimeString(builder, entry.getValue());
-        assertNotNull(computedString);
-        assertTrue(expectedConstStrings.contains(computedString));
-        expectedConstStrings.remove(computedString);
-      }
-    }
-    assertEquals(expectToSeeMethodToString, seenMethodToString);
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
index f908b95..365ced3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
@@ -37,7 +37,7 @@
   @Parameters(name = "{0}, configuration: {1}")
   public static List<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), getTestExpectations());
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(), getTestExpectations());
   }
 
   private static class StringBuilderResult {
@@ -94,14 +94,14 @@
         StringBuilderResult.create(
             Main.class.getMethod("materializingWithAdditionalAppend"),
             StringUtils.lines("Hello World", "Hello WorldObservable"),
-            1,
-            3,
-            2),
+            0,
+            0,
+            0),
         StringBuilderResult.create(
             Main.class.getMethod("appendWithNonConstant"),
             StringUtils.lines("Hello World, Hello World"),
             1,
-            3,
+            2,
             1),
         StringBuilderResult.create(
             Main.class.getMethod("simpleLoopTest"),
@@ -109,13 +109,12 @@
             1,
             1,
             1),
-        // TODO(b/222437581): Should not remove StringBuilder
         StringBuilderResult.create(
             Main.class.getMethod("simpleLoopTest2"),
             StringUtils.lines("Hello World", "Hello WorldHello World"),
-            0,
-            0,
-            0),
+            1,
+            1,
+            1),
         StringBuilderResult.create(
             Main.class.getMethod("simpleLoopWithStringBuilderInBodyTest"),
             StringUtils.lines("Hello World"),
@@ -129,27 +128,27 @@
             0,
             0),
         StringBuilderResult.create(
-            Main.class.getMethod("diamondWithUseTest"), StringUtils.lines("Hello World"), 1, 3, 1),
+            Main.class.getMethod("diamondWithUseTest"), StringUtils.lines("Hello World"), 1, 2, 1),
         StringBuilderResult.create(
             Main.class.getMethod("diamondsWithSingleUseTest"),
             StringUtils.lines("Hello World"),
             1,
-            3,
+            2,
             1),
         StringBuilderResult.create(
             Main.class.getMethod("escapeTest"), StringUtils.lines("Hello World"), 2, 2, 1),
         StringBuilderResult.create(
             Main.class.getMethod("intoPhiTest"), StringUtils.lines("Hello World"), 2, 2, 1),
         StringBuilderResult.create(
-            Main.class.getMethod("optimizePartial"), StringUtils.lines("Hello World.."), 1, 4, 1),
+            Main.class.getMethod("optimizePartial"), StringUtils.lines("Hello World.."), 1, 2, 1),
         StringBuilderResult.create(
             Main.class.getMethod("multipleToStrings"),
             StringUtils.lines("Hello World", "Hello World.."),
-            1,
-            4,
-            2),
+            0,
+            0,
+            0),
         StringBuilderResult.create(
-            Main.class.getMethod("changeAppendType"), StringUtils.lines("1 World"), 1, 3, 1),
+            Main.class.getMethod("changeAppendType"), StringUtils.lines("1 World"), 1, 2, 1),
         StringBuilderResult.create(
             Main.class.getMethod("checkCapacity"), StringUtils.lines("true"), 2, 1, 0),
         StringBuilderResult.create(
@@ -157,29 +156,29 @@
         StringBuilderResult.create(
             Main.class.getMethod("stringBuilderWithStringBuilderToString"),
             StringUtils.lines("Hello World"),
-            1,
-            1,
-            1),
+            0,
+            0,
+            0),
         StringBuilderResult.create(
             Main.class.getMethod("stringBuilderWithStringBuilder"),
             StringUtils.lines("Hello World"),
-            2,
-            2,
-            1),
+            0,
+            0,
+            0),
         StringBuilderResult.create(
             Main.class.getMethod("stringBuilderInStringBuilderConstructor"),
             StringUtils.lines("Hello World"),
-            2,
-            1,
-            1),
+            0,
+            0,
+            0),
         StringBuilderResult.create(
             Main.class.getMethod("interDependencyTest"),
             StringUtils.lines("World Hello World "),
-            2,
-            2,
-            1),
+            0,
+            0,
+            0),
         StringBuilderResult.create(
-            Main.class.getMethod("stringBuilderSelfReference"), StringUtils.lines(""), 1, 1, 1),
+            Main.class.getMethod("stringBuilderSelfReference"), StringUtils.lines(""), 0, 0, 0),
         StringBuilderResult.create(
             Main.class.getMethod("unknownStringBuilderInstruction"),
             StringUtils.lines("Hello World"),
@@ -214,8 +213,6 @@
 
   @Test
   public void testR8() throws Exception {
-    boolean hasError =
-        stringBuilderTest.getMethodName().equals("simpleLoopTest2") && parameters.isDexRuntime();
     compilationResults
         .apply(parameters)
         .inspect(
@@ -235,9 +232,7 @@
                   stringBuilderTest.toStrings, countStringBuilderToStrings(foundMethodSubject));
             })
         .run(parameters.getRuntime(), Main.class, stringBuilderTest.getMethodName())
-        // TODO(b/222437581): Incorrect result for string builder inside loop.
-        .assertSuccessWithOutputLinesIf(hasError, "Hello World", "Hello World")
-        .assertSuccessWithOutputIf(!hasError, stringBuilderTest.expected);
+        .assertSuccessWithOutput(stringBuilderTest.expected);
   }
 
   private long countStringBuilderInits(FoundMethodSubject method) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java
index bd6fffd..88f0cf8 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectMutationThroughPhiTest.java
@@ -41,10 +41,8 @@
             .inspect(
                 inspector -> {
                   MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
-                  // TODO(b/114002137): This should be optimized into having only 3 append calls,
-                  //  since append("baz").append("qux") can be optimized into append("bazqux").
                   assertEquals(
-                      4,
+                      parameters.isCfRuntime() ? 4 : 3,
                       mainMethodSubject
                           .streamInstructions()
                           .filter(isInvokeStringBuilderAppendWithString())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java
index b553e2d..81fa83b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIndirectPhiMutationThroughPhiOperandTest.java
@@ -41,10 +41,9 @@
             .inspect(
                 inspector -> {
                   MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
-                  // TODO(b/114002137): This should be optimized into having only 3 append calls,
-                  //  since append("baz").append("qux") can be optimized into append("bazqux").
+                  // TODO(b/114002137): Also run for CF
                   assertEquals(
-                      4,
+                      parameters.isCfRuntime() ? 4 : 3,
                       mainMethodSubject
                           .streamInstructions()
                           .filter(isInvokeStringBuilderAppendWithString())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
index ef8217a..617fd63 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
@@ -9,7 +9,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 
-import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
@@ -113,27 +112,27 @@
 
     method = mainClass.uniqueMethodWithName("trivialSequence");
     assertThat(method, isPresent());
-    expectedCount = isR8 ? 1 : 3;
+    expectedCount = isReleaseMode ? 1 : 3;
     assertEquals(expectedCount, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("builderWithInitialValue");
     assertThat(method, isPresent());
-    expectedCount = isR8 ? 1 : 3;
+    expectedCount = isReleaseMode ? 1 : 3;
     assertEquals(expectedCount, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("builderWithCapacity");
     assertThat(method, isPresent());
-    expectedCount = isR8 ? 1 : 0;
+    expectedCount = isReleaseMode ? 1 : 0;
     assertEquals(expectedCount, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("nonStringArgs");
     assertThat(method, isPresent());
-    expectedCount = isR8 ? 1 : 0;
+    expectedCount = isReleaseMode ? 1 : 0;
     assertEquals(expectedCount, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("typeConversion");
     assertThat(method, isPresent());
-    expectedCount = isR8 ? 1 : 0;
+    expectedCount = isReleaseMode ? 1 : 0;
     assertEquals(expectedCount, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("typeConversion_withPhis");
@@ -142,41 +141,31 @@
 
     method = mainClass.uniqueMethodWithName("nestedBuilders_appendBuilderItself");
     assertThat(method, isPresent());
-    // TODO(b/113859361): merge builders
-    expectedCount = 3;
-    assertEquals(expectedCount, countConstString(method));
+    assertEquals(isReleaseMode ? 1 : 3, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("nestedBuilders_appendBuilderResult");
     assertThat(method, isPresent());
-    // TODO(b/113859361): merge builders
-    expectedCount = 3;
-    assertEquals(expectedCount, countConstString(method));
+    assertEquals(isReleaseMode ? 1 : 3, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("nestedBuilders_conditional");
     assertThat(method, isPresent());
-    assertEquals(4, countConstString(method));
+    assertEquals(isReleaseMode ? 3 : 4, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("concatenatedBuilders_init");
     assertThat(method, isPresent());
-    // TODO(b/113859361): merge builders
-    expectedCount = 2;
-    assertEquals(expectedCount, countConstString(method));
+    assertEquals(isReleaseMode ? 1 : 2, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("concatenatedBuilders_append");
     assertThat(method, isPresent());
-    // TODO(b/113859361): merge builders
-    expectedCount = 2;
-    assertEquals(expectedCount, countConstString(method));
+    assertEquals(isReleaseMode ? 1 : 2, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("concatenatedBuilders_conditional");
     assertThat(method, isPresent());
-    // TODO(b/113859361): merge builders
-    expectedCount = 4;
-    assertEquals(expectedCount, countConstString(method));
+    assertEquals(isReleaseMode ? 2 : 4, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("simplePhi");
     assertThat(method, isPresent());
-    assertEquals(4, countConstString(method));
+    assertEquals(isReleaseMode ? 3 : 4, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("phiAtInit");
     assertThat(method, isPresent());
@@ -188,7 +177,7 @@
 
     method = mainClass.uniqueMethodWithName("conditionalPhiWithoutAppend");
     assertThat(method, isPresent());
-    assertEquals(3, countConstString(method));
+    assertEquals(isReleaseMode ? 2 : 3, countConstString(method));
 
     method = mainClass.uniqueMethodWithName("loop");
     assertThat(method, isPresent());
@@ -204,26 +193,31 @@
   }
 
   @Test
-  public void testD8() throws Exception {
+  public void testD8Debug() throws Exception {
     assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
-
-    D8TestRunResult result =
+    test(
         testForD8()
             .debug()
             .addProgramClasses(MAIN)
             .setMinApi(parameters.getApiLevel())
             .run(parameters.getRuntime(), MAIN)
-            .assertSuccessWithOutput(JAVA_OUTPUT);
-    test(result, false, false);
+            .assertSuccessWithOutput(JAVA_OUTPUT),
+        false,
+        false);
+  }
 
-    result =
+  @Test
+  public void testD8Release() throws Exception {
+    assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
+    test(
         testForD8()
             .release()
             .addProgramClasses(MAIN)
             .setMinApi(parameters.getApiLevel())
             .run(parameters.getRuntime(), MAIN)
-            .assertSuccessWithOutput(JAVA_OUTPUT);
-    test(result, false, true);
+            .assertSuccessWithOutput(JAVA_OUTPUT),
+        false,
+        true);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/stringplus/StringPlusTest.java b/src/test/java/com/android/tools/r8/kotlin/stringplus/StringPlusTest.java
index 170595a..37b20bd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/stringplus/StringPlusTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/stringplus/StringPlusTest.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.KotlinCompilerTool;
@@ -100,11 +101,18 @@
                 // TODO(b/190489514): We should be able to optimize constant stringPlus calls.
                 assertThat(methodSubject, CodeMatchers.invokesMethodWithName("stringPlus"));
               }
-              // TODO(b/219455761): StringBuilderOptimizer fails to remove constant input.
-              assertThat(
-                  methodSubject,
-                  CodeMatchers.invokesMethodWithHolderAndName(
-                      typeName(StringBuilder.class), "append"));
+              // We cannot remove the <init> -> <append> call since that changes the capacity
+              // and the string builder is escaping into System.out.
+              assertEquals(
+                  1,
+                  methodSubject
+                      .streamInstructions()
+                      .filter(
+                          instructionSubject ->
+                              CodeMatchers.isInvokeWithTarget(
+                                      typeName(StringBuilder.class), "append")
+                                  .test(instructionSubject))
+                      .count());
             })
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
         .run(parameters.getRuntime(), MAIN)
diff --git a/src/test/java/com/android/tools/r8/smali/JumboStringTest.java b/src/test/java/com/android/tools/r8/smali/JumboStringTest.java
index 20fda04..e7b5fe6 100644
--- a/src/test/java/com/android/tools/r8/smali/JumboStringTest.java
+++ b/src/test/java/com/android/tools/r8/smali/JumboStringTest.java
@@ -20,11 +20,19 @@
 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 JumboStringTest extends SmaliTestBase {
   private static Pair<StringBuilder, StringBuilder> builders;
-  private final TestParameters parameters;
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withOnlyDexRuntimeApiLevel().build();
+  }
 
   @BeforeClass
   public static void createBuilders() {
@@ -49,15 +57,6 @@
     builders = new Pair<>(builder, expectedBuilder);
   }
 
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().build();
-  }
-
-  public JumboStringTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
   @Test
   public void test() throws Exception {
     SmaliBuilder smaliBuilder = new SmaliBuilder(DEFAULT_CLASS_NAME);
@@ -82,16 +81,21 @@
     testForR8(parameters.getBackend())
         .addProgramDexFileData(smaliBuilder.compile())
         .addKeepMainRule(DEFAULT_CLASS_NAME)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            options -> {
+              options.enableStringConcatenationOptimization = false;
+            })
         .run(parameters.getRuntime(), DEFAULT_CLASS_NAME)
         .assertSuccessWithOutput(builders.getSecond().toString())
-        .inspect(inspector -> {
-          ClassSubject main = inspector.clazz(DEFAULT_CLASS_NAME);
-          assertThat(main, isPresent());
-          MethodSubject method = main.uniqueMethodWithName(DEFAULT_METHOD_NAME);
-          assertThat(method, isPresent());
-          assertTrue(method.streamInstructions().anyMatch(InstructionSubject::isJumboString));
-        });
+        .inspect(
+            inspector -> {
+              ClassSubject main = inspector.clazz(DEFAULT_CLASS_NAME);
+              assertThat(main, isPresent());
+              MethodSubject method = main.uniqueMethodWithName(DEFAULT_METHOD_NAME);
+              assertThat(method, isPresent());
+              assertTrue(method.streamInstructions().anyMatch(InstructionSubject::isJumboString));
+            });
   }
 
   @Test
@@ -128,18 +132,23 @@
         .addProgramDexFileData(smaliBuilder.compile())
         .addKeepMainRule(DEFAULT_CLASS_NAME)
         .addKeepRules("-addconfigurationdebugging")
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            options -> {
+              options.enableStringConcatenationOptimization = false;
+            })
         .run(parameters.getRuntime(), DEFAULT_CLASS_NAME)
         .assertSuccessWithOutput(builders.getSecond().toString())
-        .inspect(inspector -> {
-          ClassSubject main = inspector.clazz(DEFAULT_CLASS_NAME);
-          assertThat(main, isPresent());
-          MethodSubject method = main.uniqueMethodWithName(DEFAULT_METHOD_NAME);
-          assertThat(method, isPresent());
-          assertTrue(method.streamInstructions().anyMatch(InstructionSubject::isJumboString));
-          MethodSubject method2 = main.uniqueMethodWithName(DEFAULT_METHOD_NAME + "2");
-          assertThat(method2, isPresent());
-          assertTrue(method2.streamInstructions().anyMatch(InstructionSubject::isJumboString));
-        });
+        .inspect(
+            inspector -> {
+              ClassSubject main = inspector.clazz(DEFAULT_CLASS_NAME);
+              assertThat(main, isPresent());
+              MethodSubject method = main.uniqueMethodWithName(DEFAULT_METHOD_NAME);
+              assertThat(method, isPresent());
+              assertTrue(method.streamInstructions().anyMatch(InstructionSubject::isJumboString));
+              MethodSubject method2 = main.uniqueMethodWithName(DEFAULT_METHOD_NAME + "2");
+              assertThat(method2, isPresent());
+              assertTrue(method2.streamInstructions().anyMatch(InstructionSubject::isJumboString));
+            });
   }
 }