Merge "Change ExternalR8TestBuilder to use platform dependent path separator"
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index 50c91eb..abcf374 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -300,23 +300,38 @@
     private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
 
     public void map(DexType from, DexType to) {
+      if (from == to) {
+        return;
+      }
       typeMap.put(from, to);
     }
 
     public void map(DexMethod from, DexMethod to) {
+      if (from == to) {
+        return;
+      }
       methodMap.put(from, to);
     }
 
     public void map(DexField from, DexField to) {
+      if (from == to) {
+        return;
+      }
       fieldMap.put(from, to);
     }
 
     public void move(DexMethod from, DexMethod to) {
+      if (from == to) {
+        return;
+      }
       map(from, to);
       originalMethodSignatures.put(to, from);
     }
 
     public void move(DexField from, DexField to) {
+      if (from == to) {
+        return;
+      }
       fieldMap.put(from, to);
       originalFieldSignatures.put(to, from);
     }
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index 85cde7c..36df23e 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -214,9 +214,13 @@
     triggerDelayedParsingIfNeccessary();
     node.instructions.accept(
         new JarRegisterEffectsVisitor(method.getHolder(), registry, application));
-    node.tryCatchBlocks.forEach(tryCatchBlockNode ->
+    for (TryCatchBlockNode tryCatchBlockNode : node.tryCatchBlocks) {
+      // Exception type can be null for "catch all" used for try/finally.
+      if (tryCatchBlockNode.type != null) {
         registry.registerTypeReference(application.getTypeFromDescriptor(
-            DescriptorUtils.getDescriptorFromClassBinaryName(tryCatchBlockNode.type))));
+            DescriptorUtils.getDescriptorFromClassBinaryName(tryCatchBlockNode.type)));
+      }
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 56dc071..2499ccb 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -288,6 +288,7 @@
    * @return a class descriptor i.e. "Ljava/lang/Object;"
    */
   public static String getDescriptorFromClassBinaryName(String typeBinaryName) {
+    assert typeBinaryName != null;
     return ('L' + typeBinaryName + ';');
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 61bf1b3..c67c05c 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -3,16 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
-
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.utils.ListUtils;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
 
 public abstract class TestBuilder<RR extends TestRunResult, T extends TestBuilder<RR, T>> {
 
@@ -92,14 +88,6 @@
   }
 
   static Collection<Path> getFilesForInnerClasses(Collection<Class<?>> classes) throws IOException {
-    Set<Path> paths = new HashSet<>();
-    for (Class clazz : classes) {
-      Path path = ToolHelper.getClassFileForTestClass(clazz);
-      String prefix = path.toString().replace(CLASS_EXTENSION, "$");
-      paths.addAll(
-          ToolHelper.getClassFilesForTestDirectory(
-              path.getParent(), p -> p.toString().startsWith(prefix)));
-    }
-    return paths;
+    return ToolHelper.getClassFilesForInnerClasses(classes);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 0c84e7c..9700d56 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -4,9 +4,6 @@
 package com.android.tools.r8;
 
 import static com.android.tools.r8.TestBase.Backend.DEX;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.fail;
 
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.ToolHelper.DexVm;
@@ -83,57 +80,27 @@
   }
 
   public CR assertNoMessages() {
-    assertEquals(0, getDiagnosticMessages().getInfos().size());
-    assertEquals(0, getDiagnosticMessages().getWarnings().size());
-    assertEquals(0, getDiagnosticMessages().getErrors().size());
+    getDiagnosticMessages().assertNoMessages();
     return self();
   }
 
   public CR assertOnlyInfos() {
-    assertNotEquals(0, getDiagnosticMessages().getInfos().size());
-    assertEquals(0, getDiagnosticMessages().getWarnings().size());
-    assertEquals(0, getDiagnosticMessages().getErrors().size());
+    getDiagnosticMessages().assertOnlyInfos();
     return self();
   }
 
   public CR assertOnlyWarnings() {
-    assertEquals(0, getDiagnosticMessages().getInfos().size());
-    assertNotEquals(0, getDiagnosticMessages().getWarnings().size());
-    assertEquals(0, getDiagnosticMessages().getErrors().size());
+    getDiagnosticMessages().assertOnlyWarnings();
     return self();
   }
 
   public CR assertWarningMessageThatMatches(Matcher<String> matcher) {
-    assertNotEquals(0, getDiagnosticMessages().getWarnings().size());
-    for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) {
-      if (matcher.matches(getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage())) {
-        return self();
-      }
-    }
-    StringBuilder builder = new StringBuilder("No warning matches " + matcher.toString());
-    builder.append(System.lineSeparator());
-    if (getDiagnosticMessages().getWarnings().size() == 0) {
-      builder.append("There where no warnings.");
-    } else {
-      builder.append("There where " + getDiagnosticMessages().getWarnings().size() + " warnings:");
-      builder.append(System.lineSeparator());
-      for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) {
-        builder.append(getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage());
-        builder.append(System.lineSeparator());
-      }
-    }
-    fail(builder.toString());
+    getDiagnosticMessages().assertWarningMessageThatMatches(matcher);
     return self();
   }
 
   public CR assertNoWarningMessageThatMatches(Matcher<String> matcher) {
-    assertNotEquals(0, getDiagnosticMessages().getWarnings().size());
-    for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) {
-      String message = getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage();
-      if (matcher.matches(message)) {
-        fail("The warning: \"" + message + "\" + matches " + matcher + ".");
-      }
-    }
+    getDiagnosticMessages().assertNoWarningMessageThatMatches(matcher);
     return self();
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java
index 3e734cc..d1e55f4 100644
--- a/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java
+++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java
@@ -5,6 +5,8 @@
 package com.android.tools.r8;
 
 import java.util.List;
