Merge "Remove stale TODO regarding member rule combination for -if rule."
diff --git a/build.gradle b/build.gradle
index 1ed69a7..da3375e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -532,15 +532,6 @@
     baseName 'deps'
 }
 
-task repackageDepsForLib(type: ShadowJar) {
-    configurations = [project.configurations.runtimeClasspath]
-    mergeServiceFiles(it)
-    configureRelocations(it)
-    exclude { it.getRelativePath().getPathString() == "module-info.class" }
-    exclude { it.getRelativePath().getPathString().startsWith("META-INF/maven/") }
-    baseName 'r8lib_deps'
-}
-
 task repackageSources(type: ShadowJar) {
     from sourceSets.main.output
     mergeServiceFiles(it)
@@ -550,16 +541,19 @@
     baseName 'sources'
 }
 
-task R8libWithDeps(type: ShadowJar) {
+task r8WithRelocatedDeps(type: ShadowJar) {
     from consolidatedLicense.outputs.files
-    baseName 'r8lib_with_deps'
+    baseName 'r8_with_relocated_deps'
     classifier = null
     version = null
     manifest {
         attributes 'Main-Class': 'com.android.tools.r8.SwissArmyKnife'
     }
     from repackageSources.outputs.files
-    from repackageDepsForLib.outputs.files
+    from repackageDeps.outputs.files
+    doLast {
+        configureRelocations(it)
+    }
 }
 
 task R8(type: ShadowJar) {
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
index fcba149..48c67fd 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -7,15 +7,16 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexDefinition;
 import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.graphinfo.GraphConsumer;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.MainDexClasses;
 import com.android.tools.r8.shaking.MainDexListBuilder;
-import com.android.tools.r8.shaking.ReasonPrinter;
 import com.android.tools.r8.shaking.RootSetBuilder;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
-import com.android.tools.r8.shaking.TreePruner;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -46,7 +47,15 @@
           new AppView<>(new AppInfoWithSubtyping(application), GraphLense.getIdentityLense());
       RootSet mainDexRootSet =
           new RootSetBuilder(appView, application, options.mainDexKeepRules, options).run(executor);
-      Enqueuer enqueuer = new Enqueuer(appView, options, true);
+
+      GraphConsumer graphConsumer = options.mainDexKeptGraphConsumer;
+      WhyAreYouKeepingConsumer whyAreYouKeepingConsumer = null;
+      if (!mainDexRootSet.reasonAsked.isEmpty()) {
+        whyAreYouKeepingConsumer = new WhyAreYouKeepingConsumer(graphConsumer);
+        graphConsumer = whyAreYouKeepingConsumer;
+      }
+
+      Enqueuer enqueuer = new Enqueuer(appView, options, graphConsumer, true);
       AppInfoWithLiveness mainDexAppInfo = enqueuer.traceMainDex(mainDexRootSet, executor, timing);
       // LiveTypes is the result.
       MainDexClasses mainDexClasses =
@@ -63,12 +72,12 @@
       }
 
       // Print -whyareyoukeeping results if any.
-      if (mainDexRootSet.reasonAsked.size() > 0) {
-        // Print reasons on the application after pruning, so that we reflect the actual result.
-        TreePruner pruner = new TreePruner(application, mainDexAppInfo.withLiveness(), options);
-        application = pruner.run();
-        ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(mainDexRootSet.reasonAsked);
-        reasonPrinter.run(application);
+      if (whyAreYouKeepingConsumer != null) {
+        // TODO(b/120959039): This should be ordered!
+        for (DexDefinition definition : mainDexRootSet.reasonAsked) {
+          whyAreYouKeepingConsumer.printWhyAreYouKeeping(
+              enqueuer.getGraphNode(definition), System.out);
+        }
       }
 
       return result;
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
index ec7bc53..0b31acf 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graphinfo.GraphConsumer;
 import com.android.tools.r8.origin.CommandLineOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
@@ -25,6 +26,7 @@
 
   private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
   private final StringConsumer mainDexListConsumer;
+  private final GraphConsumer mainDexKeptGraphConsumer;
   private final DexItemFactory factory;
   private final Reporter reporter;
 
@@ -33,6 +35,7 @@
     private final DexItemFactory factory = new DexItemFactory();
     private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>();
     private StringConsumer mainDexListConsumer = null;
+    private GraphConsumer mainDexKeptGraphConsumer = null;
 
     private Builder() {
     }
@@ -114,7 +117,18 @@
       }
 
       return new GenerateMainDexListCommand(
-          factory, getAppBuilder().build(), mainDexKeepRules, mainDexListConsumer, getReporter());
+          factory,
+          getAppBuilder().build(),
+          mainDexKeepRules,
+          mainDexListConsumer,
+          mainDexKeptGraphConsumer,
+          getReporter());
+    }
+
+    public GenerateMainDexListCommand.Builder setMainDexKeptGraphConsumer(
+        GraphConsumer graphConsumer) {
+      this.mainDexKeptGraphConsumer = graphConsumer;
+      return self();
     }
   }
 
@@ -185,11 +199,13 @@
       AndroidApp inputApp,
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
       StringConsumer mainDexListConsumer,
+      GraphConsumer mainDexKeptGraphConsumer,
       Reporter reporter) {
     super(inputApp);
     this.factory = factory;
     this.mainDexKeepRules = mainDexKeepRules;
     this.mainDexListConsumer = mainDexListConsumer;
+    this.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
     this.reporter = reporter;
   }
 
@@ -198,6 +214,7 @@
     this.factory = new DexItemFactory();
     this.mainDexKeepRules = ImmutableList.of();
     this.mainDexListConsumer = null;
+    this.mainDexKeptGraphConsumer = null;
     this.reporter = new Reporter();
   }
 
@@ -206,6 +223,7 @@
     InternalOptions internal = new InternalOptions(factory, reporter);
     internal.mainDexKeepRules = mainDexKeepRules;
     internal.mainDexListConsumer = mainDexListConsumer;
+    internal.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
     internal.minimalMainDex = internal.debug;
     internal.enableSwitchMapRemoval = false;
     internal.enableInlining = false;
diff --git a/src/main/java/com/android/tools/r8/PrintSeeds.java b/src/main/java/com/android/tools/r8/PrintSeeds.java
index 5714d93..0d8524b 100644
--- a/src/main/java/com/android/tools/r8/PrintSeeds.java
+++ b/src/main/java/com/android/tools/r8/PrintSeeds.java
@@ -89,7 +89,7 @@
           new RootSetBuilder(
                   appView, application, options.proguardConfiguration.getRules(), options)
               .run(executor);
-      Enqueuer enqueuer = new Enqueuer(appView, options);
+      Enqueuer enqueuer = new Enqueuer(appView, options, null);
       AppInfoWithLiveness appInfo =
           enqueuer.traceApplication(
               rootSet, options.proguardConfiguration.getDontWarnPatterns(), executor, timing);
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index fa04ee8..7e4974b 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -15,9 +15,11 @@
 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.DexDefinition;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.graphinfo.GraphConsumer;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.optimize.EnumOrdinalMapCollector;
 import com.android.tools.r8.ir.optimize.MethodPoolCollection;
@@ -46,12 +48,12 @@
 import com.android.tools.r8.shaking.MainDexListBuilder;
 import com.android.tools.r8.shaking.ProguardClassFilter;
 import com.android.tools.r8.shaking.ProguardConfiguration;
-import com.android.tools.r8.shaking.ReasonPrinter;
 import com.android.tools.r8.shaking.RootSetBuilder;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.shaking.StaticClassMerger;
 import com.android.tools.r8.shaking.TreePruner;
 import com.android.tools.r8.shaking.VerticalClassMerger;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
@@ -287,9 +289,14 @@
                     appView, application, options.proguardConfiguration.getRules(), options)
                 .run(executorService);
 
-        Enqueuer enqueuer = new Enqueuer(appView, options, compatibility);
-        appView.setAppInfo(enqueuer.traceApplication(
-            rootSet, options.proguardConfiguration.getDontWarnPatterns(), executorService, timing));
+        Enqueuer enqueuer = new Enqueuer(appView, options, null, compatibility);
+        appView.setAppInfo(
+            enqueuer.traceApplication(
+                rootSet,
+                options.proguardConfiguration.getDontWarnPatterns(),
+                executorService,
+                timing));
+
         if (options.proguardConfiguration.isPrintSeeds()) {
           ByteArrayOutputStream bytes = new ByteArrayOutputStream();
           PrintStream out = new PrintStream(bytes);
@@ -452,11 +459,19 @@
       MainDexClasses mainDexClasses = MainDexClasses.NONE;
       if (!options.mainDexKeepRules.isEmpty()) {
         appView.setAppInfo(new AppInfoWithSubtyping(application));
-        Enqueuer enqueuer = new Enqueuer(appView, options, true);
         // Lets find classes which may have code executed before secondary dex files installation.
         RootSet mainDexRootSet =
             new RootSetBuilder(appView, application, options.mainDexKeepRules, options)
                 .run(executorService);
+
+        GraphConsumer mainDexKeptGraphConsumer = options.mainDexKeptGraphConsumer;
+        WhyAreYouKeepingConsumer whyAreYouKeepingConsumer = null;
+        if (!mainDexRootSet.reasonAsked.isEmpty()) {
+          whyAreYouKeepingConsumer = new WhyAreYouKeepingConsumer(mainDexKeptGraphConsumer);
+          mainDexKeptGraphConsumer = whyAreYouKeepingConsumer;
+        }
+
+        Enqueuer enqueuer = new Enqueuer(appView, options, mainDexKeptGraphConsumer, true);
         AppInfoWithLiveness mainDexAppInfo =
             enqueuer.traceMainDex(mainDexRootSet, executorService, timing);
 
@@ -469,13 +484,12 @@
                   mainDexRootSet, mainDexClasses.getClasses(), appView.appInfo(), options)
               .run();
         }
-        if (!mainDexRootSet.reasonAsked.isEmpty()) {
-          // If the main dex rules have -whyareyoukeeping rules build an application
-          // pruned with the main dex tracing result to reflect only on main dex content.
-          TreePruner mainDexPruner = new TreePruner(application, mainDexAppInfo, options);
-          DexApplication mainDexApplication = mainDexPruner.run();
-          ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(mainDexRootSet.reasonAsked);
-          reasonPrinter.run(mainDexApplication);
+        if (whyAreYouKeepingConsumer != null) {
+          // TODO(b/120959039): Sort the set so the order is always the same print order!
+          for (DexDefinition dexDefinition : mainDexRootSet.reasonAsked) {
+            whyAreYouKeepingConsumer.printWhyAreYouKeeping(
+                enqueuer.getGraphNode(dexDefinition), System.out);
+          }
         }
       }
 
