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);
+ }
+}