Merge "-printusage part II: print dead code."
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 3c408cb..af67b41 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -173,10 +173,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;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6216b8f..4867b54 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -37,6 +37,7 @@
 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.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -74,13 +75,15 @@
       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);
@@ -180,7 +183,7 @@
     }
   }
 
-  static CompilationResult runForTesting(
+  private static CompilationResult runForTesting(
       AndroidApp app,
       InternalOptions options,
       ExecutorService executor)
@@ -337,6 +340,7 @@
               executorService,
               application,
               appInfo,
+              application.deadCode,
               namingLens,
               proguardSeedsData,
               packageDistribution,
@@ -386,42 +390,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/bisect/Bisect.java b/src/main/java/com/android/tools/r8/bisect/Bisect.java
index d7b23c5..4fe3773 100644
--- a/src/main/java/com/android/tools/r8/bisect/Bisect.java
+++ b/src/main/java/com/android/tools/r8/bisect/Bisect.java
@@ -177,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/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 39f741f..c9da10b 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/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/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..15ca87b 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 : deadCode.getStream(closer);
+  }
+
+  /**
    * True if the proguard-map resource exists.
    */
   public boolean hasProguardMap() {
@@ -337,6 +354,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 +368,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 +383,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 +524,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 +594,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/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/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/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
index 6ceb912..e5bbad6 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,
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 416a266..001883f 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -440,7 +440,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/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 55a9fc0..5b94e6b 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -727,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..9a12d3e 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -500,6 +500,7 @@
           Executors.newSingleThreadExecutor(),
           application,
           appInfo,
+          null,
           NamingLens.getIdentityLens(),
           null,
           null,