Retrace script for automatically retracing R8 builds in platform.

User documentation at go/retraceme

Bug: 204293621
Test: atest --host r8retrace-check-retraced-stacktrace
Change-Id: I595d5c3954d5e06c43092f11fcd8e6c917e1fea3
diff --git a/Android.bp b/Android.bp
index 3ace361..8078b69 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,3 +1,18 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package {
     default_applicable_licenses: ["prebuilts_r8_license"],
 }
@@ -54,6 +69,19 @@
     static_libs: ["r8"],
 }
 
+java_binary_host {
+    name: "retrace",
+    main_class: "com.android.tools.r8.retrace.RetraceWrapper",
+    srcs: ["src/com/android/tools/r8/retrace/RetraceWrapper.java"],
+    static_libs: ["r8"],
+}
+
+java_binary_host {
+    name: "extractmarker",
+    main_class: "com.android.tools.r8.ExtractMarker",
+    static_libs: ["r8"],
+}
+
 java_import_host {
     name: "r8",
     jars: ["r8.jar"],
diff --git a/src/com/android/tools/r8/retrace/RetraceWrapper.java b/src/com/android/tools/r8/retrace/RetraceWrapper.java
new file mode 100644
index 0000000..034dcdb
--- /dev/null
+++ b/src/com/android/tools/r8/retrace/RetraceWrapper.java
@@ -0,0 +1,739 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.r8.retrace;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URISyntaxException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.OptionalInt;
+import java.util.function.BiConsumer;
+import java.util.stream.Stream;
+
+public class RetraceWrapper {
+
+  /** Default paths to search for mapping files. */
+  private static final List<String> AOSP_MAP_SEARCH_PATHS =
+      Collections.singletonList("out/target/common/obj/APPS");
+
+  private static final String USAGE =
+      String.join(
+          System.lineSeparator(),
+          "Usage: retrace [<option>]* [<file>]",
+          "where <file> is the file to retrace (default stdin)",
+          "  and <option> is one of:",
+          "  --default-map <file>     # Default map to retrace lines that don't auto-identify.",
+          "  --map-search-path <path> # Path to search for mappings that support auto-identify.",
+          "                           # Separate <path> entries by colon ':'.",
+          "                           # Default '" + String.join(":", AOSP_MAP_SEARCH_PATHS) + "'.",
+          "  --print-map-table        # Print the table of identified mapping files and exit.",
+          "  -h, --help               # Print this message.");
+
+  private static class ForwardingDiagnosticsHander implements DiagnosticsHandler {
+    @Override
+    public void error(Diagnostic error) {
+      throw RetraceWrapper.error(error.getDiagnosticMessage());
+    }
+
+    @Override
+    public void warning(Diagnostic warning) {
+      RetraceWrapper.warning(warning.getDiagnosticMessage());
+    }
+
+    @Override
+    public void info(Diagnostic info) {
+      RetraceWrapper.info(info.getDiagnosticMessage());
+    }
+  }
+
+  private static class LazyRetracer {
+    final MapInfo mapInfo;
+    final Path mapPath;
+
+    private Retracer lazyRetracer = null;
+
+    public LazyRetracer(MapInfo mapInfo, Path mapPath) {
+      this.mapInfo = mapInfo;
+      this.mapPath = mapPath;
+    }
+
+    public Retracer getRetracer() {
+      if (lazyRetracer == null) {
+        try {
+          lazyRetracer =
+              Retracer.createDefault(
+                  ProguardMapProducer.fromPath(mapPath), new ForwardingDiagnosticsHander());
+        } catch (InvalidMappingFileException e) {
+          throw new RuntimeException("Failure in mapping file: " + mapPath, e);
+        }
+      }
+      return lazyRetracer;
+    }
+  }
+
+  private static class MapInfo {
+    final String id;
+    final String hash;
+
+    public MapInfo(String id, String hash) {
+      assert id != null;
+      assert hash != null;
+      this.id = id;
+      this.hash = hash;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (this == other) {
+        return true;
+      }
+      if (other == null || getClass() != other.getClass()) {
+        return false;
+      }
+      MapInfo otherMapInfo = (MapInfo) other;
+      return Objects.equals(id, otherMapInfo.id) && Objects.equals(hash, otherMapInfo.hash);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, hash);
+    }
+
+    @Override
+    public String toString() {
+      return "MapInfo{" + "id='" + id + '\'' + ", hash='" + hash + '\'' + '}';
+    }
+  }
+
+  /** Representation of a line with a hole, ala "<prefix><hole><suffix>". */
+  private static class LineWithHole {
+    final String line;
+    final int start;
+    final int end;
+
+    public LineWithHole(String line, int start, int end) {
+      this.line = line;
+      this.start = start;
+      this.end = end;
+    }
+
+    public String plug(String string) {
+      return line.substring(0, start) + string + line.substring(end);
+    }
+  }
+
+  /** Parsed exception header line, such as "Caused by: <exception>". */
+  private static class ExceptionLine extends LineWithHole {
+    final ClassReference exception;
+
+    public ExceptionLine(String line, int start, int end, ClassReference exception) {
+      super(line, start, end);
+      this.exception = exception;
+    }
+  }
+
+  /** Parsed frame line, such as "at <class>.<method>(<source-file>:<line>)". */
+  private static class FrameLine extends LineWithHole {
+    final ClassReference clazz;
+    final String methodName;
+    final String sourceFile;
+    final OptionalInt lineNumber;
+
+    public FrameLine(
+        String line,
+        int start,
+        int end,
+        ClassReference clazz,
+        String methodName,
+        String sourceFile,
+        OptionalInt lineNumber) {
+      super(line, start, end);
+      this.clazz = clazz;
+      this.methodName = methodName;
+      this.sourceFile = sourceFile;
+      this.lineNumber = lineNumber;
+    }
+  }
+
+  /** An immutable linked list of the result lines so that a result tree can be created. */
+  private static class ResultNode {
+    final ResultNode parent;
+    final String line;
+
+    public ResultNode(ResultNode parent, String line) {
+      this.parent = parent;
+      this.line = line;
+    }
+
+    public void print() {
+      if (parent != null) {
+        parent.print();
+      }
+      System.out.println(line);
+    }
+  }
+
+  /**
+   * Indication that a line is the start of an escaping stack trace.
+   *
+   * <p>Note that this does not identify an exception that is directly printed with, e.g.,
+   * Throwable.printStackTrace(), but only one that exits the runtime. That should generally catch
+   * what we need, but could be refined to match ':' which is the only other indicator.
+   */
+  private static final String ESCAPING_EXCEPTION_MARKER = "Exception in thread \"";
+
+  /** Indication that a line is the start of a "caused by" stack trace. */
+  private static final String CAUSED_BY_EXCEPTION_MARKER = "Caused by: ";
+
+  /** Indication that a line is the start of a "suppressed" stack trace. */
+  private static final String SUPPRESSED_EXCEPTION_MARKER = "Suppressed: ";
+
+  /** Start of the source file for any R8 build withing AOSP. */
+  // TODO(zerny): Should this be a configurable prefix?
+  private static final String AOSP_SF_MARKER = "go/retraceme ";
+
+  /** Start of the source file for any R8 compiler build. */
+  private static final String R8_SF_MARKER = "R8_";
+
+  /** Mapping file header indicating the id of mapping file. */
+  private static final String MAP_ID_HEADER_MARKER = "# pg_map_id: ";
+
+  /** Mapping file header indicating the hash of mapping file. */
+  private static final String MAP_HASH_HEADER_MARKER = "# pg_map_hash: SHA-256 ";
+
+  /** Map of cached/lazy retracer instances for maps found in the local AOSP build. */
+  private static final Map<String, LazyRetracer> RETRACERS = new HashMap<>();
+
+  private static final List<String> PENDING_MESSAGES = new ArrayList<>();
+
+  private static void flushPendingMessages() {
+    PENDING_MESSAGES.forEach(System.err::println);
+  }
+
+  private static void info(String message) {
+    PENDING_MESSAGES.add("Info: " + message);
+  }
+
+  private static void warning(String message) {
+    PENDING_MESSAGES.add("Warning: " + message);
+  }
+
+  private static RuntimeException error(String message) {
+    flushPendingMessages();
+    throw new RuntimeException(message);
+  }
+
+  private static MapInfo readMapHeaderInfo(Path path) throws IOException {
+    String mapId = null;
+    String mapHash = null;
+    try (BufferedReader reader = Files.newBufferedReader(path)) {
+      while (true) {
+        String line = reader.readLine();
+        if (line == null || !line.startsWith("#")) {
+          break;
+        }
+        if (mapId == null) {
+          mapId = tryParseMapIdHeader(line);
+        }
+        if (mapHash == null) {
+          mapHash = tryParseMapHashHeader(line);
+        }
+      }
+    }
+    if (mapId != null && mapHash != null) {
+      return new MapInfo(mapId, mapHash);
+    }
+    return null;
+  }
+
+  private static Path getProjectRoot() throws URISyntaxException {
+    // The retrace.jar should be located in out/[soong/]host/<platform>/framework/retrace.jar
+    Path hostPath = Paths.get("out", "host");
+    Path hostSoongPath = Paths.get("out", "soong");
+    Path retraceJarPath =
+        Paths.get(RetraceWrapper.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+    for (Path current = retraceJarPath; current != null; current = current.getParent()) {
+      if (current.endsWith(hostPath) || current.endsWith(hostSoongPath)) {
+        return current.getParent().getParent();
+      }
+    }
+    info(
+        "Unable to determine the project root based on the retrace.jar location: "
+            + retraceJarPath);
+    return null;
+  }
+
+  private static LazyRetracer getRetracerForAosp(String sourceFile) {
+    MapInfo stackLineInfo = tryParseSourceFileMarkerForAosp(sourceFile);
+    return stackLineInfo == null ? null : RETRACERS.get(stackLineInfo.id);
+  }
+
+  private static LazyRetracer getRetracerForR8(String sourceFile) {
+    MapInfo stackLineInfo = tryParseSourceFileMarkerForR8(sourceFile);
+    if (stackLineInfo == null) {
+      return null;
+    }
+    LazyRetracer retracer = RETRACERS.get(stackLineInfo.id);
+    if (retracer == null) {
+      // TODO(zerny): Lookup the mapping file in the R8 cloud storage bucket.
+      info("Could not identify a mapping file for lines with R8 tag: " + stackLineInfo);
+    }
+    return retracer;
+  }
+
+  private static String tryParseMapIdHeader(String line) {
+    return tryParseMapHeaderLine(line, MAP_ID_HEADER_MARKER);
+  }
+
+  private static String tryParseMapHashHeader(String line) {
+    return tryParseMapHeaderLine(line, MAP_HASH_HEADER_MARKER);
+  }
+
+  private static String tryParseMapHeaderLine(String line, String headerMarker) {
+    if (line.startsWith(headerMarker)) {
+      return line.substring(headerMarker.length());
+    }
+    return null;
+  }
+
+  private static FrameLine tryParseFrameLine(String line) {
+    String atMarker = "at ";
+    int atIndex = line.indexOf(atMarker);
+    if (atIndex < 0) {
+      return null;
+    }
+    int parenStartIndex = line.indexOf('(', atIndex);
+    if (parenStartIndex < 0) {
+      return null;
+    }
+    int parenEndIndex = line.indexOf(')', parenStartIndex);
+    if (parenEndIndex < 0) {
+      return null;
+    }
+    int classAndMethodSeperatorIndex = line.lastIndexOf('.', parenStartIndex);
+    if (classAndMethodSeperatorIndex < 0) {
+      return null;
+    }
+    int classStartIndex = atIndex + atMarker.length();
+    String clazz = line.substring(classStartIndex, classAndMethodSeperatorIndex);
+    String method = line.substring(classAndMethodSeperatorIndex + 1, parenStartIndex);
+    // Source file and line may or may not be present.
+    int sourceAndLineSeperatorIndex = line.lastIndexOf(':', parenEndIndex);
+    String sourceFile;
+    OptionalInt lineNumber;
+    if (parenStartIndex < sourceAndLineSeperatorIndex) {
+      sourceFile = line.substring(parenStartIndex + 1, sourceAndLineSeperatorIndex);
+      try {
+        lineNumber =
+            OptionalInt.of(
+                Integer.parseInt(line.substring(sourceAndLineSeperatorIndex + 1, parenEndIndex)));
+      } catch (NumberFormatException e) {
+        lineNumber = OptionalInt.empty();
+      }
+    } else {
+      sourceFile = line.substring(parenStartIndex + 1, parenEndIndex);
+      lineNumber = OptionalInt.empty();
+    }
+    return new FrameLine(
+        line,
+        classStartIndex,
+        parenEndIndex + 1,
+        Reference.classFromTypeName(clazz),
+        method,
+        sourceFile,
+        lineNumber);
+  }
+
+  private static int indexOfExceptionStart(String line) {
+    int i = line.indexOf(ESCAPING_EXCEPTION_MARKER);
+    if (i >= 0) {
+      int start = line.indexOf("\" ", i + ESCAPING_EXCEPTION_MARKER.length());
+      if (start > 0) {
+        return start;
+      }
+    }
+    i = line.indexOf(CAUSED_BY_EXCEPTION_MARKER);
+    if (i >= 0) {
+      return i + CAUSED_BY_EXCEPTION_MARKER.length();
+    }
+    i = line.indexOf(SUPPRESSED_EXCEPTION_MARKER);
+    if (i >= 0) {
+      return i + SUPPRESSED_EXCEPTION_MARKER.length();
+    }
+    return -1;
+  }
+
+  private static ExceptionLine tryParseExceptionLine(String line) {
+    int start = indexOfExceptionStart(line);
+    if (start < 0) {
+      return null;
+    }
+    int end = line.indexOf(':', start);
+    if (end < 0) {
+      return null;
+    }
+    String exception = line.substring(start, end);
+    return new ExceptionLine(line, start, end, Reference.classFromTypeName(exception));
+  }
+
+  private static MapInfo tryParseSourceFileMarkerForAosp(String sourceFile) {
+    if (!sourceFile.startsWith(AOSP_SF_MARKER)) {
+      return null;
+    }
+    int hashStart = AOSP_SF_MARKER.length();
+    String mapHash = sourceFile.substring(hashStart);
+    // Currently, app builds use the map-hash as the build id.
+    return new MapInfo(mapHash, mapHash);
+  }
+
+  private static MapInfo tryParseSourceFileMarkerForR8(String sourceFile) {
+    if (!sourceFile.startsWith(R8_SF_MARKER)) {
+      return null;
+    }
+    int versionStart = R8_SF_MARKER.length();
+    int mapHashStart = sourceFile.indexOf('_', versionStart) + 1;
+    if (mapHashStart <= 0) {
+      return null;
+    }
+    String version = sourceFile.substring(versionStart, mapHashStart - 1);
+    String mapHash = sourceFile.substring(mapHashStart);
+    return new MapInfo(version, mapHash);
+  }
+
+  private static void printIdentityStackTrace(ExceptionLine exceptionLine, List<FrameLine> frames) {
+    if (exceptionLine != null) {
+      System.out.println(exceptionLine.line);
+    }
+    frames.forEach(frame -> System.out.println(frame.line));
+  }
+
+  private static void retrace(InputStream stream, LazyRetracer defaultRetracer) throws Exception {
+    BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+    String currentLine = reader.readLine();
+    List<FrameLine> frames = new ArrayList<>();
+    while (currentLine != null) {
+      ExceptionLine exceptionLine = tryParseExceptionLine(currentLine);
+      if (exceptionLine != null) {
+        currentLine = reader.readLine();
+        if (currentLine == null) {
+          // Reached end-of-file and we can't retrace the exception. Flush and exit.
+          printIdentityStackTrace(exceptionLine, Collections.emptyList());
+          return;
+        }
+      }
+      FrameLine topFrameLine = tryParseFrameLine(currentLine);
+      if (topFrameLine == null) {
+        // The line is not a frame so we can't retrace it. Flush and continue on the next line.
+        printIdentityStackTrace(exceptionLine, Collections.emptyList());
+        System.out.println(currentLine);
+        currentLine = reader.readLine();
+        continue;
+      }
+      // Collect all subsequent lines with the same source file info.
+      FrameLine frame = topFrameLine;
+      while (frame != null) {
+        if (!frame.sourceFile.equals(topFrameLine.sourceFile)) {
+          break;
+        }
+        frames.add(frame);
+        currentLine = reader.readLine();
+        frame = currentLine == null ? null : tryParseFrameLine(currentLine);
+      }
+      retraceStackTrace(defaultRetracer, exceptionLine, frames);
+      frames.clear();
+    }
+  }
+
+  private static LazyRetracer determineRetracer(String sourceFile, LazyRetracer defaultRetracer)
+      throws Exception {
+    LazyRetracer lazyRetracer = getRetracerForR8(sourceFile);
+    if (lazyRetracer != null) {
+      return lazyRetracer;
+    }
+    lazyRetracer = getRetracerForAosp(sourceFile);
+    if (lazyRetracer != null) {
+      return lazyRetracer;
+    }
+    return defaultRetracer;
+  }
+
+  private static void retraceStackTrace(
+      LazyRetracer defaultRetracer, ExceptionLine exceptionLine, List<FrameLine> frames)
+      throws Exception {
+    String sourceFile = frames.get(0).sourceFile;
+    LazyRetracer lazyRetracer = determineRetracer(sourceFile, defaultRetracer);
+    if (lazyRetracer == null) {
+      printIdentityStackTrace(exceptionLine, frames);
+      return;
+    }
+    Retracer retracer = lazyRetracer.getRetracer();
+    List<ResultNode> finalResultNodes = new ArrayList<>();
+    retraceOptionalExceptionLine(
+        retracer,
+        exceptionLine,
+        (context, parentResult) ->
+            retraceFrameRecursive(retracer, context, parentResult, 0, frames)
+                .forEach(finalResultNodes::add));
+
+    if (finalResultNodes.size() > 1) {
+      System.out.println(
+          "Printing "
+              + finalResultNodes.size()
+              + " ambiguous stacks separated by <OR>.\n"
+              + "If this is unexpected, please file a bug on R8 and attach the "
+              + "content of the raw stack trace and the mapping file: "
+              + lazyRetracer.mapPath
+              + "\nPublic tracker at https://issuetracker.google.com/issues/new?component=326788");
+    }
+    for (int i = 0; i < finalResultNodes.size(); i++) {
+      if (i > 0) {
+        System.out.println("<OR>");
+      }
+      ResultNode node = finalResultNodes.get(i);
+      node.print();
+    }
+  }
+
+  private static void retraceOptionalExceptionLine(
+      Retracer retracer,
+      ExceptionLine exceptionLine,
+      BiConsumer<RetraceStackTraceContext, ResultNode> resultCallback) {
+    // This initial result node parent is 'null', i.e., no parent.
+    ResultNode initialResultNode = null;
+    if (exceptionLine == null) {
+      // If no exception line is given, retracing starts in the empty context.
+      resultCallback.accept(RetraceStackTraceContext.empty(), initialResultNode);
+      return;
+    }
+    // If an exception line is given the result is possibly a forrest, so each individual result
+    // has a null parent.
+    retracer
+        .retraceThrownException(exceptionLine.exception)
+        .forEach(
+            element ->
+                resultCallback.accept(
+                    element.getContext(),
+                    new ResultNode(
+                        initialResultNode,
+                        exceptionLine.plug(element.getRetracedClass().getTypeName()))));
+  }
+
+  private static Stream<ResultNode> retraceFrameRecursive(
+      Retracer retracer,
+      RetraceStackTraceContext context,
+      ResultNode parentResult,
+      int frameIndex,
+      List<FrameLine> frames) {
+    if (frameIndex >= frames.size()) {
+      return Stream.of(parentResult);
+    }
+
+    // Helper to link up frame results when iterating via a closure callback.
+    class ResultLinker {
+      ResultNode current;
+
+      public ResultLinker(ResultNode current) {
+        this.current = current;
+      }
+
+      public void link(String nextResult) {
+        current = new ResultNode(current, nextResult);
+      }
+    }
+
+    FrameLine frameLine = frames.get(frameIndex);
+    return retracer
+        .retraceFrame(context, frameLine.lineNumber, frameLine.clazz, frameLine.methodName)
+        .flatMap(
+            frameElement -> {
+              // Create a linking helper to amend the result when iterating the frames.
+              ResultLinker linker = new ResultLinker(parentResult);
+              frameElement.forEachRewritten(
+                  frame -> {
+                    RetracedMethodReference method = frame.getMethodReference();
+                    RetracedClassReference holder = method.getHolderClass();
+                    int origPos = method.getOriginalPositionOrDefault(-1);
+                    linker.link(
+                        frameLine.plug(
+                            holder.getTypeName()
+                                + "."
+                                + method.getMethodName()
+                                + "("
+                                + frame.getSourceFile().getOrInferSourceFile()
+                                + (origPos >= 0 ? (":" + origPos) : "")
+                                + ")"));
+                  });
+              return retraceFrameRecursive(
+                  retracer,
+                  frameElement.getRetraceStackTraceContext(),
+                  linker.current,
+                  frameIndex + 1,
+                  frames);
+            });
+  }
+
+  private static void populateMappingFileMap(List<String> searchPaths) throws Exception {
+    Path projectRoot = getProjectRoot();
+    if (projectRoot == null) {
+      return;
+    }
+    Path prebuiltR8MapPath = projectRoot.resolve("prebuilts").resolve("r8").resolve("r8.jar.map");
+    MapInfo prebuiltR8MapInfo = readMapHeaderInfo(prebuiltR8MapPath);
+    if (prebuiltR8MapInfo == null) {
+      info("Unable to read expected prebuilt R8 map in " + prebuiltR8MapPath);
+    } else {
+      RETRACERS.put(prebuiltR8MapInfo.id, new LazyRetracer(prebuiltR8MapInfo, prebuiltR8MapPath));
+    }
+    for (String path : searchPaths) {
+      Path resolvedPath = projectRoot.resolve(Paths.get(path));
+      if (Files.notExists(resolvedPath)) {
+        error("Invalid search path entry: " + resolvedPath);
+      }
+      Files.walkFileTree(
+          resolvedPath,
+          new FileVisitor<Path>() {
+
+            final Path mapFileName = Paths.get("proguard_dictionary");
+
+            @Override
+            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
+              return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                throws IOException {
+              if (file.endsWith(mapFileName)) {
+                MapInfo mapInfo = readMapHeaderInfo(file);
+                if (mapInfo != null) {
+                  RETRACERS.put(mapInfo.id, new LazyRetracer(mapInfo, file));
+                }
+              }
+              return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult visitFileFailed(Path file, IOException exc) {
+              return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
+              return FileVisitResult.CONTINUE;
+            }
+          });
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    String stackTraceFile = null;
+    String defaultMapArg = null;
+    boolean printMappingFileTable = false;
+    List<String> searchPaths = AOSP_MAP_SEARCH_PATHS;
+    for (int i = 0; i < args.length; i++) {
+      String arg = args[i];
+      if (arg.equals("-h") || arg.equals("--help")) {
+        System.out.println(USAGE);
+        return;
+      }
+      if (arg.equals("--default-map")) {
+        i++;
+        if (i == args.length) {
+          throw error("No argument given for --default-map");
+        }
+        defaultMapArg = args[i];
+      } else if (arg.equals("--map-search-path")) {
+        i++;
+        if (i == args.length) {
+          throw error("No argument given for --map-search-path");
+        }
+        searchPaths = parseSearchPath(args[i]);
+      } else if (arg.equals("--print-map-table")) {
+        printMappingFileTable = true;
+      } else if (arg.startsWith("-")) {
+        throw error("Unknown option: " + arg);
+      } else if (stackTraceFile != null) {
+        throw error("At most one input file is supported.");
+      } else {
+        stackTraceFile = arg;
+      }
+    }
+
+    LazyRetracer defaultRetracer = null;
+    if (defaultMapArg != null) {
+      defaultRetracer = new LazyRetracer(null, Paths.get(defaultMapArg));
+    }
+
+    populateMappingFileMap(searchPaths);
+    if (printMappingFileTable) {
+      List<String> keys = new ArrayList<>(RETRACERS.keySet());
+      keys.sort(String::compareTo);
+      for (String key : keys) {
+        LazyRetracer retracer = RETRACERS.get(key);
+        System.out.println(key + " -> " + retracer.mapPath);
+      }
+      return;
+    }
+
+    if (stackTraceFile == null) {
+      retrace(System.in, defaultRetracer);
+    } else {
+      Path path = Paths.get(stackTraceFile);
+      if (!Files.exists(path)) {
+        throw error("Input file does not exist: " + stackTraceFile);
+      }
+      try (InputStream stream = Files.newInputStream(path, StandardOpenOption.READ)) {
+        retrace(stream, defaultRetracer);
+      }
+    }
+    flushPendingMessages();
+  }
+
+  private static List<String> parseSearchPath(String paths) {
+    int length = paths.length();
+    List<String> result = new ArrayList<>();
+    int start = 0;
+    do {
+      int split = paths.indexOf(':', start);
+      int end = split != -1 ? split : length;
+      String path = paths.substring(start, end).strip();
+      if (!path.isEmpty()) {
+        result.add(path);
+      }
+      start = end + 1;
+    } while (start < length);
+    return result;
+  }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 0000000..b1270eb
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,76 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Integration test for the R8 retracing tool.
+
+// The retracing tool is a developer tool and part of the build tools.
+// The following tests are structured so that the app and retrace tool
+// are invoked exactly as a normal build would. The check that they
+// produce the correct result is then postponed to a test so that a
+// retrace tool failure will not result in a build failure.
+
+// Rule to dexdump the content of a sample app.
+// The dexdump is used to simulate a raw stack trace from the app.
+genrule {
+    name: "r8retrace-dexdump-sample-app",
+    out: ["dexdump.txt"],
+    srcs: [":HelloActivityWithR8"],
+    tools: ["dexdump", "extractmarker"],
+    cmd: "$(location extractmarker) $(in) > $(out)"
+        + " && $(location dexdump) -d $(in) >> $(out)",
+}
+
+// Tool and rule to create the raw stack trace from a dexdump.
+java_binary_host {
+    name: "r8retrace-create-stacktrace-tool",
+    main_class: "com.android.tools.r8.CreateStacktraceFromDexDumpTool",
+    srcs: ["src/com/android/tools/r8/CreateStacktraceFromDexDumpTool.java"],
+}
+
+genrule {
+    name: "r8retrace-create-stacktrace",
+    out: ["stacktrace.txt"],
+    srcs: [":r8retrace-dexdump-sample-app"],
+    tools: ["r8retrace-create-stacktrace-tool"],
+    cmd: "$(location r8retrace-create-stacktrace-tool) $(in) $(out)",
+}
+
+// Run retrace on the stack trace to produce a retraced stack trace.
+genrule {
+    name: "r8retrace-run-retrace",
+    out: ["retraced-stacktrace.txt"],
+    srcs: [":r8retrace-create-stacktrace", ":HelloActivityWithR8{.proguard_map}"],
+    tools: ["retrace"],
+    cmd: "$(location retrace)"
+        + " --map-search-path $(location :HelloActivityWithR8{.proguard_map})"
+        + " $(location :r8retrace-create-stacktrace)"
+        + " > $(out)",
+}
+
+// Test checks that the raw and retraced stack traces are as expected.
+// All the output files are added as resources here so that, in case of failure, their content
+// can be included in the error message.
+java_test_host {
+    name: "r8retrace-check-retraced-stacktrace",
+    test_suites: ["general-tests"],
+    srcs: ["src/com/android/tools/r8/CheckRetracedStacktraceTest.java"],
+    static_libs: ["junit"],
+    java_resources: [
+        ":r8retrace-dexdump-sample-app",
+        ":HelloActivityWithR8{.proguard_map}",
+        ":r8retrace-create-stacktrace",
+        ":r8retrace-run-retrace",
+    ],
+}
diff --git a/tests/samples/HelloActivityWithR8/Android.bp b/tests/samples/HelloActivityWithR8/Android.bp
new file mode 100644
index 0000000..74887d0
--- /dev/null
+++ b/tests/samples/HelloActivityWithR8/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "HelloActivityWithR8",
+    srcs: ["src/**/*.java"],
+    sdk_version: "current",
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: true,
+        shrink: true,
+        optimize: true,
+        obfuscate: true,
+        proguard_compatibility: false,
+        proguard_flags_files: ["proguard.config"]
+    }
+}
diff --git a/tests/samples/HelloActivityWithR8/AndroidManifest.xml b/tests/samples/HelloActivityWithR8/AndroidManifest.xml
new file mode 100644
index 0000000..ef2e5ae
--- /dev/null
+++ b/tests/samples/HelloActivityWithR8/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Declare the contents of this Android application.  The namespace
+     attribute brings in the Android platform namespace, and the package
+     supplies a unique name for the application.  When writing your
+     own application, the package name must be changed from "com.example.*"
+     to come from a domain that you own or have control over. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.helloactivitywithr8">
+    <application android:label="Hello, Activity With R8!">
+        <activity android:name="HelloActivityWithR8"
+                android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/samples/HelloActivityWithR8/proguard.config b/tests/samples/HelloActivityWithR8/proguard.config
new file mode 100644
index 0000000..e61169d
--- /dev/null
+++ b/tests/samples/HelloActivityWithR8/proguard.config
@@ -0,0 +1 @@
+-keepattributes LineNumberTable
diff --git a/tests/samples/HelloActivityWithR8/res/layout/hello_activity.xml b/tests/samples/HelloActivityWithR8/res/layout/hello_activity.xml
new file mode 100644
index 0000000..fa6a434
--- /dev/null
+++ b/tests/samples/HelloActivityWithR8/res/layout/hello_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<EditText xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/text"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:textSize="18sp"
+    android:autoText="true"
+    android:capitalize="sentences"
+    android:text="@string/hello_activity_text_text" />
+
diff --git a/tests/samples/HelloActivityWithR8/res/values/strings.xml b/tests/samples/HelloActivityWithR8/res/values/strings.xml
new file mode 100644
index 0000000..6be1a65
--- /dev/null
+++ b/tests/samples/HelloActivityWithR8/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <string name="hello_activity_text_text">Hello, World!</string>
+
+</resources>
diff --git a/tests/samples/HelloActivityWithR8/src/com/example/android/helloactivitywithr8/HelloActivityWithR8.java b/tests/samples/HelloActivityWithR8/src/com/example/android/helloactivitywithr8/HelloActivityWithR8.java
new file mode 100644
index 0000000..f2f097c
--- /dev/null
+++ b/tests/samples/HelloActivityWithR8/src/com/example/android/helloactivitywithr8/HelloActivityWithR8.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.helloactivitywithr8;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+
+/**
+ * A simple application compiled with R8.
+ *
+ * <p>Adapted from development/samples/HelloActivity.
+ */
+public class HelloActivityWithR8 extends Activity {
+
+  /** Getter method that will be inlined by R8. */
+  private View getView() {
+    return getLayoutInflater().inflate(R.layout.hello_activity, null);
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    View view = getView();
+    setContentView(view);
+  }
+}
diff --git a/tests/src/com/android/tools/r8/CheckRetracedStacktraceTest.java b/tests/src/com/android/tools/r8/CheckRetracedStacktraceTest.java
new file mode 100644
index 0000000..8c692da
--- /dev/null
+++ b/tests/src/com/android/tools/r8/CheckRetracedStacktraceTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.r8;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CheckRetracedStacktraceTest {
+
+  private static final String FRAME_PREFIX =
+      "    at com.example.android.helloactivitywithr8.HelloActivityWithR8.";
+
+  private List<String> getResourceLines(String resource) throws Exception {
+    try (InputStream is = getClass().getResourceAsStream(resource)) {
+      return new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))
+          .lines()
+          .collect(Collectors.toList());
+    }
+  }
+
+  private String onErrorDebugInfo() throws Exception {
+    StringBuilder builder = new StringBuilder("\nAdditional debug info:\n");
+    appendResourceContent(builder, "dexdump.txt");
+    appendResourceContent(builder, "proguard_dictionary");
+    appendResourceContent(builder, "stacktrace.txt");
+    appendResourceContent(builder, "retraced-stacktrace.txt");
+    return builder.toString();
+  }
+
+  private void appendResourceContent(StringBuilder builder, String resource) throws Exception {
+    builder.append("==== ").append(resource).append('\n');
+    getResourceLines("/" + resource).forEach(l -> builder.append(l).append('\n'));
+  }
+
+  @Test
+  public void checkRawStacktrace() throws Exception {
+    String errorInfo = onErrorDebugInfo();
+
+    List<String> lines = getResourceLines("/stacktrace.txt");
+    // In release builds a single frame is present, in debug builds two.
+    Assert.assertTrue(errorInfo, 1 < lines.size() && lines.size() <= 3);
+    Assert.assertEquals(errorInfo, "java.lang.RuntimeException: error", lines.get(0));
+    // The frame lines "at line" is the qualified method and we don't check build hash
+    // and PC to allow minor changes to the test app and compiler without breaking this test.
+    for (int i = 1; i < lines.size(); i++) {
+      String frameLine = lines.get(i);
+      int sourceFileStart = frameLine.indexOf('(');
+      int lineNumberSeparator = frameLine.indexOf(':');
+      int lineNumberEnd = frameLine.lastIndexOf(')');
+      int hashInfoSeparator = frameLine.lastIndexOf(' ', lineNumberSeparator);
+      Assert.assertTrue(errorInfo, frameLine.startsWith(FRAME_PREFIX));
+      Assert.assertEquals(
+          errorInfo, "(go/retraceme", frameLine.substring(sourceFileStart, hashInfoSeparator));
+      String lineNumberString = frameLine.substring(lineNumberSeparator + 1, lineNumberEnd);
+      try {
+        int lineNumber = Integer.parseInt(lineNumberString);
+      } catch (NumberFormatException e) {
+        Assert.fail("Invalid line number: " + lineNumberString + errorInfo);
+      }
+    }
+  }
+
+  @Test
+  public void checkRetracedStacktrace() throws Exception {
+    String errorInfo = onErrorDebugInfo();
+
+    // Prefix is the qualified class on each line, suffix does not check line numbers to
+    // allow minor changes to the test app without breaking this test.
+    String suffix = "(HelloActivityWithR8.java";
+
+    List<String> lines = getResourceLines("/retraced-stacktrace.txt");
+    int expectedLines = 3;
+    Assert.assertEquals(
+        "Expected "
+            + expectedLines
+            + " lines, got: \n=====\n"
+            + String.join("\n", lines)
+            + "\n====="
+            + errorInfo,
+        expectedLines,
+        lines.size());
+    Assert.assertEquals(errorInfo, "java.lang.RuntimeException: error", lines.get(0));
+    String line1 = lines.get(1);
+    Assert.assertEquals(
+        errorInfo, FRAME_PREFIX + "getView" + suffix, line1.substring(0, line1.indexOf(':')));
+    String line2 = lines.get(2);
+    Assert.assertEquals(
+        errorInfo, FRAME_PREFIX + "onCreate" + suffix, line2.substring(0, line2.indexOf(':')));
+  }
+}
diff --git a/tests/src/com/android/tools/r8/CreateStacktraceFromDexDumpTool.java b/tests/src/com/android/tools/r8/CreateStacktraceFromDexDumpTool.java
new file mode 100644
index 0000000..abfbb35
--- /dev/null
+++ b/tests/src/com/android/tools/r8/CreateStacktraceFromDexDumpTool.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.r8;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+public class CreateStacktraceFromDexDumpTool {
+
+  private static final String CLASS = "com.example.android.helloactivitywithr8.HelloActivityWithR8";
+  private static final String SOURCE_FILE_IDX = "source_file_idx";
+  private static final String INVOKE_VIRTUAL = ": invoke-virtual";
+  private static final String INVOKE_DIRECT = ": invoke-direct";
+  private static final String METHOD_ON_CREATE = "onCreate";
+  private static final String METHOD_GET_VIEW = "getView";
+  private static final String METHOD_GET_LAYOUT_INFLATER = "getLayoutInflater";
+  private static final String R8_MARKER_PREFIX = "~~R8{";
+
+  private final List<String> inputLines;
+  private final List<String> outputLines = new ArrayList<>();
+  private final String sourceFile;
+
+  private CreateStacktraceFromDexDumpTool(List<String> lines) {
+    this.inputLines = lines;
+    sourceFile = getSourceFile(lines);
+    outputLines.add("java.lang.RuntimeException: error");
+  }
+
+  // Find the source file line.
+  private static String getSourceFile(List<String> lines) {
+    for (String line : lines) {
+      if (line.contains(SOURCE_FILE_IDX)) {
+        // Read <source-file> from line:
+        //   source_file_idx   : <idx> (<source-file>)
+        int start = line.indexOf('(');
+        if (start > 0) {
+          int end = line.indexOf(')', start);
+          if (end > 0) {
+            return line.substring(start + 1, end);
+          }
+        }
+      }
+    }
+    return "NoSourceFile";
+  }
+
+  private static String skipUntil(Iterator<String> iterator, Predicate<String> fn) {
+    while (iterator.hasNext()) {
+      String next = iterator.next();
+      if (fn.test(next)) {
+        return next;
+      }
+    }
+    return null;
+  }
+
+  private static String skipUntilInMethod(Iterator<String> iterator, Predicate<String> fn) {
+    String line = skipUntil(iterator, fn.or(l -> isMethodHeader(l)));
+    return line == null || isMethodHeader(line) ? null : line;
+  }
+
+  private static boolean isMethodHeader(String line) {
+    return line.endsWith("'") && line.contains("name");
+  }
+
+  private boolean isDebug() {
+    String marker = skipUntil(inputLines.iterator(), l -> l.startsWith(R8_MARKER_PREFIX));
+    return marker != null && marker.contains("debug");
+  }
+
+  private static String mapPcInLineNumberTable(
+      Iterator<String> iterator, int invokePcValue, String invokePcString) {
+    Map<Integer, String> lineTable = new HashMap<>();
+    String lineTableEntry;
+    do {
+      lineTableEntry = skipUntilInMethod(iterator, line -> line.contains(" line="));
+      if (lineTableEntry != null) {
+        // Read a line table mapping entry:
+        // 0x<addr> line=<linenumber>
+        String stripped = lineTableEntry.strip();
+        int split = stripped.indexOf(" line=");
+        if (split > 0 && stripped.startsWith("0x")) {
+          try {
+            int pc = Integer.parseInt(stripped.substring(2, split), 16);
+            lineTable.put(pc, stripped.substring(split + " line=".length()));
+          } catch (NumberFormatException e) {
+            return "InvalidLineTablePc";
+          }
+        }
+      }
+    } while (lineTableEntry != null);
+    // If there is no line number table return the PC as the line.
+    if (lineTable.isEmpty()) {
+      return invokePcString;
+    }
+    String lineNumber = lineTable.get(invokePcValue);
+    if (lineNumber != null) {
+      return lineNumber;
+    }
+    return "PcNotInLineNumberTable";
+  }
+
+  private void addLineFor(String methodName, String invokeType, String invokedMethod) {
+    Iterator<String> iterator = inputLines.iterator();
+    // Find the method entry.
+    if (skipUntil(iterator, line -> line.endsWith("'" + methodName + "'") && isMethodHeader(line))
+        == null) {
+      outputLines.add("MethodNotFound: " + methodName);
+      return;
+    }
+    // Find the code section.
+    if (skipUntilInMethod(iterator, line -> line.contains("insns size")) == null) {
+      outputLines.add("InstructionsNotFound: " + methodName);
+      return;
+    }
+    // Find the invoke instruction.
+    String invokeLine =
+        skipUntilInMethod(
+            iterator, line -> line.contains(invokeType) && line.contains(invokedMethod));
+    if (invokeLine == null) {
+      outputLines.add(
+          "InvokeNotFound: " + methodName + " calling " + invokeType + " " + invokedMethod);
+      return;
+    }
+    String invokePcString = "NoPcInfo";
+    int invokePcValue = -1;
+    // Read <pc> from line:
+    // <addr>: <bytes> |<pc>: <invoke-type> {vX}, <type-desc>.<method-name>:<method-desc>;
+    int end = invokeLine.indexOf(invokeType);
+    if (end > 0) {
+      int start = invokeLine.lastIndexOf('|', end);
+      if (start > 0) {
+        String pcString = invokeLine.substring(start + 1, end);
+        try {
+          int pc = Integer.parseInt(pcString, 16);
+          invokePcValue = pc;
+          invokePcString = "" + pc;
+        } catch (NumberFormatException e) {
+          invokePcString = "PcParseError";
+        }
+      }
+    }
+    String lineNumber = mapPcInLineNumberTable(iterator, invokePcValue, invokePcString);
+    outputLines.add(
+        String.format("    at %s.%s(%s:%s)", CLASS, methodName, sourceFile, lineNumber));
+  }
+
+  public static void main(String[] args) throws Exception {
+    Path dexdumpPath = Paths.get(args[0]);
+    Path outputPath = Paths.get(args[1]);
+    List<String> inputLines = Files.readAllLines(dexdumpPath);
+    CreateStacktraceFromDexDumpTool tool = new CreateStacktraceFromDexDumpTool(inputLines);
+    if (tool.isDebug()) {
+      // In debug builds onCreate calls getView which calls getLayoutInflater.
+      tool.addLineFor(METHOD_GET_VIEW, INVOKE_VIRTUAL, METHOD_GET_LAYOUT_INFLATER);
+      tool.addLineFor(METHOD_ON_CREATE, INVOKE_DIRECT, METHOD_GET_VIEW);
+    } else {
+      // In release builds getView is inlined away.
+      tool.addLineFor(METHOD_ON_CREATE, INVOKE_VIRTUAL, METHOD_GET_LAYOUT_INFLATER);
+    }
+    Files.write(
+        outputPath,
+        tool.outputLines,
+        StandardCharsets.UTF_8,
+        StandardOpenOption.CREATE,
+        StandardOpenOption.TRUNCATE_EXISTING);
+  }
+}