[Compose] Add test for retracing stack traces generated by executing R8
This will not yet test composing mapping files but is the initial test
framework for doing so.
Bug: b/241763080
Change-Id: I4da35fe549cb2903f33fba018432b2c089e7f13e
diff --git a/src/main/java/com/android/tools/r8/utils/BoxBase.java b/src/main/java/com/android/tools/r8/utils/BoxBase.java
index d38d92b..93f70da 100644
--- a/src/main/java/com/android/tools/r8/utils/BoxBase.java
+++ b/src/main/java/com/android/tools/r8/utils/BoxBase.java
@@ -6,6 +6,7 @@
import java.util.Comparator;
import java.util.Objects;
+import java.util.function.Function;
import java.util.function.Supplier;
public abstract class BoxBase<T> {
@@ -39,6 +40,12 @@
return oldValue;
}
+ public T getAndCompute(Function<T, T> newValue) {
+ T t = get();
+ set(newValue.apply(t));
+ return t;
+ }
+
void set(T value) {
this.value = value;
}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 39d0c4f..213b03a 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -123,7 +123,8 @@
// Set to true to run compilation in a single thread and without randomly shuffling the input.
// This makes life easier when running R8 in a debugger.
- public static final boolean DETERMINISTIC_DEBUGGING = false;
+ public static final boolean DETERMINISTIC_DEBUGGING =
+ System.getProperty("com.android.tools.r8.deterministicdebugging") != null;
// Use a MethodCollection where most interleavings between reading and mutating is caught.
public static final boolean USE_METHOD_COLLECTION_CONCURRENCY_CHECKED = false;
diff --git a/src/main/java/com/android/tools/r8/utils/StackTraceUtils.java b/src/main/java/com/android/tools/r8/utils/StackTraceUtils.java
new file mode 100644
index 0000000..34e9db8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/StackTraceUtils.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+
+public class StackTraceUtils {
+
+ private static final String pathToWriteStacktrace =
+ System.getProperty("com.android.tools.r8.internalPathToStacktraces");
+
+ private static final String SEPARATOR = "@@@@";
+
+ private static final PrintStream printStream = getStacktracePrintStream();
+
+ private static final int samplingInterval = getSamplingInterval();
+
+ private static int getSamplingInterval() {
+ String setSamplingInterval =
+ System.getProperty("com.android.tools.r8.internalStackTraceSamplingInterval");
+ if (setSamplingInterval == null) {
+ return 1000;
+ }
+ return Integer.parseInt(setSamplingInterval);
+ }
+
+ private static int counter = 0;
+
+ private static PrintStream getStacktracePrintStream() {
+ if (pathToWriteStacktrace == null) {
+ throw new RuntimeException("pathToWriteStacktrace is null");
+ }
+ try {
+ return new PrintStream(pathToWriteStacktrace, StandardCharsets.UTF_8.name());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Utility function with the only purpose of being able to print stack traces from various
+ * inserted points in R8. See RetraceStackTraceFunctionalCompositionTest.
+ */
+ public static void printCurrentStack(long identifier) {
+ if (counter++ < samplingInterval) {
+ new RuntimeException("------(" + identifier + "," + counter + ")------")
+ .printStackTrace(printStream);
+ printStream.println(SEPARATOR);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceStackTraceFunctionalCompositionTest.java b/src/test/java/com/android/tools/r8/retrace/RetraceStackTraceFunctionalCompositionTest.java
new file mode 100644
index 0000000..09a05ed
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceStackTraceFunctionalCompositionTest.java
@@ -0,0 +1,263 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.retrace;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.transformers.MethodTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.StackTraceUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class RetraceStackTraceFunctionalCompositionTest extends TestBase {
+
+ @Parameter() public TestParameters parameters;
+
+ private Path rewrittenR8Jar;
+ private static final int SAMPLING_SIZE = 50000;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ assumeTrue(ToolHelper.isLinux());
+ return getTestParameters().withDefaultCfRuntime().build();
+ }
+
+ private void insertPrintingOfStacktraces(Path path, Path outputPath) throws Exception {
+ String entryNameForClass = ZipUtils.zipEntryNameForClass(StackTraceUtils.class);
+ Box<Long> idBox = new Box<>(0L);
+ ZipUtils.map(
+ path,
+ outputPath,
+ (zipEntry, bytes) -> {
+ String entryName = zipEntry.getName();
+ // Only insert into the R8 namespace, this will provide a better sampling than inserting
+ // into all calls due to more inlining/outlining could have happened.
+ if (ZipUtils.isClassFile(entryName)
+ && !entryNameForClass.equals(entryName)
+ && entryName.contains("com/android/tools/r8/")) {
+ return transformer(bytes, null)
+ .addClassTransformer(
+ new ClassTransformer() {
+ @Override
+ public MethodVisitor visitMethod(
+ int access,
+ String name,
+ String descriptor,
+ String signature,
+ String[] exceptions) {
+ MethodVisitor sub =
+ super.visitMethod(access, name, descriptor, signature, exceptions);
+ if (name.equals("<clinit>")) {
+ return sub;
+ } else {
+ return new InsertStackTraceCallTransformer(
+ sub, () -> idBox.getAndCompute(x -> x + 1));
+ }
+ }
+ ;
+ })
+ .transform();
+ }
+ return bytes;
+ });
+ }
+
+ private static class InsertStackTraceCallTransformer extends MethodTransformer {
+
+ private boolean insertedStackTraceCall = false;
+ private final Supplier<Long> idGenerator;
+
+ private InsertStackTraceCallTransformer(MethodVisitor visitor, Supplier<Long> idGenerator) {
+ this.mv = visitor;
+ this.idGenerator = idGenerator;
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
+ insertPrintIfFirstInstruction();
+ super.visitFieldInsn(opcode, owner, name, descriptor);
+ }
+
+ @Override
+ public void visitInsn(int opcode) {
+ insertPrintIfFirstInstruction();
+ super.visitInsn(opcode);
+ }
+
+ @Override
+ public void visitLdcInsn(Object value) {
+ insertPrintIfFirstInstruction();
+ super.visitLdcInsn(value);
+ }
+
+ @Override
+ public void visitMethodInsn(
+ int opcode, String owner, String name, String descriptor, boolean isInterface) {
+ insertPrintIfFirstInstruction();
+ super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+ }
+
+ private void insertPrintIfFirstInstruction() {
+ if (!insertedStackTraceCall) {
+ super.visitLdcInsn(idGenerator.get());
+ super.visitMethodInsn(
+ INVOKESTATIC, binaryName(StackTraceUtils.class), "printCurrentStack", "(J)V", false);
+ insertedStackTraceCall = true;
+ }
+ }
+
+ @Override
+ public void visitMaxs(int maxStack, int maxLocals) {
+ super.visitMaxs(maxStack + 2, maxLocals);
+ }
+ }
+
+ private Path getRewrittenR8Jar() throws Exception {
+ if (rewrittenR8Jar != null) {
+ return rewrittenR8Jar;
+ }
+ rewrittenR8Jar = temp.newFolder().toPath().resolve("r8_with_deps_with_prints.jar");
+ insertPrintingOfStacktraces(ToolHelper.R8_WITH_DEPS_JAR, rewrittenR8Jar);
+ return rewrittenR8Jar;
+ }
+
+ @Test
+ public void testR8RetraceAndComposition() throws Exception {
+ Path rewrittenR8Jar = getRewrittenR8Jar();
+ List<String> originalStackTraces = generateStackTraces(rewrittenR8Jar);
+ Map<String, List<String>> originalPartitions = partitionStacktraces(originalStackTraces);
+
+ List<String> originalStackTracesDeterministicCheck = generateStackTraces(rewrittenR8Jar);
+ Map<String, List<String>> deterministicPartitions =
+ partitionStacktraces(originalStackTracesDeterministicCheck);
+ comparePartitionedStackTraces(originalPartitions, deterministicPartitions);
+
+ // Compile rewritten R8 with R8 to obtain first level
+ Pair<Path, Path> r8OfR8 = compileR8WithR8(rewrittenR8Jar);
+ List<String> firstLevelStackTraces = generateStackTraces(r8OfR8.getFirst());
+
+ // If we retrace the entire file we should get the same result as the original.
+ List<String> retracedFirstLevelStackTraces = new ArrayList<>();
+ Retrace.run(
+ RetraceCommand.builder()
+ .setRetracedStackTraceConsumer(retracedFirstLevelStackTraces::addAll)
+ .setStackTrace(firstLevelStackTraces)
+ .setMappingSupplier(
+ ProguardMappingSupplier.builder()
+ .setProguardMapProducer(ProguardMapProducer.fromPath(r8OfR8.getSecond()))
+ .build())
+ .build());
+ Map<String, List<String>> firstRoundPartitions =
+ partitionStacktraces(retracedFirstLevelStackTraces);
+ comparePartitionedStackTraces(originalPartitions, firstRoundPartitions);
+ }
+
+ private void comparePartitionedStackTraces(
+ Map<String, List<String>> one, Map<String, List<String>> other) {
+ for (Entry<String, List<String>> keyStackTraceEntry : one.entrySet()) {
+ String oneAsString = StringUtils.lines(keyStackTraceEntry.getValue());
+ String otherAsString = StringUtils.lines(other.get(keyStackTraceEntry.getKey()));
+ assertEquals(oneAsString, otherAsString);
+ }
+ assertEquals(one.keySet(), other.keySet());
+ }
+
+ private Map<String, List<String>> partitionStacktraces(List<String> allStacktraces) {
+ Map<String, List<String>> partitions = new LinkedHashMap<>();
+ int lastIndex = 0;
+ for (int i = 0; i < allStacktraces.size(); i++) {
+ if (allStacktraces.get(i).contains("@@@@")) {
+ List<String> stackTrace = allStacktraces.subList(lastIndex, i);
+ String keyForStackTrace = getKeyForStackTrace(stackTrace);
+ List<String> existing = partitions.put(keyForStackTrace, stackTrace);
+ assertNull(existing);
+ lastIndex = i + 1;
+ i++;
+ }
+ }
+ return partitions;
+ }
+
+ private String getKeyForStackTrace(List<String> stackTrace) {
+ String identifier = "java.lang.RuntimeException: ------(";
+ String firstLine = stackTrace.get(0);
+ int index = firstLine.indexOf(identifier);
+ assertEquals(0, index);
+ String endIdentifier = ")------";
+ int endIndex = firstLine.indexOf(endIdentifier);
+ assertTrue(endIndex > 0);
+ return firstLine.substring(index + identifier.length(), endIndex);
+ }
+
+ private Pair<Path, Path> compileR8WithR8(Path r8Input) throws Exception {
+ Path MAIN_KEEP = Paths.get("src/main/keep.txt");
+ Path jar = temp.newFolder().toPath().resolve("out.jar");
+ Path map = temp.newFolder().toPath().resolve("out.map");
+ testForR8(Backend.CF)
+ .setMode(CompilationMode.RELEASE)
+ .addProgramFiles(r8Input)
+ .addKeepRuleFiles(MAIN_KEEP)
+ .allowUnusedProguardConfigurationRules()
+ .addDontObfuscate()
+ .compile()
+ .apply(c -> FileUtils.writeTextFile(map, c.getProguardMap()))
+ .writeToZip(jar);
+ return Pair.create(jar, map);
+ }
+
+ private List<String> generateStackTraces(Path r8Jar) throws Exception {
+ File stacktraceOutput = new File(temp.newFolder(), "stacktraces.txt");
+ stacktraceOutput.createNewFile();
+ testForExternalR8(Backend.DEX, parameters.getRuntime())
+ .useProvidedR8(r8Jar)
+ .addProgramClasses(HelloWorld.class)
+ .addKeepMainRule(HelloWorld.class)
+ .setMinApi(AndroidApiLevel.B)
+ .addJvmFlag("-Dcom.android.tools.r8.deterministicdebugging=true")
+ .addJvmFlag("-Dcom.android.tools.r8.internalStackTraceSamplingInterval=" + SAMPLING_SIZE)
+ .addJvmFlag("-Dcom.android.tools.r8.internalPathToStacktraces=" + stacktraceOutput.toPath())
+ .compile();
+ return Files.readAllLines(stacktraceOutput.toPath(), StandardCharsets.UTF_8);
+ }
+
+ public static class HelloWorld {
+
+ public static void main(String[] args) {
+ System.out.println("Hello World");
+ }
+ }
+}