Merge "Introduce --intermediate option to D8 for native multidex mode."
diff --git a/src/main/java/com/android/tools/r8/BaseOutput.java b/src/main/java/com/android/tools/r8/BaseOutput.java
index 16e8264..b1b9166 100644
--- a/src/main/java/com/android/tools/r8/BaseOutput.java
+++ b/src/main/java/com/android/tools/r8/BaseOutput.java
@@ -8,6 +8,7 @@
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.List;
 
 abstract class BaseOutput {
 
@@ -39,9 +40,9 @@
    *   resources.get(N - 1) ~=~ classesN.dex (where N > 0).
    * </pre>
    *
-   * @return list of compiled DEX resources.
+   * @return an immutable list of compiled DEX resources.
    */
-  public ImmutableList<Resource> getDexResources() {
+  public List<Resource> getDexResources() {
     return ImmutableList.copyOf(app.getDexProgramResources());
   }
 
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 3c408cb..6988b06 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -17,7 +17,10 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Paths;
@@ -173,10 +176,11 @@
 
       CompilationResult output =
           new CompilationResult(
-              new ApplicationWriter(app, appInfo, options, NamingLens.getIdentityLens(), null)
-                .write(null, executor),
-                app,
-                appInfo);
+              new ApplicationWriter(
+                  app, appInfo, options, null, NamingLens.getIdentityLens(), null)
+                  .write(null, executor),
+              app,
+              appInfo);
 
       options.printWarnings();
       return output;
@@ -202,9 +206,11 @@
       if (options.printCfgFile == null || options.printCfgFile.isEmpty()) {
         System.out.print(printer.toString());
       } else {
-        java.io.FileWriter writer = new java.io.FileWriter(options.printCfgFile);
-        writer.write(printer.toString());
-        writer.close();
+        try (OutputStreamWriter writer = new OutputStreamWriter(
+            new FileOutputStream(options.printCfgFile),
+            StandardCharsets.UTF_8)) {
+          writer.write(printer.toString());
+        }
       }
     }
     return application;
diff --git a/src/main/java/com/android/tools/r8/D8Logger.java b/src/main/java/com/android/tools/r8/D8Logger.java
index 25f238b..f0791b0 100644
--- a/src/main/java/com/android/tools/r8/D8Logger.java
+++ b/src/main/java/com/android/tools/r8/D8Logger.java
@@ -10,8 +10,6 @@
 import java.nio.file.Paths;
 import java.util.Arrays;
 