+import org.hamcrest.Matcher;
+
 
 public interface TestDiagnosticMessages {
 
@@ -13,4 +15,20 @@
   public List<Diagnostic> getWarnings();
 
   public List<Diagnostic> getErrors();
+
+  public TestDiagnosticMessages assertNoMessages();
+
+  public TestDiagnosticMessages assertOnlyInfos();
+
+  public TestDiagnosticMessages assertOnlyWarnings();
+
+  public TestDiagnosticMessages assertInfosCount(int count);
+
+  public TestDiagnosticMessages assertWarningsCount(int count);
+
+  public TestDiagnosticMessages assertErrorsCount(int count);
+
+  public TestDiagnosticMessages assertWarningMessageThatMatches(Matcher<String> matcher);
+
+  public TestDiagnosticMessages assertNoWarningMessageThatMatches(Matcher<String> matcher);
 }
diff --git a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
index d0476b9..43d71b1 100644
--- a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
+++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
@@ -4,8 +4,13 @@
 
 package com.android.tools.r8;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+
 import java.util.ArrayList;
 import java.util.List;
+import org.hamcrest.Matcher;
 
 public class TestDiagnosticMessagesImpl implements DiagnosticsHandler, TestDiagnosticMessages {
   private final List<Diagnostic> infos = new ArrayList<>();
@@ -38,4 +43,75 @@
   public List<Diagnostic> getErrors() {
     return errors;
   }
+
+
+  public TestDiagnosticMessages assertNoMessages() {
+    assertEquals(0, getInfos().size());
+    assertEquals(0, getWarnings().size());
+    assertEquals(0, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertOnlyInfos() {
+    assertNotEquals(0, getInfos().size());
+    assertEquals(0, getWarnings().size());
+    assertEquals(0, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertOnlyWarnings() {
+    assertEquals(0, getInfos().size());
+    assertNotEquals(0, getWarnings().size());
+    assertEquals(0, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertInfosCount(int count) {
+    assertEquals(count, getInfos().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertWarningsCount(int count) {
+    assertEquals(count, getWarnings().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertErrorsCount(int count) {
+    assertEquals(count, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertWarningMessageThatMatches(Matcher<String> matcher) {
+    assertNotEquals(0, getWarnings().size());
+    for (int i = 0; i < getWarnings().size(); i++) {
+      if (matcher.matches(getWarnings().get(i).getDiagnosticMessage())) {
+        return this;
+      }
+    }
+    StringBuilder builder = new StringBuilder("No warning matches " + matcher.toString());
+    builder.append(System.lineSeparator());
+    if (getWarnings().size() == 0) {
+      builder.append("There where no warnings.");
+    } else {
+      builder.append("There where " + getWarnings().size() + " warnings:");
+      builder.append(System.lineSeparator());
+      for (int i = 0; i < getWarnings().size(); i++) {
+        builder.append(getWarnings().get(i).getDiagnosticMessage());
+        builder.append(System.lineSeparator());
+      }
+    }
+    fail(builder.toString());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertNoWarningMessageThatMatches(Matcher<String> matcher) {
+    assertNotEquals(0, getWarnings().size());
+    for (int i = 0; i < getWarnings().size(); i++) {
+      String message = getWarnings().get(i).getDiagnosticMessage();
+      if (matcher.matches(message)) {
+        fail("The warning: \"" + message + "\" + matches " + matcher + ".");
+      }
+    }
+    return this;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index c9fe628..8fe189b 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -57,6 +58,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -856,6 +858,28 @@
         Paths.get("", parts.toArray(new String[parts.size() - 1])));
   }
 
+  public static Collection<Path> getClassFilesForInnerClasses(Path path) throws IOException {
+    Set<Path> paths = new HashSet<>();
+    String prefix = path.toString().replace(CLASS_EXTENSION, "$");
+    paths.addAll(
+        ToolHelper.getClassFilesForTestDirectory(
+            path.getParent(), p -> p.toString().startsWith(prefix)));
+    return paths;
+  }
+
+  public static Collection<Path> getClassFilesForInnerClasses(Collection<Class<?>> classes)
+      throws IOException {
+    Set<Path> paths = new HashSet<>();
+    for (Class clazz : classes) {
+      Path path = ToolHelper.getClassFileForTestClass(clazz);
+      String prefix = path.toString().replace(CLASS_EXTENSION, "$");
+      paths.addAll(
+          ToolHelper.getClassFilesForTestDirectory(
+              path.getParent(), p -> p.toString().startsWith(prefix)));
+    }
+    return paths;
+  }
+
   public static Path getFileNameForTestClass(Class clazz) {
     List<String> parts = getNamePartsForTestClass(clazz);
     return Paths.get("", parts.toArray(new String[parts.size() - 1]));
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
new file mode 100644
index 0000000..767e818
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.kotlin;
+
+import com.android.tools.r8.KotlinTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ProcessKotlinReflectionLibTest extends KotlinTestBase {
+  private final Backend backend;
+
+  public ProcessKotlinReflectionLibTest(Backend backend, KotlinTargetVersion targetVersion) {
+    super(targetVersion);
+    this.backend = backend;
+  }
+
+  @Parameterized.Parameters(name = "Backend: {0} target: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(Backend.values(), KotlinTargetVersion.values());
+  }
+
+  @Test
+  public void testDontShrinkAndDontObfuscate() throws Exception {
+    testForR8(backend)
+        .addLibraryFiles(ToolHelper.getDefaultAndroidJar(), ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addKeepRules("-dontshrink")
+        .addKeepRules("-dontobfuscate")
+        .compile();
+  }
+
+  @Test
+  public void testDontShrink() throws Exception {
+    testForR8(backend)
+        .addLibraryFiles(ToolHelper.getDefaultAndroidJar(), ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addKeepRules("-dontshrink")
+        .compile();
+  }
+
+  @Test
+  public void testDontObfuscate() throws Exception {
+    testForR8(backend)
+        .addLibraryFiles(ToolHelper.getDefaultAndroidJar(), ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addKeepRules("-dontobfuscate")
+        .compile();
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
new file mode 100644
index 0000000..44047d9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.kotlin;
+
+import com.android.tools.r8.KotlinTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ProcessKotlinStdlibTest extends KotlinTestBase {
+  private final Backend backend;
+
+  public ProcessKotlinStdlibTest(Backend backend, KotlinTargetVersion targetVersion) {
+    super(targetVersion);
+    this.backend = backend;
+  }
+
+  @Parameterized.Parameters(name = "Backend: {0} target: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(Backend.values(), KotlinTargetVersion.values());
+  }
+
+  @Test
+  public void testDontShrinkAndDontObfuscate() throws Exception {
+    testForR8(backend)
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addKeepRules("-dontshrink")
+        .addKeepRules("-dontobfuscate")
+        .compile();
+  }
+
+  @Test
+  public void testDontShrink() throws Exception {
+    // TODO(b/122819537)
+    if (backend == Backend.DEX) {
+      return;
+    }
+    testForR8(backend)
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addKeepRules("-dontshrink")
+        .compile();
+  }
+
+  @Test
+  public void testDontObfuscate() throws Exception {
+    testForR8(backend)
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addKeepRules("-dontobfuscate")
+        .compile();
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java
index 1038f5c..b175785 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java
@@ -35,6 +35,7 @@
 import java.util.Iterator;
 import java.util.concurrent.ExecutionException;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -75,6 +76,7 @@
     out = temp.newFolder("out").toPath();
   }
 
+  @Ignore("b/121305642")
   @Test
   public void test044_obfuscate_and_apply() throws Exception {
     // keep rules that allow obfuscations while keeping everything.
@@ -110,7 +112,7 @@
     AndroidApp instrApp =
         runR8(
             ToolHelper.addProguardConfigurationConsumer(
-                    getCommandForInstrumentation(instrOut, flag, NAMING044_JAR, APPLYMAPPING044_JAR)
+                    getCommandForInstrumentation(instrOut, flag, out, APPLYMAPPING044_JAR)
                         .setDisableMinification(true),
                     pgConfig -> pgConfig.setApplyMappingFile(proguardMap))
                 .build());
diff --git a/tools/apk-masseur.py b/tools/apk_masseur.py
similarity index 85%
rename from tools/apk-masseur.py
rename to tools/apk_masseur.py
index 4f24f0f..6013e6f 100755
--- a/tools/apk-masseur.py
+++ b/tools/apk_masseur.py
@@ -36,10 +36,6 @@
   if len(args) != 1:
     parser.error('Expected <apk> argument, got: ' + ' '.join(args))
   apk = args[0]
-  if not options.out:
-    options.out = os.path.basename(apk)
-  if not options.keystore:
-    options.keystore = findKeystore()
   return (options, apk)
 
 def findKeystore():
@@ -81,28 +77,36 @@
   subprocess.check_call(cmd)
   return signed_apk
 
-def main():
-  (options, apk) = parse_options()
+def masseur(
+    apk, dex=None, out=None, adb_options=None, keystore=None, install=False):
+  if not out:
+    out = os.path.basename(apk)
+  if not keystore:
+    keystore = findKeystore()
   with utils.TempDir() as temp:
     processed_apk = None
-    if options.dex:
-      processed_apk = repack(options.dex, apk, temp)
+    if dex:
+      processed_apk = repack(dex, apk, temp)
     else:
       print 'Signing original APK without modifying dex files'
       processed_apk = os.path.join(temp, 'processed.apk')
       shutil.copyfile(apk, processed_apk)
-    signed_apk = sign(processed_apk, options.keystore, temp)
+    signed_apk = sign(processed_apk, keystore, temp)
     aligned_apk = align(signed_apk, temp)
-    print 'Writing result to', options.out
-    shutil.copyfile(aligned_apk, options.out)
+    print 'Writing result to', out
+    shutil.copyfile(aligned_apk, out)
     adb_cmd = ['adb']
-    if options.adb_options:
+    if adb_options:
       adb_cmd.extend(
-          [option for option in options.adb_options.split(' ') if option])
-    if options.install:
-      adb_cmd.extend(['install', '-t', '-r', '-d', options.out]);
+          [option for option in adb_options.split(' ') if option])
+    if install:
+      adb_cmd.extend(['install', '-t', '-r', '-d', out]);
       utils.PrintCmd(adb_cmd)
       subprocess.check_call(adb_cmd)
+
+def main():
+  (options, apk) = parse_options()
+  masseur(apk, **vars(options))
   return 0
 
 if __name__ == '__main__':
diff --git a/tools/as_utils.py b/tools/as_utils.py
index d5db8a3..65ab470 100644
--- a/tools/as_utils.py
+++ b/tools/as_utils.py
@@ -4,6 +4,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 from distutils.version import LooseVersion
+from HTMLParser import HTMLParser
 import os
 import shutil
 
@@ -56,7 +57,8 @@
 
 def remove_r8_dependency(checkout_dir):
   build_file = os.path.join(checkout_dir, 'build.gradle')
-  assert os.path.isfile(build_file), 'Expected a file to be present at {}'.format(build_file)
+  assert os.path.isfile(build_file), (
+      'Expected a file to be present at {}'.format(build_file))
   with open(build_file) as f:
     lines = f.readlines()
   with open(build_file, 'w') as f:
@@ -64,6 +66,54 @@
       if (utils.R8_JAR not in line) and (utils.R8LIB_JAR not in line):
         f.write(line)
 
+def IsGradleTaskName(x):
+  # Check that it is non-empty.
+  if not x:
+    return False
+  # Check that there is no whitespace.
+  for c in x:
+    if c.isspace():
+      return False
+  # Check that the first character following an optional ':' is a lower-case
+  # alphabetic character.
+  c = x[0]
+  if c == ':' and len(x) >= 2:
+    c = x[1]
+  return c.isalpha() and c.islower()
+
+def IsGradleCompilerTask(x, shrinker):
+  if 'r8' in shrinker:
+    assert 'transformClassesWithDexBuilderFor' not in x
+    assert 'transformDexArchiveWithDexMergerFor' not in x
+    return 'transformClassesAndResourcesWithR8For' in x
+
+  assert shrinker == 'proguard'
+  return ('transformClassesAndResourcesWithProguard' in x
+      or 'transformClassesWithDexBuilderFor' in x
+      or 'transformDexArchiveWithDexMergerFor' in x)
+
+def SetPrintConfigurationDirective(app, config, checkout_dir, destination):
+  proguard_config_file = FindProguardConfigurationFile(
+      app, config, checkout_dir)
+  with open(proguard_config_file) as f:
+    lines = f.readlines()
+  with open(proguard_config_file, 'w') as f:
+    for line in lines:
+      if '-printconfiguration' not in line:
+        f.write(line)
+    f.write('-printconfiguration {}\n'.format(destination))
+
+def FindProguardConfigurationFile(app, config, checkout_dir):
+  app_module = config.get('app_module', 'app')
+  candidates = ['proguard-rules.pro', 'proguard-rules.txt', 'proguard.cfg']
+  for candidate in candidates:
+    proguard_config_file = os.path.join(checkout_dir, app_module, candidate)
+    if os.path.isfile(proguard_config_file):
+      return proguard_config_file
+  # Currently assuming that the Proguard configuration file can be found at
+  # one of the predefined locations.
+  assert False
+
 def Move(src, dst):
   print('Moving `{}` to `{}`'.format(src, dst))
   dst_parent = os.path.dirname(dst)
@@ -103,3 +153,60 @@
   html_dir = os.path.dirname(html_file)
   for dir_name in ['css', 'js']:
     MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name))
+
+def ParseProfileReport(profile_dir):
+  html_file = os.path.join(profile_dir, 'index.html')
+  assert os.path.isfile(html_file)
+
+  parser = ProfileReportParser()
+  with open(html_file) as f:
+    for line in f.readlines():
+      parser.feed(line)
+  return parser.result
+
+# A simple HTML parser that recognizes the following pattern:
+#
+# <tr>
+# <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td>
+# <td class="numeric">3.490s</td>
+# <td></td>
+# </tr>
+class ProfileReportParser(HTMLParser):
+  entered_table_row = False
+  entered_task_name_cell = False
+  entered_duration_cell = False
+
+  current_task_name = None
+  current_duration = None
+
+  result = {}
+
+  def handle_starttag(self, tag, attrs):
+    entered_table_row_before = self.entered_table_row
+    entered_task_name_cell_before = self.entered_task_name_cell
+
+    self.entered_table_row = (tag == 'tr')
+    self.entered_task_name_cell = (tag == 'td' and entered_table_row_before)
+    self.entered_duration_cell = (
+        self.current_task_name
+            and tag == 'td'
+            and entered_task_name_cell_before)
+
+  def handle_endtag(self, tag):
+    if tag == 'tr':
+      if self.current_task_name and self.current_duration:
+        self.result[self.current_task_name] = self.current_duration
+      self.current_task_name = None
+      self.current_duration = None
+    self.entered_table_row = False
+
+  def handle_data(self, data):
+    stripped = data.strip()
+    if not stripped:
+      return
+    if self.entered_task_name_cell:
+      if IsGradleTaskName(stripped):
+        self.current_task_name = stripped
+    elif self.entered_duration_cell and stripped.endswith('s'):
+      self.current_duration = float(stripped[:-1])
+    self.entered_table_row = False
diff --git a/tools/run_on_as_app.py b/tools/run_on_as_app.py
index 7c96506..770a651 100755
--- a/tools/run_on_as_app.py
+++ b/tools/run_on_as_app.py
@@ -3,19 +3,21 @@
 # for details. All rights reserved. Use of this source code is governed by a
 # BSD-style license that can be found in the LICENSE file.
 
+import apk_masseur
 import apk_utils
 import gradle
 import os
 import optparse
 import subprocess
 import sys
+import tempfile
 import time
 import utils
 import zipfile
 
 import as_utils
 
-SHRINKERS = ['r8', 'r8full', 'r8-minified', 'r8full-minified', 'proguard']
+SHRINKERS = ['r8', 'r8-minified', 'r8full', 'r8full-minified', 'proguard']
 WORKING_DIR = utils.BUILD
 
 if 'R8_BENCHMARK_DIR' in os.environ and os.path.isdir(os.environ['R8_BENCHMARK_DIR']):
@@ -128,10 +130,35 @@
   subprocess.check_call(
       ['adb', '-s', emulator_id, 'install', '-r', '-d', apk_dest])
 
+def PercentageDiffAsString(before, after):
+  if after < before:
+    return '-' + str(round((1.0 - after / before) * 100)) + '%'
+  else:
+    return '+' + str(round((after - before) / before * 100)) + '%'
+
+def UninstallApkOnEmulator(app, config):
+  app_id = config.get('app_id')
+  process = subprocess.Popen(
+      ['adb', 'uninstall', app_id],
+      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  stdout, stderr = process.communicate()
+
+  if stdout.strip() == 'Success':
+    # Successfully uninstalled
+    return
+
+  if 'Unknown package: {}'.format(app_id) in stderr:
+    # Application not installed
+    return
+
+  raise Exception(
+      'Unexpected result from `adb uninstall {}\nStdout: {}\nStderr: {}'.format(
+          app_id, stdout, stderr))
+
 def WaitForEmulator():
   stdout = subprocess.check_output(['adb', 'devices'])
   if '{}\tdevice'.format(emulator_id) in stdout:
-    return
+    return True
 
   print('Emulator \'{}\' not connected; waiting for connection'.format(
       emulator_id))
@@ -144,9 +171,9 @@
     if '{}\tdevice'.format(emulator_id) not in stdout:
       print('... still waiting for connection')
       if time_waited >= 5 * 60:
-        raise Exception('No emulator connected for 5 minutes')
+        return False
     else:
-      return
+      return True
 
 def GetResultsForApp(app, config, options):
   git_repo = config['git_repo']
@@ -190,23 +217,53 @@
       apk_dest = None
       result = {}
       try:
-        (apk_dest, profile_dest_dir) = BuildAppWithShrinker(
-          app, config, shrinker, checkout_dir, options)
+        (apk_dest, profile_dest_dir, proguard_config_file) = \
+            BuildAppWithShrinker(app, config, shrinker, checkout_dir, options)
         dex_size = ComputeSizeOfDexFilesInApk(apk_dest)
         result['apk_dest'] = apk_dest,
         result['build_status'] = 'success'
         result['dex_size'] = dex_size
         result['profile_dest_dir'] = profile_dest_dir
+
+        profile = as_utils.ParseProfileReport(profile_dest_dir)
+        result['profile'] = {
+            task_name:duration for task_name, duration in profile.iteritems()
+            if as_utils.IsGradleCompilerTask(task_name, shrinker)}
       except Exception as e:
         warn('Failed to build {} with {}'.format(app, shrinker))
         if e:
           print('Error: ' + str(e))
         result['build_status'] = 'failed'
 
-      if options.monkey:
-        if result.get('build_status') == 'success':
+      if result.get('build_status') == 'success':
+        if options.monkey:
           result['monkey_status'] = 'success' if RunMonkey(
-              app, config, apk_dest) else 'failed'
+              app, config, options, apk_dest) else 'failed'
+
+        if 'r8' in shrinker and options.r8_compilation_steps > 1:
+          recompilation_results = []
+          previous_apk = apk_dest
+          for i in range(1, options.r8_compilation_steps):
+            try:
+              recompiled_apk_dest = os.path.join(
+                  checkout_dir, 'out', shrinker, 'app-release-{}.apk'.format(i))
+              RebuildAppWithShrinker(
+                  previous_apk, recompiled_apk_dest, proguard_config_file, shrinker)
+              recompilation_result = {
+                'apk_dest': recompiled_apk_dest,
+                'build_status': 'success',
+                'dex_size': ComputeSizeOfDexFilesInApk(recompiled_apk_dest)
+              }
+              if options.monkey:
+                recompilation_result['monkey_status'] = 'success' if RunMonkey(
+                    app, config, options, recompiled_apk_dest) else 'failed'
+              recompilation_results.append(recompilation_result)
+              previous_apk = recompiled_apk_dest
+            except Exception as e:
+              warn('Failed to recompile {} with {}'.format(app, shrinker))
+              recompilation_results.append({ 'build_status': 'failed' })
+              break
+          result['recompilation_results'] = recompilation_results
 
       result_per_shrinker[shrinker] = result
 
@@ -216,8 +273,10 @@
   return result_per_shrinker
 
 def BuildAppWithShrinker(app, config, shrinker, checkout_dir, options):
+  print()
   print('Building {} with {}'.format(app, shrinker))
 
+  # Add/remove 'r8.jar' from top-level build.gradle.
   if options.disable_tot:
     as_utils.remove_r8_dependency(checkout_dir)
   else:
@@ -227,7 +286,7 @@
   archives_base_name = config.get(' archives_base_name', app_module)
   flavor = config.get('flavor')
 
-  # Ensure that gradle.properties are not modified before modifying it to
+  # Ensure that gradle.properties is not modified before modifying it to
   # select shrinker.
   if IsTrackedByGit('gradle.properties'):
     GitCheckout('gradle.properties')
@@ -244,6 +303,12 @@
   if not os.path.exists(out):
     os.makedirs(out)
 
+  # Set -printconfiguration in Proguard rules.
+  proguard_config_dest = os.path.abspath(
+      os.path.join(out, 'proguard-rules.pro'))
+  as_utils.SetPrintConfigurationDirective(
+      app, config, checkout_dir, proguard_config_dest)
+
   env = os.environ.copy()
   env['ANDROID_HOME'] = android_home
   env['JAVA_OPTS'] = '-ea'
@@ -307,16 +372,43 @@
   profile_dest_dir = os.path.join(out, 'profile')
   as_utils.MoveProfileReportTo(profile_dest_dir, stdout)
 
-  return (apk_dest, profile_dest_dir)
+  return (apk_dest, profile_dest_dir, proguard_config_dest)
 
-def RunMonkey(app, config, apk_dest):
-  WaitForEmulator()
+def RebuildAppWithShrinker(apk, apk_dest, proguard_config_file, shrinker):
+  assert 'r8' in shrinker
+  assert apk_dest.endswith('.apk')
+  
+  # Compile given APK with shrinker to temporary zip file.
+  api = 28 # TODO(christofferqa): Should be the one from build.gradle
+  android_jar = os.path.join(utils.REPO_ROOT, utils.ANDROID_JAR.format(api=api))
+  r8_jar = utils.R8LIB_JAR if IsMinifiedR8(shrinker) else utils.R8_JAR
+  zip_dest = apk_dest[:-3] + '.zip'
+
+  cmd = ['java', '-ea', '-jar', r8_jar, '--release', '--pg-conf',
+      proguard_config_file, '--lib', android_jar, '--output', zip_dest, apk]
+  utils.PrintCmd(cmd)
+
+  subprocess.check_output(cmd)
+
+  # Make a copy of the given APK, move the newly generated dex files into the
+  # copied APK, and then sign the APK.
+  apk_masseur.masseur(apk, dex=zip_dest, out=apk_dest)
+
+def RunMonkey(app, config, options, apk_dest):
+  if not WaitForEmulator():
+    return False
+
+  UninstallApkOnEmulator(app, config)
   InstallApkOnEmulator(apk_dest)
 
   app_id = config.get('app_id')
-  number_of_events_to_generate = 50
+  number_of_events_to_generate = options.monkey_events
 
-  cmd = ['adb', 'shell', 'monkey', '-p', app_id,
+  # Intentionally using a constant seed such that the monkey generates the same
+  # event sequence for each shrinker.
+  random_seed = 42
+
+  cmd = ['adb', 'shell', 'monkey', '-p', app_id, '-s', str(random_seed),
       str(number_of_events_to_generate)]
   utils.PrintCmd(cmd)
 
@@ -336,8 +428,10 @@
       print('  skipped ({})'.format(error_message))
       continue
 
-    baseline = float(
-        result_per_shrinker.get('proguard', {}).get('dex_size', -1))
+    proguard_result = result_per_shrinker.get('proguard', {})
+    proguard_dex_size = float(proguard_result.get('dex_size', -1))
+    proguard_duration = sum(proguard_result.get('profile', {}).values())
+
     for shrinker in SHRINKERS:
       if shrinker not in result_per_shrinker:
         continue
@@ -348,23 +442,50 @@
       else:
         print('  {}:'.format(shrinker))
         dex_size = result.get('dex_size')
-        if dex_size != baseline and baseline >= 0:
-          if dex_size < baseline:
-            success('    dex size: {} ({}, -{}%)'.format(
-              dex_size, dex_size - baseline,
-              round((1.0 - dex_size / baseline) * 100), 1))
-          elif dex_size >= baseline:
-            warn('    dex size: {} ({}, +{}%)'.format(
-              dex_size, dex_size - baseline,
-              round((baseline - dex_size) / dex_size * 100, 1)))
+        msg = '    dex size: {}'.format(dex_size)
+        if dex_size != proguard_dex_size and proguard_dex_size >= 0:
+          msg = '{} ({}, {})'.format(
+              msg, dex_size - proguard_dex_size,
+              PercentageDiffAsString(proguard_dex_size, dex_size))
+          success(msg) if dex_size < proguard_dex_size else warn(msg)
         else:
-          print('    dex size: {}'.format(dex_size))
+          print(msg)
+
+        profile = result.get('profile')
+        duration = sum(profile.values())
+        msg = '    performance: {}s'.format(duration)
+        if duration != proguard_duration and proguard_duration > 0:
+          msg = '{} ({}s, {})'.format(
+              msg, duration - proguard_duration,
+              PercentageDiffAsString(proguard_duration, duration))
+          success(msg) if duration < proguard_duration else warn(msg)
+        else:
+          print(msg)
+        if len(profile) >= 2:
+          for task_name, task_duration in profile.iteritems():
+            print('      {}: {}s'.format(task_name, task_duration))
+
         if options.monkey:
           monkey_status = result.get('monkey_status')
           if monkey_status != 'success':
             warn('    monkey: {}'.format(monkey_status))
           else:
             success('    monkey: {}'.format(monkey_status))
+        recompilation_results = result.get('recompilation_results', [])
+        i = 1
+        for recompilation_result in recompilation_results:
+          build_status = recompilation_result.get('build_status')
+          if build_status != 'success':
+            print('    recompilation #{}: {}'.format(i, build_status))
+          else:
+            dex_size = recompilation_result.get('dex_size')
+            print('    recompilation #{}'.format(i))
+            print('      dex size: {}'.format(dex_size))
+            if options.monkey:
+              monkey_status = recompilation_result.get('monkey_status')
+              msg = '      monkey: {}'.format(monkey_status)
+              success(msg) if monkey_status == 'success' else warn(msg)
+          i += 1
 
 def ParseOptions(argv):
   result = optparse.OptionParser()
@@ -375,6 +496,10 @@
                     help='Whether to install and run app(s) with monkey',
                     default=False,
                     action='store_true')
+  result.add_option('--monkey_events',
+                    help='Number of events that the monkey should trigger',
+                    default=250,
+                    type=int)
   result.add_option('--pull',
                     help='Whether to pull the latest version of each app',
                     default=False,
@@ -386,6 +511,10 @@
   result.add_option('--shrinker',
                     help='The shrinkers to use (by default, all are run)',
                     action='append')
+  result.add_option('--r8-compilation-steps', '--r8_compilation_steps',
+                    help='Number of times R8 should be run on each app',
+                    default=2,
+                    type=int)
   result.add_option('--disable-tot', '--disable_tot',
                     help='Whether to disable the use of the ToT version of R8',
                     default=False,
@@ -395,6 +524,9 @@
                     default=False,
                     action='store_true')
   (options, args) = result.parse_args(argv)
+  if options.disable_tot:
+    # r8.jar is required for recompiling the generated APK
+    options.r8_compilation_steps = 1
   if options.shrinker:
     for shrinker in options.shrinker:
       assert shrinker in SHRINKERS