@@ -484,7 +498,18 @@
       if (options.enableTreeShaking || options.enableMinification) {
         timing.begin("Post optimization code stripping");
         try {
-          Enqueuer enqueuer = new Enqueuer(appView, options);
+
+          GraphConsumer keptGraphConsumer = null;
+          WhyAreYouKeepingConsumer whyAreYouKeepingConsumer = null;
+          if (options.enableTreeShaking) {
+            keptGraphConsumer = options.keptGraphConsumer;
+            if (!rootSet.reasonAsked.isEmpty()) {
+              whyAreYouKeepingConsumer = new WhyAreYouKeepingConsumer(keptGraphConsumer);
+              keptGraphConsumer = whyAreYouKeepingConsumer;
+            }
+          }
+
+          Enqueuer enqueuer = new Enqueuer(appView, options, keptGraphConsumer);
           appView.setAppInfo(
               enqueuer.traceApplication(
                   rootSet,
@@ -501,8 +526,13 @@
                     .appInfo()
                     .prunedCopyFrom(application, pruner.getRemovedClasses()));
             // Print reasons on the application after pruning, so that we reflect the actual result.
-            ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(rootSet.reasonAsked);
-            reasonPrinter.run(application);
+            if (whyAreYouKeepingConsumer != null) {
+              // TODO(b/120959039): Sort the set so the order is always the same print order!
+              for (DexDefinition dexDefinition : rootSet.reasonAsked) {
+                whyAreYouKeepingConsumer.printWhyAreYouKeeping(
+                    enqueuer.getGraphNode(dexDefinition), System.out);
+              }
+            }
             // Remove annotations that refer to types that no longer exist.
             new AnnotationRemover(appView.appInfo().withLiveness(), options).run();
             if (!mainDexClasses.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 338fb4f..ff74d09 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graphinfo.GraphConsumer;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.origin.StandardOutOrigin;
@@ -83,6 +84,8 @@
     private boolean disableVerticalClassMerging = false;
     private boolean forceProguardCompatibility = false;
     private StringConsumer proguardMapConsumer = null;
+    private GraphConsumer keptGraphConsumer = null;
+    private GraphConsumer mainDexKeptGraphConsumer = null;
 
     // Internal compatibility mode for use from CompatProguard tool.
     Path proguardCompatibilityRulesOutput = null;
@@ -232,6 +235,26 @@
     }
 
     /**
+     * Set a consumer for receiving kept-graph events.
+     *
+     * @param graphConsumer
+     */
+    public Builder setKeptGraphConsumer(GraphConsumer graphConsumer) {
+      this.keptGraphConsumer = graphConsumer;
+      return self();
+    }
+
+    /**
+     * Set a consumer for receiving kept-graph events for the content of the main-dex output.
+     *
+     * @param graphConsumer
+     */
+    public Builder setMainDexKeptGraphConsumer(GraphConsumer graphConsumer) {
+      this.mainDexKeptGraphConsumer = graphConsumer;
+      return self();
+    }
+
+    /**
      * Set the output path-and-mode.
      *
      * <p>Setting the output path-and-mode will override any previous set consumer or any previous
@@ -416,6 +439,8 @@
               forceProguardCompatibility,
               proguardMapConsumer,
               proguardCompatibilityRulesOutput,
+              keptGraphConsumer,
+              mainDexKeptGraphConsumer,
               isOptimizeMultidexForLinearAlloc());
 
       return command;
@@ -481,6 +506,8 @@
   private final boolean forceProguardCompatibility;
   private final StringConsumer proguardMapConsumer;
   private final Path proguardCompatibilityRulesOutput;
+  private final GraphConsumer keptGraphConsumer;
+  private final GraphConsumer mainDexKeptGraphConsumer;
 
   /** Get a new {@link R8Command.Builder}. */
   public static Builder builder() {
@@ -545,6 +572,8 @@
       boolean forceProguardCompatibility,
       StringConsumer proguardMapConsumer,
       Path proguardCompatibilityRulesOutput,
+      GraphConsumer keptGraphConsumer,
+      GraphConsumer mainDexKeptGraphConsumer,
       boolean optimizeMultidexForLinearAlloc) {
     super(inputApp, mode, programConsumer, mainDexListConsumer, minApiLevel, reporter,
         enableDesugaring, optimizeMultidexForLinearAlloc);
@@ -558,6 +587,8 @@
     this.forceProguardCompatibility = forceProguardCompatibility;
     this.proguardMapConsumer = proguardMapConsumer;
     this.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
+    this.keptGraphConsumer = keptGraphConsumer;
+    this.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
   }
 
   private R8Command(boolean printHelp, boolean printVersion) {
@@ -570,6 +601,8 @@
     forceProguardCompatibility = false;
     proguardMapConsumer = null;
     proguardCompatibilityRulesOutput = null;
+    keptGraphConsumer = null;
+    mainDexKeptGraphConsumer = null;
   }
 
   /** Get the enable-tree-shaking state. */
@@ -671,6 +704,10 @@
       internal.proguardMapConsumer = wrappedConsumer;
     }
 
+    // Set the kept-graph consumer if any. It will only be actively used if the enqueuer triggers.
+    internal.keptGraphConsumer = keptGraphConsumer;
+    internal.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
+
     internal.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
     internal.dataResourceConsumer = internal.programConsumer.getDataResourceConsumer();
 
diff --git a/src/main/java/com/android/tools/r8/graphinfo/AnnotationGraphNode.java b/src/main/java/com/android/tools/r8/graphinfo/AnnotationGraphNode.java
new file mode 100644
index 0000000..2c8482f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/AnnotationGraphNode.java
@@ -0,0 +1,44 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexItem;
+
+@Keep
+public final class AnnotationGraphNode extends GraphNode {
+
+  private final DexItem annotatedItem;
+
+  public AnnotationGraphNode(DexItem annotatedItem) {
+    assert annotatedItem != null;
+    this.annotatedItem = annotatedItem;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return this == o
+        || (o instanceof AnnotationGraphNode
+            && ((AnnotationGraphNode) o).annotatedItem == annotatedItem);
+  }
+
+  @Override
+  public int hashCode() {
+    return annotatedItem.hashCode();
+  }
+
+  public String getDescriptor() {
+    return annotatedItem.toSourceString();
+  }
+
+  /**
+   * Get a unique identity string determining this annotated-item node.
+   *
+   * <p>This is the descriptor of the concrete node type.
+   */
+  @Override
+  public String identity() {
+    return getDescriptor();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graphinfo/ClassGraphNode.java b/src/main/java/com/android/tools/r8/graphinfo/ClassGraphNode.java
new file mode 100644
index 0000000..96bebdb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/ClassGraphNode.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexType;
+
+@Keep
+public final class ClassGraphNode extends GraphNode {
+
+  private final DexType clazz;
+
+  public ClassGraphNode(DexType clazz) {
+    assert clazz != null;
+    this.clazz = clazz;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return this == o || (o instanceof ClassGraphNode && ((ClassGraphNode) o).clazz == clazz);
+  }
+
+  @Override
+  public int hashCode() {
+    return clazz.hashCode();
+  }
+
+  public String getDescriptor() {
+    return clazz.toDescriptorString();
+  }
+
+  /**
+   * Get a unique identity string determining this clazz node.
+   *
+   * <p>This is just the class descriptor.
+   */
+  @Override
+  public String identity() {
+    return getDescriptor();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graphinfo/FieldGraphNode.java b/src/main/java/com/android/tools/r8/graphinfo/FieldGraphNode.java
new file mode 100644
index 0000000..bb3b932
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/FieldGraphNode.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexField;
+
+@Keep
+public final class FieldGraphNode extends GraphNode {
+
+  private final DexField field;
+
+  public FieldGraphNode(DexField field) {
+    assert field != null;
+    this.field = field;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return this == o || (o instanceof FieldGraphNode && ((FieldGraphNode) o).field == field);
+  }
+
+  @Override
+  public int hashCode() {
+    return field.hashCode();
+  }
+
+  /**
+   * Get the class descriptor for the field holder as defined by the JVM specification.
+   *
+   * <p>For the field {@code x.y.Z a.b.C.foo}, this would be {@code La/b/C;}.
+   */
+  public String getHolderDescriptor() {
+    return field.clazz.toDescriptorString();
+  }
+
+  /**
+   * Get the field descriptor as defined by the JVM specification.
+   *
+   * <p>For the field {@code x.y.Z a.b.C.foo}, this would be {@code Lx/y/Z;}.
+   */
+  public String getFieldDescriptor() {
+    return field.type.toDescriptorString();
+  }
+
+  /**
+   * Get the (unqualified) field name.
+   *
+   * <p>For the field {@code x.y.Z a.b.C.foo} this would be {@code foo}.
+   */
+  public String getFieldName() {
+    return field.name.toString();
+  }
+
+  /**
+   * Get a unique identity string determining this field node.
+   *
+   * <p>The identity string follows the CF encoding of a field reference: {@code
+   * <holder-descriptor>.<field-name>:<field-descriptor>}, e.g., for {@code x.y.Z a.b.C.foo} this
+   * would be {@code La/b/C;foo:Lx/y/Z;}.
+   */
+  @Override
+  public String identity() {
+    return getHolderDescriptor() + getFieldName() + "\":" + getFieldDescriptor();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graphinfo/GraphConsumer.java b/src/main/java/com/android/tools/r8/graphinfo/GraphConsumer.java
new file mode 100644
index 0000000..8c124e5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/GraphConsumer.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+import com.android.tools.r8.KeepForSubclassing;
+
+@KeepForSubclassing
+public interface GraphConsumer {
+
+  /**
+   * Registers a directed edge in the graph.
+   *
+   * @param source The source node of the edge.
+   * @param target The target node of the edge.
+   * @param info Additional information about the edge.
+   */
+  void acceptEdge(GraphNode source, GraphNode target, GraphEdgeInfo info);
+}
diff --git a/src/main/java/com/android/tools/r8/graphinfo/GraphEdgeInfo.java b/src/main/java/com/android/tools/r8/graphinfo/GraphEdgeInfo.java
new file mode 100644
index 0000000..c0b3f3b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/GraphEdgeInfo.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+public class GraphEdgeInfo {
+
+  // TODO(b/120959039): Simplify these. Most of the information is present in the source node.
+  public enum EdgeKind {
+    // Prioritized list of edge types.
+    KeepRule,
+    CompatibilityRule,
+    InstantiatedIn,
+    InvokedViaSuper,
+    TargetedBySuper,
+    InvokedFrom,
+    InvokedFromLambdaCreatedIn,
+    ReferencedFrom,
+    ReachableFromLiveType,
+    ReferencedInAnnotation,
+    IsLibraryMethod,
+  }
+
+  private final EdgeKind kind;
+
+  public GraphEdgeInfo(EdgeKind kind) {
+    this.kind = kind;
+  }
+
+  public EdgeKind edgeKind() {
+    return kind;
+  }
+
+  @Override
+  public String toString() {
+    return "{edge-type:" + kind.toString() + "}";
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return this == o || (o instanceof GraphEdgeInfo && ((GraphEdgeInfo) o).kind == kind);
+  }
+
+  @Override
+  public int hashCode() {
+    return kind.hashCode();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graphinfo/GraphNode.java b/src/main/java/com/android/tools/r8/graphinfo/GraphNode.java
new file mode 100644
index 0000000..4f6251b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/GraphNode.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+import com.android.tools.r8.Keep;
+
+@Keep
+public abstract class GraphNode {
+
+  public abstract String identity();
+
+  @Override
+  public abstract boolean equals(Object o);
+
+  @Override
+  public abstract int hashCode();
+
+  @Override
+  public String toString() {
+    return identity();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graphinfo/KeepRuleGraphNode.java b/src/main/java/com/android/tools/r8/graphinfo/KeepRuleGraphNode.java
new file mode 100644
index 0000000..2f4d5e1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/KeepRuleGraphNode.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.position.TextRange;
+import com.android.tools.r8.shaking.ProguardKeepRule;
+
+@Keep
+public final class KeepRuleGraphNode extends GraphNode {
+
+  private final ProguardKeepRule rule;
+
+  public KeepRuleGraphNode(ProguardKeepRule rule) {
+    assert rule != null;
+    this.rule = rule;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return this == o || (o instanceof KeepRuleGraphNode && ((KeepRuleGraphNode) o).rule == rule);
+  }
+
+  @Override
+  public int hashCode() {
+    return rule.hashCode();
+  }
+
+  public Origin getOrigin() {
+    return rule.getOrigin();
+  }
+
+  public Position getPosition() {
+    return rule.getPosition();
+  }
+
+  public String getContent() {
+    return rule.getSource();
+  }
+
+  /**
+   * Get an identity string determining this keep rule.
+   *
+   * <p>The identity string is typically the source-file (if present) followed by the line number.
+   * {@code <keep-rule-file>:<keep-rule-start-line>:<keep-rule-start-column>}.
+   */
+  @Override
+  public String identity() {
+    return (getOrigin() == Origin.unknown() ? getContent() : getOrigin())
+        + ":"
+        + shortPositionInfo(getPosition());
+  }
+
+  private static String shortPositionInfo(Position position) {
+    if (position instanceof TextRange) {
+      TextPosition start = ((TextRange) position).getStart();
+      return start.getLine() + ":" + start.getColumn();
+    }
+    if (position instanceof TextPosition) {
+      TextPosition start = (TextPosition) position;
+      return start.getLine() + ":" + start.getColumn();
+    }
+    return position.getDescription();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graphinfo/MethodGraphNode.java b/src/main/java/com/android/tools/r8/graphinfo/MethodGraphNode.java
new file mode 100644
index 0000000..2745518
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graphinfo/MethodGraphNode.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graphinfo;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexMethod;
+
+@Keep
+public final class MethodGraphNode extends GraphNode {
+
+  private final DexMethod method;
+
+  public MethodGraphNode(DexMethod method) {
+    assert method != null;
+    this.method = method;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return this == o || (o instanceof MethodGraphNode && ((MethodGraphNode) o).method == method);
+  }
+
+  @Override
+  public int hashCode() {
+    return method.hashCode();
+  }
+
+  /**
+   * Get the class descriptor for the method holder as defined by the JVM specification.
+   *
+   * <p>For the method {@code void a.b.C.foo(x.y.Z arg)}, this would be {@code La/b/C;}.
+   */
+  public String getHolderDescriptor() {
+    return method.holder.toDescriptorString();
+  }
+
+  /**
+   * Get the method descriptor as defined by the JVM specification.
+   *
+   * <p>For the method {@code void a.b.C.foo(x.y.Z arg)}, this would be {@code (Lx/y/Z;)V}.
+   */
+  public String getMethodDescriptor() {
+    return method.proto.toDescriptorString();
+  }
+
+  /**
+   * Get the (unqualified) method name.
+   *
+   * <p>For the method {@code void a.b.C.foo(x.y.Z arg)} this would be {@code foo}.
+   */
+  public String getMethodName() {
+    return method.name.toString();
+  }
+
+  /**
+   * Get a unique identity string determining this method node.
+   *
+   * <p>The identity string follows the CF encoding of a method reference:
+   * {@code <holder-descriptor><method-name><method-descriptor>}, e.g., for
+   * {@code void a.b.C.foo(x.y.Z arg)} this will be {@code La/b/C;foo(Lx/y/Z;)V}.
+   */
+  @Override
+  public String identity() {
+    return getHolderDescriptor() + getMethodName() + getMethodDescriptor();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 6f7965e..315df31 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -333,7 +333,31 @@
   // Returns `true` if at least one method was inlined.
   boolean processInlining(IRCode code, Supplier<InliningOracle> defaultOracle) {
     replaceUsagesAsUnusedArgument(code);
-    boolean anyInlinedMethods = forceInlineExtraMethodInvocations(code, defaultOracle);
+
+    boolean anyInlinedMethods = forceInlineExtraMethodInvocations(code);
+    if (anyInlinedMethods) {
+      // Reset the collections.
+      methodCallsOnInstance.clear();
+      extraMethodCalls.clear();
+      unusedArguments.clear();
+      estimatedCombinedSizeForInlining = 0;
+
+      // Repeat user analysis
+      InstructionOrPhi ineligibleUser = areInstanceUsersEligible(null, defaultOracle);
+      if (ineligibleUser != null) {
+        // We introduced a user that we cannot handle in the class inliner as a result of force
+        // inlining. Abort gracefully from class inlining without removing the instance.
+        //
+        // Alternatively we would need to collect additional information about the behavior of
+        // methods (which is bad for memory), or we would need to analyze the called methods before
+        // inlining them. The latter could be good solution, since we are going to build IR for the
+        // methods that need to be inlined anyway.
+        return true;
+      }
+      assert extraMethodCalls.isEmpty();
+      assert unusedArguments.isEmpty();
+    }
+
     anyInlinedMethods |= forceInlineDirectMethodInvocations(code);
     removeMiscUsages(code);
     removeFieldReads(code);
@@ -359,32 +383,12 @@
     unusedArguments.clear();
   }
 
-  private boolean forceInlineExtraMethodInvocations(
-      IRCode code, Supplier<InliningOracle> defaultOracle) {
+  private boolean forceInlineExtraMethodInvocations(IRCode code) {
     if (extraMethodCalls.isEmpty()) {
       return false;
     }
-
     // Inline extra methods.
     inliner.performForcedInlining(method, code, extraMethodCalls);
-
-    // Reset the collections.
-    methodCallsOnInstance.clear();
-    extraMethodCalls.clear();
-    unusedArguments.clear();
-    estimatedCombinedSizeForInlining = 0;
-
-    // Repeat user analysis
-    InstructionOrPhi ineligibleUser = areInstanceUsersEligible(null, defaultOracle);
-    if (ineligibleUser != null) {
-      throw new Unreachable(
-          "Unexpected ineligible user in method `"
-              + method.method.toSourceString()
-              + "` after inlining of extra methods: "
-              + ineligibleUser);
-    }
-    assert extraMethodCalls.isEmpty();
-    assert unusedArguments.isEmpty();
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index ea62227..c579500 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -36,6 +36,15 @@
 import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.graph.KeyedDexItem;
 import com.android.tools.r8.graph.PresortedComparable;
+import com.android.tools.r8.graphinfo.AnnotationGraphNode;
+import com.android.tools.r8.graphinfo.ClassGraphNode;
+import com.android.tools.r8.graphinfo.FieldGraphNode;
+import com.android.tools.r8.graphinfo.GraphConsumer;
+import com.android.tools.r8.graphinfo.GraphEdgeInfo;
+import com.android.tools.r8.graphinfo.GraphEdgeInfo.EdgeKind;
+import com.android.tools.r8.graphinfo.GraphNode;
+import com.android.tools.r8.graphinfo.KeepRuleGraphNode;
+import com.android.tools.r8.graphinfo.MethodGraphNode;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.Invoke.Type;
@@ -66,7 +75,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
@@ -79,6 +87,7 @@
 import java.util.SortedSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -121,6 +130,14 @@
 
   private final Set<DexReference> identifierNameStrings = Sets.newIdentityHashSet();
 
+  // Canonicalization of external graph-nodes and edge info.
+  private final Map<DexItem, AnnotationGraphNode> annotationNodes = new IdentityHashMap<>();
+  private final Map<DexType, ClassGraphNode> classNodes = new IdentityHashMap<>();
+  private final Map<DexMethod, MethodGraphNode> methodNodes = new IdentityHashMap<>();
+  private final Map<DexField, FieldGraphNode> fieldNodes = new IdentityHashMap<>();
+  private final Map<ProguardKeepRule, KeepRuleGraphNode> ruleNodes = new IdentityHashMap<>();
+  private final Map<EdgeKind, GraphEdgeInfo> reasonInfo = new IdentityHashMap<>();
+
   /**
    * Set of method signatures used in invoke-super instructions that either cannot be resolved or
    * resolve to a private method (leading to an IllegalAccessError).
@@ -154,16 +171,15 @@
    * Set of annotation types that are instantiated.
    */
   private final Set<DexType> instantiatedAnnotations = Sets.newIdentityHashSet();
-  /**
-   * Set of types that are actually instantiated. These cannot be abstract.
-   */
-  private final SetWithReason<DexType> instantiatedTypes = new SetWithReason<>();
+  /** Set of types that are actually instantiated. These cannot be abstract. */
+  private final SetWithReason<DexType> instantiatedTypes = new SetWithReason<>(this::registerType);
   /**
    * Set of methods that are the immediate target of an invoke. They might not actually be live but
    * are required so that invokes can find the method. If a method is only a target but not live,
    * its implementation may be removed and it may be marked abstract.
    */
-  private final SetWithReason<DexEncodedMethod> targetedMethods = new SetWithReason<>();
+  private final SetWithReason<DexEncodedMethod> targetedMethods =
+      new SetWithReason<>(this::registerMethod);
   /**
    * Set of program methods that are used as the bootstrap method for an invoke-dynamic instruction.
    */
@@ -180,19 +196,21 @@
    * Set of methods that belong to live classes and can be reached by invokes. These need to be
    * kept.
    */
-  private final SetWithReason<DexEncodedMethod> liveMethods = new SetWithReason<>();
+  private final SetWithReason<DexEncodedMethod> liveMethods =
+      new SetWithReason<>(this::registerMethod);
 
   /**
-   * Set of fields that belong to live classes and can be reached by invokes. These need to be
-   * kept.
+   * Set of fields that belong to live classes and can be reached by invokes. These need to be kept.
    */
-  private final SetWithReason<DexEncodedField> liveFields = new SetWithReason<>();
+  private final SetWithReason<DexEncodedField> liveFields =
+      new SetWithReason<>(this::registerField);
 
   /**
    * Set of interface types for which a lambda expression can be reached. These never have a single
    * interface implementation.
    */
-  private final SetWithReason<DexType> instantiatedLambdas = new SetWithReason<>();
+  private final SetWithReason<DexType> instantiatedLambdas =
+      new SetWithReason<>(this::registerType);
 
   /**
    * A queue of items that need processing. Different items trigger different actions:
@@ -236,33 +254,42 @@
    */
   private final ProguardConfiguration.Builder compatibility;
 
-  public Enqueuer(AppView<? extends AppInfoWithSubtyping> appView, InternalOptions options) {
-    this(appView, options, options.forceProguardCompatibility, null);
+  private final GraphConsumer keptGraphConsumer;
+
+  public Enqueuer(
+      AppView<? extends AppInfoWithSubtyping> appView,
+      InternalOptions options,
+      GraphConsumer keptGraphConsumer) {
+    this(appView, options, keptGraphConsumer, options.forceProguardCompatibility, null);
   }
 
   public Enqueuer(
       AppView<? extends AppInfoWithSubtyping> appView,
       InternalOptions options,
+      GraphConsumer keptGraphConsumer,
       ProguardConfiguration.Builder compatibility) {
-    this(appView, options, options.forceProguardCompatibility, compatibility);
+    this(appView, options, keptGraphConsumer, options.forceProguardCompatibility, compatibility);
   }
 
   public Enqueuer(
       AppView<? extends AppInfoWithSubtyping> appView,
       InternalOptions options,
+      GraphConsumer keptGraphConsumer,
       boolean forceProguardCompatibility) {
-    this(appView, options, forceProguardCompatibility, null);
+    this(appView, options, keptGraphConsumer, forceProguardCompatibility, null);
   }
 
   public Enqueuer(
       AppView<? extends AppInfoWithSubtyping> appView,
       InternalOptions options,
+      GraphConsumer keptGraphConsumer,
       boolean forceProguardCompatibility,
       ProguardConfiguration.Builder compatibility) {
     this.appInfo = appView.appInfo();
     this.appView = appView;
     this.compatibility = compatibility;
     this.forceProguardCompatibility = forceProguardCompatibility;
+    this.keptGraphConsumer = keptGraphConsumer;
     this.options = options;
   }
 
@@ -849,9 +876,13 @@
     DexClass holder = appInfo.definitionFor(type);
     if (holder != null && !holder.isLibraryClass()) {
       if (!dontWarnPatterns.matches(context)) {
-        Diagnostic message = new StringDiagnostic("Library class " + context.toSourceString()
-            + (holder.isInterface() ? " implements " : " extends ")
-            + "program class " + type.toSourceString());
+        Diagnostic message =
+            new StringDiagnostic(
+                "Library class "
+                    + context.toSourceString()
+                    + (holder.isInterface() ? " implements " : " extends ")
+                    + "program class "
+                    + type.toSourceString());
         if (forceProguardCompatibility) {
           options.reporter.warning(message);
         } else {
@@ -1166,10 +1197,12 @@
     if (encodedField.accessFlags.isStatic()) {
       markStaticFieldAsLive(encodedField.field, reason);
     } else {
-      SetWithReason<DexEncodedField> reachable = reachableInstanceFields
-          .computeIfAbsent(encodedField.field.clazz, ignore -> new SetWithReason<>());
-      if (reachable.add(encodedField, reason) && isInstantiatedOrHasInstantiatedSubtype(
-          encodedField.field.clazz)) {
+      SetWithReason<DexEncodedField> reachable =
+          reachableInstanceFields.computeIfAbsent(
+              encodedField.field.clazz, ignore -> new SetWithReason<>((f, r) -> {}));
+      // TODO(b/120959039): The reachable.add test might be hiding other paths to the field.
+      if (reachable.add(encodedField, reason)
+          && isInstantiatedOrHasInstantiatedSubtype(encodedField.field.clazz)) {
         // We have at least one live subtype, so mark it as live.
         markInstanceFieldAsLive(encodedField, reason);
       }
@@ -1212,8 +1245,10 @@
         ? appInfo.lookupInterfaceTargets(method)
         : appInfo.lookupVirtualTargets(method);
     for (DexEncodedMethod encodedMethod : targets) {
-      SetWithReason<DexEncodedMethod> reachable = reachableVirtualMethods
-          .computeIfAbsent(encodedMethod.method.holder, (ignore) -> new SetWithReason<>());
+      // TODO(b/120959039): The reachable.add test might be hiding other paths to the method.
+      SetWithReason<DexEncodedMethod> reachable =
+          reachableVirtualMethods.computeIfAbsent(
+              encodedMethod.method.holder, (ignore) -> new SetWithReason<>((m, r) -> {}));
       if (reachable.add(encodedMethod, reason)) {
         // Abstract methods cannot be live.
         if (!encodedMethod.accessFlags.isAbstract()) {
@@ -1323,23 +1358,6 @@
     }
   }
 
-  public ReasonPrinter getReasonPrinter(Set<DexDefinition> queriedItems) {
-    // If no reason was asked, just return a no-op printer to avoid computing the information.
-    // This is the common path.
-    if (queriedItems.isEmpty()) {
-      return ReasonPrinter.getNoOpPrinter();
-    }
-    Map<DexDefinition, KeepReason> reachability = new HashMap<>();
-    for (SetWithReason<DexEncodedMethod> mappings : reachableVirtualMethods.values()) {
-      reachability.putAll(mappings.getReasons());
-    }
-    for (SetWithReason<DexEncodedField> mappings : reachableInstanceFields.values()) {
-      reachability.putAll(mappings.getReasons());
-    }
-    return new ReasonPrinter(queriedItems, liveFields.getReasons(), liveMethods.getReasons(),
-        reachability, instantiatedTypes.getReasons());
-  }
-
   public AppInfoWithLiveness traceMainDex(
       RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException {
     this.tracingMainDex = true;
@@ -1355,7 +1373,8 @@
       RootSet rootSet,
       ProguardClassFilter dontWarnPatterns,
       ExecutorService executorService,
-      Timing timing) throws ExecutionException {
+      Timing timing)
+      throws ExecutionException {
     this.rootSet = rootSet;
     this.dontWarnPatterns = dontWarnPatterns;
     // Translate the result of root-set computation into enqueuer actions.
@@ -2563,14 +2582,16 @@
   private static class SetWithReason<T> {
 
     private final Set<T> items = Sets.newIdentityHashSet();
-    private final Map<T, KeepReason> reasons = Maps.newIdentityHashMap();
+
+    private final BiConsumer<T, KeepReason> register;
+
+    public SetWithReason(BiConsumer<T, KeepReason> register) {
+      this.register = register;
+    }
 
     boolean add(T item, KeepReason reason) {
-      if (items.add(item)) {
-        reasons.put(item, reason);
-        return true;
-      }
-      return false;
+      register.accept(item, reason);
+      return items.add(item);
     }
 
     boolean contains(T item) {
@@ -2580,10 +2601,6 @@
     Set<T> getItems() {
       return ImmutableSet.copyOf(items);
     }
-
-    Map<T, KeepReason> getReasons() {
-      return ImmutableMap.copyOf(reasons);
-    }
   }
 
   private static final class TargetWithContext<T extends Descriptor<?, T>> {
@@ -2709,4 +2726,78 @@
       return false;
     }
   }
+
+  private void registerType(DexType type, KeepReason reason) {
+    assert getSourceNode(reason) != null;
+    if (keptGraphConsumer == null) {
+      return;
+    }
+    registerEdge(getClassGraphNode(type), reason);
+  }
+
+  private void registerMethod(DexEncodedMethod method, KeepReason reason) {
+    if (reason.edgeKind() == EdgeKind.IsLibraryMethod) {
+      // Don't report edges to actual library methods.
+      // TODO(b/120959039): Make sure we do have edges to methods overwriting library methods!
+      return;
+    }
+    assert getSourceNode(reason) != null;
+    if (keptGraphConsumer == null) {
+      return;
+    }
+    registerEdge(getMethodGraphNode(method.method), reason);
+  }
+
+  private void registerField(DexEncodedField field, KeepReason reason) {
+    assert getSourceNode(reason) != null;
+    if (keptGraphConsumer == null) {
+      return;
+    }
+    registerEdge(getFieldGraphNode(field.field), reason);
+  }
+
+  private void registerEdge(GraphNode target, KeepReason reason) {
+    keptGraphConsumer.acceptEdge(getSourceNode(reason), target, getEdgeInfo(reason));
+  }
+
+  private GraphNode getSourceNode(KeepReason reason) {
+    return reason.getSourceNode(this);
+  }
+
+  public GraphNode getGraphNode(DexDefinition item) {
+    if (item instanceof DexClass) {
+      return getClassGraphNode(((DexClass) item).type);
+    }
+    if (item instanceof DexEncodedMethod) {
+      return getMethodGraphNode(((DexEncodedMethod) item).method);
+    }
+    if (item instanceof DexEncodedField) {
+      return getFieldGraphNode(((DexEncodedField) item).field);
+    }
+    throw new Unreachable();
+  }
+
+  GraphEdgeInfo getEdgeInfo(KeepReason reason) {
+    return reasonInfo.computeIfAbsent(reason.edgeKind(), k -> new GraphEdgeInfo(k));
+  }
+
+  AnnotationGraphNode getAnnotationGraphNode(DexItem type) {
+    return annotationNodes.computeIfAbsent(type, AnnotationGraphNode::new);
+  }
+
+  ClassGraphNode getClassGraphNode(DexType type) {
+    return classNodes.computeIfAbsent(type, ClassGraphNode::new);
+  }
+
+  MethodGraphNode getMethodGraphNode(DexMethod context) {
+    return methodNodes.computeIfAbsent(context, MethodGraphNode::new);
+  }
+
+  FieldGraphNode getFieldGraphNode(DexField context) {
+    return fieldNodes.computeIfAbsent(context, FieldGraphNode::new);
+  }
+
+  KeepRuleGraphNode getKeepRuleGraphNode(ProguardKeepRule rule) {
+    return ruleNodes.computeIfAbsent(rule, KeepRuleGraphNode::new);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepReason.java b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
index a16c84a..82737a6 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepReason.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
@@ -6,10 +6,16 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.shaking.ReasonPrinter.ReasonFormatter;
+import com.android.tools.r8.graphinfo.GraphEdgeInfo;
+import com.android.tools.r8.graphinfo.GraphEdgeInfo.EdgeKind;
+import com.android.tools.r8.graphinfo.GraphNode;
 
 // TODO(herhut): Canonicalize reason objects.
-abstract class KeepReason {
+public abstract class KeepReason {
+
+  public abstract GraphEdgeInfo.EdgeKind edgeKind();
+
+  public abstract GraphNode getSourceNode(Enqueuer enqueuer);
 
   static KeepReason dueToKeepRule(ProguardKeepRule rule) {
     return new DueToKeepRule(rule);
@@ -51,8 +57,6 @@
     return new ReferencedInAnnotation(holder);
   }
 
-  public abstract void print(ReasonFormatter formatter);
-
   public boolean isDueToKeepRule() {
     return false;
   }
@@ -78,6 +82,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.KeepRule;
+    }
+
+    @Override
     public boolean isDueToKeepRule() {
       return true;
     }
@@ -88,18 +97,8 @@
     }
 
     @Override
-    public void print(ReasonFormatter formatter) {
-      formatter.addReason("referenced in keep rule:");
-      formatter.addMessage("  " + keepRule.toShortString() + " {");
-      int ruleCount = 0;
-      for (ProguardMemberRule memberRule : keepRule.getMemberRules()) {
-        formatter.addMessage("    " + memberRule + ";");
-        if (++ruleCount > 10) {
-          formatter.addMessage("      <...>");
-          break;
-        }
-      }
-      formatter.addMessage("  };");
+    public GraphNode getSourceNode(Enqueuer enqueuer) {
+      return enqueuer.getKeepRuleGraphNode(keepRule);
     }
   }
 
@@ -109,6 +108,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.CompatibilityRule;
+    }
+
+    @Override
     public boolean isDueToProguardCompatibility() {
       return true;
     }
@@ -125,11 +129,9 @@
     abstract String getKind();
 
     @Override
-    public void print(ReasonFormatter formatter) {
-      formatter.addReason("is " + getKind() + " " + method.toSourceString());
-      formatter.addMethodReferenceReason(method);
+    public GraphNode getSourceNode(Enqueuer enqueuer) {
+      return enqueuer.getMethodGraphNode(method.method);
     }
-
   }
 
   private static class InstatiatedIn extends BasedOnOtherMethod {
@@ -139,6 +141,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.InstantiatedIn;
+    }
+
+    @Override
     String getKind() {
       return "instantiated in";
     }
@@ -151,6 +158,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.InvokedViaSuper;
+    }
+
+    @Override
     String getKind() {
       return "invoked via super from";
     }
@@ -163,6 +175,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.TargetedBySuper;
+    }
+
+    @Override
     String getKind() {
       return "targeted by super from";
     }
@@ -175,6 +192,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.InvokedFrom;
+    }
+
+    @Override
     String getKind() {
       return "invoked from";
     }
@@ -187,6 +209,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.InvokedFromLambdaCreatedIn;
+    }
+
+    @Override
     String getKind() {
       return "invoked from lambda created in";
     }
@@ -199,6 +226,11 @@
     }
 
     @Override
+    public EdgeKind edgeKind() {
+      return EdgeKind.ReferencedFrom;
+    }
+
+    @Override
     String getKind() {
       return "referenced from";
     }
@@ -213,9 +245,13 @@
     }
 
     @Override
-    public void print(ReasonFormatter formatter) {
-      formatter.addReason("is reachable from type " + type.toSourceString());
-      formatter.addTypeLivenessReason(type);
+    public EdgeKind edgeKind() {
+      return EdgeKind.ReachableFromLiveType;
+    }
+
+    @Override
+    public GraphNode getSourceNode(Enqueuer enqueuer) {
+      return enqueuer.getClassGraphNode(type);
     }
   }
 
@@ -225,8 +261,13 @@
     }
 
     @Override
-    public void print(ReasonFormatter formatter) {
-      formatter.addReason("is defined in a library.");
+    public EdgeKind edgeKind() {
+      return EdgeKind.IsLibraryMethod;
+    }
+
+    @Override
+    public GraphNode getSourceNode(Enqueuer enqueuer) {
+      return null;
     }
   }
 
@@ -239,8 +280,13 @@
     }
 
     @Override
-    public void print(ReasonFormatter formatter) {
-      formatter.addReason("is referenced in annotation on " + holder.toSourceString());
+    public EdgeKind edgeKind() {
+      return EdgeKind.ReferencedInAnnotation;
+    }
+
+    @Override
+    public GraphNode getSourceNode(Enqueuer enqueuer) {
+      return enqueuer.getAnnotationGraphNode(holder);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/ReasonPrinter.java b/src/main/java/com/android/tools/r8/shaking/ReasonPrinter.java
deleted file mode 100644
index 1eb43f5..0000000
--- a/src/main/java/com/android/tools/r8/shaking/ReasonPrinter.java
+++ /dev/null
@@ -1,243 +0,0 @@
-// 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.shaking;
-
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexDefinition;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItem;
-import com.android.tools.r8.graph.DexType;
-import com.google.common.collect.Sets;
-import java.io.PrintStream;
-import java.util.ArrayDeque;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.Map;
-import java.util.Set;
-
-public class ReasonPrinter {
-
-  private final Set<DexDefinition> itemsQueried;
-  private ReasonFormatter formatter;
-
-  private final Map<DexEncodedField, KeepReason> liveFields;
-  private final Map<DexEncodedMethod, KeepReason> liveMethods;
-  private final Map<DexDefinition, KeepReason> reachablityReasons;
-  private final Map<DexType, KeepReason> instantiatedTypes;
-
-  ReasonPrinter(Set<DexDefinition> itemsQueried, Map<DexEncodedField, KeepReason> liveFields,
-      Map<DexEncodedMethod, KeepReason> liveMethods, Map<DexDefinition, KeepReason> reachablityReasons,
-      Map<DexType, KeepReason> instantiatedTypes) {
-    this.itemsQueried = itemsQueried;
-    this.liveFields = liveFields;
-    this.liveMethods = liveMethods;
-    this.reachablityReasons = reachablityReasons;
-    this.instantiatedTypes = instantiatedTypes;
-  }
-
-  public void run(DexApplication application) {
-    // TODO(herhut): Instead of traversing the app, sort the queried items.
-    formatter = new ReasonFormatter();
-    for (DexClass clazz : application.classes()) {
-      if (itemsQueried.contains(clazz)) {
-        printReasonFor(clazz);
-      }
-      Arrays.stream(clazz.staticFields()).filter(itemsQueried::contains)
-          .forEach(this::printReasonFor);
-      Arrays.stream(clazz.instanceFields()).filter(itemsQueried::contains)
-          .forEach(this::printReasonFor);
-      Arrays.stream(clazz.directMethods()).filter(itemsQueried::contains)
-          .forEach(this::printReasonFor);
-      Arrays.stream(clazz.virtualMethods()).filter(itemsQueried::contains)
-          .forEach(this::printReasonFor);
-    }
-  }
-
-  private void printNoIdeaWhy(DexItem item, ReasonFormatter formatter) {
-    formatter.startItem(item);
-    formatter.pushEmptyPrefix();
-    formatter.addReason("is kept for unknown reason.");
-    formatter.popPrefix();
-    formatter.endItem();
-  }
-
-  private void printOnlyAbstractShell(DexItem item, ReasonFormatter formatter) {
-    formatter.startItem(item);
-    KeepReason reachableReason = reachablityReasons.get(item);
-    if (reachableReason != null) {
-      formatter.pushPrefix(
-          "is not kept, only its abstract declaration is needed because it ");
-      reachableReason.print(formatter);
-      formatter.popPrefix();
-    } else {
-      formatter.pushEmptyPrefix();
-      formatter.addReason("is not kept, only its abstract declaration is.");
-      formatter.popPrefix();
-    }
-    formatter.endItem();
-  }
-
-  private void printReasonFor(DexClass item) {
-    KeepReason reason = instantiatedTypes.get(item.type);
-    if (reason == null) {
-      if (item.accessFlags.isAbstract()) {
-        printOnlyAbstractShell(item, formatter);
-      } else {
-        printNoIdeaWhy(item, formatter);
-      }
-    } else {
-      formatter.startItem(item);
-      formatter.pushIsLivePrefix();
-      reason.print(formatter);
-      formatter.popPrefix();
-      formatter.endItem();
-    }
-  }
-
-  private void printReasonFor(DexEncodedMethod item) {
-    KeepReason reasonLive = liveMethods.get(item);
-    if (reasonLive == null) {
-      if (item.accessFlags.isAbstract()) {
-        printOnlyAbstractShell(item, formatter);
-      } else {
-        printNoIdeaWhy(item.method, formatter);
-      }
-    } else {
-      formatter.addMethodReferenceReason(item);
-    }
-  }
-
-  private void printReasonFor(DexEncodedField item) {
-    KeepReason reason = liveFields.get(item);
-    if (reason == null) {
-      printNoIdeaWhy(item.field, formatter);
-    } else {
-      formatter.startItem(item.field);
-      formatter.pushIsLivePrefix();
-      reason.print(formatter);
-      formatter.popPrefix();
-      reason = reachablityReasons.get(item);
-      if (reason != null) {
-        formatter.pushIsReachablePrefix();
-        reason.print(formatter);
-        formatter.popPrefix();
-      }
-      formatter.endItem();
-    }
-  }
-
-  class ReasonFormatter {
-
-    private final Set<DexItem> seen = Sets.newIdentityHashSet();
-    private final Deque<String> prefixes = new ArrayDeque<>();
-
-    private int indentation = -1;
-
-    private PrintStream output = System.out;
-
-    void pushIsLivePrefix() {
-      prefixes.push("is live because ");
-    }
-
-    void pushIsReachablePrefix() {
-      prefixes.push("is reachable because ");
-    }
-
-    void pushPrefix(String prefix) {
-      prefixes.push(prefix);
-    }
-
-    void pushEmptyPrefix() {
-      prefixes.push("");
-    }
-
-    void popPrefix() {
-      prefixes.pop();
-    }
-
-    void startItem(DexItem item) {
-      indentation++;
-      indent();
-      output.println(item.toSourceString());
-    }
-
-    private void indent() {
-      for (int i = 0; i < indentation; i++) {
-        output.print("  ");
-      }
-    }
-
-    void addReason(String thing) {
-      indent();
-      output.print("|- ");
-      String prefix = prefixes.peek();
-      output.print(prefix);
-      output.println(thing);
-    }
-
-    void addMessage(String thing) {
-      indent();
-      output.print("|  ");
-      output.println(thing);
-    }
-
-    void endItem() {
-      indentation--;
-    }
-
-    void addMethodReferenceReason(DexEncodedMethod method) {
-      if (!seen.add(method.method)) {
-        return;
-      }
-      startItem(method);
-      KeepReason reason = reachablityReasons.get(method);
-      if (reason != null) {
-        pushIsReachablePrefix();
-        reason.print(this);
-        popPrefix();
-      }
-      reason = liveMethods.get(method);
-      if (reason != null) {
-        pushIsLivePrefix();
-        reason.print(this);
-        popPrefix();
-      }
-      endItem();
-    }
-
-    void addTypeLivenessReason(DexType type) {
-      if (!seen.add(type)) {
-        return;
-      }
-      startItem(type);
-      pushIsLivePrefix();
-      KeepReason reason = instantiatedTypes.get(type);
-      if (reason != null) {
-        reason.print(this);
-      }
-      popPrefix();
-      endItem();
-    }
-  }
-
-  public static ReasonPrinter getNoOpPrinter() {
-    return new NoOpReasonPrinter();
-  }
-
-  private static class NoOpReasonPrinter extends ReasonPrinter {
-
-    NoOpReasonPrinter() {
-      super(Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(),
-          Collections.emptyMap(), Collections.emptyMap());
-    }
-
-    @Override
-    public void run(DexApplication application) {
-      // Intentionally left empty.
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/shaking/WhyAreYouKeepingConsumer.java b/src/main/java/com/android/tools/r8/shaking/WhyAreYouKeepingConsumer.java
new file mode 100644
index 0000000..de43dc6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/WhyAreYouKeepingConsumer.java
@@ -0,0 +1,285 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graphinfo.ClassGraphNode;
+import com.android.tools.r8.graphinfo.FieldGraphNode;
+import com.android.tools.r8.graphinfo.GraphConsumer;
+import com.android.tools.r8.graphinfo.GraphEdgeInfo;
+import com.android.tools.r8.graphinfo.GraphEdgeInfo.EdgeKind;
+import com.android.tools.r8.graphinfo.GraphNode;
+import com.android.tools.r8.graphinfo.KeepRuleGraphNode;
+import com.android.tools.r8.graphinfo.MethodGraphNode;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.position.TextRange;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.StringUtils.BraceType;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class WhyAreYouKeepingConsumer implements GraphConsumer {
+
+  // Single-linked path description when BF searching for a path.
+  private static class GraphPath {
+    final GraphNode node;
+    final GraphPath path;
+
+    public GraphPath(GraphNode node, GraphPath path) {
+      assert node != null;
+      this.node = node;
+      this.path = path;
+    }
+  }
+
+  // Possible sub-consumer that is also inspecting the kept-graph.
+  private final GraphConsumer subConsumer;
+
+  // Directional map backwards from targets to direct sources.
+  private final Map<GraphNode, Map<GraphNode, Set<GraphEdgeInfo>>> target2sources =
+      new IdentityHashMap<>();
+
+  public WhyAreYouKeepingConsumer(GraphConsumer subConsumer) {
+    this.subConsumer = subConsumer;
+  }
+
+  @Override
+  public void acceptEdge(GraphNode source, GraphNode target, GraphEdgeInfo info) {
+    target2sources
+        .computeIfAbsent(target, k -> new IdentityHashMap<>())
+        .computeIfAbsent(source, k -> new HashSet<>())
+        .add(info);
+    if (subConsumer != null) {
+      subConsumer.acceptEdge(source, target, info);
+    }
+  }
+
+  /** Print the shortest path from a root to a node in the graph. */
+  public void printWhyAreYouKeeping(String descriptor, PrintStream out) {
+    assert DescriptorUtils.isClassDescriptor(descriptor);
+    for (GraphNode node : target2sources.keySet()) {
+      if (node.identity().equals(descriptor)) {
+        printWhyAreYouKeeping(node, out);
+        return;
+      }
+    }
+    printNothingKeeping(descriptor, out);
+  }
+
+  public void printWhyAreYouKeeping(GraphNode node, PrintStream out) {
+    Formatter formatter = new Formatter(out);
+    List<Pair<GraphNode, GraphEdgeInfo>> path = findShortestPathTo(node);
+    if (path == null) {
+      printNothingKeeping(node, out);
+      return;
+    }
+    formatter.startItem(getNodeString(node));
+    for (int i = path.size() - 1; i >= 0; i--) {
+      Pair<GraphNode, GraphEdgeInfo> edge = path.get(i);
+      printEdge(edge.getFirst(), edge.getSecond(), formatter);
+    }
+    formatter.endItem();
+  }
+
+  private void printNothingKeeping(GraphNode node, PrintStream out) {
+    out.print("Nothing is keeping ");
+    out.println(getNodeString(node));
+  }
+
+  private void printNothingKeeping(String descriptor, PrintStream out) {
+    out.print("Nothing is keeping ");
+    out.println(DescriptorUtils.descriptorToJavaType(descriptor));
+  }
+
+  private List<Pair<GraphNode, GraphEdgeInfo>> findShortestPathTo(final GraphNode node) {
+    Deque<GraphPath> queue;
+    {
+      Map<GraphNode, Set<GraphEdgeInfo>> sources = target2sources.get(node);
+      if (sources == null) {
+        // The node is not targeted at all (it is not reachable).
+        return null;
+      }
+      queue = new LinkedList<>();
+      for (GraphNode source : sources.keySet()) {
+        queue.addLast(new GraphPath(source, null));
+      }
+    }
+    Map<GraphNode, GraphNode> seen = new IdentityHashMap<>();
+    while (!queue.isEmpty()) {
+      GraphPath path = queue.removeFirst();
+      Map<GraphNode, Set<GraphEdgeInfo>> sources = target2sources.get(path.node);
+      if (sources == null) {
+        return getCanonicalPath(path, node);
+      }
+      for (GraphNode source : sources.keySet()) {
+        if (seen.containsKey(source)) {
+          continue;
+        }
+        seen.put(source, source);
+        queue.addLast(new GraphPath(source, path));
+      }
+    }
+    throw new Unreachable("Failed to find a root from node: " + node);
+  }
+
+  // Convert a internal path representation to the external API and compute the edge reasons.
+  private List<Pair<GraphNode, GraphEdgeInfo>> getCanonicalPath(
+      GraphPath path, GraphNode endTarget) {
+    assert path != null;
+    List<Pair<GraphNode, GraphEdgeInfo>> canonical = new ArrayList<>();
+    while (path.path != null) {
+      GraphNode source = path.node;
+      GraphNode target = path.path.node;
+      Set<GraphEdgeInfo> infos = target2sources.get(target).get(source);
+      canonical.add(new Pair<>(source, getCanonicalInfo(infos)));
+      path = path.path;
+    }
+    Set<GraphEdgeInfo> infos = target2sources.get(endTarget).get(path.node);
+    canonical.add(new Pair<>(path.node, getCanonicalInfo(infos)));
+    return canonical;
+  }
+
+  // Compute the most meaningful edge reason.
+  private GraphEdgeInfo getCanonicalInfo(Set<GraphEdgeInfo> infos) {
+    // TODO(b/120959039): this is pretty bad...
+    for (EdgeKind kind : EdgeKind.values()) {
+      for (GraphEdgeInfo info : infos) {
+        if (info.edgeKind() == kind) {
+          return info;
+        }
+      }
+    }
+    throw new Unreachable("Unexpected empty set of graph edge info");
+  }
+
+  private void printEdge(GraphNode node, GraphEdgeInfo info, Formatter formatter) {
+    formatter.addReason("is " + getInfoPrefix(info) + ":");
+    addNodeMessage(node, formatter);
+  }
+
+  private String getInfoPrefix(GraphEdgeInfo info) {
+    switch (info.edgeKind()) {
+      case KeepRule:
+      case CompatibilityRule:
+        return "referenced in keep rule";
+      case InstantiatedIn:
+        return "instantiated in";
+      case InvokedViaSuper:
+        return "invoked via super from";
+      case TargetedBySuper:
+        return "targeted by super from";
+      case InvokedFrom:
+        return "invoked from";
+      case InvokedFromLambdaCreatedIn:
+        return "invoked from lambda created in";
+      case ReferencedFrom:
+        return "referenced from";
+      case ReachableFromLiveType:
+        return "reachable from";
+      case ReferencedInAnnotation:
+        return "referenced in annotation";
+      case IsLibraryMethod:
+        return "defined in library";
+      default:
+        throw new Unreachable("Unexpected edge kind: " + info.edgeKind());
+    }
+  }
+
+  private String getNodeString(GraphNode node) {
+    if (node instanceof ClassGraphNode) {
+      return DescriptorUtils.descriptorToJavaType(((ClassGraphNode) node).getDescriptor());
+    }
+    if (node instanceof MethodGraphNode) {
+      MethodGraphNode methodNode = (MethodGraphNode) node;
+      MethodSignature signature =
+          MethodSignature.fromSignature(
+              methodNode.getMethodName(), methodNode.getMethodDescriptor());
+      return signature.type
+          + ' '
+          + DescriptorUtils.descriptorToJavaType(methodNode.getHolderDescriptor())
+          + '.'
+          + methodNode.getMethodName()
+          + StringUtils.join(Arrays.asList(signature.parameters), ",", BraceType.PARENS);
+    }
+    if (node instanceof FieldGraphNode) {
+      FieldGraphNode fieldNode = (FieldGraphNode) node;
+      return DescriptorUtils.descriptorToJavaType(fieldNode.getFieldDescriptor())
+          + ' '
+          + DescriptorUtils.descriptorToJavaType(fieldNode.getHolderDescriptor())
+          + '.'
+          + fieldNode.getFieldName();
+    }
+    if (node instanceof KeepRuleGraphNode) {
+      KeepRuleGraphNode keepRuleNode = (KeepRuleGraphNode) node;
+      return keepRuleNode.getOrigin() == Origin.unknown()
+          ? keepRuleNode.getContent()
+          : keepRuleNode.getOrigin() + ":" + shortPositionInfo(keepRuleNode.getPosition());
+    }
+    throw new Unreachable("Unexpected graph node type: " + node);
+  }
+
+  private void addNodeMessage(GraphNode node, Formatter formatter) {
+    for (String line : StringUtils.splitLines(getNodeString(node))) {
+      formatter.addMessage(line);
+    }
+  }
+
+  private static String shortPositionInfo(Position position) {
+    if (position instanceof TextRange) {
+      TextPosition start = ((TextRange) position).getStart();
+      return start.getLine() + ":" + start.getColumn();
+    }
+    return position.getDescription();
+  }
+
+  private static class Formatter {
+    private final PrintStream output;
+    private int indentation = -1;
+
+    public Formatter(PrintStream output) {
+      this.output = output;
+    }
+
+    void startItem(String itemString) {
+      indentation++;
+      indent();
+      output.println(itemString);
+    }
+
+    private void indent() {
+      for (int i = 0; i < indentation; i++) {
+        output.print("  ");
+      }
+    }
+
+    void addReason(String thing) {
+      indent();
+      output.print("|- ");
+      output.println(thing);
+    }
+
+    void addMessage(String thing) {
+      indent();
+      output.print("|  ");
+      output.println(thing);
+    }
+
+    void endItem() {
+      indentation--;
+    }
+  }
+}
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 c8486dd..56dc071 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -164,7 +164,8 @@
    * @return Java type name
    */
   public static String descriptorToJavaType(String descriptor, ClassNameMapper classNameMapper) {
-    switch (descriptor.charAt(0)) {
+    char c = descriptor.charAt(0);
+    switch (c) {
       case 'L':
         assert descriptor.charAt(descriptor.length() - 1) == ';';
         String clazz = descriptor.substring(1, descriptor.length() - 1)
@@ -175,6 +176,13 @@
       case '[':
         return descriptorToJavaType(descriptor.substring(1, descriptor.length()), classNameMapper)
             + "[]";
+      default:
+        return primitiveDescriptorToJavaType(c);
+    }
+  }
+
+  public static String primitiveDescriptorToJavaType(char primitive) {
+    switch (primitive) {
       case 'V':
         return "void";
       case 'Z':
@@ -194,7 +202,7 @@
       case 'D':
         return "double";
       default:
-        throw new Unreachable("Unknown type " + descriptor);
+        throw new Unreachable("Unknown type " + primitive);
     }
   }
 
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 065ece4..d9d23b0 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graphinfo.GraphConsumer;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardConfiguration;
@@ -338,6 +339,16 @@
   // If non-null, configuration must be passed to the consumer.
   public StringConsumer configurationConsumer = null;
 
+  // If null, no graph information needs to be provided for the keep/inclusion of classes
+  // in the output. If non-null, each edge pertaining to kept parts of the resulting program
+  // must be reported to the consumer.
+  public GraphConsumer keptGraphConsumer = null;
+
+  // If null, no graph information needs to be provided for the keep/inclusion of classes
+  // in the main-dex output. If non-null, each edge pertaining to kept parts in the main-dex output
+  // of the resulting program must be reported to the consumer.
+  public GraphConsumer mainDexKeptGraphConsumer = null;
+
   public Path proguardCompatibilityRulesOutput = null;
 
   public static boolean assertionsEnabled() {
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 07eea98..8c4f682 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.TestBase.R8Mode;
+import com.android.tools.r8.graphinfo.GraphConsumer;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
@@ -131,4 +132,14 @@
     builder.allowTestProguardOptions();
     return self();
   }
+
+  public R8TestBuilder setKeptGraphConsumer(GraphConsumer graphConsumer) {
+    builder.setKeptGraphConsumer(graphConsumer);
+    return self();
+  }
+
+  public R8TestBuilder setMainDexKeptGraphConsumer(GraphConsumer graphConsumer) {
+    builder.setMainDexKeptGraphConsumer(graphConsumer);
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 9425df9..e9ebd4e 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.base.Suppliers;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.function.Consumer;
@@ -40,6 +41,7 @@
   private StringConsumer mainDexListConsumer;
   private AndroidApiLevel defaultMinApiLevel = ToolHelper.getMinApiLevelForDexVm();
   private Consumer<InternalOptions> optionsConsumer = DEFAULT_OPTIONS;
+  private PrintStream stdout = null;
 
   TestCompilerBuilder(TestState state, B builder, Backend backend) {
     super(state);
@@ -70,7 +72,17 @@
     if (backend == Backend.DEX && defaultMinApiLevel != null) {
       builder.setMinApiLevel(defaultMinApiLevel.getLevel());
     }
-    return internalCompile(builder, optionsConsumer, Suppliers.memoize(sink::build));
+    PrintStream oldOut = System.out;
+    try {
+      if (stdout != null) {
+        System.setOut(stdout);
+      }
+      return internalCompile(builder, optionsConsumer, Suppliers.memoize(sink::build));
+    } finally {
+      if (stdout != null) {
+        System.setOut(oldOut);
+      }
+    }
   }
 
   @Override
@@ -151,4 +163,10 @@
     builder.setDisableDesugaring(true);
     return self();
   }
+
+  public T redirectStdOut(PrintStream printStream) {
+    assert stdout == null;
+    stdout = printStream;
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 1691232..ae7cb4f 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -49,6 +49,13 @@
     return self();
   }
 
+  public T addKeepClassAndMembersRules(Class<?>... classes) {
+    for (Class<?> clazz : classes) {
+      addKeepRules("-keep class " + clazz.getTypeName() + " { *; }");
+    }
+    return self();
+  }
+
   public T addKeepPackageRules(Package pkg) {
     return addKeepRules("-keep class " + pkg.getName() + ".*");
   }
@@ -64,5 +71,4 @@
             "  public static void main(java.lang.String[]);",
             "}"));
   }
-
 }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index c46cef0..eea2ec6 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -112,7 +112,8 @@
   private static final String RETRACE = RETRACE6_0_1;
 
   public static final Path R8_JAR = Paths.get(LIBS_DIR, "r8.jar");
-  public static final Path R8_LIB_JAR = Paths.get(LIBS_DIR, "r8lib_with_deps.jar");
+  public static final Path R8_WITH_RELOCATED_DEPS_JAR =
+      Paths.get(LIBS_DIR, "r8_with_relocated_deps.jar");
 
   public enum DexVm {
     ART_4_0_4_TARGET(Version.V4_0_4, Kind.TARGET),
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationProguardCompatTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationProguardCompatTest.java
new file mode 100644
index 0000000..f6b7b8f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/AccessRelaxationProguardCompatTest.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.accessrelaxation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPrivate;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import org.junit.Test;
+
+/**
+ * Tests that both R8 and Proguard may change the visibility of a field or method that is explicitly
+ * kept.
+ */
+public class AccessRelaxationProguardCompatTest extends TestBase {
+
+  private static Class<?> clazz = AccessRelaxationProguardCompatTestClass.class;
+  private static Class<?> clazzWithGetter = TestClassWithGetter.class;
+
+  @Test
+  public void r8Test() throws Exception {
+    testForR8(Backend.DEX)
+        .addProgramClasses(clazz, clazzWithGetter)
+        .addKeepMainRule(clazz)
+        .addKeepRules(
+            "-allowaccessmodification",
+            "-keep class " + TestClassWithGetter.class.getTypeName() + " {",
+            "  private int field;",
+            "}")
+        .compile()
+        .inspect(AccessRelaxationProguardCompatTest::inspect);
+  }
+
+  @Test
+  public void proguardTest() throws Exception {
+    testForProguard()
+        .addProgramClasses(clazz, clazzWithGetter)
+        .addKeepMainRule(clazz)
+        .addKeepRules(
+            "-allowaccessmodification",
+            "-keep class " + TestClassWithGetter.class.getTypeName() + " {",
+            "  private int field;",
+            "}")
+        .compile()
+        .inspect(AccessRelaxationProguardCompatTest::inspect);
+  }
+
+  private static void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(clazzWithGetter);
+    assertThat(classSubject, isPresent());
+
+    FieldSubject fieldSubject = classSubject.uniqueFieldWithName("field");
+    assertThat(fieldSubject, isPresent());
+
+    // Although this field was explicitly kept, it is no longer private.
+    assertThat(fieldSubject, not(isPrivate()));
+  }
+}
+
+class AccessRelaxationProguardCompatTestClass {
+
+  public static void main(String[] args) {
+    TestClassWithGetter obj = new TestClassWithGetter();
+    System.out.println(obj.get());
+  }
+}
+
+class TestClassWithGetter {
+
+  private int field = 0;
+
+  public int get() {
+    System.out.println("In method()");
+    return field;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java b/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java
index 2cca0f4..a0cd03d 100644
--- a/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java
+++ b/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java
@@ -84,8 +84,8 @@
     if (testExternal) {
       R8Result output =
           runExternalR8(
-              ToolHelper.R8_LIB_JAR,
-              ToolHelper.R8_LIB_JAR,
+              ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
+              ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
               testFolder.newFolder().toPath(),
               MAIN_KEEP,
               mode);
@@ -96,7 +96,7 @@
           R8Command.builder()
               .setMode(CompilationMode.RELEASE)
               .addLibraryFiles(runtimeJar(Backend.CF))
-              .addProgramFiles(ToolHelper.R8_LIB_JAR)
+              .addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR)
               .setOutput(jar, OutputMode.ClassFile)
               .build());
     }
@@ -125,10 +125,20 @@
   private void compareR8(Path program, ProcessResult runResult, String[] keep, String... args)
       throws Exception {
     R8Result runR8Debug =
-        runExternalR8(ToolHelper.R8_LIB_JAR, program, temp.newFolder().toPath(), keep, "--debug");
+        runExternalR8(
+            ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
+            program,
+            temp.newFolder().toPath(),
+            keep,
+            "--debug");
     assertEquals(runResult.toString(), ToolHelper.runJava(runR8Debug.outputJar, args).toString());
     R8Result runR8Release =
-        runExternalR8(ToolHelper.R8_LIB_JAR, program, temp.newFolder().toPath(), keep, "--release");
+        runExternalR8(
+            ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
+            program,
+            temp.newFolder().toPath(),
+            keep,
+            "--release");
     assertEquals(runResult.toString(), ToolHelper.runJava(runR8Release.outputJar, args).toString());
     RunR8AndCheck(r8R8Debug, program, runR8Debug, keep, "--debug");
     RunR8AndCheck(r8R8Debug, program, runR8Release, keep, "--release");
diff --git a/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java b/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java
index 66b3b21..a33a5ad 100644
--- a/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java
+++ b/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java
@@ -9,6 +9,7 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.jasmin.JasminBuilder;
@@ -24,12 +25,12 @@
   @BeforeClass
   public static void setUp() throws Exception {
     JasminBuilder builder = new JasminBuilder();
-    JasminBuilder.ClassBuilder clazz = builder.addClass("junit.framework.TestCase");
+    builder.addClass("junit.framework.TestCase");
     junitClasses = builder.buildClasses();
   }
 
   @Test
-  public void testFullModeError() throws Exception {
+  public void testFullModeError() {
     try {
       testForR8(Backend.DEX)
           .setMinApi(AndroidApiLevel.O)
@@ -44,7 +45,7 @@
 
   @Test
   public void testCompatibilityModeWarning() throws Exception {
-    TestCompileResult result = testForR8Compat(Backend.DEX)
+    TestCompileResult<R8TestRunResult> result = testForR8Compat(Backend.DEX)
         .setMinApi(AndroidApiLevel.O)
         .addProgramClassFileData(junitClasses)
         .addKeepAllClassesRule()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTestBase.java b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTestBase.java
index 27ceebe..e681c77 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTestBase.java
@@ -36,7 +36,7 @@
             appView, dexApplication, TEST_OPTIONS.proguardConfiguration.getRules(), TEST_OPTIONS)
         .run(executorService);
     Enqueuer enqueuer =
-        new Enqueuer(appView, TEST_OPTIONS, TEST_OPTIONS.forceProguardCompatibility);
+        new Enqueuer(appView, TEST_OPTIONS, null, TEST_OPTIONS.forceProguardCompatibility);
     return enqueuer.traceApplication(
         rootSet, TEST_OPTIONS.proguardConfiguration.getDontWarnPatterns(), executorService, timing);
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/BuilderWithInheritanceTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/BuilderWithInheritanceTest.java
new file mode 100644
index 0000000..c3deab3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/BuilderWithInheritanceTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Regression test for b/120182628. */
+@RunWith(Parameterized.class)
+public class BuilderWithInheritanceTest extends TestBase {
+
+  private final Backend backend;
+
+  @Parameters(name = "Backend: {0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  public BuilderWithInheritanceTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    CodeInspector inspector =
+        testForR8(backend)
+            .addInnerClasses(BuilderWithInheritanceTest.class)
+            .addKeepMainRule(TestClass.class)
+            .enableInliningAnnotations()
+            .enableMergeAnnotations()
+            .run(TestClass.class)
+            .assertSuccessWithOutput("42")
+            .inspector();
+    assertThat(inspector.clazz(Builder.class), isPresent());
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      A a = new Builder(42).build();
+      System.out.print(a.get());
+    }
+  }
+
+  static class A {
+
+    private final int f;
+
+    public A(int f) {
+      this.f = f;
+    }
+
+    public int get() {
+      return f;
+    }
+  }
+
+  @NeverMerge
+  static class BuilderBase {
+
+    protected int f;
+
+    public BuilderBase(int f) {
+      this.f = f;
+    }
+
+    @NeverInline
+    public static int get(BuilderBase obj) {
+      return obj.f;
+    }
+  }
+
+  static class Builder extends BuilderBase {
+
+    public Builder(int f) {
+      super(f);
+    }
+
+    public A build() {
+      // After force inlining of get() there will be a field-read "this.f", which is not allowed by
+      // the class inliner, because the class inliner only handles reads from fields that are
+      // declared on Builder.
+      return new A(get(this));
+    }
+  }
+}
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 e013b02..0a832c9 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION;
+import static org.hamcrest.CoreMatchers.containsString;
 
 import com.android.tools.r8.GenerateMainDexList;
 import com.android.tools.r8.GenerateMainDexListCommand;
@@ -14,6 +15,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
@@ -59,11 +61,55 @@
         Paths.get(EXAMPLE_SRC_DIR, "multidex001", "ref-list-1.txt"),
         AndroidApiLevel.I);
     String output = new String(baos.toByteArray(), Charset.defaultCharset());
-    Assert.assertTrue(output.contains("is live because referenced in keep rule:"));
+    Assert.assertThat(output, containsString("is referenced in keep rule:"));
     System.setOut(stdout);
   }
 
   @Test
+  public void traceMainDexList001_whyareyoukeeping_consumer() throws Throwable {
+    WhyAreYouKeepingConsumer graphConsumer = new WhyAreYouKeepingConsumer(null);
+    doTest(
+        "traceMainDexList001_1",
+        "multidex001",
+        EXAMPLE_BUILD_DIR,
+        Paths.get(EXAMPLE_SRC_DIR, "multidex", "main-dex-rules.txt"),
+        Paths.get(EXAMPLE_SRC_DIR, "multidex001", "ref-list-1.txt"),
+        Paths.get(EXAMPLE_SRC_DIR, "multidex001", "ref-list-1.txt"),
+        AndroidApiLevel.I,
+        options -> {
+          options.enableInlining = false;
+          options.mainDexKeptGraphConsumer = graphConsumer;
+        });
+    {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      graphConsumer.printWhyAreYouKeeping("Lmultidex001/MainActivity;", new PrintStream(baos));
+      String output = new String(baos.toByteArray(), Charset.defaultCharset());
+      String expected =
+          StringUtils.lines(
+              "multidex001.MainActivity",
+              "|- is referenced in keep rule:",
+              "|  src/test/examples/multidex/main-dex-rules.txt:14:1");
+      Assert.assertEquals(expected, output);
+    }
+    {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      graphConsumer.printWhyAreYouKeeping("Lmultidex001/ClassForMainDex;", new PrintStream(baos));
+      String output = new String(baos.toByteArray(), Charset.defaultCharset());
+      // TODO(b/120951570): We should be able to get the reason for ClassForMainDex too.
+      String expected =
+          true
+              ? StringUtils.lines("Nothing is keeping multidex001.ClassForMainDex")
+              : StringUtils.lines(
+                  "multidex001.ClassForMainDex",
+                  "|- is direct reference from:",
+                  "|  multidex001.MainActivity",
+                  "|- is referenced in keep rule:",
+                  "|  src/test/examples/multidex/main-dex-rules.txt:14:1");
+      Assert.assertEquals(expected, output);
+    }
+  }
+
+  @Test
   public void traceMainDexList001_1() throws Throwable {
     doTest(
         "traceMainDexList001_1",
diff --git a/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java b/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
index 2713c6d..01265bb 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
@@ -4,21 +4,29 @@
 
 package com.android.tools.r8.maindexlist.whyareyoukeeping;
 
-import static org.hamcrest.core.StringContains.containsString;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
+import com.android.tools.r8.GenerateMainDexList;
+import com.android.tools.r8.GenerateMainDexListCommand;
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graphinfo.GraphConsumer;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
-import java.nio.charset.Charset;
+import java.util.List;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
 class HelloWorldMain {
   public static void main(String[] args) {
@@ -30,38 +38,113 @@
 
 class NonMainDexClass {}
 
+@RunWith(Parameterized.class)
 public class MainDexListWhyAreYouKeeping extends TestBase {
-  public String runTest(String whyAreYouKeepingRule) throws Exception {
-    R8Command command =
-        ToolHelper.prepareR8CommandBuilder(
-                readClasses(HelloWorldMain.class, MainDexClass.class, NonMainDexClass.class))
+
+  private static final List<Class<?>> CLASSES =
+      ImmutableList.of(HelloWorldMain.class, MainDexClass.class, NonMainDexClass.class);
+
+  private enum Command {
+    R8,
+    Generator
+  }
+
+  private enum ApiUse {
+    WhyAreYouKeepingRule,
+    KeptGraphConsumer
+  }
+
+  @Parameters(name = "{0} {1}")
+  public static List<Object[]> parameters() {
+    return buildParameters(Command.values(), ApiUse.values());
+  }
+
+  private final Command command;
+  private final ApiUse apiUse;
+
+  public MainDexListWhyAreYouKeeping(Command command, ApiUse apiUse) {
+    this.command = command;
+    this.apiUse = apiUse;
+  }
+
+  public void runTestWithGenerator(GraphConsumer consumer, String rule) throws Exception {
+    GenerateMainDexListCommand.Builder builder =
+        GenerateMainDexListCommand.builder()
+            .addProgramFiles(ListUtils.map(CLASSES, ToolHelper::getClassFileForTestClass))
+            .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
             .addMainDexRules(
                 ImmutableList.of(keepMainProguardConfiguration(HelloWorldMain.class)),
                 Origin.unknown())
-            .addMainDexRules(ImmutableList.of(whyAreYouKeepingRule), Origin.unknown())
-            .setOutput(temp.getRoot().toPath(), OutputMode.DexIndexed)
-            .setMode(CompilationMode.RELEASE)
-            .build();
+            .setMainDexKeptGraphConsumer(consumer);
+    if (rule != null) {
+      builder.addMainDexRules(ImmutableList.of(rule), Origin.unknown());
+    }
+    GenerateMainDexList.run(builder.build());
+  }
+
+  public void runTestWithR8(GraphConsumer consumer, String rule) throws Exception {
+    R8TestBuilder builder =
+        testForR8(Backend.DEX)
+            .setMinApi(AndroidApiLevel.L)
+            .addProgramClasses(CLASSES)
+            .addMainDexRules(keepMainProguardConfiguration(HelloWorldMain.class))
+            .setMainDexKeptGraphConsumer(consumer);
+    if (rule != null) {
+      builder.addMainDexRules(rule);
+    }
+    builder.compile();
+  }
+
+  private String runTest(Class clazz) throws Exception {
     PrintStream stdout = System.out;
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    System.setOut(new PrintStream(baos));
-    ToolHelper.runR8(command);
-    String output = new String(baos.toByteArray(), Charset.defaultCharset());
-    System.setOut(stdout);
-    return output;
+    String rule = null;
+    WhyAreYouKeepingConsumer consumer = null;
+    if (apiUse == ApiUse.KeptGraphConsumer) {
+      consumer = new WhyAreYouKeepingConsumer(null);
+    } else {
+      assert apiUse == ApiUse.WhyAreYouKeepingRule;
+      rule = "-whyareyoukeeping class " + clazz.getTypeName();
+      System.setOut(new PrintStream(baos));
+    }
+    switch (command) {
+      case R8:
+        runTestWithR8(consumer, rule);
+        break;
+      case Generator:
+        runTestWithGenerator(consumer, rule);
+        break;
+      default:
+        throw new Unreachable();
+    }
+    if (consumer != null) {
+      consumer.printWhyAreYouKeeping(
+          DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()), new PrintStream(baos));
+    } else {
+      System.setOut(stdout);
+    }
+    return baos.toString();
   }
 
   @Test
   public void testMainDexClassWhyAreYouKeeping() throws Exception {
-    String output = runTest("-whyareyoukeeping class " + MainDexClass.class.getCanonicalName());
-    assertThat(
-        output, containsString("com.android.tools.r8.maindexlist.whyareyoukeeping.MainDexClass"));
-    assertThat(output, containsString("- is live because referenced in keep rule:"));
+    String expected =
+        StringUtils.lines(
+            "com.android.tools.r8.maindexlist.whyareyoukeeping.MainDexClass",
+            "|- is instantiated in:",
+            "|  void com.android.tools.r8.maindexlist.whyareyoukeeping.HelloWorldMain.main(java.lang.String[])",
+            "|- is referenced in keep rule:",
+            "|  -keep class com.android.tools.r8.maindexlist.whyareyoukeeping.HelloWorldMain {",
+            "|    public static void main(java.lang.String[]);",
+            "|  }");
+    assertEquals(expected, runTest(MainDexClass.class));
   }
 
   @Test
   public void testNonMainDexWhyAreYouKeeping() throws Exception {
-    String output = runTest("-whyareyoukeeping class " + NonMainDexClass.class.getCanonicalName());
-    assertTrue(output.isEmpty());
+    String expected =
+        StringUtils.lines(
+            "Nothing is keeping com.android.tools.r8.maindexlist.whyareyoukeeping.NonMainDexClass");
+    assertEquals(expected, runTest(NonMainDexClass.class));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
index 191559b..3d9edfc 100644
--- a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
@@ -82,10 +82,9 @@
           new RootSetBuilder(appView, program, configuration.getRules(), options).run(executor);
     }
 
-    Enqueuer enqueuer = new Enqueuer(appView, options, options.forceProguardCompatibility);
+    Enqueuer enqueuer = new Enqueuer(appView, options, null, options.forceProguardCompatibility);
     AppInfoWithSubtyping appInfo =
-        enqueuer.traceApplication(
-            rootSet, configuration.getDontWarnPatterns(), executor, timing);
+        enqueuer.traceApplication(rootSet, configuration.getDontWarnPatterns(), executor, timing);
     return new Minifier(appInfo.withLiveness(), rootSet, Collections.emptySet(), options)
         .run(timing);
   }
diff --git a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
index 7157ed5..df6f682 100644
--- a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
@@ -115,8 +115,9 @@
                 buildKeepRuleForClass(Main.class, application.dexItemFactory),
                 options)
             .run(executor);
-    appInfo = new Enqueuer(appView, options).traceApplication(
-        rootSet, ProguardClassFilter.empty(), executor, timing);
+    appInfo =
+        new Enqueuer(appView, options, null)
+            .traceApplication(rootSet, ProguardClassFilter.empty(), executor, timing);
     // We do not run the tree pruner to ensure that the hierarchy is as designed and not modified
     // due to liveness.
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingTest.java b/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingTest.java
index 1f06e72..8ae7452 100644
--- a/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/whyareyoukeeping/WhyAreYouKeepingTest.java
@@ -7,36 +7,75 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
-import com.google.common.collect.ImmutableList;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
 class A {
 
 }
 
+@RunWith(Parameterized.class)
 public class WhyAreYouKeepingTest extends TestBase {
+
+  public static final String expected =
+      StringUtils.joinLines(
+          "com.android.tools.r8.shaking.whyareyoukeeping.A",
+          "|- is referenced in keep rule:",
+          "|  -keep class com.android.tools.r8.shaking.whyareyoukeeping.A { *; }",
+          "");
+
+  @Parameters(name = "{0}")
+  public static Backend[] parameters() {
+    return Backend.values();
+  }
+
+  public final Backend backend;
+
+  public WhyAreYouKeepingTest(Backend backend) {
+    this.backend = backend;
+  }
+
   @Test
-  public void test() throws Exception {
-    String proguardConfig = String.join("\n", ImmutableList.of(
-        "-keep class " + A.class.getCanonicalName() + " { *; }",
-        "-whyareyoukeeping class " + A.class.getCanonicalName()
-    ));
-    PrintStream stdout = System.out;
+  public void testWhyAreYouKeepingViaProguardConfig() throws Exception {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    System.setOut(new PrintStream(baos));
-    compileWithR8(ImmutableList.of(A.class), proguardConfig);
-    String output = new String(baos.toByteArray(), Charset.defaultCharset());
-    System.setOut(stdout);
-    String expected = String.join(System.lineSeparator(), ImmutableList.of(
-        "com.android.tools.r8.shaking.whyareyoukeeping.A",
-        "|- is live because referenced in keep rule:",
-        "|    -keep class com.android.tools.r8.shaking.whyareyoukeeping.A {",
-        "|      *;",
-        "|    };",
-        ""));
+    testForR8(backend)
+        .addProgramClasses(A.class)
+        .addKeepClassAndMembersRules(A.class)
+        .addKeepRules("-whyareyoukeeping class " + A.class.getTypeName())
+        // Clear the default library and ignore missing classes to avoid processing the library.
+        .addLibraryFiles()
+        .addOptionsModification(o -> o.ignoreMissingClasses = true)
+        // Redirect the compilers stdout to intercept the '-whyareyoukeeping' output
+        .redirectStdOut(new PrintStream(baos))
+        .compile();
+    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
+    assertEquals(expected, output);
+  }
+
+  @Test
+  public void testWhyAreYouKeepingViaConsumer() throws Exception {
+    WhyAreYouKeepingConsumer graphConsumer = new WhyAreYouKeepingConsumer(null);
+    testForR8(backend)
+        .addProgramClasses(A.class)
+        .addKeepClassAndMembersRules(A.class)
+        // Clear the default library and ignore missing classes to avoid processing the library.
+        .addLibraryFiles()
+        .addOptionsModification(o -> o.ignoreMissingClasses = true)
+        .setKeptGraphConsumer(graphConsumer)
+        .compile();
+
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    String descriptor = DescriptorUtils.javaTypeToDescriptor(A.class.getTypeName());
+    graphConsumer.printWhyAreYouKeeping(descriptor, new PrintStream(baos));
+    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
     assertEquals(expected, output);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentFieldSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentFieldSubject.java
index 336d079..408fc15 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentFieldSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentFieldSubject.java
@@ -17,6 +17,21 @@
   }
 
   @Override
+  public boolean isProtected() {
+    throw new Unreachable("Cannot determine if an absent field is protected");
+  }
+
+  @Override
+  public boolean isPrivate() {
+    throw new Unreachable("Cannot determine if an absent field is private");
+  }
+
+  @Override
+  public boolean isPackagePrivate() {
+    throw new Unreachable("Cannot determine if an absent field is package-private");
+  }
+
+  @Override
   public boolean isStatic() {
     throw new Unreachable("Cannot determine if an absent field is static");
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
index e9dd541..5eb2f22 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
@@ -26,6 +26,21 @@
   }
 
   @Override
+  public boolean isProtected() {
+    throw new Unreachable("Cannot determine if an absent method is protected");
+  }
+
+  @Override
+  public boolean isPrivate() {
+    throw new Unreachable("Cannot determine if an absent method is private");
+  }
+
+  @Override
+  public boolean isPackagePrivate() {
+    throw new Unreachable("Cannot determine if an absent method is package-private");
+  }
+
+  @Override
   public boolean isStatic() {
     throw new Unreachable("Cannot determine if an absent method is static");
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java
index f9a669a..c182f74 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexValue;
 
 public abstract class FieldSubject extends MemberSubject {
+
   public abstract boolean hasExplicitStaticValue();
 
   public abstract DexEncodedField getField();
@@ -19,4 +20,14 @@
   public abstract String getOriginalSignatureAttribute();
 
   public abstract String getFinalSignatureAttribute();
+
+  @Override
+  public FieldSubject asFieldSubject() {
+    return this;
+  }
+
+  @Override
+  public boolean isFieldSubject() {
+    return true;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java
index e65a8b0..7efc794 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java
@@ -29,6 +29,21 @@
   }
 
   @Override
+  public boolean isProtected() {
+    return dexField.accessFlags.isProtected();
+  }
+
+  @Override
+  public boolean isPrivate() {
+    return dexField.accessFlags.isPrivate();
+  }
+
+  @Override
+  public boolean isPackagePrivate() {
+    return !isPublic() && !isProtected() && !isPrivate();
+  }
+
+  @Override
   public boolean isStatic() {
     return dexField.accessFlags.isStatic();
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
index f4f1660..6ce0496 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
@@ -56,6 +56,21 @@
   }
 
   @Override
+  public boolean isProtected() {
+    return dexMethod.accessFlags.isProtected();
+  }
+
+  @Override
+  public boolean isPrivate() {
+    return dexMethod.accessFlags.isPrivate();
+  }
+
+  @Override
+  public boolean isPackagePrivate() {
+    return !isPublic() && !isProtected() && !isPrivate();
+  }
+
+  @Override
   public boolean isStatic() {
     return dexMethod.accessFlags.isStatic();
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 7f310ed..a9abba1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AccessFlags;
 import com.google.common.collect.ImmutableList;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -11,6 +13,33 @@
 
 public class Matchers {
 
+  private enum Visibility {
+    PUBLIC,
+    PROTECTED,
+    PRIVATE,
+    PACKAGE_PRIVATE;
+
+    @Override
+    public String toString() {
+      switch (this) {
+        case PUBLIC:
+          return "public";
+
+        case PROTECTED:
+          return "protected";
+
+        case PRIVATE:
+          return "private";
+
+        case PACKAGE_PRIVATE:
+          return "package-private";
+
+        default:
+          throw new Unreachable("Unexpected visibility");
+      }
+    }
+  }
+
   private static String type(Subject subject) {
     String type = "<unknown subject type>";
     if (subject instanceof ClassSubject) {
@@ -154,22 +183,67 @@
     };
   }
 
-  public static Matcher<MethodSubject> isPublic() {
-    return new TypeSafeMatcher<MethodSubject>() {
+  public static <T extends MemberSubject> Matcher<T> isPrivate() {
+    return hasVisibility(Visibility.PRIVATE);
+  }
+
+  public static <T extends MemberSubject> Matcher<T> isPublic() {
+    return hasVisibility(Visibility.PUBLIC);
+  }
+
+  private static <T extends MemberSubject> Matcher<T> hasVisibility(Visibility visibility) {
+    return new TypeSafeMatcher<T>() {
       @Override
-      public boolean matchesSafely(final MethodSubject method) {
-        return method.isPresent() && method.isPublic();
+      public boolean matchesSafely(final T subject) {
+        if (subject.isPresent()) {
+          switch (visibility) {
+            case PUBLIC:
+              return subject.isPublic();
+
+            case PROTECTED:
+              return subject.isProtected();
+
+            case PRIVATE:
+              return subject.isPrivate();
+
+            case PACKAGE_PRIVATE:
+              return subject.isPackagePrivate();
+
+            default:
+              throw new Unreachable("Unexpected visibility: " + visibility);
+          }
+        }
+        return false;
       }
 
       @Override
       public void describeTo(final Description description) {
-        description.appendText("method public");
+        description.appendText("method " + visibility);
       }
 
       @Override
-      public void describeMismatchSafely(final MethodSubject method, Description description) {
+      public void describeMismatchSafely(final T subject, Description description) {
         description
-            .appendText("method ").appendValue(method.getOriginalName()).appendText(" was not");
+            .appendText("method ")
+            .appendValue(subject.getOriginalName())
+            .appendText(" was ");
+        if (subject.isPresent()) {
+          AccessFlags accessFlags =
+              subject.isMethodSubject()
+                  ? subject.asMethodSubject().getMethod().accessFlags
+                  : subject.asFieldSubject().getField().accessFlags;
+          if (accessFlags.isPublic()) {
+            description.appendText("public");
+          } else if (accessFlags.isProtected()) {
+            description.appendText("protected");
+          } else if (accessFlags.isPrivate()) {
+            description.appendText("private");
+          } else {
+            description.appendText("package-private");
+          }
+        } else {
+          description.appendText(" was absent");
+        }
       }
     };
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java
index 9e90fde..a95b6e6 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java
@@ -10,6 +10,12 @@
 
   public abstract boolean isPublic();
 
+  public abstract boolean isProtected();
+
+  public abstract boolean isPrivate();
+
+  public abstract boolean isPackagePrivate();
+
   public abstract boolean isStatic();
 
   public abstract boolean isFinal();
@@ -27,4 +33,20 @@
     Signature finalSignature = getFinalSignature();
     return finalSignature == null ? null : finalSignature.name;
   }
+
+  public FieldSubject asFieldSubject() {
+    return null;
+  }
+
+  public boolean isFieldSubject() {
+    return false;
+  }
+
+  public MethodSubject asMethodSubject() {
+    return null;
+  }
+
+  public boolean isMethodSubject() {
+    return false;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
index 98bb8d0..adcbf31 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
@@ -46,4 +46,14 @@
   public Stream<InstructionSubject> streamInstructions() {
     return Streams.stream(iterateInstructions());
   }
+
+  @Override
+  public MethodSubject asMethodSubject() {
+    return this;
+  }
+
+  @Override
+  public boolean isMethodSubject() {
+    return true;
+  }
 }
diff --git a/tools/internal_test.py b/tools/internal_test.py
index 47f5a97..7d126b0 100755
--- a/tools/internal_test.py
+++ b/tools/internal_test.py
@@ -197,8 +197,6 @@
   assert not get_magic_file_exists(READY_FOR_TESTING)
   git_hash = utils.get_HEAD_sha1()
   put_magic_file(READY_FOR_TESTING, git_hash)
-  print("TODO(118647285): Reenable when bug fixed")
-  return
   begin = time.time()
   while True:
     if time.time() - begin > BOT_RUN_TIMEOUT:
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index 3e7d78c..76fc59b 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -121,6 +121,7 @@
 # do Bug: #BUG in the commit message of disabling to ensure re-enabling
 DISABLED_PERMUTATIONS = [
   # (app, version, type), e.g., ('gmail', '180826.15', 'deploy'),
+  ('youtube', '13.37', 'deploy'), # b/120977564
 ]
 
 def get_permutations():
@@ -171,6 +172,8 @@
 def run_with_options(options, args):
   app_provided_pg_conf = False;
   extra_args = []
+  # todo(121018500): remove when memory is under control
+  extra_args.append('-Xmx8GB')
   if options.golem:
     golem.link_third_party()
     options.out = os.getcwd()
diff --git a/tools/test.py b/tools/test.py
index 0e4625f..279f58b 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -120,8 +120,8 @@
   if utils.is_bot():
     gradle.RunGradle(['clean'])
 
-  # Build R8lib with dependencies for bootstrapping tests before adding test sources
-  gradle.RunGradle(['r8libwithdeps'])
+  # Build an R8 with dependencies for bootstrapping tests before adding test sources
+  gradle.RunGradle(['r8WithRelocatedDeps'])
 
   gradle_args = ['--stacktrace']
   # Set all necessary Gradle properties and options first.
diff --git a/tools/update_prebuilds_in_android.py b/tools/update_prebuilds_in_android.py
index 41881b0..6ef56c7 100755
--- a/tools/update_prebuilds_in_android.py
+++ b/tools/update_prebuilds_in_android.py
@@ -15,12 +15,20 @@
 BUILD_ROOT = "http://storage.googleapis.com/r8-releases/raw/"
 MASTER_BUILD_ROOT = "%smaster/" % BUILD_ROOT
 
-JAR_TARGETS = [
-  utils.D8,
-  utils.R8,
-  utils.COMPATDX,
-  utils.COMPATPROGUARD,
-]
+JAR_TARGETS_MAP = {
+  'full': [
+    (utils.D8, 'd8-master'),
+    (utils.R8, 'r8-master'),
+    (utils.COMPATDX, 'compatdx-master'),
+    (utils.COMPATPROGUARD, 'compatproguard-master'),
+  ],
+  'lib': [
+    (utils.R8LIB, 'r8-master'),
+    (utils.COMPATDXLIB, 'compatdx-master'),
+    (utils.COMPATPROGUARDLIB, 'compatproguard-master'),
+  ],
+}
+
 OTHER_TARGETS = ["LICENSE"]
 
 def parse_arguments():
@@ -30,20 +38,36 @@
       help='Android checkout root.')
   parser.add_argument('--commit_hash', default=None, help='Commit hash')
   parser.add_argument('--version', default=None, help='The version to download')
+  parser.add_argument(
+    '--targets',
+    required=True,
+    choices=['full', 'lib'],
+    help="Use 'full' to download the full, non-optimized jars (legacy" +
+      " behaviour) and 'lib' for the R8-processed, optimized jars (this" +
+      " one omits d8.jar)",
+  )
+  parser.add_argument(
+    '--maps',
+    action='store_true',
+    help="Download proguard maps for jars, use only with '--target lib'.",
+  )
   return parser.parse_args()
 
-def copy_targets(root, target_root, srcs, dests):
+def copy_targets(root, target_root, srcs, dests, maps=False):
   assert len(srcs) == len(dests)
   for i in range(len(srcs)):
     src = os.path.join(root, srcs[i])
     dest = os.path.join(target_root, 'prebuilts', 'r8', dests[i])
     print 'Copying: ' + src + ' -> ' + dest
     copyfile(src, dest)
+    if maps:
+      print 'Copying: ' + src + '.map -> ' + dest + '.map'
+      copyfile(src + '.map', dest + '.map')
 
-def copy_jar_targets(root, target_root):
-  srcs = map((lambda t: t + '.jar'), JAR_TARGETS)
-  dests = map((lambda t: t + '-master.jar'), JAR_TARGETS)
-  copy_targets(root, target_root, srcs, dests)
+def copy_jar_targets(root, target_root, jar_targets, maps):
+  srcs = map((lambda t: t[0] + '.jar'), jar_targets)
+  dests = map((lambda t: t[1] + '.jar'), jar_targets)
+  copy_targets(root, target_root, srcs, dests, maps=maps)
 
 def copy_other_targets(root, target_root):
   copy_targets(root, target_root, OTHER_TARGETS, OTHER_TARGETS)
@@ -65,22 +89,29 @@
 
 def Main():
   args = parse_arguments()
+  if args.maps and args.targets != 'lib':
+    raise Exception("Use '--maps' only with '--targets lib.")
   target_root = args.android_root[0]
+  jar_targets = JAR_TARGETS_MAP[args.targets]
   if args.commit_hash == None and args.version == None:
-    gradle.RunGradle(JAR_TARGETS)
-    copy_jar_targets(utils.LIBS, target_root)
+    gradle.RunGradle(map(lambda t: t[0], jar_targets))
+    copy_jar_targets(utils.LIBS, target_root, jar_targets, args.maps)
     copy_other_targets(utils.GENERATED_LICENSE_DIR, target_root)
   else:
     assert args.commit_hash == None or args.version == None
-    targets = map((lambda t: t + '.jar'), JAR_TARGETS) + OTHER_TARGETS
+    targets = map((lambda t: t[0] + '.jar'), jar_targets) + OTHER_TARGETS
     with utils.TempDir() as root:
       for target in targets:
         if args.commit_hash:
           download_hash(root, args.commit_hash, target)
+          if args.maps and target not in OTHER_TARGETS:
+            download_hash(root, args.commit_hash, target + '.map')
         else:
           assert args.version
           download_version(root, args.version, target)
-      copy_jar_targets(root, target_root)
+          if args.maps and target not in OTHER_TARGETS:
+            download_version(root, args.version, target + '.map')
+      copy_jar_targets(root, target_root, jar_targets, args.maps)
       copy_other_targets(root, target_root)
 
 if __name__ == '__main__':