-import static java.util.Arrays.stream;
-
 public final class D8Logger {
 
   private static final int STATUS_ERROR = 1;
diff --git a/src/main/java/com/android/tools/r8/DexSegments.java b/src/main/java/com/android/tools/r8/DexSegments.java
index 9902d3b..3827cae 100644
--- a/src/main/java/com/android/tools/r8/DexSegments.java
+++ b/src/main/java/com/android/tools/r8/DexSegments.java
@@ -110,7 +110,7 @@
     Map<String, Integer> result = new HashMap<>();
     try (Closer closer = Closer.create()) {
       for (Resource resource : app.getDexProgramResources()) {
-        for (Segment segment: DexFileReader.parseMapFrom(resource.getStream(closer))) {
+        for (Segment segment: DexFileReader.parseMapFrom(closer.register(resource.getStream()))) {
           int value = result.computeIfAbsent(segment.typeName(), (key) -> 0);
           result.put(segment.typeName(), value + segment.size());
         }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 8fa9543..19fc358 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -37,17 +37,20 @@
 import com.android.tools.r8.shaking.TreePruner;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.InternalOptions.AttributeRemovalOptions;
 import com.android.tools.r8.utils.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
 import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
@@ -65,8 +68,7 @@
   private final Timing timing = new Timing("R8");
   private final InternalOptions options;
 
-  // TODO(zerny): Refactor tests to go through testing methods and make this private.
-  public R8(InternalOptions options) {
+  private R8(InternalOptions options) {
     this.options = options;
     options.itemFactory.resetSortedIndices();
   }
@@ -75,27 +77,40 @@
       ExecutorService executorService,
       DexApplication application,
       AppInfo appInfo,
+      byte[] deadCode,
       NamingLens namingLens,
       byte[] proguardSeedsData,
       PackageDistribution packageDistribution,
       InternalOptions options)
       throws ExecutionException {
     try {
-      return new ApplicationWriter(application, appInfo, options, namingLens, proguardSeedsData)
+      return new ApplicationWriter(
+          application, appInfo, options, deadCode, namingLens, proguardSeedsData)
           .write(packageDistribution, executorService);
     } catch (IOException e) {
       throw new RuntimeException("Cannot write dex application", e);
     }
   }
 
-  public DexApplication optimize(DexApplication application, AppInfoWithSubtyping appInfo)
+  static DexApplication optimize(
+      DexApplication application,
+      AppInfoWithSubtyping appInfo,
+      InternalOptions options)
+      throws ProguardRuleParserException, ExecutionException, IOException {
+    return new R8(options).optimize(application, appInfo);
+  }
+
+  private DexApplication optimize(DexApplication application, AppInfoWithSubtyping appInfo)
       throws IOException, ProguardRuleParserException, ExecutionException {
     return optimize(application, appInfo, GraphLense.getIdentityLense(),
         Executors.newSingleThreadExecutor());
   }
 
-  public DexApplication optimize(DexApplication application, AppInfoWithSubtyping appInfo,
-      GraphLense graphLense, ExecutorService executorService)
+  private DexApplication optimize(
+      DexApplication application,
+      AppInfoWithSubtyping appInfo,
+      GraphLense graphLense,
+      ExecutorService executorService)
       throws IOException, ProguardRuleParserException, ExecutionException {
     final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
 
@@ -123,9 +138,11 @@
       if (options.printCfgFile == null || options.printCfgFile.isEmpty()) {
         System.out.print(printer.toString());
       } else {
-        java.io.FileWriter writer = new java.io.FileWriter(options.printCfgFile);
-        writer.write(printer.toString());
-        writer.close();
+        try (OutputStreamWriter writer = new OutputStreamWriter(
+            new FileOutputStream(options.printCfgFile),
+            StandardCharsets.UTF_8)) {
+          writer.write(printer.toString());
+        }
       }
     }
     return application;
@@ -181,7 +198,7 @@
     }
   }
 
-  static CompilationResult runForTesting(
+  private static CompilationResult runForTesting(
       AndroidApp app,
       InternalOptions options,
       ExecutorService executor)
@@ -237,10 +254,6 @@
           appInfo = appInfo.withLiveness().prunedCopyFrom(application);
           new AbstractMethodRemover(appInfo).run();
           new AnnotationRemover(appInfo.withLiveness(), options).run();
-        } else if (!options.skipMinification) {
-          // TODO(38188583): Ensure signatures are removed when minifying.
-          new AnnotationRemover(appInfo.withLiveness(), true,
-              AttributeRemovalOptions.filterOnlySignatures());
         }
       } finally {
         timing.end();
@@ -342,6 +355,7 @@
               executorService,
               application,
               appInfo,
+              application.deadCode,
               namingLens,
               proguardSeedsData,
               packageDistribution,
@@ -391,42 +405,46 @@
     if (options.printMapping && !options.skipMinification) {
       assert outputApp.hasProguardMap();
       try (Closer closer = Closer.create()) {
-        OutputStream mapOut = openPathWithDefault(
+        OutputStream mapOut = FileUtils.openPathWithDefault(
             closer,
             options.printMappingFile,
-            System.out);
+            System.out,
+            StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
         outputApp.writeProguardMap(closer, mapOut);
       }
     }
     if (options.printSeeds) {
       assert outputApp.hasProguardSeeds();
       try (Closer closer = Closer.create()) {
-        OutputStream seedsOut = openPathWithDefault(closer, options.seedsFile, System.out);
+        OutputStream seedsOut = FileUtils.openPathWithDefault(
+            closer,
+            options.seedsFile,
+            System.out,
+            StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
         outputApp.writeProguardSeeds(closer, seedsOut);
       }
     }
     if (options.printMainDexList && outputApp.hasMainDexList()) {
       try (Closer closer = Closer.create()) {
         OutputStream mainDexOut =
-            openPathWithDefault(closer, options.printMainDexListFile, System.out);
+            FileUtils.openPathWithDefault(
+                closer,
+                options.printMainDexListFile,
+                System.out,
+                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
         outputApp.writeMainDexList(closer, mainDexOut);
       }
     }
-  }
-
-  private static OutputStream openPathWithDefault(Closer closer,
-      Path file,
-      PrintStream defaultOutput) throws IOException {
-    OutputStream mapOut;
-    if (file == null) {
-      mapOut = defaultOutput;
-    } else {
-      mapOut =
-          Files.newOutputStream(
-              file, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
-      closer.register(mapOut);
+    if (options.printUsage && outputApp.hasDeadCode()) {
+      try (Closer closer = Closer.create()) {
+        OutputStream deadCodeOut = FileUtils.openPathWithDefault(
+            closer,
+            options.printUsageFile,
+            System.out,
+            StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+        outputApp.writeDeadCode(closer, deadCodeOut);
+      }
     }
-    return mapOut;
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/Resource.java b/src/main/java/com/android/tools/r8/Resource.java
index eaabc99..3208e52 100644
--- a/src/main/java/com/android/tools/r8/Resource.java
+++ b/src/main/java/com/android/tools/r8/Resource.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8;
 
-import com.google.common.io.Closer;
 import java.io.ByteArrayInputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -48,7 +47,7 @@
   public abstract Set<String> getClassDescriptors();
 
   /** Get the resource as a stream. */
-  public abstract InputStream getStream(Closer closer) throws IOException;
+  public abstract InputStream getStream() throws IOException;
 
   /** File based application resource. */
   private static class FileResource extends Resource {
@@ -66,8 +65,8 @@
     }
 
     @Override
-    public InputStream getStream(Closer closer) throws IOException {
-      return closer.register(new FileInputStream(file.toFile()));
+    public InputStream getStream() throws IOException {
+      return new FileInputStream(file.toFile());
     }
   }
 
@@ -89,8 +88,7 @@
     }
 
     @Override
-    public InputStream getStream(Closer closer) throws IOException {
-      // Note: closing a byte-array input stream is a no-op.
+    public InputStream getStream() throws IOException {
       return new ByteArrayInputStream(bytes);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/bisect/Bisect.java b/src/main/java/com/android/tools/r8/bisect/Bisect.java
index 1c76f46..4fe3773 100644
--- a/src/main/java/com/android/tools/r8/bisect/Bisect.java
+++ b/src/main/java/com/android/tools/r8/bisect/Bisect.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
@@ -31,7 +30,6 @@
 public class Bisect {
 
   private final BisectOptions options;
-  private final DexItemFactory factory = new DexItemFactory();
   private final Timing timing = new Timing("bisect");
 
   public interface Command {
@@ -179,7 +177,7 @@
       throws IOException, ExecutionException {
     InternalOptions options = new InternalOptions();
     AppInfo appInfo = new AppInfo(app);
-    ApplicationWriter writer = new ApplicationWriter(app, appInfo, options, null, null);
+    ApplicationWriter writer = new ApplicationWriter(app, appInfo, options, null, null, null);
     AndroidApp outApp = writer.write(null, executor);
     outApp.writeToDirectory(output, OutputMode.Indexed);
   }
diff --git a/src/main/java/com/android/tools/r8/bisect/BisectState.java b/src/main/java/com/android/tools/r8/bisect/BisectState.java
index 148c167..489c78d 100644
--- a/src/main/java/com/android/tools/r8/bisect/BisectState.java
+++ b/src/main/java/com/android/tools/r8/bisect/BisectState.java
@@ -142,7 +142,6 @@
   }
 
   private final String signature;
-  private final DexApplication goodApp;
   private final DexApplication badApp;
   private final List<DexProgramClass> sortedGoodClasses;
   private final Map<DexType, Integer> indexMap;
@@ -154,7 +153,6 @@
   private Range nextRange = null;
 
   public BisectState(DexApplication goodApp, DexApplication badApp, File stateFile) {
-    this.goodApp = goodApp;
     this.badApp = badApp;
     this.stateFile = stateFile;
     signature = makeSignature(goodApp);
@@ -320,10 +318,7 @@
   }
 
   private static List<DexProgramClass> getSortedClasses(DexApplication app) {
-    List<DexProgramClass> classes = new ArrayList<>();
-    for (DexProgramClass clazz : app.classes()) {
-      classes.add(clazz);
-    }
+    List<DexProgramClass> classes = new ArrayList<>(app.classes());
     app.dexItemFactory.sort(NamingLens.getIdentityLens());
     classes.sort((a, b) -> a.type.compareTo(b.type));
     app.dexItemFactory.resetSortedIndices();
diff --git a/src/main/java/com/android/tools/r8/code/Format4rcc.java b/src/main/java/com/android/tools/r8/code/Format4rcc.java
index 7072805..ceac4c8 100644
--- a/src/main/java/com/android/tools/r8/code/Format4rcc.java
+++ b/src/main/java/com/android/tools/r8/code/Format4rcc.java
@@ -56,7 +56,7 @@
       return false;
     }
     Format4rcc o = (Format4rcc) other;
-    return o.AA == AA && o.CCCC == CCCC && o.BBBB.equals(BBBB) & o.HHHH.equals(HHHH);
+    return o.AA == AA && o.CCCC == CCCC && o.BBBB.equals(BBBB) && o.HHHH.equals(HHHH);
   }
 
   public String toString(ClassNameMapper naming) {
diff --git a/src/main/java/com/android/tools/r8/code/InvokePolymorphicRange.java b/src/main/java/com/android/tools/r8/code/InvokePolymorphicRange.java
index 9b102b2..e6a16f5 100644
--- a/src/main/java/com/android/tools/r8/code/InvokePolymorphicRange.java
+++ b/src/main/java/com/android/tools/r8/code/InvokePolymorphicRange.java
@@ -39,7 +39,7 @@
   }
 
   public DexMethod getMethod() {
-    return (DexMethod) BBBB;
+    return BBBB;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index c871e75..6bbb406 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -472,8 +472,7 @@
                 + "Reduce the input-program size or run with --multi-dex enabled");
       }
       if (isDexFile(output)) {
-        try (Closer closer = Closer.create()) {
-          InputStream stream = result.getDexResources().get(0).getStream(closer);
+        try (InputStream stream = result.getDexResources().get(0).getStream()) {
           Files.copy(stream, output, StandardCopyOption.REPLACE_EXISTING);
         }
         return;
@@ -544,7 +543,7 @@
         // Add dex files.
         List<Resource> dexProgramSources = output.getDexResources();
         for (int i = 0; i < dexProgramSources.size(); i++) {
-          addEntry(getDexFileName(i), dexProgramSources.get(i).getStream(closer), out);
+          addEntry(getDexFileName(i), closer.register(dexProgramSources.get(i).getStream()), out);
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 36b65d7..355080f 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -183,7 +183,7 @@
         List<DexFileReader> fileReaders = new ArrayList<>(dexSources.size());
         int computedMinApiLevel = options.minApiLevel;
         for (Resource input : dexSources) {
-          DexFile file = new DexFile(input.getStream(closer));
+          DexFile file = new DexFile(closer.register(input.getStream()));
           computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
           fileReaders.add(new DexFileReader(file, classKind, itemFactory));
         }
@@ -207,7 +207,7 @@
       JarClassFileReader reader = new JarClassFileReader(
           application, classKind.bridgeConsumer(classes::add));
       for (Resource input : classSources) {
-        reader.read(DEFAULT_DEX_FILENAME, classKind, input.getStream(closer));
+        reader.read(DEFAULT_DEX_FILENAME, classKind, closer.register(input.getStream()));
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index b3f51ca..6586a57 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -42,6 +42,7 @@
 
   public final DexApplication application;
   public final AppInfo appInfo;
+  public final byte[] deadCode;
   public final NamingLens namingLens;
   public final byte[] proguardSeedsData;
   public final InternalOptions options;
@@ -107,6 +108,7 @@
       DexApplication application,
       AppInfo appInfo,
       InternalOptions options,
+      byte[] deadCode,
       NamingLens namingLens,
       byte[] proguardSeedsData) {
     assert application != null;
@@ -114,6 +116,7 @@
     this.appInfo = appInfo;
     assert options != null;
     this.options = options;
+    this.deadCode = deadCode;
     this.namingLens = namingLens;
     this.proguardSeedsData = proguardSeedsData;
   }
@@ -171,6 +174,9 @@
       } catch (InterruptedException e) {
         throw new RuntimeException("Interrupted while waiting for future.", e);
       }
+      if (deadCode != null) {
+        builder.setDeadCode(deadCode);
+      }
       // Write the proguard map file after writing the dex files, as the map writer traverses
       // the DexProgramClass structures, which are destructively updated during dex file writing.
       byte[] proguardMapResult = writeProguardMapFile();
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index c216cec..977d703 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -12,6 +12,7 @@
   public static final int ANDROID_N_API = 24;
   public static final int ANDROID_L_API = 21;
   public static final int ANDROID_K_API = 19;
+  public static final int ANDROID_I_API = 14;
   public static final int DEFAULT_ANDROID_API = 1;
 
   /** dex file version number for Android O (API level 26) */
@@ -26,7 +27,6 @@
 
   public static final int DEX_MAGIC_SIZE = 8;
 
-  public static final int HEADER_SIZE = 0x70;
   public static final int MAGIC_OFFSET = 0;
   public static final int CHECKSUM_OFFSET = MAGIC_OFFSET + DEX_MAGIC_SIZE;
   public static final int SIGNATURE_OFFSET = CHECKSUM_OFFSET + 4;
diff --git a/src/main/java/com/android/tools/r8/dex/DexFileReader.java b/src/main/java/com/android/tools/r8/dex/DexFileReader.java
index 1c37073..768f207 100644
--- a/src/main/java/com/android/tools/r8/dex/DexFileReader.java
+++ b/src/main/java/com/android/tools/r8/dex/DexFileReader.java
@@ -771,8 +771,7 @@
           for (int j = 0; j < realHsize; j++) {
             int typeIdx = file.getUleb128();
             int addr = file.getUleb128();
-            pairs[j] = new TypeAddrPair(indexedItems.getType(typeIdx), addr,
-                encodedCatchHandlerOffset);
+            pairs[j] = new TypeAddrPair(indexedItems.getType(typeIdx), addr);
           }
           int catchAllAddr = -1;
           if (hsize <= 0) {
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index b56f712..332f292 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -234,7 +234,7 @@
     layout.setEndOfFile(dest.position());
 
     // Now that we have all mixedSectionOffsets, lets write the indexed items.
-    dest.moveTo(Constants.HEADER_SIZE);
+    dest.moveTo(Constants.TYPE_HEADER_ITEM_SIZE);
     writeFixedSectionItems(mapping.getStrings(), layout.stringIdsOffset, this::writeStringItem);
     writeFixedSectionItems(mapping.getTypes(), layout.typeIdsOffset, this::writeTypeItem);
     writeFixedSectionItems(mapping.getProtos(), layout.protoIdsOffset, this::writeProtoItem);
@@ -790,7 +790,7 @@
     // Leave out checksum and signature for now.
     dest.moveTo(Constants.FILE_SIZE_OFFSET);
     dest.putInt(layout.getEndOfFile());
-    dest.putInt(Constants.HEADER_SIZE);
+    dest.putInt(Constants.TYPE_HEADER_ITEM_SIZE);
     dest.putInt(Constants.ENDIAN_CONSTANT);
     dest.putInt(0);
     dest.putInt(0);
@@ -897,7 +897,7 @@
     static Layout from(ObjectToOffsetMapping mapping) {
       int offset = 0;
       return new Layout(
-          offset = Constants.HEADER_SIZE,
+          offset = Constants.TYPE_HEADER_ITEM_SIZE,
           offset += mapping.getStrings().length * Constants.TYPE_STRING_ID_ITEM_SIZE,
           offset += mapping.getTypes().length * Constants.TYPE_TYPE_ID_ITEM_SIZE,
           offset += mapping.getProtos().length * Constants.TYPE_PROTO_ID_ITEM_SIZE,
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 2c9366c..0d2edcb 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.primitives.Bytes;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
@@ -38,6 +39,7 @@
   private LibraryClassCollection libraryClasses;
 
   public final ImmutableSet<DexType> mainDexList;
+  public final byte[] deadCode;
 
   private final ClassNameMapper proguardMap;
 
@@ -55,6 +57,7 @@
       ClasspathClassCollection classpathClasses,
       LibraryClassCollection libraryClasses,
       ImmutableSet<DexType> mainDexList,
+      byte[] deadCode,
       DexItemFactory dexItemFactory,
       DexString highestSortingString,
       Timing timing) {
@@ -64,6 +67,7 @@
     this.classpathClasses = classpathClasses;
     this.libraryClasses = libraryClasses;
     this.mainDexList = mainDexList;
+    this.deadCode = deadCode;
     this.dexItemFactory = dexItemFactory;
     this.highestSortingString = highestSortingString;
     this.timing = timing;
@@ -305,16 +309,18 @@
     private LibraryClassCollection libraryClasses;
 
     public final DexItemFactory dexItemFactory;
-    public ClassNameMapper proguardMap;
+    ClassNameMapper proguardMap;
     private final Timing timing;
 
-    public DexString highestSortingString;
+    DexString highestSortingString;
+    private byte[] deadCode;
     private final Set<DexType> mainDexList = Sets.newIdentityHashSet();
 
     public Builder(DexItemFactory dexItemFactory, Timing timing) {
       this.programClasses = new ArrayList<>();
       this.dexItemFactory = dexItemFactory;
       this.timing = timing;
+      this.deadCode = null;
       this.classpathClasses = null;
       this.libraryClasses = null;
     }
@@ -328,6 +334,7 @@
       highestSortingString = application.highestSortingString;
       dexItemFactory = application.dexItemFactory;
       mainDexList.addAll(application.mainDexList);
+      deadCode = application.deadCode;
     }
 
     public synchronized Builder setProguardMap(ClassNameMapper proguardMap) {
@@ -343,6 +350,19 @@
       return this;
     }
 
+    public Builder appendDeadCode(byte[] deadCodeAtAnotherRound) {
+      if (deadCodeAtAnotherRound == null) {
+        return this;
+      }
+      if (this.deadCode == null) {
+        this.deadCode = deadCodeAtAnotherRound;
+        return this;
+      }
+      // Concatenate existing byte[] and the given byte[].
+      this.deadCode = Bytes.concat(this.deadCode, deadCodeAtAnotherRound);
+      return this;
+    }
+
     public synchronized Builder setHighestSortingString(DexString value) {
       highestSortingString = value;
       return this;
@@ -389,6 +409,7 @@
           classpathClasses,
           libraryClasses,
           ImmutableSet.copyOf(mainDexList),
+          deadCode,
           dexItemFactory,
           highestSortingString,
           timing);
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index a029a61..aedb6df 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -11,7 +11,6 @@
 import com.google.common.base.MoreObjects;
 
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.function.Consumer;
 
 public abstract class DexClass extends DexItem {
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 3535bfa..d939868 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -428,12 +428,10 @@
 
       public final DexType type;
       public final /* offset */ int addr;
-      public final /* offset to the start of an encoded_catch_handler. */ int offset;
 
-      public TypeAddrPair(DexType type, int addr, int offset) {
+      public TypeAddrPair(DexType type, int addr) {
         this.type = type;
         this.addr = addr;
-        this.offset = offset;
       }
 
       public void collectIndexedItems(IndexedItemCollection indexedItems) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
index 4410f2a..39ec7d0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
@@ -9,6 +9,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Builder to construct a "per position" representation of the debug information.
@@ -160,8 +161,9 @@
 
   private ImmutableMap<Integer, DebugLocalInfo> getLocals() {
     ImmutableMap.Builder<Integer, DebugLocalInfo> builder = ImmutableMap.builder();
-    for (Integer register : locals.keySet()) {
-      LocalEntry entry = locals.get(register);
+    for (Entry<Integer, LocalEntry> mapEntry : locals.entrySet()) {
+      Integer register = mapEntry.getKey();
+      LocalEntry entry = mapEntry.getValue();
       if (entry.current != null) {
         builder.put(register, entry.current);
       }
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 c8fbbea..572bea9 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -14,10 +14,8 @@
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.InvokeDirect;
 import com.android.tools.r8.code.InvokeStatic;
-import com.android.tools.r8.code.InvokeSuper;
 import com.android.tools.r8.code.NewInstance;
 import com.android.tools.r8.code.Throw;
-import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.ir.code.IRCode;
@@ -33,7 +31,6 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
-import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 
 public class DexEncodedMethod extends KeyedDexItem<DexMethod> {
@@ -379,7 +376,7 @@
   public void registerReachableDefinitions(UseRegistry registry) {
     if (code != null) {
       if (Log.ENABLED) {
-        Log.verbose((Class) getClass(), "Registering definitions reachable from `%s`.", method);
+        Log.verbose(getClass(), "Registering definitions reachable from `%s`.", method);
       }
       code.registerReachableDefinitions(registry);
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 04e34a5..6500e8f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -228,7 +228,6 @@
     public final DexMethod appendObject;
     public final DexMethod appendString;
     public final DexMethod appendStringBuffer;
-    public final DexMethod toString;
 
     private StringBuildingMethods(DexType receiver) {
       DexType sbufType = createType(createString("Ljava/lang/StringBuffer;"));
@@ -251,7 +250,6 @@
       appendObject = createMethod(receiver, createProto(receiver, objectType), append);
       appendString = createMethod(receiver, createProto(receiver, stringType), append);
       appendStringBuffer = createMethod(receiver, createProto(receiver, sbufType), append);
-      toString = createMethod(receiver, createProto(stringType), toStringMethodName);
     }
 
     public void forEachAppendMethod(Consumer<DexMethod> consumer) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 234773f..ac7d9ee 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -364,9 +364,8 @@
     int leadingSquareBrackets = getNumberOfLeadingSquareBrackets();
     byte[] content = new byte[newBase.descriptor.content.length + leadingSquareBrackets];
     Arrays.fill(content, 0, leadingSquareBrackets, (byte) '[');
-    for (int i = 0; i < newBase.descriptor.content.length; i++) {
-      content[leadingSquareBrackets + i] = newBase.descriptor.content[i];
-    }
+    System.arraycopy(newBase.descriptor.content, 0, content, leadingSquareBrackets,
+        newBase.descriptor.content.length);
     DexString newDesc = dexItemFactory
         .createString(newBase.descriptor.size + leadingSquareBrackets, content);
     return dexItemFactory.createType(newDesc);
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index a0cfa7c..29f59e5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -593,6 +593,10 @@
       this.values = values;
     }
 
+    public DexValue[] getValues() {
+      return values;
+    }
+
     @Override
     public void collectIndexedItems(IndexedItemCollection indexedItems) {
       collectAll(indexedItems, values);
diff --git a/src/main/java/com/android/tools/r8/graph/IndexedDexItem.java b/src/main/java/com/android/tools/r8/graph/IndexedDexItem.java
index 6c7f3b3..d3d36e5 100644
--- a/src/main/java/com/android/tools/r8/graph/IndexedDexItem.java
+++ b/src/main/java/com/android/tools/r8/graph/IndexedDexItem.java
@@ -30,7 +30,7 @@
    * as many entries in the index as there are files (we only expand when we need to). If we lookup
    * the value of an entry that is out of bounds it is equivalent to {@link #UNASSOCIATED_VALUE}
    *
-   * <p>This field is initialized on first write in {@link #updateVirtualFileData(int, int)}}. It
+   * <p>This field is initialized on first write in {@link #updateVirtualFileData(int)}}. It
    * is assumed that multiple files are processed concurrently and thus the allocation of the
    * array is synchronized. However, for any a given file id, sequential access is assumed.
    */
@@ -106,7 +106,7 @@
    * Assigns an actual index for this item in the given file.
    *
    * <p>May only be used after this item has been assigned to the file using {@link
-   * #assignToVirtualFile(int, int)}.
+   * #assignToVirtualFile(int)}.
    */
   public void assignVirtualFileIndex(int virtualFileId, int index) {
     assert virtualFileIndexes != null;
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 26a85b9..290a1a2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -15,7 +15,6 @@
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -143,7 +142,7 @@
   public void removeSuccessor(BasicBlock block) {
     int index = successors.indexOf(block);
     assert index >= 0 : "removeSuccessor did not find the successor to remove";
-    removeSuccessorsByIndex(Arrays.asList(index));
+    removeSuccessorsByIndex(Collections.singletonList(index));
   }
 
   public void removePredecessor(BasicBlock block) {
@@ -469,7 +468,7 @@
     successor.predecessors.add(this);
   }
 
-  private boolean allPredecessorsDominated(BasicBlock block, DominatorTree dominator) {
+  private static boolean allPredecessorsDominated(BasicBlock block, DominatorTree dominator) {
     for (BasicBlock pred : block.predecessors) {
       if (!dominator.dominatedBy(pred, block)) {
         return false;
@@ -478,7 +477,7 @@
     return true;
   }
 
-  private boolean blocksClean(List<BasicBlock> blocks) {
+  private static boolean blocksClean(List<BasicBlock> blocks) {
     blocks.forEach((b) -> {
       assert b.predecessors.size() == 0;
       assert b.successors.size() == 0;
@@ -591,7 +590,7 @@
   // The proper incoming register for a catch successor (that is otherwise shadowed by the out-value
   // of a throwing instruction) is stored at the negative register-index in the definitions map.
   // (See readCurrentDefinition/writeCurrentDefinition/updateCurrentDefinition).
-  private int onThrowValueRegister(int register) {
+  private static int onThrowValueRegister(int register) {
     return -(register + 1);
   }
 
@@ -713,16 +712,16 @@
     return incompletePhis.keySet();
   }
 
-  private void appendBasicBlockList(
+  private static void appendBasicBlockList(
       StringBuilder builder, List<BasicBlock> list, Function<BasicBlock, String> postfix) {
     if (list.size() > 0) {
       for (BasicBlock block : list) {
         builder.append(block.number >= 0 ? block.number : "<unknown>");
         builder.append(postfix.apply(block));
-        builder.append(" ");
+        builder.append(' ');
       }
     } else {
-      builder.append("-");
+      builder.append('-');
     }
   }
 
@@ -748,7 +747,7 @@
     builder.append(number);
     builder.append(" (");
     builder.append(System.identityHashCode(this));
-    builder.append(")");
+    builder.append(')');
     builder.append(", pred-counts: " + predecessors.size());
     if (unfilledPredecessorsCount > 0) {
       builder.append(" (" + unfilledPredecessorsCount + " unfilled)");
@@ -756,10 +755,10 @@
     builder.append(", succ-count: " + successors.size());
     builder.append(", filled: " + isFilled());
     builder.append(", sealed: " + isSealed());
-    builder.append("\n");
+    builder.append('\n');
     builder.append("predecessors: ");
     appendBasicBlockList(builder, predecessors, b -> "");
-    builder.append("\n");
+    builder.append('\n');
     builder.append("successors: ");
     appendBasicBlockList(builder, successors, this::predecessorPostfix);
     if (successors.size() > 0) {
@@ -771,14 +770,14 @@
       }
       builder.append(" try/catch successors)");
     }
-    builder.append("\n");
+    builder.append('\n');
     if (phis != null && phis.size() > 0) {
       for (Phi phi : phis) {
         builder.append(phi.printPhi());
         if (incompletePhis.values().contains(phi)) {
           builder.append(" (incomplete)");
         }
-        builder.append("\n");
+        builder.append('\n');
       }
     } else {
       builder.append("no phis\n");
@@ -786,13 +785,13 @@
     if (localsAtEntry != null) {
       builder.append("locals: ");
       StringUtils.append(builder, localsAtEntry.int2ReferenceEntrySet(), ", ", BraceType.NONE);
-      builder.append("\n");
+      builder.append('\n');
     }
     for (Instruction instruction : instructions) {
       StringUtils.appendLeftPadded(builder, Integer.toString(instruction.getNumber()), 6);
       builder.append(": ");
       StringUtils.appendRightPadded(builder, instruction.toString(), 20);
-      builder.append("\n");
+      builder.append('\n');
     }
     return builder.toString();
   }
@@ -1036,7 +1035,7 @@
     newBlock.setNumber(blockNumber);
 
     // Copy all successors including catch handlers to the new block, and update predecessors.
-    successors.forEach(newBlock.successors::add);
+    newBlock.successors.addAll(successors);
     for (BasicBlock successor : newBlock.getSuccessors()) {
       successor.replacePredecessor(this, newBlock);
     }
@@ -1172,7 +1171,7 @@
    * this method should only be called from either {@link #moveCatchHandlers} or
    * {@link #copyCatchHandlers} which know how to handle phis.
    *
-   * @returns the catch successors that are reused in both blocks after appending.
+   * @return the catch successors that are reused in both blocks after appending.
    */
   private List<BasicBlock> appendCatchHandlers(BasicBlock fromBlock) {
     assert fromBlock.hasCatchHandlers();
diff --git a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
index e814dbe..b5a7b75 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
@@ -11,7 +11,6 @@
 
 public class DominatorTree {
 
-  IRCode code;
   private BasicBlock[] sorted;
   private BasicBlock[] doms;
 
@@ -21,7 +20,6 @@
 
   // TODO(sgjesse) Get rid of this constructor and blocksToIgnore.
   DominatorTree(IRCode code, List<BasicBlock> blocksToIgnore) {
-    this.code = code;
     this.sorted = code.topologicallySortedBlocks(blocksToIgnore);
     numberBlocks();
     build();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
index 9e5c972..f38614f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import java.util.Arrays;
 
 public class InstanceGet extends FieldInstruction {
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index ac4781d..bb88b64 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -45,7 +45,7 @@
   /**
    * Continue to call {@link #next} while {@code predicate} tests {@code false}.
    *
-   * @returns the instruction that matched the predicate or {@code null} if all instructions fails
+   * @return the instruction that matched the predicate or {@code null} if all instructions fails
    * the predicate test
    */
   default Instruction nextUntil(Predicate<Instruction> predicate) {
@@ -168,7 +168,7 @@
       List<BasicBlock> blocksToRemove, DexType downcast);
 
   /**
-   * See {@link #inlineInvoke(IRCode, IRCode, ListIterator<BasicBlock>, List<BasicBlock>, DexType)}.
+   * See {@link #inlineInvoke(IRCode, IRCode, ListIterator, List, DexType)}.
    */
   default BasicBlock inlineInvoke(IRCode code, IRCode inlinee) {
     List<BasicBlock> blocksToRemove = new ArrayList<>();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 8b348a0..70e156b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -21,7 +21,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -46,7 +45,7 @@
  */
 public class CallGraph {
 
-  private class Node {
+  private static class Node {
 
     public final DexEncodedMethod method;
     private int invokeCount = 0;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 737c7e5..737dc46 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -588,7 +588,7 @@
           assert i == handlerGroup.getGuards().size() - 1;
           catchAllOffset = targetOffset;
         } else {
-          pairs.add(new TypeAddrPair(type, targetOffset, -1));
+          pairs.add(new TypeAddrPair(type, targetOffset));
         }
       }
       TypeAddrPair[] pairsArray = pairs.toArray(new TypeAddrPair[pairs.size()]);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index ba63d80..ec09396 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -86,6 +86,8 @@
 import it.unimi.dsi.fastutil.ints.IntIterator;
 import it.unimi.dsi.fastutil.ints.IntList;
 import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.longs.Long2ObjectArrayMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -241,11 +243,11 @@
   private BasicBlock currentBlock = null;
 
   // Mappings for canonicalizing constants of a given type at IR construction time.
-  private Map<Long, ConstNumber> intConstants = new HashMap<>();
-  private Map<Long, ConstNumber> longConstants = new HashMap<>();
-  private Map<Long, ConstNumber> floatConstants = new HashMap<>();
-  private Map<Long, ConstNumber> doubleConstants = new HashMap<>();
-  private Map<Long, ConstNumber> nullConstants = new HashMap<>();
+  private Long2ObjectMap<ConstNumber> intConstants = new Long2ObjectArrayMap<>();
+  private Long2ObjectMap<ConstNumber> longConstants = new Long2ObjectArrayMap<>();
+  private Long2ObjectMap<ConstNumber> floatConstants = new Long2ObjectArrayMap<>();
+  private Long2ObjectMap<ConstNumber> doubleConstants = new Long2ObjectArrayMap<>();
+  private Long2ObjectMap<ConstNumber> nullConstants = new Long2ObjectArrayMap<>();
 
   private List<BasicBlock> exitBlocks = new ArrayList<>();
   private BasicBlock normalExitBlock;
@@ -669,7 +671,7 @@
   // to disable constant canonicalization in debug builds to make sure we have separate values
   // for separate locals.
   private void canonicalizeAndAddConst(
-      ConstType type, int dest, long value, Map<Long, ConstNumber> table) {
+      ConstType type, int dest, long value, Long2ObjectMap<ConstNumber> table) {
     ConstNumber existing = table.get(value);
     if (existing != null) {
       currentBlock.writeCurrentDefinition(dest, existing.outValue(), ThrowingInfo.NO_THROW);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index a2c1cbd..12d8b6a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -195,7 +195,7 @@
       JarApplicationReader application) {
     DebugLocalInfo info = new DebugLocalInfo(
         application.getString(node.name),
-        application.getType(Type.getType(node.desc)),
+        application.getTypeFromDescriptor(node.desc),
         node.signature == null ? null : application.getString(node.signature));
     DebugLocalInfo canonical = canonicalLocalVariables.putIfAbsent(info, info);
     return canonical != null ? canonical : info;
@@ -892,15 +892,6 @@
     }
   }
 
-  static Type getArrayElementType(Type array) {
-    if (array == JarState.NULL_TYPE) {
-      return null;
-    }
-    String desc = array.getDescriptor();
-    assert desc.charAt(0) == '[';
-    return Type.getType(desc.substring(1));
-  }
-
   private static Type makeArrayType(Type elementType) {
     return Type.getObjectType("[" + elementType.getDescriptor());
   }
@@ -1153,8 +1144,7 @@
       case Opcodes.CALOAD:
       case Opcodes.SALOAD: {
         state.pop();
-        Slot array = state.pop(JarState.ARRAY_TYPE);
-        Type elementType = getArrayElementType(array.type);
+        Type elementType = state.pop(JarState.ARRAY_TYPE).getArrayElementType();
         if (elementType == null) {
           // We propagate the null type, which will then get resolved to an
           // actual type if we have a non-null type on another flow edge.
@@ -1860,7 +1850,7 @@
       case Opcodes.SALOAD: {
         Slot index = state.pop(Type.INT_TYPE);
         Slot array = state.pop(JarState.ARRAY_TYPE);
-        Type elementType = getArrayElementType(array.type);
+        Type elementType = array.getArrayElementType();
         if (elementType == null) {
           elementType = getArrayElementTypeForOpcode(opcode);
         }
@@ -1880,7 +1870,7 @@
         Slot value = state.pop();
         Slot index = state.pop(Type.INT_TYPE);
         Slot array = state.pop(JarState.ARRAY_TYPE);
-        Type elementType = getArrayElementType(array.type);
+        Type elementType = array.getArrayElementType();
         if (elementType == null) {
           elementType = getArrayElementTypeForOpcode(opcode);
         }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
index 51a8117..9d11fae 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.conversion;
 
-import static com.android.tools.r8.ir.conversion.JarSourceCode.getArrayElementType;
-
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
@@ -18,6 +16,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.tree.LabelNode;
 import org.objectweb.asm.tree.LocalVariableNode;
@@ -37,9 +36,6 @@
   // Type representative for the null value (non-existent but works for tracking the types here).
   public static final Type NULL_TYPE = Type.getObjectType("<null>");
 
-  // Type representative for an address type (used by JSR/RET).
-  public static final Type ADDR_TYPE = Type.getObjectType("<address>");
-
   // Typed mapping from a local slot or stack slot to a virtual register.
   public static class Slot {
     public final int register;
@@ -66,6 +62,14 @@
       return isCategory1(type);
     }
 
+    public Type getArrayElementType() {
+      assert type == NULL_TYPE || type == ARRAY_TYPE || type.getSort() == Type.ARRAY;
+      if (type == JarState.NULL_TYPE) {
+        return null;
+      }
+      return getArrayElementType(type);
+    }
+
     public static boolean isCategory1(Type type) {
       return type != Type.LONG_TYPE && type != Type.DOUBLE_TYPE;
     }
@@ -90,6 +94,12 @@
       return type.equals(other);
     }
 
+    private static Type getArrayElementType(Type type) {
+      String desc = type.getDescriptor();
+      assert desc.charAt(0) == '[';
+      return Type.getType(desc.substring(1));
+    }
+
     private static boolean isIntCompatible(int sort) {
       return Type.BOOLEAN <= sort && sort <= Type.INT;
     }
@@ -357,9 +367,7 @@
     Snapshot snapshot = targetStates.get(offset);
     assert snapshot != null;
     assert locals.length == snapshot.locals.length;
-    for (int i = 0; i < locals.length; i++) {
-      locals[i] = snapshot.locals[i];
-    }
+    System.arraycopy(snapshot.locals, 0, locals, 0, locals.length);
     stack.clear();
     stack.addAll(snapshot.stack);
     topOfStack = startOfStack + 2 * stack.size();
@@ -382,13 +390,14 @@
         }
       }
       // TODO(zerny): Precompute and sort the local ranges.
-      for (LocalVariableNode node : localVariables.keySet()) {
+      for (Entry<LocalVariableNode, DebugLocalInfo> entry : localVariables.entrySet()) {
+        LocalVariableNode node = entry.getKey();
         int startOffset = source.getOffset(node.start);
         int endOffset = source.getOffset(node.end);
         if (startOffset <= target && target < endOffset) {
           int register = getLocalRegister(node.index, Type.getType(node.desc));
           Local local = locals[register];
-          locals[register] = new Local(local.slot, localVariables.get(node));
+          locals[register] = new Local(local.slot, entry.getValue());
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/SwitchPayloadResolver.java b/src/main/java/com/android/tools/r8/ir/conversion/SwitchPayloadResolver.java
index 5367a3a..1c20efb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/SwitchPayloadResolver.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/SwitchPayloadResolver.java
@@ -19,14 +19,12 @@
 
     public final static int NO_SIZE = -1;
     public int userOffset;
-    public int fallthroughOffset;
     public int[] absoluteTargets = null;
     public int[] keys = null;
     public int size = NO_SIZE;
 
-    public PayloadData(int userOffset, int fallthroughOffset) {
+    public PayloadData(int userOffset) {
       this.userOffset = userOffset;
-      this.fallthroughOffset = fallthroughOffset;
     }
   }
 
@@ -36,7 +34,7 @@
   public void addPayloadUser(Instruction dex) {
     int offset = dex.getOffset();
     int payloadOffset = offset + dex.getPayloadOffset();
-    payloadToData.put(payloadOffset, new PayloadData(offset, offset + dex.getSize()));
+    payloadToData.put(payloadOffset, new PayloadData(offset));
     if (unresolvedPayload.containsKey(payloadOffset)) {
       SwitchPayload payload = unresolvedPayload.remove(payloadOffset);
       resolve(payload);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
index 4a51135..850f37d 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
@@ -162,7 +162,7 @@
     boolean staticTarget = implHandle.type.isInvokeStatic();
     boolean instanceTarget = implHandle.type.isInvokeInstance();
     boolean initTarget = implHandle.type.isInvokeConstructor();
-    assert instanceTarget || staticTarget | initTarget;
+    assert instanceTarget || staticTarget || initTarget;
 
     if (targetMethod == null) {
       // The target cannot be a private method, since otherwise it
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
index 9ec776f..bfaee32 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
@@ -464,7 +464,7 @@
   private int addPrimitiveBoxing(int register, DexType primitiveType, DexType boxType) {
     // Generate factory method fo boxing.
     DexItemFactory factory = factory();
-    DexProto proto = factory.createProto(boxType, new DexType[]{primitiveType});
+    DexProto proto = factory.createProto(boxType, primitiveType);
     DexMethod method = factory.createMethod(boxType, proto, factory.valueOfMethodName);
 
     MoveType moveType = MoveType.fromDexType(primitiveType);
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 203ca80..014a064 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
@@ -1126,7 +1126,7 @@
     }
   }
 
-  private class ExpressionEquivalence extends Equivalence<Instruction> {
+  private static class ExpressionEquivalence extends Equivalence<Instruction> {
 
     @Override
     protected boolean doEquivalent(Instruction a, Instruction b) {
@@ -1457,8 +1457,7 @@
     DexType javaLangSystemType = dexItemFactory.createType("Ljava/lang/System;");
     DexType javaIoPrintStreamType = dexItemFactory.createType("Ljava/io/PrintStream;");
 
-    DexProto proto = dexItemFactory.createProto(
-        dexItemFactory.voidType, new DexType[]{dexItemFactory.objectType});
+    DexProto proto = dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.objectType);
     DexMethod print = dexItemFactory.createMethod(javaIoPrintStreamType, proto, "print");
     DexMethod printLn = dexItemFactory.createMethod(javaIoPrintStreamType, proto, "println");
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index df5cb27..8a02435 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -37,7 +37,7 @@
     ASSUME_VALUES
   }
 
-  private class ProguardMemberRuleLookup {
+  private static class ProguardMemberRuleLookup {
 
     final RuleType type;
     final ProguardMemberRule rule;
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index fe59b79..0bd7188 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -39,7 +39,6 @@
 import it.unimi.dsi.fastutil.ints.IntSet;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 9f26fb5..d8d3b8d 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -5,11 +5,17 @@
 
 import com.android.tools.r8.graph.DexAnnotation;
 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.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueArray;
+import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.DexValue.DexValueType;
+import com.android.tools.r8.naming.signature.GenericSignatureAction;
+import com.android.tools.r8.naming.signature.GenericSignatureParser;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -22,8 +28,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 
-public class ClassNameMinifier {
+class ClassNameMinifier {
 
   private final AppInfoWithLiveness appInfo;
   private final RootSet rootSet;
@@ -31,11 +38,16 @@
   private final Set<DexString> usedTypeNames = Sets.newIdentityHashSet();
 
   private final Map<DexType, DexString> renaming = Maps.newIdentityHashMap();
-  private final Map<String, NamingState> states = new HashMap<>();
+  private final Map<String, ClassNamingState> states = new HashMap<>();
   private final List<String> dictionary;
   private final boolean keepInnerClassStructure;
 
-  public ClassNameMinifier(AppInfoWithLiveness appInfo, RootSet rootSet, String packagePrefix,
+  private GenericSignatureRewriter genericSignatureRewriter = new GenericSignatureRewriter();
+
+  private GenericSignatureParser<DexType> genericSignatureParser =
+      new GenericSignatureParser<>(genericSignatureRewriter);
+
+  ClassNameMinifier(AppInfoWithLiveness appInfo, RootSet rootSet, String packagePrefix,
       List<String> dictionary, boolean keepInnerClassStructure) {
     this.appInfo = appInfo;
     this.rootSet = rootSet;
@@ -44,26 +56,61 @@
     this.keepInnerClassStructure = keepInnerClassStructure;
   }
 
-  public Map<DexType, DexString> computeRenaming() {
+  Map<DexType, DexString> computeRenaming() {
     Iterable<DexProgramClass> classes = appInfo.classes();
     // Collect names we have to keep.
-    for (DexClass clazz : appInfo.classes()) {
+    for (DexClass clazz : classes) {
       if (rootSet.noObfuscation.contains(clazz)) {
         assert !renaming.containsKey(clazz.type);
         registerClassAsUsed(clazz.type);
       }
     }
-    for (DexClass clazz : appInfo.classes()) {
+    for (DexClass clazz : classes) {
       if (!renaming.containsKey(clazz.type)) {
         DexString renamed = computeName(clazz);
         renaming.put(clazz.type, renamed);
       }
     }
+
+    renameTypesInGenericSignatures();
+
     appInfo.dexItemFactory.forAllTypes(this::renameArrayTypeIfNeeded);
 
     return Collections.unmodifiableMap(renaming);
   }
 
+  private void renameTypesInGenericSignatures() {
+    for (DexClass clazz : appInfo.classes()) {
+      rewriteGenericSignatures(clazz.annotations.annotations,
+          genericSignatureParser::parseClassSignature);
+      clazz.forEachField(field -> rewriteGenericSignatures(
+          field.annotations.annotations, genericSignatureParser::parseFieldSignature));
+      clazz.forEachMethod(method -> rewriteGenericSignatures(
+          method.annotations.annotations, genericSignatureParser::parseMethodSignature));
+    }
+  }
+
+  private void rewriteGenericSignatures(DexAnnotation[] annotations, Consumer<String> parser) {
+    for (int i = 0; i < annotations.length; i++) {
+      DexAnnotation annotation = annotations[i];
+      if (DexAnnotation.isSignatureAnnotation(annotation, appInfo.dexItemFactory)) {
+        parser.accept(getSignatureFromAnnotation(annotation));
+        annotations[i] = DexAnnotation.createSignatureAnnotation(
+            genericSignatureRewriter.getRenamedSignature(),
+            appInfo.dexItemFactory);
+      }
+    }
+  }
+
+  private static String getSignatureFromAnnotation(DexAnnotation signatureAnnotation) {
+    DexValueArray elements = (DexValueArray) signatureAnnotation.annotation.elements[0].value;
+    StringBuilder signature = new StringBuilder();
+    for (DexValue element : elements.getValues()) {
+      signature.append(((DexValueString) element).value.toString());
+    }
+    return signature.toString();
+  }
+
   /**
    * Registers the given type as used.
    * <p>
@@ -105,7 +152,7 @@
   }
 
   private DexString computeName(DexClass clazz) {
-    NamingState state = null;
+    ClassNamingState state = null;
     if (keepInnerClassStructure) {
       // When keeping the nesting structure of inner classes, we have to insert the name
       // of the outer class for the $ prefix.
@@ -129,11 +176,11 @@
     }
   }
 
-  private NamingState getStateFor(String packageName) {
-    return states.computeIfAbsent(packageName, NamingState::new);
+  private ClassNamingState getStateFor(String packageName) {
+    return states.computeIfAbsent(packageName, ClassNamingState::new);
   }
 
-  private NamingState getStateForOuterClass(DexType outer) {
+  private ClassNamingState getStateForOuterClass(DexType outer) {
     String prefix = DescriptorUtils
         .getClassBinaryNameFromDescriptor(outer.toDescriptorString());
     return states.computeIfAbsent(prefix, k -> {
@@ -150,7 +197,7 @@
         }
       }
       String binaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(renamed.toString());
-      return new NamingState(binaryName, "$");
+      return new ClassNamingState(binaryName, "$");
     });
   }
 
@@ -171,21 +218,19 @@
     }
   }
 
-  private class NamingState {
+  private class ClassNamingState {
 
     private final char[] packagePrefix;
-    private final String separator;
     private int typeCounter = 1;
     private Iterator<String> dictionaryIterator;
 
-    NamingState(String packageName) {
+    ClassNamingState(String packageName) {
       this(packageName, "/");
     }
 
-    NamingState(String packageName, String separator) {
+    ClassNamingState(String packageName, String separator) {
       this.packagePrefix = ("L" + packageName + (packageName.isEmpty() ? "" : separator))
           .toCharArray();
-      this.separator = separator;
       this.dictionaryIterator = dictionary.iterator();
     }
 
@@ -211,4 +256,63 @@
       return candidate;
     }
   }
+
+  private class GenericSignatureRewriter implements GenericSignatureAction<DexType> {
+
+    private StringBuilder renamedSignature;
+
+    public String getRenamedSignature() {
+      return renamedSignature.toString();
+    }
+
+    @Override
+    public void parsedSymbol(char symbol) {
+      renamedSignature.append(symbol);
+    }
+
+    @Override
+    public void parsedIdentifier(String identifier) {
+      renamedSignature.append(identifier);
+    }
+
+    @Override
+    public DexType parsedTypeName(String name) {
+      DexType type = appInfo.dexItemFactory.createType(
+          DescriptorUtils.getDescriptorFromClassBinaryName(name));
+      DexString renamedDescriptor = renaming.getOrDefault(type, type.descriptor);
+      renamedSignature.append(DescriptorUtils.getClassBinaryNameFromDescriptor(
+          renamedDescriptor.toString()));
+      return type;
+    }
+
+    @Override
+    public DexType parsedInnerTypeName(DexType enclosingType, String name) {
+      assert enclosingType.isClassType();
+      String enclosingDescriptor = enclosingType.toDescriptorString();
+      DexType type =
+          appInfo.dexItemFactory.createType(
+              DescriptorUtils.getDescriptorFromClassBinaryName(
+                  DescriptorUtils.getClassBinaryNameFromDescriptor(enclosingDescriptor)
+                  + '$' + name));
+      String enclosingRenamedBinaryName =
+          DescriptorUtils.getClassBinaryNameFromDescriptor(renaming.getOrDefault(enclosingType,
+              enclosingType.descriptor).toString());
+      String renamed = DescriptorUtils.getClassBinaryNameFromDescriptor(
+          renaming.getOrDefault(type, type.descriptor).toString());
+      assert renamed.startsWith(enclosingRenamedBinaryName + '$');
+      String outName = renamed.substring(enclosingRenamedBinaryName.length() + 1);
+      renamedSignature.append(outName);
+      return type;
+    }
+
+    @Override
+    public void start() {
+      renamedSignature = new StringBuilder();
+    }
+
+    @Override
+    public void stop() {
+      // nothing to do
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
index 3a72512..8177e89 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
@@ -14,7 +14,7 @@
 import java.util.List;
 import java.util.Map;
 
-public class FieldNameMinifier {
+class FieldNameMinifier {
 
   private final AppInfoWithSubtyping appInfo;
   private final RootSet rootSet;
@@ -22,7 +22,7 @@
   private final List<String> dictionary;
   private final Map<DexType, NamingState<DexType>> states = new IdentityHashMap<>();
 
-  public FieldNameMinifier(AppInfoWithSubtyping appInfo, RootSet rootSet, List<String> dictionary) {
+  FieldNameMinifier(AppInfoWithSubtyping appInfo, RootSet rootSet, List<String> dictionary) {
     this.appInfo = appInfo;
     this.rootSet = rootSet;
     this.dictionary = dictionary;
@@ -50,18 +50,17 @@
       return;
     }
     NamingState<DexType> newState = states.computeIfAbsent(type, t -> state.createChild());
-    reserveFieldNames(newState, holder.instanceFields(), holder.isLibraryClass());
-    reserveFieldNames(newState, holder.staticFields(), holder.isLibraryClass());
+    holder.forEachField(field -> reserveFieldName(field, newState, holder.isLibraryClass()));
     type.forAllExtendsSubtypes(subtype -> reserveNamesInSubtypes(subtype, newState));
   }
 
-  private void reserveFieldNames(NamingState<DexType> state, DexEncodedField[] fields,
+  private void reserveFieldName(
+      DexEncodedField encodedField,
+      NamingState<DexType> state,
       boolean isLibrary) {
-    for (DexEncodedField encodedField : fields) {
-      if (isLibrary || rootSet.noObfuscation.contains(encodedField)) {
-        DexField field = encodedField.field;
-        state.reserveName(field.name, field.type);
-      }
+    if (isLibrary || rootSet.noObfuscation.contains(encodedField)) {
+      DexField field = encodedField.field;
+      state.reserveName(field.name, field.type);
     }
   }
 
@@ -72,17 +71,14 @@
     }
     NamingState<DexType> state = states.get(clazz.type);
     assert state != null;
-    renameFields(clazz.instanceFields(), state);
-    renameFields(clazz.staticFields(), state);
+    clazz.forEachField(field -> renameField(field, state));
     type.forAllExtendsSubtypes(this::renameFieldsInSubtypes);
   }
 
-  private void renameFields(DexEncodedField[] fields, NamingState<DexType> state) {
-    for (DexEncodedField encodedField : fields) {
-      DexField field = encodedField.field;
-      if (!state.isReserved(field.name, field.type)) {
-        renaming.put(field, state.assignNewNameFor(field.name, field.type, false));
-      }
+  private void renameField(DexEncodedField encodedField, NamingState<DexType> state) {
+    DexField field = encodedField.field;
+    if (!state.isReserved(field.name, field.type)) {
+      renaming.put(field, state.assignNewNameFor(field.name, field.type, false));
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
index ec7fa46..4aca460 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
@@ -84,7 +84,7 @@
  * TODO(herhut): Currently, we do not minify members of annotation interfaces, as this would require
  * parsing and minification of the string arguments to annotations.
  */
-public class MethodNameMinifier {
+class MethodNameMinifier {
 
   private final AppInfoWithSubtyping appInfo;
   private final RootSet rootSet;
@@ -93,7 +93,7 @@
   private MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get();
   private final List<String> dictionary;
 
-  public MethodNameMinifier(AppInfoWithSubtyping appInfo, RootSet rootSet,
+  MethodNameMinifier(AppInfoWithSubtyping appInfo, RootSet rootSet,
       List<String> dictionary) {
     this.appInfo = appInfo;
     this.rootSet = rootSet;
@@ -101,7 +101,7 @@
     this.globalState = NamingState.createRoot(appInfo.dexItemFactory, dictionary);
   }
 
-  public Map<DexMethod, DexString> computeRenaming(Timing timing) {
+  Map<DexMethod, DexString> computeRenaming(Timing timing) {
     // Phase 1: Reserve all the names that need to be kept and allocate linked state in the
     //          library part.
     timing.begin("Phase 1");
@@ -198,10 +198,8 @@
       DexClass clazz = appInfo.definitionFor(iface);
       if (clazz != null) {
         Set<NamingState<DexProto>> collectedStates = getReachableStates(iface, frontierMap);
-        addStatesToGlobalMapForMethods(clazz.directMethods(), collectedStates, globalStateMap,
-            sourceMethodsMap, originStates, iface);
-        addStatesToGlobalMapForMethods(clazz.virtualMethods(), collectedStates, globalStateMap,
-            sourceMethodsMap, originStates, iface);
+        clazz.forEachMethod(method -> addStatesToGlobalMapForMethod(
+            method, collectedStates, globalStateMap, sourceMethodsMap, originStates, iface));
       }
     });
     timing.end();
@@ -237,7 +235,6 @@
 
 
   private void collectSubInterfaces(DexType iface, Set<DexType> interfaces) {
-    DexClass clazz = appInfo.definitionFor(iface);
     iface.forAllExtendsSubtypes(subtype -> {
       assert subtype.isInterface();
       if (interfaces.add(subtype)) {
@@ -246,19 +243,17 @@
     });
   }
 
-  private void addStatesToGlobalMapForMethods(
-      DexEncodedMethod[] methods, Set<NamingState<DexProto>> collectedStates,
+  private void addStatesToGlobalMapForMethod(
+      DexEncodedMethod method, Set<NamingState<DexProto>> collectedStates,
       Map<Wrapper<DexMethod>, Set<NamingState<DexProto>>> globalStateMap,
       Map<Wrapper<DexMethod>, Set<DexMethod>> sourceMethodsMap,
       Map<Wrapper<DexMethod>, NamingState<DexProto>> originStates, DexType originInterface) {
-    for (DexEncodedMethod method : methods) {
-      Wrapper<DexMethod> key = equivalence.wrap(method.method);
-      Set<NamingState<DexProto>> stateSet = globalStateMap
-          .computeIfAbsent(key, k -> new HashSet<>());
-      stateSet.addAll(collectedStates);
-      sourceMethodsMap.computeIfAbsent(key, k -> new HashSet<>()).add(method.method);
-      originStates.putIfAbsent(key, states.get(originInterface));
-    }
+    Wrapper<DexMethod> key = equivalence.wrap(method.method);
+    Set<NamingState<DexProto>> stateSet =
+        globalStateMap.computeIfAbsent(key, k -> new HashSet<>());
+    stateSet.addAll(collectedStates);
+    sourceMethodsMap.computeIfAbsent(key, k -> new HashSet<>()).add(method.method);
+    originStates.putIfAbsent(key, states.get(originInterface));
   }
 
   private void assignNameForInterfaceMethodInAllStates(DexMethod method,
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureAction.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureAction.java
new file mode 100644
index 0000000..6753e06
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureAction.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming.signature;
+
+/**
+ * Actions triggered by the generic signature parser.
+ */
+public interface GenericSignatureAction<T> {
+
+  public void parsedSymbol(char symbol);
+
+  public void parsedIdentifier(String identifier);
+
+  public T parsedTypeName(String name);
+
+  public T parsedInnerTypeName(T enclosingType, String name);
+
+  public void start();
+
+  public void stop();
+}
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
new file mode 100644
index 0000000..c5d78f3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
@@ -0,0 +1,403 @@
+// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming.signature;
+
+import java.lang.reflect.GenericSignatureFormatError;
+
+/**
+ * Implements a parser for the generics signature attribute as defined by JVMS 7 $ 4.3.4.
+ * Uses a top-down, recursive descent parsing approach for the following grammar:
+ * <pre>
+ * ClassSignature ::=
+ *     OptFormalTypeParams SuperclassSignature {SuperinterfaceSignature}.
+ * SuperclassSignature ::= ClassTypeSignature.
+ * SuperinterfaceSignature ::= ClassTypeSignature.
+ *
+ * OptFormalTypeParams ::=
+ *     ["<" FormalTypeParameter {FormalTypeParameter} ">"].
+ *
+ * FormalTypeParameter ::= Ident ClassBound {InterfaceBound}.
+ * ClassBound ::= ":" [FieldTypeSignature].
+ * InterfaceBound ::= ":" FieldTypeSignature.
+ *
+ * FieldTypeSignature ::=
+ *     ClassTypeSignature | ArrayTypeSignature | TypeVariableSignature.
+ * ArrayTypeSignature ::= "[" TypSignature.
+ *
+ * ClassTypeSignature ::=
+ *     "L" {Ident "/"} Ident OptTypeArguments {"." Ident OptTypeArguments} ";".
+ *
+ * OptTypeArguments ::= "<" TypeArgument {TypeArgument} ">".
+ *
+ * TypeArgument ::= ([WildcardIndicator] FieldTypeSignature) | "*".
+ * WildcardIndicator ::= "+" | "-".
+ *
+ * TypeVariableSignature ::= "T" Ident ";".
+ *
+ * TypSignature ::= FieldTypeSignature | BaseType.
+ * BaseType ::= "B" | "C" | "D" | "F" | "I" | "J" | "S" | "Z".
+ *
+ * MethodTypeSignature ::=
+ *     OptFormalTypeParams "(" {TypeSignature} ")" ReturnType {ThrowsSignature}.
+ * ThrowsSignature ::= ("^" ClassTypeSignature) | ("^" TypeVariableSignature).
+ *
+ * ReturnType ::= TypSignature | VoidDescriptor.
+ * VoidDescriptor ::= "V".
+ * </pre>
+ */
+public class GenericSignatureParser<T> {
+
+  private final GenericSignatureAction<T> actions;
+
+  /*
+   * Parser:
+   */
+  private char symbol; // 0: eof; else valid term symbol or first char of identifier.
+
+  private String identifier;
+
+  /*
+   * Scanner:
+   * eof is private to the scan methods
+   * and it's set only when a scan is issued at the end of the buffer.
+   */
+  private boolean eof;
+
+  private char[] buffer;
+
+  private int pos;
+
+  public GenericSignatureParser(GenericSignatureAction<T> actions) {
+    this.actions = actions;
+  }
+
+  public void parseClassSignature(String signature) {
+    actions.start();
+    setInput(signature);
+    parseClassSignature();
+    actions.stop();
+  }
+
+  public void parseMethodSignature(String signature) {
+    actions.start();
+    setInput(signature);
+    parseMethodTypeSignature();
+    actions.stop();
+  }
+
+  public void parseFieldSignature(String signature) {
+    actions.start();
+    setInput(signature);
+    parseFieldTypeSignature();
+    actions.stop();
+  }
+
+  private void setInput(String input) {
+    this.buffer = input.toCharArray();
+    this.eof = false;
+    pos = 0;
+    symbol = 0;
+    identifier = null;
+    scanSymbol();
+  }
+
+  //
+  // Parser:
+  //
+
+  void parseClassSignature() {
+    // ClassSignature ::= OptFormalTypeParameters SuperclassSignature {SuperinterfaceSignature}.
+
+    parseOptFormalTypeParameters();
+
+    // SuperclassSignature ::= ClassTypeSignature.
+    parseClassTypeSignature();
+
+    while (symbol > 0) {
+      // SuperinterfaceSignature ::= ClassTypeSignature.
+      parseClassTypeSignature();
+    }
+  }
+
+  void parseOptFormalTypeParameters() {
+    // OptFormalTypeParameters ::= ["<" FormalTypeParameter {FormalTypeParameter} ">"].
+
+    if (symbol == '<') {
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+
+      updateFormalTypeParameter();
+
+      while ((symbol != '>') && (symbol > 0)) {
+        updateFormalTypeParameter();
+      }
+
+      actions.parsedSymbol(symbol);
+      expect('>');
+    }
+  }
+
+  void updateFormalTypeParameter() {
+    // FormalTypeParameter ::= Ident ClassBound {InterfaceBound}.
+    scanIdentifier();
+    assert identifier != null;
+    actions.parsedIdentifier(identifier);
+
+    // ClassBound ::= ":" [FieldTypeSignature].
+    actions.parsedSymbol(symbol);
+    expect(':');
+
+    if (symbol == 'L' || symbol == '[' || symbol == 'T') {
+      parseFieldTypeSignature();
+    }
+
+    while (symbol == ':') {
+      // InterfaceBound ::= ":" FieldTypeSignature.
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+      parseFieldTypeSignature();
+    }
+  }
+
+  private void parseFieldTypeSignature() {
+    // FieldTypeSignature ::= ClassTypeSignature | ArrayTypeSignature | TypeVariableSignature.
+    switch (symbol) {
+      case 'L':
+        parseClassTypeSignature();
+        break;
+      case '[':
+        // ArrayTypeSignature ::= "[" TypSignature.
+        actions.parsedSymbol(symbol);
+        scanSymbol();
+        updateTypeSignature();
+        break;
+      case 'T':
+        updateTypeVariableSignature();
+        break;
+      default:
+        throw new GenericSignatureFormatError();
+    }
+  }
+
+  private void parseClassTypeSignature() {
+    // ClassTypeSignature ::= "L" {Ident "/"} Ident OptTypeArguments {"." Ident OptTypeArguments}
+    //  ";".
+    actions.parsedSymbol(symbol);
+    expect('L');
+
+    StringBuilder qualIdent = new StringBuilder();
+    scanIdentifier();
+    assert identifier != null;
+    while (symbol == '/') {
+      qualIdent.append(identifier).append(symbol);
+      scanSymbol();
+      scanIdentifier();
+      assert identifier != null;
+    }
+
+    qualIdent.append(this.identifier);
+    T parsedEnclosingType = actions.parsedTypeName(qualIdent.toString());
+
+    updateOptTypeArguments();
+
+    while (symbol == '.') {
+      // Deal with Member Classes:
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+      scanIdentifier();
+      assert identifier != null;
+      parsedEnclosingType = actions.parsedInnerTypeName(parsedEnclosingType, identifier);
+      updateOptTypeArguments();
+    }
+
+    actions.parsedSymbol(symbol);
+    expect(';');
+  }
+
+  private void updateOptTypeArguments() {
+    // OptTypeArguments ::= "<" TypeArgument {TypeArgument} ">".
+    if (symbol == '<') {
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+
+      updateTypeArgument();
+      while ((symbol != '>') && (symbol > 0)) {
+        updateTypeArgument();
+      }
+
+      actions.parsedSymbol(symbol);
+      expect('>');
+    }
+  }
+
+  private void updateTypeArgument() {
+    // TypeArgument ::= (["+" | "-"] FieldTypeSignature) | "*".
+    if (symbol == '*') {
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+    } else if (symbol == '+') {
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+      parseFieldTypeSignature();
+    } else if (symbol == '-') {
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+      parseFieldTypeSignature();
+    } else {
+      parseFieldTypeSignature();
+    }
+  }
+
+ private  void updateTypeVariableSignature() {
+    // TypeVariableSignature ::= "T" Ident ";".
+    actions.parsedSymbol(symbol);
+    expect('T');
+
+    scanIdentifier();
+    assert identifier != null;
+    actions.parsedIdentifier(identifier);
+
+    actions.parsedSymbol(symbol);
+    expect(';');
+  }
+
+  private void updateTypeSignature() {
+    switch (symbol) {
+      case 'B':
+      case 'C':
+      case 'D':
+      case 'F':
+      case 'I':
+      case 'J':
+      case 'S':
+      case 'Z':
+        actions.parsedSymbol(symbol);
+        scanSymbol();
+        break;
+      default:
+        // Not an elementary type, but a FieldTypeSignature.
+        parseFieldTypeSignature();
+    }
+  }
+
+  private void parseMethodTypeSignature() {
+    // MethodTypeSignature ::= [FormalTypeParameters] "(" {TypeSignature} ")" ReturnType
+    //  {ThrowsSignature}.
+    parseOptFormalTypeParameters();
+
+    actions.parsedSymbol(symbol);
+    expect('(');
+
+    while (symbol != ')' && (symbol > 0)) {
+      updateTypeSignature();
+    }
+
+    actions.parsedSymbol(symbol);
+    expect(')');
+
+    updateReturnType();
+
+    if (symbol == '^') {
+      do {
+        actions.parsedSymbol(symbol);
+        scanSymbol();
+
+        // ThrowsSignature ::= ("^" ClassTypeSignature) | ("^" TypeVariableSignature).
+        if (symbol == 'T') {
+          updateTypeVariableSignature();
+        } else {
+          parseClassTypeSignature();
+        }
+      } while (symbol == '^');
+    }
+  }
+
+  private void updateReturnType() {
+    // ReturnType ::= TypeSignature | "V".
+    if (symbol != 'V') {
+      updateTypeSignature();
+    } else {
+      actions.parsedSymbol(symbol);
+      scanSymbol();
+    }
+  }
+
+
+  //
+  // Scanner:
+  //
+
+  private void scanSymbol() {
+    if (!eof) {
+      assert buffer != null;
+      if (pos < buffer.length) {
+        symbol = buffer[pos];
+        pos++;
+      } else {
+        symbol = 0;
+        eof = true;
+      }
+    } else {
+      throw new GenericSignatureFormatError();
+    }
+  }
+
+  private void expect(char c) {
+    if (symbol == c) {
+      scanSymbol();
+    } else {
+      throw new GenericSignatureFormatError();
+    }
+  }
+
+ private  boolean isStopSymbol(char ch) {
+    switch (ch) {
+      case ':':
+      case '/':
+      case ';':
+      case '<':
+      case '.':
+        return true;
+    }
+    return false;
+  }
+
+  // PRE: symbol is the first char of the identifier.
+  // POST: symbol = the next symbol AFTER the identifier.
+  private void scanIdentifier() {
+    if (!eof) {
+      StringBuilder identBuf = new StringBuilder(32);
+      if (!isStopSymbol(symbol)) {
+        identBuf.append(symbol);
+
+        // FINDBUGS
+        char[] bufferLocal = buffer;
+        assert bufferLocal != null;
+        do {
+          char ch = bufferLocal[pos];
+          if ((ch >= 'a') && (ch <= 'z') || (ch >= 'A') && (ch <= 'Z')
+              || !isStopSymbol(ch)) {
+            identBuf.append(bufferLocal[pos]);
+            pos++;
+          } else {
+            identifier = identBuf.toString();
+            scanSymbol();
+            return;
+          }
+        } while (pos != bufferLocal.length);
+        identifier = identBuf.toString();
+        symbol = 0;
+        eof = true;
+      } else {
+        // Ident starts with incorrect char.
+        symbol = 0;
+        eof = true;
+        throw new GenericSignatureFormatError();
+      }
+    } else {
+      throw new GenericSignatureFormatError();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 4cd44ef..3106c0c 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -21,17 +21,15 @@
 public class AnnotationRemover {
 
   private final AppInfoWithLiveness appInfo;
-  private final boolean minificationEnabled;
   private final AttributeRemovalOptions keep;
 
   public AnnotationRemover(AppInfoWithLiveness appInfo, InternalOptions options) {
-    this(appInfo, !options.skipMinification, options.attributeRemoval);
+    this(appInfo, options.attributeRemoval);
   }
 
-  public AnnotationRemover(AppInfoWithLiveness appInfo, boolean minificationEnabled,
+  public AnnotationRemover(AppInfoWithLiveness appInfo,
       AttributeRemovalOptions keep) {
     this.appInfo = appInfo;
-    this.minificationEnabled = minificationEnabled;
     this.keep = keep;
   }
 
@@ -104,7 +102,7 @@
   }
 
   public void run() {
-    keep.ensureValid(minificationEnabled);
+    keep.ensureValid();
     for (DexProgramClass clazz : appInfo.classes()) {
       clazz.annotations = stripAnnotations(clazz.annotations, this::filterAnnotations);
       clazz.forEachMethod(this::processMethod);
diff --git a/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java b/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java
index a7a27fe..e675525 100644
--- a/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java
+++ b/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
-import java.util.Arrays;
 import java.util.Set;
 
 public class DiscardedChecker {
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexListBuilder.java b/src/main/java/com/android/tools/r8/shaking/MainDexListBuilder.java
index f6ac140..b589535 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexListBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexListBuilder.java
@@ -11,10 +11,8 @@
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCallSite;
 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.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProgramClass;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 3b78e1b..cc29125 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -159,8 +159,6 @@
         if (isOptionalArgumentGiven()) {
           configurationBuilder.setPrintUsageFile(parseFileName());
         }
-        // TODO(b/36799826): once fully implemented, no longer necessary to warn.
-        System.out.println("WARNING: Ignoring option: -printusage");
       } else if (acceptString("verbose")) {
         configurationBuilder.setVerbose(true);
       } else if (acceptString("ignorewarnings")) {
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 3968c9a..2a5c9e0 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -22,25 +23,27 @@
   private DexApplication application;
   private final AppInfoWithLiveness appInfo;
   private final InternalOptions options;
+  private UsagePrinter usagePrinter;
 
   public TreePruner(
       DexApplication application, AppInfoWithLiveness appInfo, InternalOptions options) {
     this.application = application;
     this.appInfo = appInfo;
     this.options = options;
+    this.usagePrinter = options.printUsage ? new UsagePrinter() : UsagePrinter.DONT_PRINT;
   }
 
-  public DexApplication run() {
+  public DexApplication run() throws IOException {
     application.timing.begin("Pruning application...");
     if (options.debugKeepRules && !options.skipMinification) {
       System.out.println(
-          "NOTE: Debugging keep rules on a minified build might yield broken builds, as\n" +
-              "      minifcation also depends on the used keep rules. We recommend using\n" +
-              "      --skip-minification.");
+          "NOTE: Debugging keep rules on a minified build might yield broken builds, as\n"
+              + "      minification also depends on the used keep rules. We recommend using\n"
+              + "      --skip-minification.");
     }
     DexApplication result;
     try {
-      result = removeUnused(application).build();
+      result = removeUnused(application).appendDeadCode(usagePrinter.toByteArray()).build();
     } finally {
       application.timing.end();
     }
@@ -60,6 +63,7 @@
         if (Log.ENABLED) {
           Log.debug(getClass(), "Removing class: " + clazz);
         }
+        usagePrinter.printUnusedClass(clazz);
       } else {
         newClasses.add(clazz);
         if (!appInfo.instantiatedTypes.contains(clazz.type) &&
@@ -78,10 +82,12 @@
         }
         // The class is used and must be kept. Remove the unused fields and methods from
         // the class.
+        usagePrinter.visiting(clazz);
         clazz.directMethods = reachableMethods(clazz.directMethods(), clazz);
         clazz.virtualMethods = reachableMethods(clazz.virtualMethods(), clazz);
         clazz.instanceFields = reachableFields(clazz.instanceFields());
         clazz.staticFields = reachableFields(clazz.staticFields());
+        usagePrinter.visited();
       }
     }
     return newClasses;
@@ -122,18 +128,18 @@
       reachableMethods.add(methods[i]);
     }
     for (int i = firstUnreachable; i < methods.length; i++) {
+      DexEncodedMethod method = methods[i];
       if (appInfo.liveMethods.contains(methods[i].getKey())) {
-        reachableMethods.add(methods[i]);
-      } else if (options.debugKeepRules && isDefaultConstructor(methods[i])) {
+        reachableMethods.add(method);
+      } else if (options.debugKeepRules && isDefaultConstructor(method)) {
         // Keep the method but rewrite its body, if it has one.
         reachableMethods.add(methods[i].accessFlags.isAbstract()
-            ? methods[i]
-            : methods[i].toMethodThatLogsError(application.dexItemFactory));
-      } else if (appInfo.targetedMethods.contains(methods[i].getKey())) {
+            ? method
+            : method.toMethodThatLogsError(application.dexItemFactory));
+      } else if (appInfo.targetedMethods.contains(method.getKey())) {
         if (Log.ENABLED) {
-          Log.debug(getClass(), "Making method %s abstract.", methods[i].method);
+          Log.debug(getClass(), "Making method %s abstract.", method.method);
         }
-        DexEncodedMethod method = methods[i];
         // Final classes cannot be abstract, so we have to keep the method in that case.
         // Also some other kinds of methods cannot be abstract, so keep them around.
         boolean allowAbstract = clazz.accessFlags.isAbstract()
@@ -144,10 +150,13 @@
         // By construction, private and static methods cannot be reachable but non-live.
         assert !method.accessFlags.isPrivate() && !method.accessFlags.isStatic();
         reachableMethods.add(allowAbstract
-            ? methods[i].toAbstractMethod()
-            : methods[i].toEmptyThrowingMethod());
-      } else if (Log.ENABLED) {
-        Log.debug(getClass(), "Removing method %s.", methods[i].method);
+            ? method.toAbstractMethod()
+            : method.toEmptyThrowingMethod());
+      } else {
+        if (Log.ENABLED) {
+          Log.debug(getClass(), "Removing method %s.", method.method);
+        }
+        usagePrinter.printUnusedMethod(method);
       }
     }
     return reachableMethods.toArray(new DexEncodedMethod[reachableMethods.size()]);
@@ -155,21 +164,27 @@
 
   private DexEncodedField[] reachableFields(DexEncodedField[] fields) {
     int firstUnreachable = firstUnreachableIndex(fields, appInfo.liveFields);
+    // Return the original array if all fields are used.
     if (firstUnreachable == -1) {
       return fields;
     }
     if (Log.ENABLED) {
       Log.debug(getClass(), "Removing field: " + fields[firstUnreachable]);
     }
+    usagePrinter.printUnusedField(fields[firstUnreachable]);
     ArrayList<DexEncodedField> reachableFields = new ArrayList<>(fields.length);
     for (int i = 0; i < firstUnreachable; i++) {
       reachableFields.add(fields[i]);
     }
     for (int i = firstUnreachable + 1; i < fields.length; i++) {
-      if (appInfo.liveFields.contains(fields[i].getKey())) {
-        reachableFields.add(fields[i]);
-      } else if (Log.ENABLED) {
-        Log.debug(getClass(), "Removing field: " + fields[i]);
+      DexEncodedField field = fields[i];
+      if (appInfo.liveFields.contains(field.getKey())) {
+        reachableFields.add(field);
+      } else {
+        if (Log.ENABLED) {
+          Log.debug(getClass(), "Removing field: " + field);
+        }
+        usagePrinter.printUnusedField(field);
       }
     }
     return reachableFields.toArray(new DexEncodedField[reachableFields.size()]);
diff --git a/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java b/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java
new file mode 100644
index 0000000..7e41671
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import java.nio.charset.StandardCharsets;
+
+class UsagePrinter {
+  private static final String INDENT = "    ";
+
+  static final UsagePrinter DONT_PRINT = new NoOpUsagePrinter();
+
+  private final StringBuilder writer;
+  private DexProgramClass enclosingClazz = null;
+  private boolean clazzPrefixPrinted = false;
+
+  UsagePrinter() {
+    writer = new StringBuilder();
+  }
+
+  byte[] toByteArray() {
+    return writer.toString().getBytes(StandardCharsets.UTF_8);
+  }
+
+  void printUnusedClass(DexProgramClass clazz) {
+    writer.append(clazz.toSourceString());
+    writer.append('\n');
+  }
+
+  // Visiting methods and fields of the given clazz.
+  void visiting(DexProgramClass clazz) {
+    assert enclosingClazz == null;
+    enclosingClazz = clazz;
+  }
+
+  // Visited methods and fields of the top at the clazz stack.
+  void visited() {
+    enclosingClazz = null;
+    clazzPrefixPrinted = false;
+  }
+
+  private void printClazzPrefixIfNecessary() {
+    assert enclosingClazz != null;
+    if (!clazzPrefixPrinted) {
+      writer.append(enclosingClazz.toSourceString());
+      writer.append('\n');
+      clazzPrefixPrinted = true;
+    }
+  }
+
+  void printUnusedMethod(DexEncodedMethod method) {
+    printClazzPrefixIfNecessary();
+    writer.append(INDENT);
+    String accessFlags = method.accessFlags.toString();
+    if (!accessFlags.isEmpty()) {
+      writer.append(accessFlags).append(' ');
+    }
+    writer.append(method.method.proto.returnType.toSourceString()).append(' ');
+    writer.append(method.method.name.toSourceString());
+    writer.append('(');
+    for (int i = 0; i < method.method.proto.parameters.values.length; i++) {
+      if (i != 0) {
+        writer.append(',');
+      }
+      writer.append(method.method.proto.parameters.values[i].toSourceString());
+    }
+    writer.append(')');
+    writer.append('\n');
+  }
+
+  void printUnusedField(DexEncodedField field) {
+    printClazzPrefixIfNecessary();
+    writer.append(INDENT);
+    String accessFlags = field.accessFlags.toString();
+    if (!accessFlags.isEmpty()) {
+      writer.append(accessFlags).append(' ');
+    }
+    writer.append(field.field.type.toSourceString()).append(" ");
+    writer.append(field.field.name.toSourceString());
+    writer.append('\n');
+  }
+
+  // Empty implementation to silently ignore printing dead code.
+  private static class NoOpUsagePrinter extends UsagePrinter {
+
+    @Override
+    byte[] toByteArray() {
+      return null;
+    }
+
+    @Override
+    void printUnusedClass(DexProgramClass clazz) {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void visiting(DexProgramClass clazz) {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void visited() {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void printUnusedMethod(DexEncodedMethod method) {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void printUnusedField(DexEncodedField field) {
+      // Intentionally left empty.
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 3877f1a..e767030 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -52,6 +52,7 @@
   private final ImmutableList<Resource> programResources;
   private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
   private final ImmutableList<ClassFileResourceProvider> libraryResourceProviders;
+  private final Resource deadCode;
   private final Resource proguardMap;
   private final Resource proguardSeeds;
   private final Resource packageDistribution;
@@ -62,6 +63,7 @@
       ImmutableList<Resource> programResources,
       ImmutableList<ClassFileResourceProvider> classpathResourceProviders,
       ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
+      Resource deadCode,
       Resource proguardMap,
       Resource proguardSeeds,
       Resource packageDistribution,
@@ -69,6 +71,7 @@
     this.programResources = programResources;
     this.classpathResourceProviders = classpathResourceProviders;
     this.libraryResourceProviders = libraryResourceProviders;
+    this.deadCode = deadCode;
     this.proguardMap = proguardMap;
     this.proguardSeeds = proguardSeeds;
     this.packageDistribution = packageDistribution;
@@ -169,6 +172,20 @@
   }
 
   /**
+   * True if the dead-code resource exists.
+   */
+  public boolean hasDeadCode() {
+    return deadCode != null;
+  }
+
+  /**
+   * Get the input stream of the dead-code resource if exists.
+   */
+  public InputStream getDeadCode(Closer closer) throws IOException {
+    return deadCode == null ? null : closer.register(deadCode.getStream());
+  }
+
+  /**
    * True if the proguard-map resource exists.
    */
   public boolean hasProguardMap() {
@@ -179,7 +196,7 @@
    * Get the input stream of the proguard-map resource if it exists.
    */
   public InputStream getProguardMap(Closer closer) throws IOException {
-    return proguardMap == null ? null : proguardMap.getStream(closer);
+    return proguardMap == null ? null : closer.register(proguardMap.getStream());
   }
 
   /**
@@ -193,7 +210,7 @@
    * Get the input stream of the proguard-seeds resource if it exists.
    */
   public InputStream getProguardSeeds(Closer closer) throws IOException {
-    return proguardSeeds == null ? null : proguardSeeds.getStream(closer);
+    return proguardSeeds == null ? null : closer.register(proguardSeeds.getStream());
   }
 
   /**
@@ -207,7 +224,7 @@
    * Get the input stream of the package distribution resource if it exists.
    */
   public InputStream getPackageDistribution(Closer closer) throws IOException {
-    return packageDistribution == null ? null : packageDistribution.getStream(closer);
+    return packageDistribution == null ? null : closer.register(packageDistribution.getStream());
   }
 
   /**
@@ -221,7 +238,7 @@
    * Get the input stream of the main dex list resource if it exists.
    */
   public InputStream getMainDexList(Closer closer) throws IOException {
-    return mainDexList == null ? null : mainDexList.getStream(closer);
+    return mainDexList == null ? null : closer.register(mainDexList.getStream());
   }
 
   /**
@@ -254,7 +271,7 @@
         if (!Files.exists(filePath.getParent())) {
           Files.createDirectories(filePath.getParent());
         }
-        Files.copy(dexProgramSources.get(i).getStream(closer), filePath, options);
+        Files.copy(closer.register(dexProgramSources.get(i).getStream()), filePath, options);
       }
     }
   }
@@ -290,7 +307,7 @@
       List<Resource> dexProgramSources = getDexProgramResources();
       for (int i = 0; i < dexProgramSources.size(); i++) {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
-        ByteStreams.copy(dexProgramSources.get(i).getStream(closer), out);
+        ByteStreams.copy(closer.register(dexProgramSources.get(i).getStream()), out);
         dex.add(out.toByteArray());
       }
       // TODO(sgjesse): Add Proguard map and seeds.
@@ -309,7 +326,8 @@
         List<Resource> dexProgramSources = getDexProgramResources();
         for (int i = 0; i < dexProgramSources.size(); i++) {
           ZipEntry zipEntry = new ZipEntry(outputMode.getOutputPath(dexProgramSources.get(i), i));
-          byte[] bytes = ByteStreams.toByteArray(dexProgramSources.get(i).getStream(closer));
+          byte[] bytes =
+              ByteStreams.toByteArray(closer.register(dexProgramSources.get(i).getStream()));
           zipEntry.setSize(bytes.length);
           out.putNextEntry(zipEntry);
           out.write(bytes);
@@ -337,6 +355,12 @@
     out.write(ByteStreams.toByteArray(input));
   }
 
+  public void writeDeadCode(Closer closer, OutputStream out) throws IOException {
+    InputStream input = getDeadCode(closer);
+    assert input != null;
+    out.write(ByteStreams.toByteArray(input));
+  }
+
   /**
    * Builder interface for constructing an AndroidApp.
    */
@@ -345,6 +369,7 @@
     private final List<Resource> programResources = new ArrayList<>();
     private final List<ClassFileResourceProvider> classpathResourceProviders = new ArrayList<>();
     private final List<ClassFileResourceProvider> libraryResourceProviders = new ArrayList<>();
+    private Resource deadCode;
     private Resource proguardMap;
     private Resource proguardSeeds;
     private Resource packageDistribution;
@@ -359,6 +384,7 @@
       programResources.addAll(app.programResources);
       classpathResourceProviders.addAll(app.classpathResourceProviders);
       libraryResourceProviders.addAll(app.libraryResourceProviders);
+      deadCode = app.deadCode;
       proguardMap = app.proguardMap;
       proguardSeeds = app.proguardSeeds;
       packageDistribution = app.packageDistribution;
@@ -499,6 +525,14 @@
     }
 
     /**
+     * Set dead-code data.
+     */
+    public Builder setDeadCode(byte[] content) {
+      deadCode = content == null ? null : Resource.fromBytes(null, content);
+      return this;
+    }
+
+    /**
      * Set proguard-map file.
      */
     public Builder setProguardMapFile(Path file) {
@@ -561,6 +595,7 @@
           ImmutableList.copyOf(programResources),
           ImmutableList.copyOf(classpathResourceProviders),
           ImmutableList.copyOf(libraryResourceProviders),
+          deadCode,
           proguardMap,
           proguardSeeds,
           packageDistribution,
diff --git a/src/main/java/com/android/tools/r8/utils/ClassProvider.java b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
index 25abdce..63a847a 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
@@ -100,7 +100,7 @@
         try (Closer closer = Closer.create()) {
           JarClassFileReader classReader =
               new JarClassFileReader(reader, classKind.bridgeConsumer(classConsumer));
-          classReader.read(DEFAULT_DEX_FILENAME, classKind, resource.getStream(closer));
+          classReader.read(DEFAULT_DEX_FILENAME, classKind, closer.register(resource.getStream()));
         } catch (IOException e) {
           throw new CompilationError("Failed to load class: " + descriptor, e);
         }
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 eae33d1..277ee0f 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -148,6 +148,18 @@
     return classDescriptor.substring(1, classDescriptor.length() - 1);
   }
 
+
+  /**
+   * Convert a class binary name to a descriptor.
+   *
+   * @param typeBinaryName class binary name i.e. "java/lang/Object"
+   * @return a class descriptor i.e. "Ljava/lang/Object;"
+   */
+  public static String getDescriptorFromClassBinaryName(String typeBinaryName) {
+    return ('L' + typeBinaryName + ';');
+  }
+
+
   /**
    * Get class name from its binary name.
    *
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 16f41cc..5c5a776 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -4,11 +4,16 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.CompilationException;
+import com.google.common.io.Closer;
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.OpenOption;
 import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -79,10 +84,27 @@
       if (!isJarOrZip  && !(Files.exists(path) && Files.isDirectory(path))) {
         throw new CompilationException(
             "Invalid output: "
-                + path +
-                "\nOutput must be a .zip or .jar archive or an existing directory");
+                + path
+                + "\nOutput must be a .zip or .jar archive or an existing directory");
       }
     }
     return path;
   }
+
+  public static OutputStream openPathWithDefault(
+      Closer closer,
+      Path file,
+      PrintStream defaultOutput,
+      OpenOption... openOptions)
+      throws IOException {
+    OutputStream mapOut;
+    if (file == null) {
+      mapOut = defaultOutput;
+    } else {
+      mapOut = Files.newOutputStream(file, openOptions);
+      closer.register(mapOut);
+    }
+    return mapOut;
+  }
+
 }
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 6149846..eb746d7 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -7,14 +7,12 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.ir.conversion.CallGraph;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.shaking.ProguardTypeMatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
 import java.util.List;
-import java.util.function.BiFunction;
 import java.util.function.Function;
 
 public class InternalOptions {
@@ -251,7 +249,7 @@
       annotationDefault = update(annotationDefault, ANNOTATION_DEFAULT, patterns);
     }
 
-    public void ensureValid(boolean isMinifying) {
+    public void ensureValid() {
       if (innerClasses && !enclosingMethod) {
         throw new CompilationError("Attribute InnerClasses requires EnclosingMethod attribute. "
             + "Check -keepattributes directive.");
@@ -261,10 +259,6 @@
       } else if (signature && !innerClasses) {
         throw new CompilationError("Attribute Signature requires InnerClasses attribute. Check "
             + "-keepattributes directive.");
-      } else if (signature && isMinifying) {
-        // TODO(38188583): Allow this once we can minify signatures.
-        throw new CompilationError("Attribute Signature cannot be kept when minifying. "
-            + "Check -keepattributes directive.");
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/utils/PackageDistribution.java b/src/main/java/com/android/tools/r8/utils/PackageDistribution.java
index 6d36bc3..b242b9d 100644
--- a/src/main/java/com/android/tools/r8/utils/PackageDistribution.java
+++ b/src/main/java/com/android/tools/r8/utils/PackageDistribution.java
@@ -154,7 +154,7 @@
   }
 
   public int maxReferencedIndex() {
-    return map.values().stream().max(Integer::compare).orElseGet(() -> 0);
+    return map.values().stream().max(Integer::compare).orElse(0);
   }
 
   public Set<String> getFiles() {
diff --git a/src/test/examples/minifygeneric/AA.java b/src/test/examples/minifygeneric/AA.java
new file mode 100644
index 0000000..66b7aaf8
--- /dev/null
+++ b/src/test/examples/minifygeneric/AA.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygeneric;
+
+public class AA {
+}
diff --git a/src/test/examples/minifygeneric/BB.java b/src/test/examples/minifygeneric/BB.java
new file mode 100644
index 0000000..c29ca97
--- /dev/null
+++ b/src/test/examples/minifygeneric/BB.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygeneric;
+
+public class BB {
+}
diff --git a/src/test/examples/minifygeneric/Generic.java b/src/test/examples/minifygeneric/Generic.java
new file mode 100644
index 0000000..53f9e3d
--- /dev/null
+++ b/src/test/examples/minifygeneric/Generic.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygeneric;
+
+import java.util.List;
+import java.util.Map;
+
+public class Generic<T extends AA> {
+
+  public <U extends List> T m (Object o, T[] t, Map<T,U> m) {
+    return null;
+  }
+
+  public <U extends Map> T m2 (Object o, T[] t, Map<T,U> m) {
+    return null;
+  }
+
+  public <U extends List> T m3 (Object o, T t, Map<T,U> m) {
+    return null;
+  }
+
+  public <U extends List> T m4 (Object o, T[] t, List<U> m) {
+    return null;
+  }
+
+  public Generic<T> f;
+  public Generic<T> get() {
+    return this;
+  }
+
+}
diff --git a/src/test/examples/minifygeneric/Minifygeneric.java b/src/test/examples/minifygeneric/Minifygeneric.java
new file mode 100644
index 0000000..36406bb
--- /dev/null
+++ b/src/test/examples/minifygeneric/Minifygeneric.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygeneric;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+public class Minifygeneric {
+
+  public static void main(String[] args) throws NoSuchMethodException, SecurityException, NoSuchFieldException {
+    for (TypeVariable<Class<Generic>> var : Generic.class.getTypeParameters()) {
+      System.out.println(var.getName());
+      Type bound = var.getBounds()[0];
+      System.out.println(((Class<?>) bound).getName().equals(AA.class.getName()));
+    }
+
+    Field f = Generic.class.getField("f");
+    ParameterizedType fieldType = (java.lang.reflect.ParameterizedType)f.getGenericType();
+    checkOneParameterType(fieldType, Generic.class, AA.class);
+
+    ParameterizedType methodReturnType =
+        (ParameterizedType) Generic.class.getMethod("get").getGenericReturnType();
+    checkOneParameterType(methodReturnType, Generic.class, AA.class);
+  }
+
+  private static void checkOneParameterType(ParameterizedType toCheck, Class<?> rawType,
+      Class<?>... bounds) {
+    System.out.println(((Class<?>) toCheck.getRawType()).getName()
+        .equals(rawType.getName()));
+    Type[] parameters = toCheck.getActualTypeArguments();
+    System.out.println(parameters.length);
+    TypeVariable<?> parameter = (TypeVariable<?>) parameters[0];
+    System.out.println(parameter.getName());
+    Type[] actualBounds = parameter.getBounds();
+    for (int i = 0; i < bounds.length; i++) {
+      System.out.println(((Class<?>) actualBounds[i]).getName().equals(bounds[i].getName()));
+    }
+  }
+}
diff --git a/src/test/examples/minifygeneric/keep-rules.txt b/src/test/examples/minifygeneric/keep-rules.txt
new file mode 100644
index 0000000..741d5c3
--- /dev/null
+++ b/src/test/examples/minifygeneric/keep-rules.txt
@@ -0,0 +1,15 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+-keep,allowobfuscation class ** {
+*;
+}
+-keep class *.Minifygeneric {
+*;
+}
+-keepclassmembernames class *.Generic {
+*;
+}
+-keepattributes *
+-allowaccessmodification
diff --git a/src/test/examples/minifygenericwithinner/AA.java b/src/test/examples/minifygenericwithinner/AA.java
new file mode 100644
index 0000000..ff283da
--- /dev/null
+++ b/src/test/examples/minifygenericwithinner/AA.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygenericwithinner;
+
+public class AA {
+}
diff --git a/src/test/examples/minifygenericwithinner/BB.java b/src/test/examples/minifygenericwithinner/BB.java
new file mode 100644
index 0000000..31ceb33
--- /dev/null
+++ b/src/test/examples/minifygenericwithinner/BB.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygenericwithinner;
+
+public class BB {
+}
diff --git a/src/test/examples/minifygenericwithinner/Generic.java b/src/test/examples/minifygenericwithinner/Generic.java
new file mode 100644
index 0000000..203561e
--- /dev/null
+++ b/src/test/examples/minifygenericwithinner/Generic.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygenericwithinner;
+
+import java.util.List;
+import java.util.Map;
+
+public class Generic<T extends AA> {
+
+  public <U extends List> T m (Object o, T[] t, Map<T,U> m) {
+    return null;
+  }
+
+  public <U extends Map> T m2 (Object o, T[] t, Map<T,U> m) {
+    return null;
+  }
+
+  public <U extends List> T m3 (Object o, T t, Map<T,U> m) {
+    return null;
+  }
+
+  public <U extends List> T m4 (Object o, T[] t, List<U> m) {
+    return null;
+  }
+
+  public <V extends BB> Inner<V> getInner(V obj) {
+    return new Inner<>();
+  }
+
+  public class Inner<V extends BB> {
+
+    public Generic<T>.Inner<V> f;
+    public <U extends List> T m5 (V o, T[] t, Map<T,U> m) {
+      return m(o, t, m);
+    }
+
+   public Generic<T>.Inner<V> get() {
+      return this;
+    }
+  }
+}
diff --git a/src/test/examples/minifygenericwithinner/Minifygenericwithinner.java b/src/test/examples/minifygenericwithinner/Minifygenericwithinner.java
new file mode 100644
index 0000000..882fa83
--- /dev/null
+++ b/src/test/examples/minifygenericwithinner/Minifygenericwithinner.java
@@ -0,0 +1,52 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package minifygenericwithinner;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+public class Minifygenericwithinner {
+
+  public static void main(String[] args)
+      throws NoSuchMethodException, SecurityException, NoSuchFieldException {
+    for (TypeVariable<Class<Generic>> var : Generic.class.getTypeParameters()) {
+      System.out.println(var.getName());
+      Type bound = var.getBounds()[0];
+      System.out.println(((Class<?>) bound).getName().equals(AA.class.getName()));
+    }
+    for (TypeVariable<Class<Generic.Inner>> var : Generic.Inner.class.getTypeParameters()) {
+      System.out.println(var.getName());
+      Type bound = var.getBounds()[0];
+      System.out.println(((Class<?>) bound).getName().equals(BB.class.getName()));
+    }
+
+    Field f = Generic.Inner.class.getField("f");
+    ParameterizedType fieldType = (java.lang.reflect.ParameterizedType)f.getGenericType();
+    checkOneParameterType(fieldType, Generic.Inner.class, BB.class);
+    ParameterizedType ownerType = (ParameterizedType) fieldType.getOwnerType();
+    checkOneParameterType(ownerType, Generic.class, AA.class);
+
+    ParameterizedType methodReturnType =
+        (ParameterizedType) Generic.Inner.class.getMethod("get").getGenericReturnType();
+    checkOneParameterType(methodReturnType, Generic.Inner.class, BB.class);
+    ownerType = (ParameterizedType) methodReturnType.getOwnerType();
+    checkOneParameterType(ownerType, Generic.class, AA.class);
+  }
+
+  private static void checkOneParameterType(ParameterizedType toCheck, Class<?> rawType,
+      Class<?>... bounds) {
+    System.out.println(((Class<?>) toCheck.getRawType()).getName()
+        .equals(rawType.getName()));
+    Type[] parameters = toCheck.getActualTypeArguments();
+    System.out.println(parameters.length);
+    TypeVariable<?> parameter = (TypeVariable<?>) parameters[0];
+    System.out.println(parameter.getName());
+    Type[] actualBounds = parameter.getBounds();
+    for (int i = 0; i < bounds.length; i++) {
+      System.out.println(((Class<?>) actualBounds[i]).getName().equals(bounds[i].getName()));
+    }
+  }
+}
diff --git a/src/test/examples/minifygenericwithinner/keep-rules.txt b/src/test/examples/minifygenericwithinner/keep-rules.txt
new file mode 100644
index 0000000..357b321
--- /dev/null
+++ b/src/test/examples/minifygenericwithinner/keep-rules.txt
@@ -0,0 +1,16 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+-keep,allowobfuscation class ** {
+*;
+}
+-keep class *.Minifygenericwithinner {
+*;
+}
+-keepclassmembernames class *.Generic$* {
+*;
+}
+
+-keepattributes *
+-allowaccessmodification
diff --git a/src/test/examples/shaking1/keep-rules-printusage.txt b/src/test/examples/shaking1/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking1/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking12/keep-rules-printusage.txt b/src/test/examples/shaking12/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking12/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking2/keep-rules-printusage.txt b/src/test/examples/shaking2/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking2/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking4/keep-rules-printusage.txt b/src/test/examples/shaking4/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking4/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking8/keep-rules-printusage.txt b/src/test/examples/shaking8/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking8/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking9/keep-rules-printusage.txt b/src/test/examples/shaking9/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking9/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
index d8b9bbc..803d760 100644
--- a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
@@ -294,7 +294,7 @@
     ByteArrayOutputStream output = new ByteArrayOutputStream();
     byte[] buffer = new byte[16384];
     try (Closer closer = Closer.create()) {
-      InputStream stream = resource.getStream(closer);
+      InputStream stream = closer.register(resource.getStream());
       int read;
       while ((read = stream.read(buffer, 0, buffer.length)) != -1) {
         output.write(buffer, 0, read);
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 9aec981..38868b6 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -61,18 +61,16 @@
     }
 
     TestRunner withClassCheck(Consumer<FoundClassSubject> check) {
-      withDexCheck(inspector -> inspector.forAllClasses(check));
-      return this;
+      return withDexCheck(inspector -> inspector.forAllClasses(check));
     }
 
     TestRunner withMethodCheck(Consumer<FoundMethodSubject> check) {
-      withClassCheck(clazz -> clazz.forAllMethods(check));
-      return this;
+      return withClassCheck(clazz -> clazz.forAllMethods(check));
     }
 
-    <T extends InstructionSubject> TestRunner
-    withInstructionCheck(Predicate<InstructionSubject> filter, Consumer<T> check) {
-      withMethodCheck(method -> {
+    <T extends InstructionSubject> TestRunner withInstructionCheck(
+        Predicate<InstructionSubject> filter, Consumer<T> check) {
+      return withMethodCheck(method -> {
         if (method.isAbstract()) {
           return;
         }
@@ -81,7 +79,6 @@
           check.accept(iterator.next());
         }
       });
-      return this;
     }
 
     TestRunner withOptionConsumer(Consumer<InternalOptions> consumer) {
@@ -267,7 +264,7 @@
   @Test
   public void paramNames() throws Throwable {
     test("paramnames", "paramnames", "ParameterNames")
-        .withMinApiLevel(26)
+        .withMinApiLevel(ANDROID_O_API)
         .withOptionConsumer((internalOptions) -> internalOptions.allowParameterName = true)
         .run();
   }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index e01ecc0..d0032cd 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
@@ -56,7 +57,7 @@
   public static final String LINE_SEPARATOR = System.getProperty("line.separator");
 
   private static final String ANDROID_JAR_PATTERN = "third_party/android_jar/lib-v%d/android.jar";
-  private static final int DEFAULT_MIN_SDK = 14;
+  private static final int DEFAULT_MIN_SDK = Constants.ANDROID_I_API;
 
   public enum DexVm {
     ART_4_4_4("4.4.4"),
@@ -487,6 +488,14 @@
             .build());
   }
 
+  public static DexApplication optimizeWithR8(
+      DexApplication application,
+      AppInfoWithSubtyping appInfo,
+      InternalOptions options)
+      throws ProguardRuleParserException, ExecutionException, IOException {
+    return R8.optimize(application, appInfo, options);
+  }
+
   public static AndroidApp runD8(AndroidApp app) throws CompilationException, IOException {
     return runD8(app, null);
   }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index 8fdcfa5..fc9ee3f 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -62,8 +62,8 @@
       List<Resource> files2 = app2.getDexProgramResources();
       assertEquals(files1.size(), files2.size());
       for (int index = 0; index < files1.size(); index++) {
-        InputStream file1 = files1.get(index).getStream(closer);
-        InputStream file2 = files2.get(index).getStream(closer);
+        InputStream file1 = closer.register(files1.get(index).getStream());
+        InputStream file2 = closer.register(files2.get(index).getStream());
         byte[] bytes1 = ByteStreams.toByteArray(file1);
         byte[] bytes2 = ByteStreams.toByteArray(file2);
         assertArrayEquals("File index " + index, bytes1, bytes2);
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreTreeShakeJarVerificationTest.java
index d28c37d..fa193d4 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreTreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreTreeShakeJarVerificationTest.java
@@ -34,7 +34,7 @@
     int bytes = 0;
     try (Closer closer = Closer.create()) {
       for (Resource dex : app.getDexProgramResources()) {
-        bytes += ByteStreams.toByteArray(dex.getStream(closer)).length;
+        bytes += ByteStreams.toByteArray(closer.register(dex.getStream())).length;
       }
     }
     assertTrue("Expected max size of " + maxSize + ", got " + bytes, bytes < maxSize);
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/YouTubeTreeShakeJarVerificationTest.java
index b26d1aa..8ee3674 100644
--- a/src/test/java/com/android/tools/r8/internal/YouTubeTreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeTreeShakeJarVerificationTest.java
@@ -36,7 +36,7 @@
     int bytes = 0;
     try (Closer closer = Closer.create()) {
       for (Resource dex : app.getDexProgramResources()) {
-        bytes += ByteStreams.toByteArray(dex.getStream(closer)).length;
+        bytes += ByteStreams.toByteArray(closer.register(dex.getStream())).length;
       }
     }
     assertTrue("Expected max size of " + maxSize + ", got " + bytes, bytes < maxSize);
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
index 6ceb912..04d84e4 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
@@ -85,6 +85,7 @@
             Executors.newSingleThreadExecutor(),
             app,
             info,
+            null,
             NamingLens.getIdentityLens(),
             null,
             null,
@@ -131,6 +132,6 @@
 
   protected static DexApplication process(DexApplication app, InternalOptions options)
       throws IOException, ProguardRuleParserException, ExecutionException {
-    return new R8(options).optimize(app, new AppInfoWithSubtyping(app));
+    return ToolHelper.optimizeWithR8(app, new AppInfoWithSubtyping(app), options);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index f4baa2d..69fdd65 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -477,7 +477,7 @@
     DexApplication application = builder.build();
     AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
     ApplicationWriter writer =
-        new ApplicationWriter(application, appInfo, options, NamingLens.getIdentityLens(), null);
+        new ApplicationWriter(application, appInfo, options, null, NamingLens.getIdentityLens(), null);
     ExecutorService executor = ThreadUtils.getExecutorService(options);
     try {
       return writer.write(null, executor);
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
index 2549ff1..57c8cf6 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.maindexlist;
 
+import static com.android.tools.r8.dex.Constants.ANDROID_I_API;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION;
 
@@ -44,7 +45,7 @@
         EXAMPLE_BUILD_DIR,
         Paths.get(EXAMPLE_SRC_DIR, "multidex", "main-dex-rules.txt"),
         Paths.get(EXAMPLE_SRC_DIR, "multidex001", "ref-list-1.txt"),
-        14);
+        ANDROID_I_API);
   }
 
   @Test
@@ -55,7 +56,7 @@
         EXAMPLE_BUILD_DIR,
         Paths.get(EXAMPLE_SRC_DIR, "multidex001", "main-dex-rules-2.txt"),
         Paths.get(EXAMPLE_SRC_DIR, "multidex001", "ref-list-2.txt"),
-        14);
+        ANDROID_I_API);
   }
 
   @Test
@@ -66,7 +67,7 @@
         EXAMPLE_BUILD_DIR,
         Paths.get(EXAMPLE_SRC_DIR, "multidex", "main-dex-rules.txt"),
         Paths.get(EXAMPLE_SRC_DIR, "multidex002", "ref-list-1.txt"),
-        14);
+        ANDROID_I_API);
   }
 
   @Test
@@ -77,7 +78,7 @@
         EXAMPLE_BUILD_DIR,
         Paths.get(EXAMPLE_SRC_DIR, "multidex", "main-dex-rules.txt"),
         Paths.get(EXAMPLE_SRC_DIR, "multidex003", "ref-list-1.txt"),
-        14);
+        ANDROID_I_API);
   }
 
   @Test
@@ -88,7 +89,7 @@
         EXAMPLE_O_BUILD_DIR,
         Paths.get(EXAMPLE_SRC_DIR, "multidex", "main-dex-rules.txt"),
         Paths.get(EXAMPLE_O_SRC_DIR, "multidex004", "ref-list-1.txt"),
-        14);
+        ANDROID_I_API);
   }
 
   private void doTest(
@@ -137,9 +138,9 @@
       CompilationResult result = ToolHelper.runR8WithFullResult(command, optionsConsumer);
       List<String> resultMainDexList =
           result.dexApplication.mainDexList.stream()
-          .filter(dexType -> isApplicationClass(dexType, result) != null)
-          .map(dexType -> dexType.descriptor.toString())
-          .collect(Collectors.toList());
+              .filter(dexType -> isApplicationClass(dexType, result))
+              .map(dexType -> dexType.descriptor.toString())
+              .collect(Collectors.toList());
       Collections.sort(resultMainDexList);
       String[] refList = new String(Files.readAllBytes(
           expectedMainDexList), StandardCharsets.UTF_8).split("\n");
@@ -161,7 +162,7 @@
     }
   }
 
-  private Object isApplicationClass(DexType dexType, CompilationResult result) {
+  private boolean isApplicationClass(DexType dexType, CompilationResult result) {
     DexClass clazz = result.appInfo.definitionFor(dexType);
     return clazz != null && clazz.isProgramClass();
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java b/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java
new file mode 100644
index 0000000..47c6330
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java
@@ -0,0 +1,255 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+import static com.android.tools.r8.shaking.TreeShakingTest.getTestOptionalParameter;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.shaking.PrintUsageTest.PrintUsageInspector.ClassSubject;
+import com.android.tools.r8.utils.ListUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PrintUsageTest {
+  private static final String ANDROID_JAR = ToolHelper.getDefaultAndroidJar();
+  private static final String PRINT_USAGE_FILE_SUFFIX = "-print-usage.txt";
+
+  private final String test;
+  private final String programFile;
+  private final List<String> keepRulesFiles;
+  private final Consumer<PrintUsageInspector> inspection;
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  public PrintUsageTest(
+      String test,
+      List<String> keepRulesFiles,
+      Consumer<PrintUsageInspector> inspection) {
+    this.test = test;
+    this.programFile = ToolHelper.EXAMPLES_BUILD_DIR + test + ".jar";
+    this.keepRulesFiles = keepRulesFiles;
+    this.inspection = inspection;
+  }
+
+  @Before
+  public void runR8andGetPrintUsage()
+      throws IOException, ProguardRuleParserException, ExecutionException, CompilationException {
+    Path out = temp.getRoot().toPath();
+    R8Command command =
+        R8Command.builder()
+            .setOutputPath(out)
+            .addProgramFiles(Paths.get(programFile))
+            .addProguardConfigurationFiles(ListUtils.map(keepRulesFiles, Paths::get))
+            .addLibraryFiles(Paths.get(ANDROID_JAR))
+            .build();
+    ToolHelper.runR8(command, options -> {
+      options.printUsage = true;
+      options.printUsageFile = out.resolve(test + PRINT_USAGE_FILE_SUFFIX);
+    });
+  }
+
+  @Test
+  public void printUsageTest() throws IOException, ExecutionException {
+    Path out = temp.getRoot().toPath();
+    Path printUsageFile = out.resolve(test + PRINT_USAGE_FILE_SUFFIX);
+    if (inspection != null) {
+      PrintUsageInspector inspector = new PrintUsageInspector(printUsageFile);
+      inspection.accept(inspector);
+    }
+  }
+
+  @Parameters(name = "test: {0} keep: {1}")
+  public static Collection<Object[]> data() {
+    List<String> tests = Arrays.asList(
+        "shaking1", "shaking2", "shaking4", "shaking8", "shaking9", "shaking12");
+
+    Map<String, Consumer<PrintUsageInspector>> inspections = new HashMap<>();
+    inspections.put("shaking1:keep-rules-printusage.txt", PrintUsageTest::inspectShaking1);
+    inspections.put("shaking2:keep-rules-printusage.txt", PrintUsageTest::inspectShaking2);
+    inspections.put("shaking4:keep-rules-printusage.txt", PrintUsageTest::inspectShaking4);
+    inspections.put("shaking8:keep-rules-printusage.txt", PrintUsageTest::inspectShaking8);
+    inspections.put("shaking9:keep-rules-printusage.txt", PrintUsageTest::inspectShaking9);
+    inspections.put("shaking12:keep-rules-printusage.txt", PrintUsageTest::inspectShaking12);
+
+    List<Object[]> testCases = new ArrayList<>();
+    Set<String> usedInspections = new HashSet<>();
+    for (String test : tests) {
+      File[] keepFiles = new File(ToolHelper.EXAMPLES_DIR + test)
+          .listFiles(file -> file.isFile() && file.getName().endsWith(".txt"));
+      for (File keepFile : keepFiles) {
+        String keepName = keepFile.getName();
+        Consumer<PrintUsageInspector> inspection =
+            getTestOptionalParameter(inspections, usedInspections, test, keepName);
+        if (inspection != null) {
+          testCases.add(new Object[]{test, ImmutableList.of(keepFile.getPath()), inspection});
+        }
+      }
+    }
+    assert usedInspections.size() == inspections.size();
+    return testCases;
+  }
+
+  private static void inspectShaking1(PrintUsageInspector inspector) {
+    assertTrue(inspector.clazz("shaking1.Unused").isPresent());
+    assertFalse(inspector.clazz("shaking1.Used").isPresent());
+  }
+
+  private static void inspectShaking2(PrintUsageInspector inspector) {
+    Optional<ClassSubject> staticFields = inspector.clazz("shaking2.StaticFields");
+    assertTrue(staticFields.isPresent());
+    assertTrue(staticFields.get().field("int", "completelyUnused"));
+    assertTrue(staticFields.get().field("int", "unused"));
+    Optional<ClassSubject> subClass1 = inspector.clazz("shaking2.SubClass1");
+    assertTrue(subClass1.isPresent());
+    assertTrue(subClass1.get().method("void", "unusedVirtualMethod", Collections.emptyList()));
+    Optional<ClassSubject> superClass = inspector.clazz("shaking2.SuperClass");
+    assertTrue(superClass.isPresent());
+    assertTrue(superClass.get().method("void", "unusedStaticMethod", Collections.emptyList()));
+  }
+
+  private static void inspectShaking4(PrintUsageInspector inspector) {
+    assertTrue(inspector.clazz("shaking4.Interface").isPresent());
+  }
+
+  private static void inspectShaking8(PrintUsageInspector inspector) {
+    Optional<ClassSubject> thing = inspector.clazz("shaking8.Thing");
+    assertTrue(thing.isPresent());
+    assertTrue(thing.get().field("int", "aField"));
+    assertFalse(inspector.clazz("shaking8.OtherThing").isPresent());
+    assertTrue(inspector.clazz("shaking8.YetAnotherThing").isPresent());
+  }
+
+  private static void inspectShaking9(PrintUsageInspector inspector) {
+    assertFalse(inspector.clazz("shaking9.Superclass").isPresent());
+    Optional<ClassSubject> subClass = inspector.clazz("shaking9.Subclass");
+    assertTrue(subClass.isPresent());
+    assertTrue(subClass.get().method("void", "aMethod", Collections.emptyList()));
+  }
+
+  private static void inspectShaking12(PrintUsageInspector inspector) {
+    assertFalse(inspector.clazz("shaking12.PeopleClass").isPresent());
+    Optional<ClassSubject> animal = inspector.clazz("shaking12.AnimalClass");
+    assertTrue(animal.isPresent());
+    assertTrue(animal.get().method("java.lang.String", "getName", Collections.emptyList()));
+  }
+
+  static class PrintUsageInspector {
+    private Map<String, ClassSubject> printedUsage;
+
+    PrintUsageInspector(Path printUsageFile) throws IOException {
+      printedUsage = new HashMap<>();
+      try (Stream<String> lines = Files.lines(printUsageFile)) {
+        lines.forEach(line -> {
+          if (line.startsWith("    ")) {
+            if (line.contains("(") && line.contains(")")) {
+              readMethod(line);
+            } else {
+              readField(line);
+            }
+          } else {
+            readClazz(line);
+          }
+        });
+      }
+    }
+
+    private ClassSubject lastClazz = null;
+
+    private void readClazz(String line) {
+      if (printedUsage.containsKey(line)) {
+        lastClazz = printedUsage.get(line);
+      } else {
+        lastClazz = new ClassSubject();
+        printedUsage.put(line, lastClazz);
+      }
+    }
+
+    private void readMethod(String line) {
+      assert lastClazz != null;
+      lastClazz.putMethod(line);
+    }
+
+    private void readField(String line) {
+      assert lastClazz != null;
+      lastClazz.putField(line);
+    }
+
+    public Optional<ClassSubject> clazz(String name) {
+      if (printedUsage.containsKey(name)) {
+        return Optional.of(printedUsage.get(name));
+      }
+      return Optional.empty();
+    }
+
+    static class ClassSubject {
+      private Set<String> methods;
+      private Set<String> fields;
+
+      public ClassSubject() {
+        methods = new HashSet<>();
+        fields = new HashSet<>();
+      }
+
+      void putMethod(String line) {
+        String[] tokens = line.split(" ");
+        assert tokens.length >= 2;
+        methods.add(tokens[tokens.length - 2] + " " + tokens[tokens.length - 1]);
+      }
+
+      void putField(String line) {
+        String[] tokens = line.split(" ");
+        assert tokens.length >= 2;
+        fields.add(tokens[tokens.length - 2] + " " + tokens[tokens.length - 1]);
+      }
+
+      public boolean method(String returnType, String name, List<String> parameters) {
+        StringBuilder builder = new StringBuilder();
+        builder.append(returnType).append(" ").append(name);
+        builder.append("(");
+        for (int i = 0; i < parameters.size(); i++) {
+          if (i != 0) {
+            builder.append(",");
+          }
+          builder.append(parameters.get(i));
+        }
+        builder.append(")");
+        return methods.contains(builder.toString());
+      }
+
+      public boolean field(String type, String name) {
+        return fields.contains(type + " " + name);
+      }
+    }
+  }
+}
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 3487694..5b94e6b 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -69,7 +69,12 @@
       "shaking16:keep-rules-2.txt:DEX:false",
       "shaking16:keep-rules-2.txt:JAR:false",
       "shaking15:keep-rules.txt:DEX:false",
-      "shaking15:keep-rules.txt:JAR:false"
+      "shaking15:keep-rules.txt:JAR:false",
+      "minifygeneric:keep-rules.txt:DEX:false",
+      "minifygeneric:keep-rules.txt:JAR:false",
+      "minifygenericwithinner:keep-rules.txt:DEX:false",
+      "minifygenericwithinner:keep-rules.txt:JAR:false"
+
   );
   private final boolean minify;
 
@@ -534,6 +539,8 @@
             "shaking16",
             "inlining",
             "minification",
+            "minifygeneric",
+            "minifygenericwithinner",
             "assumenosideeffects1",
             "assumenosideeffects2",
             "assumenosideeffects3",
@@ -720,7 +727,7 @@
     }
   }
 
-  private static <T> T getTestOptionalParameter(
+  static <T> T getTestOptionalParameter(
       Map<String, T> specifications, Set<String> usedSpecifications, String test,
       String keepName) {
     T parameter = specifications.get(test);
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index 71ee09f..f070130 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -385,8 +385,7 @@
 
   protected DexApplication processApplication(DexApplication application, InternalOptions options) {
     try {
-      R8 r8 = new R8(options);
-      return r8.optimize(application, new AppInfoWithSubtyping(application));
+      return ToolHelper.optimizeWithR8(application, new AppInfoWithSubtyping(application), options);
     } catch (IOException | ProguardRuleParserException | ExecutionException e) {
       throw new RuntimeException(e);
     }
@@ -500,6 +499,7 @@
           Executors.newSingleThreadExecutor(),
           application,
           appInfo,
+          null,
           NamingLens.getIdentityLens(),
           null,
           null,
diff --git a/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java b/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java
index e49ea84..f10add0 100644
--- a/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.R8;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.code.Const;
 import com.android.tools.r8.code.Const4;
 import com.android.tools.r8.code.ConstHigh16;
@@ -284,7 +285,7 @@
         "    return");
 
     DexApplication app = builder.read();
-    app = new R8(new InternalOptions()).optimize(app, new AppInfoWithSubtyping(app));
+    app = ToolHelper.optimizeWithR8(app, new AppInfoWithSubtyping(app), new InternalOptions());
 
     MethodSignature signature = new MethodSignature("Test", "test", "int", ImmutableList.of("int"));
     DexEncodedMethod method = getMethod(app, signature);
@@ -344,7 +345,7 @@
         "    return");
 
     DexApplication app = builder.read();
-    app = new R8(new InternalOptions()).optimize(app, new AppInfoWithSubtyping(app));
+    app = ToolHelper.optimizeWithR8(app, new AppInfoWithSubtyping(app), new InternalOptions());
 
     MethodSignature signature = new MethodSignature("Test", "test", "int", ImmutableList.of("int"));
     DexEncodedMethod method = getMethod(app, signature);
@@ -417,7 +418,7 @@
         "    return");
 
     DexApplication app = builder.read();
-    app = new R8(new InternalOptions()).optimize(app, new AppInfoWithSubtyping(app));
+    app = ToolHelper.optimizeWithR8(app, new AppInfoWithSubtyping(app), new InternalOptions());
 
     MethodSignature signature = new MethodSignature("Test", "test", "int", ImmutableList.of("int"));
     DexEncodedMethod method = getMethod(app, signature);
diff --git a/src/test/java/com/android/tools/r8/utils/D8CommandTest.java b/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
index 5bae2ed..6f45137 100644
--- a/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
@@ -172,11 +172,11 @@
         tmpClassesDir.toString());
     AndroidApp inputApp = ToolHelper.getApp(command);
     assertEquals(1, inputApp.getClasspathResourceProviders().size());
-    assertEquals(tmpClassesDir,
-        ((DirectoryClassFileProvider) inputApp.getClasspathResourceProviders().get(0)).getRoot());
+    assertTrue(Files.isSameFile(tmpClassesDir,
+        ((DirectoryClassFileProvider) inputApp.getClasspathResourceProviders().get(0)).getRoot()));
     assertEquals(1, inputApp.getLibraryResourceProviders().size());
-    assertEquals(tmpClassesDir,
-        ((DirectoryClassFileProvider) inputApp.getLibraryResourceProviders().get(0)).getRoot());
+    assertTrue(Files.isSameFile(tmpClassesDir,
+        ((DirectoryClassFileProvider) inputApp.getLibraryResourceProviders().get(0)).getRoot()));
   }
 
   @Test
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index a207ed8..1dabbd3 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -100,6 +100,7 @@
     return os.path.abspath(f.name)
 
 def main(argv):
+  utils.check_java_version()
   app_provided_pg_conf = False;
   (options, args) = ParseOptions(argv)
   outdir = options.out
diff --git a/tools/run_proguard_dx_on_app.py b/tools/run_proguard_dx_on_app.py
index 8578d1e..92a75ba 100755
--- a/tools/run_proguard_dx_on_app.py
+++ b/tools/run_proguard_dx_on_app.py
@@ -56,6 +56,7 @@
   return parser.parse_args(argv)
 
 def Main(argv):
+  utils.check_java_version()
   options = parse_arguments(argv)
 
   outdir = options.out
diff --git a/tools/test_framework.py b/tools/test_framework.py
index 04a12d3..b328375 100755
--- a/tools/test_framework.py
+++ b/tools/test_framework.py
@@ -59,6 +59,7 @@
   return parser.parse_args()
 
 def Main():
+  utils.check_java_version()
   args = parse_arguments()
 
   with utils.TempDir() as temp_dir:
diff --git a/tools/utils.py b/tools/utils.py
index dfc35d7..dab1dd1 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -170,3 +170,18 @@
   for segment_name, size in getDexSegmentSizes(dex_files).items():
     print('{}-{}(CodeSize): {}'
         .format(prefix, segment_name, size))
+
+# ensure that java version is 1.8.*-internal,
+# as opposed to e.g. 1.7* or 1.8.*-google-v7
+def check_java_version():
+  cmd= ['java', '-version']
+  output = subprocess.check_output(cmd, stderr = subprocess.STDOUT)
+  m = re.search('openjdk version "([^"]*)"', output)
+  if m is None:
+    raise Exception("Can't check java version: no version string in output"
+        " of 'java -version': '{}'".format(output))
+  version = m.groups(0)[0]
+  m = re.search('1[.]8[.].*-internal', version)
+  if m is None:
+    raise Exception("Incorrect java version, expected: '1.8.*-internal',"
+        " actual: {}".format(version))