Merge "Remove trivial phis for all string new-instance values."
diff --git a/build.gradle b/build.gradle
index f379bbf..8ea2b3f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1239,6 +1239,18 @@
 }
 
 test {
+    if (project.hasProperty('generate_golden_files_to')) {
+        systemProperty 'generate_golden_files_to', project.property('generate_golden_files_to')
+        assert project.hasProperty('HEAD_sha1')
+        systemProperty 'test_git_HEAD_sha1', project.property('HEAD_sha1')
+    }
+
+    if (project.hasProperty('use_golden_files_in')) {
+        systemProperty 'use_golden_files_in', project.property('use_golden_files_in')
+        assert project.hasProperty('HEAD_sha1')
+        systemProperty 'test_git_HEAD_sha1', project.property('HEAD_sha1')
+    }
+
     dependsOn getJarsFromSupportLibs
     testLogging.exceptionFormat = 'full'
     if (project.hasProperty('print_test_stdout')) {
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index 4f64416..9b1599f 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -161,6 +161,7 @@
     InternalOptions getInternalOptions() {
       InternalOptions internal = new InternalOptions();
       internal.useSmaliSyntax = useSmali;
+      internal.enableCfFrontend = true;
       return internal;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index f907ffb..66664e6 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -59,6 +59,8 @@
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.DescriptorUtils;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
 import it.unimi.dsi.fastutil.ints.IntList;
@@ -80,15 +82,18 @@
   private final Map<CfLabel, String> labels;
 
   private final StringBuilder builder = new StringBuilder();
+  private final ClassNameMapper mapper;
 
   /** Entry for printing single instructions without global knowledge (eg, label numbers). */
   public CfPrinter() {
     indent = "";
     labels = null;
+    mapper = null;
   }
 
   /** Entry for printing a complete code object. */
-  public CfPrinter(CfCode code) {
+  public CfPrinter(CfCode code, ClassNameMapper mapper) {
+    this.mapper = mapper;
     indent = "  ";
     labels = new HashMap<>();
     int nextLabelNumber = 0;
@@ -284,12 +289,13 @@
   }
 
   public void print(CfFrame frame) {
-    StringBuilder builder = new StringBuilder("frame: [");
+    indent();
+    builder.append("; frame: [");
     {
       String separator = "";
       for (Entry<FrameType> entry : frame.getLocals().int2ReferenceEntrySet()) {
         builder.append(separator).append(entry.getIntKey()).append(':');
-        print(entry.getValue(), builder);
+        print(entry.getValue());
         separator = ", ";
       }
     }
@@ -298,17 +304,18 @@
       String separator = "";
       for (FrameType element : frame.getStack()) {
         builder.append(separator);
-        print(element, builder);
+        print(element);
         separator = ", ";
       }
     }
     builder.append(']');
-    comment(builder.toString());
   }
 
-  private void print(FrameType type, StringBuilder builder) {
+  private void print(FrameType type) {
     if (type.isUninitializedNew()) {
       builder.append("uninitialized ").append(getLabel(type.getUninitializedLabel()));
+    } else if (type.isInitialized()) {
+      appendType(type.getInitializedType());
     } else {
       builder.append(type.toString());
     }
@@ -566,19 +573,50 @@
   }
 
   private void appendDescriptor(DexType type) {
+    if (mapper != null) {
+      builder.append(DescriptorUtils.javaTypeToDescriptor(mapper.originalNameOf(type)));
+      return;
+    }
     builder.append(type.toDescriptorString());
   }
 
+  private void appendType(DexType type) {
+    if (type.isArrayType() || type.isClassType()) {
+      appendClass(type);
+    } else {
+      builder.append(type);
+    }
+  }
+
   private void appendClass(DexType type) {
-    builder.append(type.getInternalName());
+    assert type.isArrayType() || type.isClassType();
+    if (mapper == null) {
+      builder.append(type.getInternalName());
+    } else if (type == DexItemFactory.nullValueType) {
+      builder.append("NULL");
+    } else {
+      builder.append(
+          DescriptorUtils.descriptorToInternalName(
+              DescriptorUtils.javaTypeToDescriptor(mapper.originalNameOf(type))));
+    }
   }
 
   private void appendField(DexField field) {
+    if (mapper != null) {
+      builder.append(mapper.originalSignatureOf(field).toString());
+      return;
+    }
     appendClass(field.getHolder());
     builder.append('/').append(field.name);
   }
 
   private void appendMethod(DexMethod method) {
+    if (mapper != null) {
+      MethodSignature signature = (MethodSignature) mapper.originalSignatureOf(method);
+      builder.append(mapper.originalNameOf(method.holder)).append('.');
+      builder.append(signature.name).append(signature.toDescriptor());
+      return;
+    }
     builder.append(method.qualifiedName());
     builder.append(method.proto.toDescriptorString());
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
index 63a2958..89abadd 100644
--- a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
@@ -79,7 +79,7 @@
   void writeMethod(DexEncodedMethod method, PrintStream ps) {
     ClassNameMapper naming = application.getProguardMap();
     String methodName = naming != null
-        ? naming.originalSignatureOf(method.method).toString()
+        ? naming.originalSignatureOf(method.method).name
         : method.method.name.toString();
     ps.println("#");
     ps.println("# Method: '" + methodName + "':");
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 1fd4a73..ffa34d1 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -251,11 +251,11 @@
 
   @Override
   public String toString() {
-    return new CfPrinter(this).toString();
+    return new CfPrinter(this, null).toString();
   }
 
   @Override
   public String toString(DexEncodedMethod method, ClassNameMapper naming) {
-    return null;
+    return new CfPrinter(this, naming).toString();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 9f97f88..831edbe 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -43,12 +43,9 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableMap;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 import java.util.function.Consumer;
 
 public class DexEncodedMethod extends KeyedDexItem<DexMethod> implements ResolutionResult {
@@ -582,6 +579,14 @@
     return m1.method.slowCompareTo(m2.method);
   }
 
+  public static class ClassInlinerEligibility {
+    public final boolean returnsReceiver;
+
+    public ClassInlinerEligibility(boolean returnsReceiver) {
+      this.returnsReceiver = returnsReceiver;
+    }
+  }
+
   public static class OptimizationInfo {
 
     private int returnedArgument = -1;
@@ -593,8 +598,9 @@
     private boolean useIdentifierNameString = false;
     private boolean checksNullReceiverBeforeAnySideEffect = false;
     private boolean triggersClassInitBeforeAnySideEffect = false;
-    private Set<DexField> receiverOnlyUsedForReadingFields = null;
-    private Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects = null;
+    // Stores information about instance methods and constructors for
+    // class inliner, null value indicates that the method is not eligible.
+    private ClassInlinerEligibility classInlinerEligibility = null;
 
     private OptimizationInfo() {
       // Intentionally left empty.
@@ -631,13 +637,12 @@
       return returnsConstant;
     }
 
-    public boolean isReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
-      return receiverOnlyUsedForReadingFields != null &&
-          fields.containsAll(receiverOnlyUsedForReadingFields);
+    private void setClassInlinerEligibility(ClassInlinerEligibility eligibility) {
+      this.classInlinerEligibility = eligibility;
     }
 
-    public Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects() {
-      return onlyInitializesFieldsWithNoOtherSideEffects;
+    public ClassInlinerEligibility getClassInlinerEligibility() {
+      return this.classInlinerEligibility;
     }
 
     public long getReturnedConstant() {
@@ -681,19 +686,6 @@
       returnedConstant = value;
     }
 
-    private void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
-      receiverOnlyUsedForReadingFields = fields;
-    }
-
-    private void markOnlyInitializesFieldsWithNoOtherSideEffects(Map<DexField, Integer> mapping) {
-      if (mapping == null) {
-        onlyInitializesFieldsWithNoOtherSideEffects = null;
-      } else {
-        onlyInitializesFieldsWithNoOtherSideEffects = mapping.isEmpty()
-            ? Collections.emptyMap() : ImmutableMap.copyOf(mapping);
-      }
-    }
-
     private void markForceInline() {
       forceInline = true;
     }
@@ -751,13 +743,8 @@
     ensureMutableOI().markReturnsConstant(value);
   }
 
-  synchronized public void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
-    ensureMutableOI().markReceiverOnlyUsedForReadingFields(fields);
-  }
-
-  synchronized public void markOnlyInitializesFieldsWithNoOtherSideEffects(
-      Map<DexField, Integer> mapping) {
-    ensureMutableOI().markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+  synchronized public void setClassInlinerEligibility(ClassInlinerEligibility eligibility) {
+    ensureMutableOI().setClassInlinerEligibility(eligibility);
   }
 
   synchronized public void markForceInline() {
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index 8ecc1ab..454ed29 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -203,7 +203,7 @@
 
   @Override
   public String toString(DexEncodedMethod method, ClassNameMapper naming) {
-    return null;
+    return asCfCode().toString(method, naming);
   }
 
   private static class ClassCodeVisitor extends ClassVisitor {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 186839b..51850a0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -76,6 +76,10 @@
   }
 
   public void addOperands(List<Value> operands) {
+    addOperands(operands, true);
+  }
+
+  public void addOperands(List<Value> operands, boolean removeTrivialPhi) {
     // Phi operands are only filled in once to complete the phi. Some phis are incomplete for a
     // period of time to break cycles. When the cycle has been resolved they are completed
     // exactly once by adding the operands.
@@ -91,7 +95,9 @@
     if (!canBeNull) {
       markNeverNull();
     }
-    removeTrivialPhi();
+    if (removeTrivialPhi) {
+      removeTrivialPhi();
+    }
   }
 
   public void addDebugValue(Value value) {
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 306d7a1..81dc436 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
@@ -169,7 +169,8 @@
     }
     this.classInliner =
         (options.enableClassInlining && options.enableInlining && inliner != null)
-            ? new ClassInliner(appInfo.dexItemFactory) : null;
+            ? new ClassInliner(appInfo.dexItemFactory, options.classInliningInstructionLimit)
+            : null;
   }
 
   /**
@@ -756,10 +757,7 @@
     }
 
     // Analysis must be done after method is rewritten by logArgumentTypes()
-    codeRewriter.identifyReceiverOnlyUsedForReadingFields(method, code, feedback);
-    if (method.isInstanceInitializer()) {
-      codeRewriter.identifyOnlyInitializesFieldsWithNoOtherSideEffects(method, code, feedback);
-    }
+    codeRewriter.identifyClassInlinerEligibility(method, code, feedback);
 
     printMethod(code, "Optimized IR (SSA)");
     finalizeIR(method, code, feedback);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index 26ca6b2..16a77c8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -5,10 +5,8 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
-import java.util.Map;
-import java.util.Set;
 
 public interface OptimizationFeedback {
   void methodReturnsArgument(DexEncodedMethod method, int argument);
@@ -18,7 +16,5 @@
   void markProcessed(DexEncodedMethod method, Constraint state);
   void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
   void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
-  void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields);
-  void markOnlyInitializesFieldsWithNoOtherSideEffects(
-      DexEncodedMethod method, Map<DexField, Integer> mapping);
+  void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
index 645df64..7303b6b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -5,10 +5,8 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
-import java.util.Map;
-import java.util.Set;
 
 public class OptimizationFeedbackDirect implements OptimizationFeedback {
 
@@ -48,13 +46,8 @@
   }
 
   @Override
-  public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
-    method.markReceiverOnlyUsedForReadingFields(fields);
-  }
-
-  @Override
-  public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
-      Map<DexField, Integer> mapping) {
-    method.markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+  public void setClassInlinerEligibility(
+      DexEncodedMethod method, ClassInlinerEligibility eligibility) {
+    method.setClassInlinerEligibility(eligibility);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index 97fe052..6915a2f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -5,10 +5,8 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
-import java.util.Map;
-import java.util.Set;
 
 public class OptimizationFeedbackIgnore implements OptimizationFeedback {
 
@@ -34,11 +32,7 @@
   public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {}
 
   @Override
-  public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
-  }
-
-  @Override
-  public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
-      Map<DexField, Integer> mapping) {
+  public void setClassInlinerEligibility(
+      DexEncodedMethod method, ClassInlinerEligibility eligibility) {
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 6ab4845..bd3442c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -47,8 +48,6 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.If.Type;
-import com.android.tools.r8.ir.code.InstanceGet;
-import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -740,120 +739,72 @@
     }
   }
 
-  public void identifyReceiverOnlyUsedForReadingFields(
+  public void identifyClassInlinerEligibility(
       DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
-    if (!method.isNonAbstractVirtualMethod()) {
+    // Method eligibility is calculated in similar way for regular method
+    // and for the constructor. To be eligible method should only be using its
+    // receiver in the following ways:
+    //
+    //  (1) as a receiver of reads/writes of instance fields of the holder class
+    //  (2) as a return value
+    //  (3) as a receiver of a call to the superclass initializer
+    //
+    boolean instanceInitializer = method.isInstanceInitializer();
+    if (method.accessFlags.isNative() ||
+        (!method.isNonAbstractVirtualMethod() && !instanceInitializer)) {
       return;
     }
 
-    feedback.markReceiverOnlyUsedForReadingFields(method, null);
+    feedback.setClassInlinerEligibility(method, null);  // To allow returns below.
 
-    Value instance = code.getThis();
-    if (instance.numberOfPhiUsers() > 0) {
-      return;
-    }
-
-    Set<DexField> fields = Sets.newIdentityHashSet();
-    for (Instruction insn : instance.uniqueUsers()) {
-      if (!insn.isInstanceGet()) {
-        return;
-      }
-      InstanceGet get = insn.asInstanceGet();
-      if (get.dest() == instance || get.object() != instance) {
-        return;
-      }
-      fields.add(get.getField());
-    }
-    feedback.markReceiverOnlyUsedForReadingFields(method, fields);
-  }
-
-  public void identifyOnlyInitializesFieldsWithNoOtherSideEffects(
-      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
-    assert method.isInstanceInitializer();
-
-    feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, null);
-
-    if (code.hasCatchHandlers()) {
-      return;
-    }
-
-    List<Value> args = code.collectArguments(true /* not interested in receiver */);
-    Map<DexField, Integer> mapping = new IdentityHashMap<>();
     Value receiver = code.getThis();
-
-    InstructionIterator it = code.instructionIterator();
-    while (it.hasNext()) {
-      Instruction instruction = it.next();
-
-      // Mark an argument.
-      if (instruction.isArgument()) {
-        continue;
-      }
-
-      // Allow super call to java.lang.Object.<init>() on 'this'.
-      if (instruction.isInvokeDirect()) {
-        InvokeDirect invokedDirect = instruction.asInvokeDirect();
-        if (invokedDirect.getInvokedMethod() != dexItemFactory.objectMethods.constructor ||
-            invokedDirect.getReceiver() != receiver) {
-          return;
-        }
-        continue;
-      }
-
-      // Allow final return.
-      if (instruction.isReturn()) {
-        continue;
-      }
-
-      // Allow assignment to this class' fields. If the assigned value is an argument
-      // reference update the mep. Otherwise just allow assigning any value, since all
-      // invalid values should be filtered out at the definitions.
-      if (instruction.isInstancePut()) {
-        InstancePut instancePut = instruction.asInstancePut();
-        DexField field = instancePut.getField();
-        if (instancePut.object() != receiver) {
-          return;
-        }
-
-        Value value = instancePut.value();
-        if (value.isArgument() && !value.isThis()) {
-          assert (args.contains(value));
-          mapping.put(field, args.indexOf(value));
-        } else {
-          mapping.remove(field);
-        }
-        continue;
-      }
-
-      // Allow non-throwing constants.
-      if (instruction.isConstInstruction()) {
-        if (instruction.instructionTypeCanThrow()) {
-          return;
-        }
-        continue;
-      }
-
-      // Allow goto instructions jumping to the next block.
-      if (instruction.isGoto()) {
-        if (instruction.getBlock().getNumber() + 1 !=
-            instruction.asGoto().getTarget().getNumber()) {
-          return;
-        }
-        continue;
-      }
-
-      // Allow binary and unary instructions if they don't throw.
-      if (instruction.isBinop() || instruction.isUnop()) {
-        if (instruction.instructionTypeCanThrow()) {
-          return;
-        }
-        continue;
-      }
-
+    if (receiver.numberOfPhiUsers() > 0) {
       return;
     }
 
-    feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, mapping);
+    boolean receiverUsedAsReturnValue = false;
+    boolean seenSuperInitCall = false;
+    for (Instruction insn : receiver.uniqueUsers()) {
+      if (insn.isReturn()) {
+        receiverUsedAsReturnValue = true;
+        continue;
+      }
+
+      if (insn.isInstanceGet() ||
+          (insn.isInstancePut() && insn.asInstancePut().object() == receiver)) {
+        DexField field = insn.asFieldInstruction().getField();
+        if (field.clazz == method.method.holder) {
+          // Since class inliner currently only supports classes directly extending
+          // java.lang.Object, we don't need to worry about fields defined in superclasses.
+          continue;
+        }
+        return;
+      }
+
+      // If this is an instance initializer allow one call
+      // to java.lang.Object.<init>() on 'this'.
+      if (instanceInitializer && insn.isInvokeDirect()) {
+        InvokeDirect invokedDirect = insn.asInvokeDirect();
+        if (invokedDirect.getInvokedMethod() == dexItemFactory.objectMethods.constructor &&
+            invokedDirect.getReceiver() == receiver &&
+            !seenSuperInitCall) {
+          seenSuperInitCall = true;
+          continue;
+        }
+        return;
+      }
+
+      // Other receiver usages make the method not eligible.
+      return;
+    }
+
+    if (instanceInitializer && !seenSuperInitCall) {
+      // Call to super constructor not found?
+      return;
+    }
+
+    feedback.setClassInlinerEligibility(
+        method, new ClassInlinerEligibility(receiverUsedAsReturnValue));
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 7a6a846..92bda1d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -86,6 +86,7 @@
           });
     }
 
+    code.removeAllTrivialPhis();
     assert code.isConsistentSSA();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 845e17b..01dcf0d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -315,6 +315,12 @@
   }
 
   @Override
+  public boolean isValidTarget(InvokeMethod invoke, DexEncodedMethod target, IRCode inlinee) {
+    return !target.isInstanceInitializer()
+        || inliner.legalConstructorInline(method, invoke, inlinee);
+  }
+
+  @Override
   public boolean exceededAllowance() {
     return instructionAllowance < 0;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index 86d40d9..b56dc21 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -61,6 +61,11 @@
   }
 
   @Override
+  public boolean isValidTarget(InvokeMethod invoke, DexEncodedMethod target, IRCode inlinee) {
+    return true;
+  }
+
+  @Override
   public ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
       ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor) {
     return blockIterator;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index b75597d..25cd969 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -285,7 +285,7 @@
     return numOfInstructions;
   }
 
-  private boolean legalConstructorInline(DexEncodedMethod method,
+  boolean legalConstructorInline(DexEncodedMethod method,
       InvokeMethod invoke, IRCode code) {
 
     // In the Java VM Specification section "4.10.2.4. Instance Initialization Methods and
@@ -447,8 +447,7 @@
 
               // Make sure constructor inlining is legal.
               assert !target.isClassInitializer();
-              if (target.isInstanceInitializer()
-                  && !legalConstructorInline(method, invoke, inlinee)) {
+              if (!strategy.isValidTarget(invoke, target, inlinee)) {
                 continue;
               }
               DexType downcast = createDowncastIfNeeded(strategy, invoke, target);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
index 7b88b38..2443d11 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -18,6 +18,8 @@
 
   void ensureMethodProcessed(DexEncodedMethod target, IRCode inlinee);
 
+  boolean isValidTarget(InvokeMethod invoke, DexEncodedMethod target, IRCode inlinee);
+
   ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
       ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor);
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index 9ff44eb..7bdff33 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -9,68 +9,75 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
 import com.android.tools.r8.graph.DexField;
 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.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstanceGet;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.google.common.collect.Streams;
-import java.util.Collections;
+import java.util.ArrayList;
 import java.util.IdentityHashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 public final class ClassInliner {
   private final DexItemFactory factory;
+  private final int totalMethodInstructionLimit;
   private final ConcurrentHashMap<DexType, Boolean> knownClasses = new ConcurrentHashMap<>();
 
-  private static final Map<DexField, Integer> NO_MAPPING = new IdentityHashMap<>();
-
   public interface InlinerAction {
     void inline(Map<InvokeMethodWithReceiver, InliningInfo> methods);
   }
 
-  public ClassInliner(DexItemFactory factory) {
+  public ClassInliner(DexItemFactory factory, int totalMethodInstructionLimit) {
     this.factory = factory;
+    this.totalMethodInstructionLimit = totalMethodInstructionLimit;
   }
 
   // Process method code and inline eligible class instantiations, in short:
   //
   // - collect all 'new-instance' instructions in the original code. Note that class
-  // inlining, if happens, mutates code and can add 'new-instance' instructions.
-  // Processing them as well is possible, but does not seem to have much value.
+  // inlining, if happens, mutates code and may add new 'new-instance' instructions.
+  // Processing them as well is possible, but does not seem to bring much value.
   //
   // - for each 'new-instance' we check if it is eligible for inlining, i.e:
   //     -> the class of the new instance is 'eligible' (see computeClassEligible(...))
-  //     -> the instance is initialized with 'eligible' constructor (see
-  //        onlyInitializesFieldsWithNoOtherSideEffects flag in method's optimization
-  //        info); eligible constructor also defines a set of instance fields directly
-  //        initialized with parameter values, called field initialization mapping below
+  //     -> the instance is initialized with 'eligible' constructor (see comments in
+  //        CodeRewriter::identifyClassInlinerEligibility(...))
   //     -> has only 'eligible' uses, i.e:
-  //          * it is a receiver of a field read if the field is present in the
-  //            field initialization mapping
-  //          * it is a receiver of virtual or interface call with single target being
-  //            a method only reading fields in the current field initialization mapping
+  //          * as a receiver of a field read/write for a field defined in same class
+  //            as method.
+  //          * as a receiver of virtual or interface call with single target being
+  //            an eligible method according to identifyClassInlinerEligibility(...);
+  //            NOTE: if method receiver is used as a return value, the method call
+  //            should ignore return value
   //
   // - inline eligible 'new-instance' instructions, i.e:
-  //     -> force inline methods called on the instance (which may introduce additional
-  //        instance field reads, but only for fields present in the current field
-  //        initialization mapping)
-  //     -> replace instance field reads with appropriate values passed to the constructor
-  //        according to field initialization mapping
-  //     -> remove constructor call
+  //     -> force inline methods called on the instance (including the initializer);
+  //        (this may introduce additional instance field reads/writes on the receiver)
+  //     -> replace instance field reads with appropriate values calculated based on
+  //        fields writes
+  //     -> remove the call to superclass initializer
+  //     -> remove all field writes
   //     -> remove 'new-instance' instructions
   //
   // For example:
@@ -117,7 +124,6 @@
         .map(Instruction::asNewInstance)
         .collect(Collectors.toList());
 
-    nextNewInstance:
     for (NewInstance newInstance : newInstances) {
       Value eligibleInstance = newInstance.outValue();
       if (eligibleInstance == null) {
@@ -129,177 +135,281 @@
         continue;
       }
 
-      // No Phi users.
-      if (eligibleInstance.numberOfPhiUsers() > 0) {
+      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = checkInstanceUsersEligibility(
+          appInfo, method, isProcessedConcurrently, newInstance, eligibleInstance, eligibleClass);
+      if (methodCalls == null) {
         continue;
       }
 
-      Set<Instruction> uniqueUsers = eligibleInstance.uniqueUsers();
+      if (getTotalEstimatedMethodSize(methodCalls) >= totalMethodInstructionLimit) {
+        continue;
+      }
 
-      // Find an initializer invocation.
-      InvokeDirect eligibleInitCall = null;
-      Map<DexField, Integer> mappings = null;
-      for (Instruction user : uniqueUsers) {
-        if (!user.isInvokeDirect()) {
+      // Inline the class instance.
+      forceInlineAllMethodInvocations(inliner, methodCalls);
+      removeSuperClassInitializerAndFieldReads(code, newInstance, eligibleInstance);
+      removeFieldWrites(eligibleInstance, eligibleClass);
+      removeInstruction(newInstance);
+
+      // Restore normality.
+      code.removeAllTrivialPhis();
+      assert code.isConsistentSSA();
+    }
+  }
+
+  private Map<InvokeMethodWithReceiver, InliningInfo> checkInstanceUsersEligibility(
+      AppInfoWithSubtyping appInfo, DexEncodedMethod method,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      NewInstance newInstanceInsn, Value receiver, DexType clazz) {
+
+    // No Phi users.
+    if (receiver.numberOfPhiUsers() > 0) {
+      return null; // Not eligible.
+    }
+
+    Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
+
+    for (Instruction user : receiver.uniqueUsers()) {
+      // Field read/write.
+      if (user.isInstanceGet() ||
+          (user.isInstancePut() && user.asInstancePut().value() != receiver)) {
+        if (user.asFieldInstruction().getField().clazz == newInstanceInsn.clazz) {
+          // Eligible field read or write. Note: as long as we consider only classes eligible
+          // if they directly extend java.lang.Object we don't need to check if the field
+          // really exists in the class.
           continue;
         }
+        return null; // Not eligible.
+      }
 
-        InvokeDirect candidate = user.asInvokeDirect();
-        DexMethod candidateInit = candidate.getInvokedMethod();
-        if (factory.isConstructor(candidateInit) &&
-            candidate.inValues().lastIndexOf(eligibleInstance) == 0) {
+      // Eligible constructor call.
+      if (user.isInvokeDirect()) {
+        InliningInfo inliningInfo = isEligibleConstructorCall(appInfo, method,
+            user.asInvokeDirect(), receiver, clazz, isProcessedConcurrently);
+        if (inliningInfo != null) {
+          methodCalls.put(user.asInvokeDirect(), inliningInfo);
+          continue;
+        }
+        return null; // Not eligible.
+      }
 
-          if (candidateInit.holder != eligibleClass) {
-            // Inlined constructor call? We won't get field initialization mapping in this
-            // case, but since we only support eligible classes extending java.lang.Object,
-            // it's safe to assume an empty mapping.
-            if (candidateInit.holder == factory.objectType) {
-              mappings = Collections.emptyMap();
-            }
+      // Eligible virtual method call.
+      if (user.isInvokeVirtual() || user.isInvokeInterface()) {
+        InliningInfo inliningInfo = isEligibleMethodCall(
+            appInfo, method, user.asInvokeMethodWithReceiver(),
+            receiver, clazz, isProcessedConcurrently);
+        if (inliningInfo != null) {
+          methodCalls.put(user.asInvokeMethodWithReceiver(), inliningInfo);
+          continue;
+        }
+        return null;  // Not eligible.
+      }
 
-          } else {
-            // Is it a call to an *eligible* constructor?
-            mappings = getConstructorFieldMappings(appInfo, candidateInit, isProcessedConcurrently);
-          }
+      return null;  // Not eligible.
+    }
+    return methodCalls;
+  }
 
-          eligibleInitCall = candidate;
+  // Remove call to superclass initializer, replace field reads with appropriate
+  // values, insert phis when needed.
+  private void removeSuperClassInitializerAndFieldReads(
+      IRCode code, NewInstance newInstance, Value eligibleInstance) {
+    Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
+    for (Instruction user : eligibleInstance.uniqueUsers()) {
+      // Remove the call to superclass constructor.
+      if (user.isInvokeDirect() &&
+          user.asInvokeDirect().getInvokedMethod() == factory.objectMethods.constructor) {
+        removeInstruction(user);
+        continue;
+      }
+
+      if (user.isInstanceGet()) {
+        // Replace a field read with appropriate value.
+        replaceFieldRead(code, newInstance, user.asInstanceGet(), fieldHelpers);
+        continue;
+      }
+
+      if (user.isInstancePut()) {
+        // Skip in this iteration since these instructions are needed to
+        // properly calculate what value should field reads be replaced with.
+        continue;
+      }
+
+      throw new Unreachable("Unexpected usage left after method inlining: " + user);
+    }
+  }
+
+  private void removeFieldWrites(Value receiver, DexType clazz) {
+    for (Instruction user : receiver.uniqueUsers()) {
+      if (!user.isInstancePut()) {
+        throw new Unreachable("Unexpected usage left after field reads removed: " + user);
+      }
+      if (user.asInstancePut().getField().clazz != clazz) {
+        throw new Unreachable("Unexpected field write left after field reads removed: " + user);
+      }
+      removeInstruction(user);
+    }
+  }
+
+  private int getTotalEstimatedMethodSize(Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) {
+    int totalSize = 0;
+    for (InliningInfo info : methodCalls.values()) {
+      totalSize += info.target.getCode().estimatedSizeForInlining();
+    }
+    return totalSize;
+  }
+
+  private void replaceFieldRead(IRCode code, NewInstance newInstance,
+      InstanceGet fieldRead, Map<DexField, FieldValueHelper> fieldHelpers) {
+
+    Value value = fieldRead.outValue();
+    if (value != null) {
+      FieldValueHelper helper = fieldHelpers.computeIfAbsent(
+          fieldRead.getField(), field -> new FieldValueHelper(field, code, newInstance));
+      Value newValue = helper.getValueForFieldRead(fieldRead.getBlock(), fieldRead);
+      value.replaceUsers(newValue);
+      for (FieldValueHelper fieldValueHelper : fieldHelpers.values()) {
+        fieldValueHelper.replaceValue(value, newValue);
+      }
+      assert value.numberOfAllUsers() == 0;
+    }
+    removeInstruction(fieldRead);
+  }
+
+  // Describes and caches what values are supposed to be used instead of field reads.
+  private static final class FieldValueHelper {
+    final DexField field;
+    final IRCode code;
+    final NewInstance newInstance;
+
+    private Value defaultValue = null;
+    private final Map<BasicBlock, Value> ins = new IdentityHashMap<>();
+    private final Map<BasicBlock, Value> outs = new IdentityHashMap<>();
+
+    private FieldValueHelper(DexField field, IRCode code, NewInstance newInstance) {
+      this.field = field;
+      this.code = code;
+      this.newInstance = newInstance;
+    }
+
+    void replaceValue(Value oldValue, Value newValue) {
+      for (Entry<BasicBlock, Value> entry : ins.entrySet()) {
+        if (entry.getValue() == oldValue) {
+          entry.setValue(newValue);
+        }
+      }
+      for (Entry<BasicBlock, Value> entry : outs.entrySet()) {
+        if (entry.getValue() == oldValue) {
+          entry.setValue(newValue);
+        }
+      }
+    }
+
+    Value getValueForFieldRead(BasicBlock block, Instruction valueUser) {
+      assert valueUser != null;
+      Value value = getValueDefinedInTheBlock(block, valueUser);
+      return value != null ? value : getOrCreateInValue(block);
+    }
+
+    private Value getOrCreateOutValue(BasicBlock block) {
+      Value value = outs.get(block);
+      if (value != null) {
+        return value;
+      }
+
+      value = getValueDefinedInTheBlock(block, null);
+      if (value == null) {
+        // No value defined in the block.
+        value = getOrCreateInValue(block);
+      }
+
+      assert value != null;
+      outs.put(block, value);
+      return value;
+    }
+
+    private Value getOrCreateInValue(BasicBlock block) {
+      Value value = ins.get(block);
+      if (value != null) {
+        return value;
+      }
+
+      List<BasicBlock> predecessors = block.getPredecessors();
+      if (predecessors.size() == 1) {
+        value = getOrCreateOutValue(predecessors.get(0));
+        ins.put(block, value);
+      } else {
+        // Create phi, add it to the block, cache in ins map for future use.
+        Phi phi = new Phi(code.valueNumberGenerator.next(),
+            block, ValueType.fromDexType(field.type), null);
+        ins.put(block, phi);
+
+        List<Value> operands = new ArrayList<>();
+        for (BasicBlock predecessor : block.getPredecessors()) {
+          operands.add(getOrCreateOutValue(predecessor));
+        }
+        // Add phi, but don't remove trivial phis; since we cache the phi
+        // we just created for future use we should delay removing trivial
+        // phis until we are done with replacing fields reads.
+        phi.addOperands(operands, false);
+        value = phi;
+      }
+
+      assert value != null;
+      return value;
+    }
+
+    private Value getValueDefinedInTheBlock(BasicBlock block, Instruction stopAt) {
+      InstructionListIterator iterator = stopAt == null ?
+          block.listIterator(block.getInstructions().size()) : block.listIterator(stopAt);
+
+      Instruction valueProducingInsn = null;
+      while (iterator.hasPrevious()) {
+        Instruction instruction = iterator.previous();
+        assert instruction != null;
+
+        if (instruction == newInstance ||
+            (instruction.isInstancePut() &&
+                instruction.asInstancePut().getField() == field &&
+                instruction.asInstancePut().object() == newInstance.outValue())) {
+          valueProducingInsn = instruction;
           break;
         }
       }
 
-      if (mappings == null) {
-        continue;
+      if (valueProducingInsn == null) {
+        return null;
+      }
+      if (valueProducingInsn.isInstancePut()) {
+        return valueProducingInsn.asInstancePut().value();
       }
 
-      // Check all regular users.
-      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
-
-      for (Instruction user : uniqueUsers) {
-        if (user == eligibleInitCall) {
-          continue /* next user */;
-        }
-
-        if (user.isInstanceGet()) {
-          InstanceGet instanceGet = user.asInstanceGet();
-          if (mappings.containsKey(instanceGet.getField())) {
-            continue /* next user */;
-          }
-
-          // Not replaceable field read.
-          continue nextNewInstance;
-        }
-
-        if (user.isInvokeVirtual() || user.isInvokeInterface()) {
-          InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
-          if (invoke.inValues().lastIndexOf(eligibleInstance) > 0) {
-            continue nextNewInstance; // Instance must only be passes as a receiver.
-          }
-
-          DexEncodedMethod singleTarget =
-              findSingleTarget(appInfo, invoke, eligibleClass);
-          if (singleTarget == null) {
-            continue nextNewInstance;
-          }
-          if (isProcessedConcurrently.test(singleTarget)) {
-            continue nextNewInstance;
-          }
-          if (method == singleTarget) {
-            continue nextNewInstance; // Don't inline itself.
-          }
-
-          if (!singleTarget.getOptimizationInfo()
-              .isReceiverOnlyUsedForReadingFields(mappings.keySet())) {
-            continue nextNewInstance; // Target must be trivial.
-          }
-
-          if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
-            // We won't be able to inline it here.
-
-            // Note that there may be some false negatives here since the method may
-            // reference private fields of its class which are supposed to be replaced
-            // with arguments after inlining. We should try and improve it later.
-
-            // Using -allowaccessmodification mitigates this.
-            continue nextNewInstance;
-          }
-
-          methodCalls.put(invoke, new InliningInfo(singleTarget, eligibleClass));
-          continue /* next user */;
-        }
-
-        continue nextNewInstance; // Unsupported user.
+      assert newInstance == valueProducingInsn;
+      if (defaultValue == null) {
+        // If we met newInstance it means that default value is supposed to be used.
+        defaultValue = code.createValue(ValueType.fromDexType(field.type));
+        ConstNumber defaultValueInsn = new ConstNumber(defaultValue, 0);
+        defaultValueInsn.setPosition(newInstance.getPosition());
+        LinkedList<Instruction> instructions = block.getInstructions();
+        instructions.add(instructions.indexOf(newInstance) + 1, defaultValueInsn);
+        defaultValueInsn.setBlock(block);
       }
-
-      // Force-inline of method invocation if any.
-      inlineAllCalls(inliner, methodCalls);
-      assert assertOnlyConstructorAndFieldReads(eligibleInstance, eligibleInitCall, mappings);
-
-      // Replace all field reads with arguments passed to the constructor.
-      patchFieldReads(eligibleInstance, eligibleInitCall, mappings);
-      assert assertOnlyConstructor(eligibleInstance, eligibleInitCall);
-
-      // Remove constructor call and new-instance instructions.
-      removeInstruction(eligibleInitCall);
-      removeInstruction(newInstance);
-      code.removeAllTrivialPhis();
+      return defaultValue;
     }
   }
 
-  private void inlineAllCalls(
+  private void forceInlineAllMethodInvocations(
       InlinerAction inliner, Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) {
     if (!methodCalls.isEmpty()) {
       inliner.inline(methodCalls);
     }
   }
 
-  private void patchFieldReads(
-      Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
-    for (Instruction user : instance.uniqueUsers()) {
-      if (!user.isInstanceGet()) {
-        continue;
-      }
-      InstanceGet fieldRead = user.asInstanceGet();
-
-      // Replace the field read with
-      assert mappings.containsKey(fieldRead.getField());
-      Value arg = invokeMethod.inValues().get(1 + mappings.get(fieldRead.getField()));
-      assert arg != null;
-      Value value = fieldRead.outValue();
-      if (value != null) {
-        value.replaceUsers(arg);
-        assert value.numberOfAllUsers() == 0;
-      }
-
-      // Remove instruction.
-      removeInstruction(fieldRead);
-    }
-  }
-
   private void removeInstruction(Instruction instruction) {
     instruction.inValues().forEach(v -> v.removeUser(instruction));
     instruction.getBlock().removeInstruction(instruction);
   }
 
-  private boolean assertOnlyConstructorAndFieldReads(
-      Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
-    for (Instruction user : instance.uniqueUsers()) {
-      if (user != invokeMethod &&
-          !(user.isInstanceGet() && mappings.containsKey(user.asFieldInstruction().getField()))) {
-        throw new Unreachable("Not all calls are inlined!");
-      }
-    }
-    return true;
-  }
-
-  private boolean assertOnlyConstructor(Value instance, InvokeDirect invokeMethod) {
-    for (Instruction user : instance.uniqueUsers()) {
-      if (user != invokeMethod) {
-        throw new Unreachable("Not all field reads are substituted!");
-      }
-    }
-    return true;
-  }
-
   private DexEncodedMethod findSingleTarget(
       AppInfo appInfo, InvokeMethodWithReceiver invoke, DexType instanceType) {
 
@@ -322,24 +432,94 @@
     return null;
   }
 
-  private Map<DexField, Integer> getConstructorFieldMappings(
-      AppInfo appInfo, DexMethod init, Predicate<DexEncodedMethod> isProcessedConcurrently) {
-    assert isClassEligible(appInfo, init.holder);
+  private InliningInfo isEligibleConstructorCall(
+      AppInfoWithSubtyping appInfo,
+      DexEncodedMethod method,
+      InvokeDirect initInvoke,
+      Value receiver,
+      DexType inlinedClass,
+      Predicate<DexEncodedMethod> isProcessedConcurrently) {
+
+    // Must be a constructor of the exact same class.
+    DexMethod init = initInvoke.getInvokedMethod();
+    if (!factory.isConstructor(init)) {
+      return null;
+    }
+    // Must be a constructor called on the receiver.
+    if (initInvoke.inValues().lastIndexOf(receiver) != 0) {
+      return null;
+    }
+
+    assert init.holder == inlinedClass
+        : "Inlined constructor? [invoke: " + initInvoke +
+        ", expected class: " + inlinedClass + "]";
 
     DexEncodedMethod definition = appInfo.definitionFor(init);
-    if (definition == null) {
-      return NO_MAPPING;
+    if (definition == null || isProcessedConcurrently.test(definition)) {
+      return null;
     }
 
-    if (isProcessedConcurrently.test(definition)) {
-      return NO_MAPPING;
+    if (!definition.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
+      // We won't be able to inline it here.
+
+      // Note that there may be some false negatives here since the method may
+      // reference private fields of its class which are supposed to be replaced
+      // with arguments after inlining. We should try and improve it later.
+
+      // Using -allowaccessmodification mitigates this.
+      return null;
     }
 
-    if (definition.accessFlags.isAbstract() || definition.accessFlags.isNative()) {
-      return NO_MAPPING;
+    return definition.getOptimizationInfo().getClassInlinerEligibility() != null
+        ? new InliningInfo(definition, inlinedClass) : null;
+  }
+
+  private InliningInfo isEligibleMethodCall(
+      AppInfoWithSubtyping appInfo,
+      DexEncodedMethod method,
+      InvokeMethodWithReceiver invoke,
+      Value receiver,
+      DexType inlinedClass,
+      Predicate<DexEncodedMethod> isProcessedConcurrently) {
+
+    if (invoke.inValues().lastIndexOf(receiver) > 0) {
+      return null; // Instance passed as an argument.
     }
 
-    return definition.getOptimizationInfo().onlyInitializesFieldsWithNoOtherSideEffects();
+    DexEncodedMethod singleTarget =
+        findSingleTarget(appInfo, invoke, inlinedClass);
+    if (singleTarget == null || isProcessedConcurrently.test(singleTarget)) {
+      return null;
+    }
+    if (method == singleTarget) {
+      return null; // Don't inline itself.
+    }
+
+    ClassInlinerEligibility eligibility =
+        singleTarget.getOptimizationInfo().getClassInlinerEligibility();
+    if (eligibility == null) {
+      return null;
+    }
+
+    // If the method returns receiver and the return value is actually
+    // used in the code the method is not eligible.
+    if (eligibility.returnsReceiver &&
+        invoke.outValue() != null && invoke.outValue().numberOfAllUsers() > 0) {
+      return null;
+    }
+
+    if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
+      // We won't be able to inline it here.
+
+      // Note that there may be some false negatives here since the method may
+      // reference private fields of its class which are supposed to be replaced
+      // with arguments after inlining. We should try and improve it later.
+
+      // Using -allowaccessmodification mitigates this.
+      return null;
+    }
+
+    return new InliningInfo(singleTarget, inlinedClass);
   }
 
   private boolean isClassEligible(AppInfo appInfo, DexType clazz) {
@@ -354,7 +534,7 @@
   }
 
   // Class is eligible for this optimization. Eligibility implementation:
-  //   - not an abstract or interface
+  //   - is not an abstract class or interface
   //   - directly extends java.lang.Object
   //   - does not declare finalizer
   //   - does not trigger any static initializers
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 436c94f..5f0ec45 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -101,6 +101,7 @@
   public boolean enableInlining =
       !Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null;
   public boolean enableClassInlining = true;
+  public int classInliningInstructionLimit = 50;
   public int inliningInstructionLimit = 5;
   public boolean enableSwitchMapRemoval = true;
   public final OutlineOptions outline = new OutlineOptions();
diff --git a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
index ede7e53..e01373c 100644
--- a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
@@ -15,6 +15,7 @@
 import org.hamcrest.core.IsInstanceOf;
 import org.hamcrest.core.StringContains;
 import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.internal.matchers.ThrowableMessageMatcher;
 
@@ -156,16 +157,20 @@
     // TODO check compilation warnings are correctly reported
 
     // Missing interface B is causing the wrong code to be executed.
-    if (ToolHelper.artSupported()) {
-      thrown.expect(AssertionError.class);
-      execute(
-          "testMissingInterfaceDesugared2AndroidK",
-          "desugaringwithmissingclasstest2.Main",
-          new Path[] {
-              lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
-          },
-          new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+    if (!ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
+      return;
     }
+    if (ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
+      thrown.expect(AssertionError.class);
+    }
+    execute(
+        "testMissingInterfaceDesugared2AndroidK",
+        "desugaringwithmissingclasstest2.Main",
+        new Path[] {
+            lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
+        },
+        new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+
   }
 
   @Test
@@ -256,17 +261,20 @@
     Path testDex = test.build();
     // TODO check compilation warnings are correctly reported
 
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
     // Missing interface B is causing the wrong method to be executed.
-    if (ToolHelper.artSupported()) {
+    if (ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
       thrown.expect(AssertionError.class);
-      execute(
-          "testCallToMissingSuperInterfaceDesugaredAndroidK",
-          "desugaringwithmissingclasstest3.Main",
-          new Path[] {
-              lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
-          },
-          new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
     }
+    execute(
+        "testCallToMissingSuperInterfaceDesugaredAndroidK",
+        "desugaringwithmissingclasstest3.Main",
+        new Path[] {
+            lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
+        },
+        new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index ca6feed..2032866 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Kind;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
@@ -22,6 +23,7 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -1010,8 +1012,7 @@
       // Test depends on exception produced for missing method or similar cases, but
       // after class inlining removes class instantiations and references the exception
       // is not produced.
-      "042-new-instance",
-      "075-verification-error"
+      "435-new-instance"
   );
 
   private static List<String> hasMissingClasses = ImmutableList.of(
@@ -1363,6 +1364,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   public R8RunArtTestsTest(String name, DexTool toolchain) {
     this.name = name;
     this.toolchain = toolchain;
@@ -1554,6 +1558,8 @@
                 if (disableClassInlining) {
                   options.enableClassInlining = false;
                 }
+                // Make sure we don't depend on this settings.
+                options.classInliningInstructionLimit = 10000;
                 options.lineNumberOptimization = LineNumberOptimization.OFF;
                 // Some tests actually rely on missing classes for what they test.
                 options.ignoreMissingClasses = hasMissingClasses;
@@ -1768,7 +1774,7 @@
         specification.disableInlining, specification.disableClassInlining,
         specification.hasMissingClasses);
 
-    if (!ToolHelper.artSupported()) {
+    if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
       return;
     }
 
@@ -1789,9 +1795,11 @@
 
     boolean compileOnly = System.getProperty("jctf_compile_only", "0").equals("1");
     if (compileOnly || specification.skipArt) {
-      // verify dex code instead of running it
-      Path oatFile = temp.getRoot().toPath().resolve("all.oat");
-      ToolHelper.runDex2Oat(processedFile.toPath(), oatFile);
+      if (ToolHelper.isDex2OatSupported()) {
+        // verify dex code instead of running it
+        Path oatFile = temp.getRoot().toPath().resolve("all.oat");
+        ToolHelper.runDex2Oat(processedFile.toPath(), oatFile);
+      }
       return;
     }
 
@@ -1963,7 +1971,7 @@
           specification.hasMissingClasses);
     }
 
-    if (!specification.skipArt && ToolHelper.artSupported()) {
+    if (!specification.skipArt && (ToolHelper.artSupported() || ToolHelper.dealsWithGoldenFiles())) {
       File originalFile;
       File processedFile;
 
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index 62dd4af..fe5ae72 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import java.io.IOException;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
@@ -21,6 +22,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -69,6 +71,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   private final Input input;
   private final CompilerUnderTest compiler;
   private final CompilationMode mode;
@@ -183,9 +188,8 @@
 
   @Test
   public void outputIsIdentical() throws IOException, InterruptedException, ExecutionException {
-    if (!ToolHelper.artSupported()) {
-      return;
-    }
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
 
     DexVm vm = ToolHelper.getDexVm();
 
diff --git a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
index fc373d2..0f3cadc 100644
--- a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
@@ -91,6 +92,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Parameters(name = "{0}")
   public static Collection<String[]> data() {
     return Arrays.asList(new String[][]{
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
index bb8c686..83df845 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
@@ -73,7 +74,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -118,6 +119,8 @@
 
   @Rule public ExpectedException thrown = ExpectedException.none();
 
+  @Rule public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Test
   public void staticInterfaceMethods() throws Throwable {
     test("staticinterfacemethods", "interfacemethods", "StaticInterfaceMethods")
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 9fdbf0a..2c6a72a 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -47,6 +48,7 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestWatcher;
 
 public abstract class RunExamplesAndroidOTest
       <B extends BaseCommand.Builder<? extends BaseCommand, B>> {
@@ -148,7 +150,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -270,6 +272,9 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   boolean failsOn(Map<ToolHelper.DexVm.Version, List<String>> failsOn, String name) {
     Version vmVersion = ToolHelper.getDexVm().getVersion();
     return failsOn.containsKey(vmVersion)
@@ -553,14 +558,14 @@
       throws IOException {
 
     boolean expectedToFail = expectedToFail(testName);
-    if (expectedToFail) {
+    if (expectedToFail && !ToolHelper.compareAgaintsGoldenFiles()) {
       thrown.expect(Throwable.class);
     }
     String output = ToolHelper.runArtNoVerificationErrors(
         Arrays.stream(dexes).map(path -> path.toString()).collect(Collectors.toList()),
         qualifiedMainClass,
         null);
-    if (!expectedToFail && !skipRunningOnJvm(testName)) {
+    if (!expectedToFail && !skipRunningOnJvm(testName) && !ToolHelper.compareAgaintsGoldenFiles()) {
       ToolHelper.ProcessResult javaResult =
           ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
       assertEquals("JVM run failed", javaResult.exitCode, 0);
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index bc898bd..182fe80 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -143,7 +144,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -193,6 +194,9 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   boolean failsOn(Map<DexVm.Version, List<String>> failsOn, String name) {
     DexVm.Version vmVersion = ToolHelper.getDexVm().getVersion();
     return failsOn.containsKey(vmVersion)
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index 281b0da..284ed68 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
@@ -143,7 +144,7 @@
 
       build(inputFile, out);
 
-      if (!ToolHelper.artSupported()) {
+      if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
         return;
       }
 
@@ -224,6 +225,9 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   boolean failsOn(Map<DexVm.Version, List<String>> failsOn, String name) {
     DexVm.Version vmVersion = ToolHelper.getDexVm().getVersion();
     return failsOn.containsKey(vmVersion)
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 331af0a..add86c2 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.PreloadedClassFileProvider;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -59,6 +60,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   /**
    * Check if tests should also run Proguard when applicable.
    */
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 68d87c1..000076e 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -23,18 +23,23 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.CharStreams;
+import com.google.gson.Gson;
 import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -702,6 +707,10 @@
     return true;
   }
 
+  public static boolean isDex2OatSupported() {
+    return !isWindows();
+  }
+
   public static Path getClassPathForTests() {
     return Paths.get(BUILD_DIR, "classes", "test");
   }
@@ -793,12 +802,22 @@
   }
 
   public static R8Command.Builder prepareR8CommandBuilder(AndroidApp app) {
-    return R8Command.builder(app).setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    return prepareR8CommandBuilder(app, DexIndexedConsumer.emptyConsumer());
+  }
+
+  public static R8Command.Builder prepareR8CommandBuilder(
+      AndroidApp app, ProgramConsumer programConsumer) {
+    return R8Command.builder(app).setProgramConsumer(programConsumer);
   }
 
   public static AndroidApp runR8(AndroidApp app) throws IOException {
+    return runR8WithProgramConsumer(app, DexIndexedConsumer.emptyConsumer());
+  }
+
+  public static AndroidApp runR8WithProgramConsumer(AndroidApp app, ProgramConsumer programConsumer)
+      throws IOException {
     try {
-      return runR8(prepareR8CommandBuilder(app).build());
+      return runR8(prepareR8CommandBuilder(app, programConsumer).build());
     } catch (CompilationFailedException e) {
       throw new RuntimeException(e);
     }
@@ -1011,17 +1030,197 @@
     return runArtRaw(files, mainClass, extras, null);
   }
 
+  // Index used to name directory aimed at storing dex files and process result
+  // for one invokation of runArtRaw() in order to avoid conflicts in case of
+  // multiple calls within the same test.
+  private static int testOutputPathIndex = 0;
+
   public static ProcessResult runArtRaw(List<String> files, String mainClass,
       Consumer<ArtCommandBuilder> extras, DexVm version)
       throws IOException {
-    ArtCommandBuilder builder =
-        version != null ? new ArtCommandBuilder(version) : new ArtCommandBuilder();
-    files.forEach(builder::appendClasspath);
-    builder.setMainClass(mainClass);
-    if (extras != null) {
-      extras.accept(builder);
+
+      ArtCommandBuilder builder =
+          version != null ? new ArtCommandBuilder(version) : new ArtCommandBuilder();
+      files.forEach(builder::appendClasspath);
+      builder.setMainClass(mainClass);
+      if (extras != null) {
+        extras.accept(builder);
+      }
+
+      ProcessResult processResult = null;
+
+      // Whenever we start a new test method we reset the index count.
+      String reset_output_index = System.getProperty("reset_output_index");
+      if (reset_output_index != null) {
+        System.clearProperty("reset_output_index");
+        testOutputPathIndex = 0;
+      } else {
+        assert testOutputPathIndex >= 0;
+        testOutputPathIndex++;
+      }
+
+      String goldenFilesDirInProp = System.getProperty("use_golden_files_in");
+      if (goldenFilesDirInProp != null) {
+        File goldenFileDir = new File(goldenFilesDirInProp);
+        assert goldenFileDir.isDirectory();
+        processResult = compareAgainstGoldenFiles(
+            files.stream().map(f -> new File(f)).collect(Collectors.toList()), goldenFileDir);
+        if (processResult.exitCode == 0) {
+          processResult = readProcessResult(goldenFileDir);
+        }
+      } else {
+        processResult = runArtProcessRaw(builder);
+      }
+
+      String goldenFilesDirToProp = System.getProperty("generate_golden_files_to");
+      if (goldenFilesDirToProp != null) {
+        File goldenFileDir = new File(goldenFilesDirToProp);
+        assert goldenFileDir.isDirectory();
+        storeAsGoldenFiles(files.stream().map(f -> new File(f)).collect(Collectors.toList()),
+            goldenFileDir);
+        storeProcessResult(processResult, goldenFileDir);
+      }
+
+      return processResult;
+  }
+
+  private static Path findNonConflictingDestinationFilePath(Path testOutputPath) {
+    int index = 0;
+    Path destFilePath;
+    do {
+      destFilePath = Paths.get(testOutputPath.toString(),
+          "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION);
+      index++;
+    } while (destFilePath.toFile().exists());
+
+    return destFilePath;
+  }
+
+  private static Path getTestOutputPath(File destDir) throws IOException {
+    assert destDir.exists();
+    assert destDir.isDirectory();
+
+    String testClassName = System.getProperty("test_class_name");
+    String testName = System.getProperty("test_name");
+    String headSha1 = System.getProperty("test_git_HEAD_sha1");
+
+    assert testClassName != null;
+    assert testName != null;
+    assert headSha1 != null;
+
+    return Files.createDirectories(
+         Paths.get(destDir.getAbsolutePath(), headSha1, testClassName, testName + "-" + String
+             .format("%03d", testOutputPathIndex)));
+  }
+
+  private static List<File> unzipDexFilesArchive(File zipFile) throws IOException {
+    File tmpDir = Files.createTempDirectory("r8-test-").toFile();
+    tmpDir.deleteOnExit();
+    return ZipUtils.unzip(zipFile.getAbsolutePath(), tmpDir);
+  }
+
+  private static void storeAsGoldenFiles(List<File> files, File destDir) throws IOException {
+    Path testOutputPath = getTestOutputPath(destDir);
+
+    for (File f : files) {
+      Path filePath = f.toPath();
+      // TODO(jmhenaff): Check it's been produced by D8/R8?
+      List<File> testFiles = Collections.singletonList(f);
+      if (FileUtils.isArchive(filePath)) {
+        testFiles = unzipDexFilesArchive(f);
+      }
+      for (File testFile : testFiles) {
+        Path testFilePath = testFile.toPath();
+        if (FileUtils.isDexFile(testFilePath)) {
+          Path destFile = findNonConflictingDestinationFilePath(testOutputPath);
+          FileUtils.writeToFile(destFile, null, Files.readAllBytes(testFilePath));
+        }
+      }
     }
-    return runArtProcessRaw(builder);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void storeProcessResult(ProcessResult processResult, File dest)
+      throws IOException {
+    Gson gson = new Gson();
+    Path testOutputPath = getTestOutputPath(dest);
+    try (FileWriter fw = new FileWriter(new File(testOutputPath.toFile(), "processResult.json"))) {
+      gson.toJson(processResult, ProcessResult.class, fw);
+    }
+  }
+
+  private static ProcessResult readProcessResult(File dest) throws IOException {
+    File processResultFile = new File(getTestOutputPath(dest).toFile(), "processResult.json");
+    Gson gson = new Gson();
+    try (FileReader fr = new FileReader(processResultFile)) {
+      return gson.fromJson(fr, ProcessResult.class);
+    }
+  }
+
+  private static ProcessResult compareAgainstGoldenFiles(List<File> files, File destDir)
+      throws IOException {
+    Path testOutputPath = getTestOutputPath(destDir);
+
+    int index = 0;
+    String stdErr = "";
+    boolean passed = true;
+    for (File f : files) {
+      Path filePath = f.toPath();
+
+      List<File> testFiles = Collections.singletonList(f);
+      if (FileUtils.isArchive(filePath)) {
+        testFiles = unzipDexFilesArchive(f);
+      }
+
+      for (File testFile : testFiles) {
+        Path testFilePath = testFile.toPath();
+        // TODO(jmhenaff): Check it's been produced by D8/R8?
+        if (FileUtils.isDexFile(testFilePath)) {
+          File goldenFile = Paths.get(testOutputPath.toString(),
+              "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile();
+          if (!goldenFile.exists()) {
+            String fileDesc = "'" + testFile.getAbsolutePath() + "'";
+            if (FileUtils.isZipFile(filePath)) {
+              fileDesc += " (extracted from '" + f.getAbsolutePath() + "')";
+            }
+            stdErr += "Cannot find golden file '" + goldenFile.getAbsolutePath()
+                + "' to compare against test file " + fileDesc + "\n";
+            passed = false;
+          } else if (!com.google.common.io.Files.equal(testFile, goldenFile)) {
+            String fileDesc = "'" + testFile.getAbsolutePath() + "'";
+            if (FileUtils.isZipFile(filePath)) {
+              fileDesc += " (extracted from '" + f.getAbsolutePath() + "')";
+            }
+            stdErr +=
+                "File " + fileDesc + " differs from golden file '" + goldenFile.getAbsolutePath()
+                    + "'\n";
+            passed = false;
+          }
+          index++;
+        }
+      }
+    }
+    // Ensure we processed as many files as there are golden files
+    File goldenFile = Paths.get(testOutputPath.toString(),
+        "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile();
+    if (goldenFile.exists()) {
+      stdErr += "Less dex files have been produced: there is at least one more golden file ('"
+          + goldenFile.getAbsolutePath() + "'\n";
+      passed = false;
+    }
+    return new ProcessResult(passed ? 0 : -1, "", stdErr);
+  }
+
+  public static boolean dealsWithGoldenFiles() {
+    return compareAgaintsGoldenFiles() || generateGoldenFiles();
+  }
+
+  public static boolean compareAgaintsGoldenFiles() {
+    return System.getProperty("use_golden_files_in") != null;
+  }
+
+  public static boolean generateGoldenFiles() {
+    return System.getProperty("generate_golden_files_to") != null;
   }
 
   public static ProcessResult runArtNoVerificationErrorsRaw(String file, String mainClass)
@@ -1076,7 +1275,7 @@
   }
 
   private static ProcessResult runArtProcessRaw(ArtCommandBuilder builder) throws IOException {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(artSupported() || dealsWithGoldenFiles());
     ProcessResult result;
     if (builder.isForDevice()) {
       try {
@@ -1151,7 +1350,7 @@
   }
 
   public static void runDex2Oat(Path file, Path outFile, DexVm vm) throws IOException {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(ToolHelper.isDex2OatSupported());
     // TODO(jmhenaff): find a way to run this on windows (push dex and run on device/emulator?)
     Assume.assumeTrue(!ToolHelper.isWindows());
     assert Files.exists(file);
diff --git a/src/test/java/com/android/tools/r8/VmTestRunner.java b/src/test/java/com/android/tools/r8/VmTestRunner.java
index 978cbe7..a3e76a4 100644
--- a/src/test/java/com/android/tools/r8/VmTestRunner.java
+++ b/src/test/java/com/android/tools/r8/VmTestRunner.java
@@ -68,7 +68,7 @@
   @Override
   protected boolean isIgnored(FrameworkMethod child) {
     // Do not run VM tests if running VMs is not even supported.
-    if (!ToolHelper.artSupported()) {
+    if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
       return true;
     }
     if (super.isIgnored(child)) {
diff --git a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
index a24e95a..7d88649 100644
--- a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import java.nio.file.Path;
 import org.junit.Rule;
 import org.junit.Test;
@@ -25,6 +26,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Test
   public void test() throws Exception {
     ProcessResult runInput =
diff --git a/src/test/java/com/android/tools/r8/cf/IdenticalCatchHandlerTest.java b/src/test/java/com/android/tools/r8/cf/IdenticalCatchHandlerTest.java
new file mode 100644
index 0000000..fd696bc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/IdenticalCatchHandlerTest.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2018, 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.cf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexCode.Try;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import java.nio.file.Paths;
+import java.util.Set;
+import org.junit.Test;
+
+public class IdenticalCatchHandlerTest extends TestBase {
+
+  private static class TestClass {
+    public void foo(Object a, Object b, Object c) {
+      if (a == null) {
+        try {
+          System.out.println(b.toString());
+        } catch (RuntimeException e) {
+        }
+      } else {
+        try {
+          System.out.println(c.toString());
+        } catch (RuntimeException e) {
+        }
+      }
+      System.out.println("Hello");
+    }
+  }
+
+  @Test
+  public void test() throws Exception {
+    byte[] inputBytes = ToolHelper.getClassAsBytes(TestClass.class);
+    AndroidApp inputApp =
+        AndroidApp.builder()
+            .addClassProgramData(inputBytes, Origin.unknown())
+            .addLibraryFiles(Paths.get(ToolHelper.JAVA_8_RUNTIME))
+            .build();
+    assertEquals(2, countCatchHandlers(inputApp));
+    AndroidApp outputDexApp = ToolHelper.runR8(inputApp);
+    assertEquals(1, countCatchHandlers(outputDexApp));
+    AndroidApp outputCfApp =
+        ToolHelper.runR8WithProgramConsumer(inputApp, ClassFileConsumer.emptyConsumer());
+    // TODO(b/80514966): Change to assertEquals when fixed.
+    assertNotEquals(1, countCatchHandlers(outputCfApp));
+  }
+
+  private int countCatchHandlers(AndroidApp inputApp) throws Exception {
+    DexInspector inspector = new DexInspector(inputApp, o -> o.enableCfFrontend = true);
+    DexClass dexClass = inspector.clazz(TestClass.class).getDexClass();
+    Code code = dexClass.virtualMethods()[0].getCode();
+    if (code.isCfCode()) {
+      CfCode cfCode = code.asCfCode();
+      Set<CfLabel> targets = Sets.newIdentityHashSet();
+      for (CfTryCatch tryCatch : cfCode.getTryCatchRanges()) {
+        targets.addAll(tryCatch.targets);
+      }
+      return targets.size();
+    } else if (code.isDexCode()) {
+      DexCode dexCode = code.asDexCode();
+      IntSet targets = new IntOpenHashSet();
+      for (Try aTry : dexCode.tries) {
+        targets.add(aTry.handlerOffset);
+      }
+      return targets.size();
+    } else {
+      throw new Unimplemented(code.getClass().getName());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
index e43ffd7..6069b4c 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -31,6 +31,7 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
+
 @RunWith(Parameterized.class)
 public class MethodHandleTestRunner extends TestBase {
   static final Class<?> CLASS = MethodHandleTest.class;
@@ -143,8 +144,8 @@
     if (runInput.exitCode != runDex.exitCode) {
       System.out.println(runDex.stderr);
     }
-    assertEquals(runInput.stdout, runDex.stdout);
     assertEquals(runInput.exitCode, runDex.exitCode);
+    assertEquals(runInput.stdout, runDex.stdout);
   }
 
   private void build(ProgramConsumer programConsumer) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 25356ae..d41ee84 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongList;
@@ -113,6 +114,9 @@
   @Rule
   public TestName testName = new TestName();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   protected static final boolean supportsDefaultMethod(DebugTestConfig config) {
     return config.isCfRuntime()
         || ToolHelper.getMinApiLevelForDexVm().getLevel() >= AndroidApiLevel.N.getLevel();
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
index d0ad1df..6f85b18 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -31,6 +32,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   static AndroidApp compileWithD8(Class... classes) throws IOException, CompilationFailedException {
     D8Command.Builder builder = D8Command.builder();
     for (Class clazz : classes) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
index 6760bdd..cb6df88 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
@@ -35,7 +35,7 @@
   }
 
   private void buildAndTest(String folder, String mainClass) throws Exception {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
 
     Path proguardRules = Paths.get(ToolHelper.EXAMPLES_DIR, folder, "keep-rules.txt");
     Path jarFile =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index e9319d2..d36e6ad 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -16,6 +16,11 @@
 import com.android.tools.r8.VmTestRunner;
 import com.android.tools.r8.code.NewInstance;
 import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.ir.optimize.classinliner.builders.BuildersTestClass;
+import com.android.tools.r8.ir.optimize.classinliner.builders.ControlFlow;
+import com.android.tools.r8.ir.optimize.classinliner.builders.Pair;
+import com.android.tools.r8.ir.optimize.classinliner.builders.PairBuilder;
+import com.android.tools.r8.ir.optimize.classinliner.builders.Tuple;
 import com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal;
 import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB;
 import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceBA;
@@ -75,8 +80,7 @@
         collectNewInstanceTypes(clazz, "testConstructorMapping1"));
 
     assertEquals(
-        Collections.singleton(
-            "com.android.tools.r8.ir.optimize.classinliner.trivial.ReferencedFields"),
+        Collections.emptySet(),
         collectNewInstanceTypes(clazz, "testConstructorMapping2"));
 
     assertEquals(
@@ -120,6 +124,60 @@
     assertFalse(inspector.clazz(CycleReferenceBA.class).isPresent());
   }
 
+  @Test
+  public void testBuilders() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(BuildersTestClass.class),
+        ToolHelper.getClassAsBytes(BuildersTestClass.Pos.class),
+        ToolHelper.getClassAsBytes(Tuple.class),
+        ToolHelper.getClassAsBytes(Pair.class),
+        ToolHelper.getClassAsBytes(PairBuilder.class),
+        ToolHelper.getClassAsBytes(ControlFlow.class),
+    };
+    String main = BuildersTestClass.class.getCanonicalName();
+    ProcessResult javaOutput = runOnJava(main, classes);
+    assertEquals(0, javaOutput.exitCode);
+
+    AndroidApp app = runR8(buildAndroidApp(classes), BuildersTestClass.class);
+
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazz = inspector.clazz(BuildersTestClass.class);
+
+    assertEquals(
+        Sets.newHashSet(
+            "com.android.tools.r8.ir.optimize.classinliner.builders.Pair",
+            "java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testSimpleBuilder"));
+
+    // Note that Pair created instances were also inlined in the following method since
+    // we use 'System.out.println(pX.toString())', if we used 'System.out.println(pX)'
+    // as in the above method, the instance of pair would be passed to println() which
+    // would make it not eligible for inlining.
+    assertEquals(
+        Collections.singleton("java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testSimpleBuilderWithMultipleBuilds"));
+
+    assertFalse(inspector.clazz(PairBuilder.class).isPresent());
+
+    assertEquals(
+        Collections.singleton("java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testBuilderConstructors"));
+
+    assertFalse(inspector.clazz(Tuple.class).isPresent());
+
+    assertEquals(
+        Collections.singleton("java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testWithControlFlow"));
+
+    assertFalse(inspector.clazz(ControlFlow.class).isPresent());
+
+    assertEquals(
+        Collections.emptySet(),
+        collectNewInstanceTypes(clazz, "testWithMoreControlFlow"));
+
+    assertFalse(inspector.clazz(BuildersTestClass.Pos.class).isPresent());
+  }
+
   private Set<String> collectNewInstanceTypes(
       ClassSubject clazz, String methodName, String... params) {
     assertNotNull(clazz);
@@ -135,7 +193,11 @@
         + "-dontobfuscate\n"
         + "-allowaccessmodification";
 
-    AndroidApp compiled = compileWithR8(app, config, o -> o.enableClassInlining = true);
+    AndroidApp compiled = compileWithR8(app, config,
+        o -> {
+          o.enableClassInlining = true;
+          o.classInliningInstructionLimit = 10000;
+        });
 
     // Materialize file for execution.
     Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/BuildersTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/BuildersTestClass.java
new file mode 100644
index 0000000..1516e14
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/BuildersTestClass.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2018, 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.classinliner.builders;
+
+public class BuildersTestClass {
+  private static int ID = 0;
+
+  private static int nextInt() {
+    return ID++;
+  }
+
+  private static String next() {
+    return Integer.toString(nextInt());
+  }
+
+  public static void main(String[] args) {
+    BuildersTestClass test = new BuildersTestClass();
+    test.testSimpleBuilder();
+    test.testSimpleBuilderWithMultipleBuilds();
+    test.testBuilderConstructors();
+    test.testWithControlFlow();
+    test.testWithMoreControlFlow();
+  }
+
+  private synchronized void testSimpleBuilder() {
+    System.out.println(
+        new PairBuilder<String, String>().setFirst("f-" + next()).build().toString());
+    testSimpleBuilder2();
+    testSimpleBuilder3();
+  }
+
+  private synchronized void testSimpleBuilder2() {
+    System.out.println(
+        new PairBuilder<String, String>().setSecond("s-" + next()).build().toString());
+  }
+
+  private synchronized void testSimpleBuilder3() {
+    System.out.println(new PairBuilder<String, String>()
+        .setFirst("f-" + next()).setSecond("s-" + next()).build().toString());
+  }
+
+  private synchronized void testSimpleBuilderWithMultipleBuilds() {
+    PairBuilder<String, String> builder = new PairBuilder<>();
+    Pair p1 = builder.build();
+    System.out.println(p1.toString());
+    builder.setFirst("f-" + next());
+    Pair p2 = builder.build();
+    System.out.println(p2.toString());
+    builder.setSecond("s-" + next());
+    Pair p3 = builder.build();
+    System.out.println(p3.toString());
+  }
+
+  private synchronized void testBuilderConstructors() {
+    System.out.println(new Tuple().toString());
+    System.out.println(new Tuple(true, (byte) 77, (short) 9977, '#', 42,
+        987654321123456789L, -12.34f, 43210.98765, "s-" + next() + "-s").toString());
+  }
+
+  private synchronized void testWithControlFlow() {
+    ControlFlow flow = new ControlFlow(-1, 2, 7);
+    for (int k = 0; k < 25; k++) {
+      if (k % 3 == 0) {
+        flow.foo(k);
+      } else if (k % 3 == 1) {
+        flow.bar(nextInt(), nextInt(), nextInt(), nextInt());
+      }
+    }
+    System.out.println("flow = " + flow.toString());
+  }
+
+  private synchronized void testWithMoreControlFlow() {
+    String str = "1234567890";
+    Pos pos = new Pos();
+    while (pos.y < str.length()) {
+      pos.x = pos.y;
+      pos.y = pos.x;
+
+      if (str.charAt(pos.x) != '*') {
+        if ('0' <= str.charAt(pos.y) && str.charAt(pos.y) <= '9') {
+          while (pos.y < str.length() && '0' <= str.charAt(pos.y) && str.charAt(pos.y) <= '9') {
+            pos.y++;
+          }
+        }
+      }
+    }
+  }
+
+  public static class Pos {
+    public int x = 0;
+    public int y = 0;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/ControlFlow.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/ControlFlow.java
new file mode 100644
index 0000000..7b95dc1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/ControlFlow.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2018, 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.classinliner.builders;
+
+public class ControlFlow {
+  int a;
+  int b;
+  int c = 1234;
+  int d;
+  String s = ">";
+
+  ControlFlow(int b, int c, int d) {
+    this.s += this.a++ + ">";
+    this.s += this.b + ">";
+    this.b = b;
+    this.s += this.b + ">";
+    this.s += this.c + ">";
+    this.c += c;
+    this.s += this.c + ">";
+    this.s += (this.d = d) + ">";
+  }
+
+  public void foo(int count) {
+    for (int i = 0; i < count; i++) {
+      switch (i % 4) {
+        case 0:
+          this.s += ++this.a + ">";
+          break;
+        case 1:
+          this.c += this.b;
+          this.s += this.c + ">";
+          break;
+        case 2:
+          this.d += this.d++ + this.c++ + this.b++ + this.a++;
+          this.s += this.d + ">";
+          break;
+      }
+    }
+  }
+
+  public void bar(int a, int b, int c, int d) {
+    this.a += a;
+    this.b += b;
+    this.c += c;
+    this.d += d;
+  }
+
+  @Override
+  public String toString() {
+    return s;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Pair.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Pair.java
new file mode 100644
index 0000000..af45cab
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Pair.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2018, 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.classinliner.builders;
+
+public class Pair<F, S> {
+  public final F first;
+  public final S second;
+
+  public Pair(F first, S second) {
+    this.first = first;
+    this.second = second;
+  }
+
+  @Override
+  public String toString() {
+    return "Pair(" +
+        (first == null ? "<null>" : first) + ", " +
+        (second == null ? "<null>" : second) + ")";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/PairBuilder.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/PairBuilder.java
new file mode 100644
index 0000000..0c80c53
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/PairBuilder.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2018, 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.classinliner.builders;
+
+public class PairBuilder<F, S> {
+  public F first;
+  public S second = null;
+
+  public PairBuilder<F, S> setFirst(F first) {
+    System.out.println("[before] first = " + this.first);
+    this.first = first;
+    System.out.println("[after] first = " + this.first);
+    return this;
+  }
+
+  public PairBuilder<F, S> setSecond(S second) {
+    System.out.println("[before] second = " + this.second);
+    this.second = second;
+    System.out.println("[after] second = " + this.second);
+    return this;
+  }
+
+  public Pair<F, S> build() {
+    return new Pair<>(first, second);
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Tuple.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Tuple.java
new file mode 100644
index 0000000..3a77cc2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Tuple.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2018, 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.classinliner.builders;
+
+public class Tuple {
+  public boolean z;
+  public byte b;
+  public short s;
+  public char c;
+  public int i;
+  public long l;
+  public float f;
+  public double d;
+  public Object o;
+
+  Tuple() {
+  }
+
+  Tuple(boolean z, byte b, short s, char c, int i, long l, float f, double d, Object o) {
+    this.z = z;
+    this.b = b;
+    this.s = s;
+    this.c = c;
+    this.i = i;
+    this.l = l;
+    this.f = f;
+    this.d = d;
+    this.o = o;
+  }
+
+  @Override
+  public String toString() {
+    return "Tuple1(" + z + ", " + b + ", " + s + ", " +
+        ((int) c) + ", " + i + ", " + l + ", " + f + ", " +
+        d + ", " + (o == null ? "<null>" : o) + ")";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
index 57d694d..86c15a2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
@@ -87,4 +87,4 @@
     assertEquals(0, artOutput.exitCode);
     assertEquals(javaOutput.stdout.trim(), artOutput.stdout.trim());
   }
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java b/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
index bdedf43..3028525 100644
--- a/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
+++ b/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.io.File;
@@ -30,6 +31,7 @@
 import org.junit.Assume;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
@@ -267,6 +269,9 @@
   @ClassRule
   public static TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @BeforeClass
   public static void compileLibraries() throws Exception {
     // Selects appropriate jar according to min api level for the selected runtime.
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index c175e04..f99934e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -223,7 +223,7 @@
 
   protected void runTest(String folder, String mainClass, String extraProguardRules,
       Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
-    Assume.assumeTrue(ToolHelper.artSupported());
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
 
     String proguardRules = buildProguardRules(mainClass);
     if (extraProguardRules != null) {
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index c8c8aec..73d1e50 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -10,7 +10,9 @@
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collections;
+import java.util.function.Consumer;
 import org.junit.Test;
 
 public class R8KotlinDataClassTest extends AbstractR8KotlinTestBase {
@@ -33,13 +35,15 @@
   private static final MethodSignature COPY_DEFAULT_METHOD =
       TEST_DATA_CLASS.getCopyDefaultSignature();
 
+  private Consumer<InternalOptions> disableClassInliner = o -> o.enableClassInlining = false;
+
   @Test
   public void test_dataclass_gettersOnly() throws Exception {
     final String mainClassName = "dataclass.MainGettersOnlyKt";
     final MethodSignature testMethodSignature =
         new MethodSignature("testDataClassGetters", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, (app) -> {
+    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
 
@@ -74,7 +78,7 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testAllDataClassComponentFunctions", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, (app) -> {
+    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
 
@@ -109,7 +113,7 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testSomeDataClassComponentFunctions", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, (app) -> {
+    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
 
@@ -144,7 +148,7 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testDataClassCopy", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, (app) -> {
+    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
 
@@ -159,7 +163,7 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testDataClassCopyWithDefault", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, (app) -> {
+    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
 
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
index e2dfb6d..46a439d 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -26,6 +27,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -60,6 +62,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   public MemberRebindingTest(TestConfiguration configuration) {
     this.kind = configuration.kind;
     originalDex = configuration.getDexPath();
@@ -319,9 +324,8 @@
 
   @Test
   public void memberRebindingTest() throws IOException, InterruptedException, ExecutionException {
-    if (!ToolHelper.artSupported()) {
-      return;
-    }
+    Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
     String out = temp.getRoot().getCanonicalPath();
     Path processed = Paths.get(out, "classes.dex");
 
diff --git a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
index 2df0612..66161e1 100644
--- a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
+++ b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -23,6 +24,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   @Test
   public void dataResourceTest() throws IOException, CompilationFailedException {
     String packageName = "dataresource";
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index f341969..c9488b4 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.DexInspector.FoundFieldSubject;
 import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -76,6 +77,9 @@
   @Rule
   public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
+  @Rule
+  public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
   protected TreeShakingTest(
       String name, String mainClass, Frontend frontend, Backend backend, MinifyMode minify) {
     this.name = name;
@@ -204,7 +208,7 @@
       }
       return;
     }
-    if (!ToolHelper.artSupported()) {
+    if (!ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
       return;
     }
     Consumer<ArtCommandBuilder> extraArtArgs = builder -> {
diff --git a/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java b/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java
new file mode 100644
index 0000000..5295c0e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2018, 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.utils;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * This {@link TestWatcher} passes test description information
+ * (namely test class and method names) to the test framework
+ * via system properties.
+ */
+public class TestDescriptionWatcher extends TestWatcher {
+
+  @Override
+  protected void starting(Description description) {
+    System.setProperty("test_class_name", description.getClassName());
+    System.setProperty("test_name", description.getMethodName());
+    System.setProperty("reset_output_index", "true");
+  }
+
+  @Override
+  protected void finished(Description description) {
+    System.clearProperty("test_class_name");
+    System.clearProperty("test_name");
+  }
+
+}
diff --git a/tools/download_from_x20.py b/tools/download_from_x20.py
index 01ba4e1..5f64684 100755
--- a/tools/download_from_x20.py
+++ b/tools/download_from_x20.py
@@ -19,17 +19,10 @@
 def parse_options():
   return optparse.OptionParser().parse_args()
 
-def extract_dir(filename):
-  return filename[0:len(filename) - len('.tar.gz')]
-
-def unpack_archive(filename):
-  dest_dir = extract_dir(filename)
-  if os.path.exists(dest_dir):
-    print 'Deleting existing dir %s' % dest_dir
-    shutil.rmtree(dest_dir)
-  dirname = os.path.dirname(os.path.abspath(filename))
-  with tarfile.open(filename, 'r:gz') as tar:
-    tar.extractall(path=dirname)
+def download(src, dest):
+  print 'Downloading %s to %s' % (src, dest)
+  shutil.copyfile(src, dest)
+  utils.unpack_archive(dest)
 
 def Main():
   (options, args) = parse_options()
@@ -41,20 +34,18 @@
     sha1 = input_sha.readline()
   if os.path.exists(dest) and utils.get_sha1(dest) == sha1:
     print 'sha1 matches, not downloading'
-    dest_dir = extract_dir(dest)
+    dest_dir = utils.extract_dir(dest)
     if os.path.exists(dest_dir):
       print 'destination directory exists, no extraction'
     else:
-      unpack_archive(dest)
+      utils.unpack_archive(dest)
     return
   src = os.path.join(GMSCORE_DEPS, sha1)
   if not os.path.exists(src):
     print 'File (%s) does not exist on x20' % src
     print 'Maybe pass -Pno_internal to your gradle invocation'
     return 42
-  print 'Downloading %s to %s' % (src, dest)
-  shutil.copyfile(src, dest)
-  unpack_archive(dest)
+  download(src, dest)
 
 if __name__ == '__main__':
   sys.exit(Main())
diff --git a/tools/test.py b/tools/test.py
index ed99c1d..b92c8dc 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -15,6 +15,7 @@
 import utils
 import uuid
 import notify
+import upload_to_x20
 
 
 ALL_ART_VMS = ["default", "7.0.0", "6.0.1", "5.1.1", "4.4.4", "4.0.4"]
@@ -78,6 +79,13 @@
           ' Note that the directory will not be cleared before the test.')
   result.add_option('--java_home',
       help='Use a custom java version to run tests.')
+  result.add_option('--generate_golden_files_to',
+      help='Store dex files produced by tests in the specified directory.'
+           ' It is aimed to be read on platforms with no host runtime available'
+           ' for comparison.')
+  result.add_option('--use_golden_files_in',
+      help='Download golden files hierarchy for this commit in the specified'
+           ' location and use them instead of executing on host runtime.')
 
   return result.parse_args()
 
@@ -93,7 +101,7 @@
 def Main():
   (options, args) = ParseOptions()
 
-  gradle_args = []
+  gradle_args = ['--stacktrace']
   # Set all necessary Gradle properties and options first.
   if options.verbose:
     gradle_args.append('-Pprint_test_stdout')
@@ -134,7 +142,16 @@
       os.makedirs(options.test_dir)
   if options.java_home:
     gradle_args.append('-Dorg.gradle.java.home=' + options.java_home)
-
+  if options.generate_golden_files_to:
+    gradle_args.append('-Pgenerate_golden_files_to=' + options.generate_golden_files_to)
+    if not os.path.exists(options.generate_golden_files_to):
+      os.makedirs(options.generate_golden_files_to)
+    gradle_args.append('-PHEAD_sha1=' + utils.get_HEAD_sha1())
+  if options.use_golden_files_in:
+    gradle_args.append('-Puse_golden_files_in=' + options.use_golden_files_in)
+    if not os.path.exists(options.use_golden_files_in):
+      os.makedirs(options.use_golden_files_in)
+    gradle_args.append('-PHEAD_sha1=' + utils.get_HEAD_sha1())
   # Add Gradle tasks
   gradle_args.append('cleanTest')
   gradle_args.append('test')
@@ -146,12 +163,29 @@
     # Create Jacoco report after tests.
     gradle_args.append('jacocoTestReport')
 
+  if options.use_golden_files_in:
+    sha1 = '%s' % utils.get_HEAD_sha1()
+    with utils.ChangedWorkingDirectory(options.use_golden_files_in):
+      utils.download_file_from_cloud_storage(
+                                    'gs://r8-test-results/golden-files/%s.tar.gz' % sha1,
+                                    '%s.tar.gz' % sha1)
+      utils.unpack_archive('%s.tar.gz' % sha1)
+
+
   # Now run tests on selected runtime(s).
   vms_to_test = [options.dex_vm] if options.dex_vm != "all" else ALL_ART_VMS
   for art_vm in vms_to_test:
     vm_kind_to_test = "_" + options.dex_vm_kind if art_vm != "default" else ""
     return_code = gradle.RunGradle(gradle_args + ['-Pdex_vm=%s' % (art_vm + vm_kind_to_test)],
                                    throw_on_failure=False)
+
+    if options.generate_golden_files_to:
+      sha1 = '%s' % utils.get_HEAD_sha1()
+      with utils.ChangedWorkingDirectory(options.generate_golden_files_to):
+        archive = utils.create_archive(sha1)
+        utils.upload_file_to_cloud_storage(archive,
+                                           'gs://r8-test-results/golden-files/' + archive)
+
     if return_code != 0:
       if options.archive_failures and os.name != 'nt':
         archive_failures()
diff --git a/tools/upload_to_x20.py b/tools/upload_to_x20.py
index 1efacf5..2d08362 100755
--- a/tools/upload_to_x20.py
+++ b/tools/upload_to_x20.py
@@ -20,11 +20,10 @@
 def parse_options():
   return optparse.OptionParser().parse_args()
 
-def create_archive(name):
-  tarname = '%s.tar.gz' % name
-  with tarfile.open(tarname, 'w:gz') as tar:
-    tar.add(name)
-  return tarname
+def uploadFile(filename, dest):
+  print 'Uploading to %s' % dest
+  shutil.copyfile(filename, dest)
+  subprocess.check_call(['chmod', '664', dest])
 
 def Main():
   (options, args) = parse_options()
@@ -34,12 +33,10 @@
   if not name in os.listdir('.'):
     print 'You must be standing directly below the directory you are uploading'
     return 1
-  filename = create_archive(name)
+  filename = utils.create_archive(name)
   sha1 = utils.get_sha1(filename)
   dest = os.path.join(GMSCORE_DEPS, sha1)
-  print 'Uploading to %s' % dest
-  shutil.copyfile(filename, dest)
-  subprocess.check_call(['chmod', '664', dest])
+  uploadFile(filename, dest)
   sha1_file = '%s.sha1' % filename
   with open(sha1_file, 'w') as output:
     output.write(sha1)
diff --git a/tools/utils.py b/tools/utils.py
index 3aab572..859da97 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -10,6 +10,7 @@
 import shutil
 import subprocess
 import sys
+import tarfile
 import tempfile
 
 TOOLS_DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..')))
@@ -71,6 +72,12 @@
       sha1.update(chunk)
   return sha1.hexdigest()
 
+def get_HEAD_sha1():
+  cmd = ['git', 'rev-parse', 'HEAD']
+  PrintCmd(cmd)
+  with ChangedWorkingDirectory(REPO_ROOT):
+    return subprocess.check_output(cmd).strip()
+
 def makedirs_if_needed(path):
   try:
     os.makedirs(path)
@@ -92,6 +99,29 @@
   PrintCmd(cmd)
   subprocess.check_call(cmd)
 
+def download_file_from_cloud_storage(source, destination):
+  cmd = ['gsutil.py', 'cp', source, destination]
+  PrintCmd(cmd)
+  subprocess.check_call(cmd)
+
+def create_archive(name):
+  tarname = '%s.tar.gz' % name
+  with tarfile.open(tarname, 'w:gz') as tar:
+    tar.add(name)
+  return tarname
+
+def extract_dir(filename):
+  return filename[0:len(filename) - len('.tar.gz')]
+
+def unpack_archive(filename):
+  dest_dir = extract_dir(filename)
+  if os.path.exists(dest_dir):
+    print 'Deleting existing dir %s' % dest_dir
+    shutil.rmtree(dest_dir)
+  dirname = os.path.dirname(os.path.abspath(filename))
+  with tarfile.open(filename, 'r:gz') as tar:
+    tar.extractall(path=dirname)
+
 # Note that gcs is eventually consistent with regards to list operations.
 # This is not a problem in our case, but don't ever use this method
 # for synchronization.