| // 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 com.android.tools.r8.graph.AppView; | |
| import com.android.tools.r8.graph.DexEncodedMethod; | |
| import com.android.tools.r8.graph.DexProgramClass; | |
| import com.android.tools.r8.graph.ProgramClass; | |
| import com.android.tools.r8.utils.internal.StringUtils; | |
| import com.android.tools.r8.utils.internal.ThrowingConsumer; | |
| import java.io.BufferedReader; | |
| import java.io.BufferedWriter; | |
| import java.io.Closeable; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| import java.io.OutputStreamWriter; | |
| import java.io.Writer; | |
| import java.nio.charset.StandardCharsets; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.util.ArrayList; | |
| import java.util.Comparator; | |
| import java.util.List; | |
| public class DeterminismChecker { | |
| /** | |
| * Create a new checker with a directory of log files as backing | |
| * | |
| * <p>The checker must be created as a new instance for each compilation. Withing the directory | |
| * the respective check points will be emitted/checked via successive log files. | |
| */ | |
| public static DeterminismChecker createWithFileBacking(Path directory) { | |
| return new DeterminismChecker( | |
| new LineCallbackSupplier() { | |
| // Index of the "checkpoint", there can be several if checked at multiple points during | |
| // compiler execution. | |
| private int index = 0; | |
| @Override | |
| public LineCallback createCallback() throws IOException { | |
| // This is called on each "checkpoint" and the index is bumped. | |
| Path log = directory.resolve("" + index++ + ".log"); | |
| if (Files.exists(log)) { | |
| System.out.println("Checking against determinism log: " + log); | |
| return new LineCallbackChecker(Files.newBufferedReader(log, StandardCharsets.UTF_8)); | |
| } else { | |
| System.out.println("Writing determinism log: " + log); | |
| // Note that Files.newBufferedWriter will cause issues in presence of malformed input | |
| // and unmappable character errors, since Files.newBufferedWriter uses the | |
| // java.nio.charset.CharsetDecoder default action, which is to report such errors, | |
| // instead of dealing with them in java.nio.charset.CharsetDecoder#onMalformedInput. | |
| BufferedWriter bufferedWriter = | |
| new BufferedWriter( | |
| new OutputStreamWriter( | |
| new FileOutputStream(log.toFile()), StandardCharsets.UTF_8)); | |
| return new LineCallbackWriter(bufferedWriter); | |
| } | |
| } | |
| }); | |
| } | |
| /** Supplier/factory to support multiple checkpoints. */ | |
| public interface LineCallbackSupplier { | |
| LineCallback createCallback() throws IOException; | |
| } | |
| /** Interface for the write-or-check of each line. */ | |
| public interface LineCallback extends Closeable { | |
| boolean onLine(String line) throws IOException; | |
| } | |
| private final LineCallbackSupplier callbackFactory; | |
| private DeterminismChecker(LineCallbackSupplier callbackFactory) { | |
| this.callbackFactory = callbackFactory; | |
| } | |
| private static String fmtClass(DexProgramClass clazz) { | |
| return clazz.getType().toSourceString() | |
| + " " | |
| + clazz.getMethodCollection().getBackingDescriptionString(); | |
| } | |
| private static String fmtMethod(DexEncodedMethod method) { | |
| return method.getReference().toSourceString(); | |
| } | |
| public <E extends Exception> void accept(ThrowingConsumer<LineCallback, E> consumer) | |
| throws E, IOException { | |
| try (LineCallback callback = callbackFactory.createCallback()) { | |
| consumer.accept(callback); | |
| } | |
| } | |
| public void check(AppView<?> appView) { | |
| try (LineCallback callback = callbackFactory.createCallback()) { | |
| List<DexProgramClass> classes = new ArrayList<>(appView.appInfo().classes()); | |
| classes.sort(Comparator.comparing(ProgramClass::getType)); | |
| for (int i = 0; i < classes.size(); i++) { | |
| DexProgramClass clazz = classes.get(i); | |
| checkClass(callback, clazz); | |
| } | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| private void checkClass(LineCallback callback, DexProgramClass clazz) throws IOException { | |
| String line = fmtClass(clazz); | |
| if (!callback.onLine(line)) { | |
| return; | |
| } | |
| for (DexEncodedMethod method : clazz.methods()) { | |
| checkMethod(callback, method); | |
| } | |
| } | |
| private void checkMethod(LineCallback callback, DexEncodedMethod method) throws IOException { | |
| String header = fmtMethod(method); | |
| if (!callback.onLine(header)) { | |
| return; | |
| } | |
| if (method.hasCode()) { | |
| List<String> lines = StringUtils.splitLines(method.getCode().toString()); | |
| for (String line : lines) { | |
| if (!callback.onLine(line)) { | |
| return; | |
| } | |
| } | |
| } else { | |
| if (!callback.onLine("<nocode>")) { | |
| return; | |
| } | |
| } | |
| } | |
| /** Shared escape function. */ | |
| private static String escape(String line) { | |
| // Replace \r so the line splitting is always consistently on \n. | |
| return line.replace("\r", "<CR>"); | |
| } | |
| /** Implementation of the checker (must be consistent with the writer). */ | |
| private static class LineCallbackChecker implements LineCallback { | |
| private final BufferedReader reader; | |
| public LineCallbackChecker(BufferedReader reader) { | |
| this.reader = reader; | |
| } | |
| @Override | |
| public boolean onLine(String unescapedLine) throws IOException { | |
| String line = escape(unescapedLine); | |
| String dumpLine = reader.readLine(); | |
| if (!dumpLine.equals(line)) { | |
| // The line might contain a non unicode points. If so, the dump line will have mapped those | |
| // to ? when writing. Decode the string and retry equals. | |
| String decoded = new String(line.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); | |
| if (!decoded.equals(dumpLine)) { | |
| throw new AssertionError( | |
| "\nMismatch for line: " + decoded + "\n" + " and dump-line: " + dumpLine); | |
| } | |
| } | |
| return true; | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| reader.close(); | |
| } | |
| } | |
| /** Implementation of the writer (must be consistent with the checker). */ | |
| private static class LineCallbackWriter implements LineCallback { | |
| private final Writer writer; | |
| public LineCallbackWriter(Writer writer) { | |
| this.writer = writer; | |
| } | |
| @Override | |
| public boolean onLine(String line) throws IOException { | |
| writer.write(escape(line)); | |
| writer.write('\n'); | |
| return true; | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| writer.close(); | |
| } | |
| } | |
| } |