| // Copyright 2018 The Bazel Authors. All rights reserved. |
| // |
| // 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.google.devtools.build.execlog; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.devtools.build.lib.exec.Protos.SpawnExec; |
| import com.google.devtools.common.options.OptionsParser; |
| import java.io.BufferedWriter; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.util.ArrayDeque; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.PriorityQueue; |
| import java.util.Queue; |
| |
| /** |
| * A tool to inspect and parse the Bazel execution log. |
| */ |
| final class ExecLogParser { |
| |
| static final String DELIMITER = "\n---------------------------------------------------------\n"; |
| |
| @VisibleForTesting |
| interface Parser { |
| SpawnExec getNext() throws IOException; |
| } |
| |
| @VisibleForTesting |
| static class FilteringLogParser implements Parser { |
| final InputStream in; |
| final String restrictToRunner; |
| |
| FilteringLogParser(InputStream in, String restrictToRunner) { |
| this.in = in; |
| this.restrictToRunner = restrictToRunner; |
| } |
| |
| @Override |
| public SpawnExec getNext() throws IOException { |
| SpawnExec ex; |
| // Find the next record whose runner matches |
| do { |
| if (in.available() <= 0) { |
| // End of file |
| return null; |
| } |
| ex = SpawnExec.parseDelimitedFrom(in); |
| } while (restrictToRunner != null && !restrictToRunner.equals(ex.getRunner())); |
| return ex; |
| } |
| } |
| |
| static String getFirstOutput(SpawnExec e) { |
| if (e.getListedOutputsCount() > 0) { |
| return e.getListedOutputs(0); |
| } |
| return null; |
| } |
| |
| @VisibleForTesting |
| static class ReorderingParser implements Parser { |
| |
| public static class Golden { |
| // A map of positions of actions in the first file. |
| // Key: first output filename of the action |
| // Value: the position of the action in the file (e.g., 0th, 1st etc). |
| private final Map<String, Integer> positions; |
| private int index; |
| |
| public Golden() { |
| positions = new HashMap<>(); |
| index = 0; |
| } |
| |
| public void addSpawnExec(SpawnExec ex) { |
| String key = getFirstOutput(ex); |
| if (key != null) { |
| positions.put(key, index++); |
| } |
| } |
| |
| public int positionFor(SpawnExec ex) { |
| String key = getFirstOutput(ex); |
| if (key != null && positions.containsKey(key)) { |
| return positions.get(key); |
| } |
| return -1; |
| } |
| } |
| |
| private static class Element { |
| public int position; |
| public SpawnExec element; |
| |
| public Element(int position, SpawnExec element) { |
| this.position = position; |
| this.element = element; |
| } |
| } |
| |
| private final Golden golden; |
| |
| ReorderingParser(Golden golden, Parser input) throws IOException { |
| this.golden = golden; |
| processInputFile(input); |
| } |
| |
| // actions from input that appear in golden, indexed by their position in the golden. |
| PriorityQueue<Element> sameActions; |
| // actions in input that are not in the golden, in order received. |
| Queue<SpawnExec> uniqueActions; |
| |
| private void processInputFile(Parser input) throws IOException { |
| sameActions = new PriorityQueue<>((e1, e2) -> (e1.position - e2.position)); |
| uniqueActions = new ArrayDeque<>(); |
| |
| SpawnExec ex; |
| while ((ex = input.getNext()) != null) { |
| int position = golden.positionFor(ex); |
| if (position >= 0) { |
| sameActions.add(new Element(position, ex)); |
| } else { |
| uniqueActions.add(ex); |
| } |
| } |
| } |
| |
| @Override |
| public SpawnExec getNext() { |
| if (sameActions.isEmpty()) { |
| return uniqueActions.poll(); |
| } |
| return sameActions.remove().element; |
| } |
| } |
| |
| public static void output(Parser p, OutputStream outStream, ReorderingParser.Golden golden) |
| throws IOException { |
| PrintWriter out = |
| new PrintWriter(new BufferedWriter(new OutputStreamWriter(outStream, UTF_8)), true); |
| SpawnExec ex; |
| while ((ex = p.getNext()) != null) { |
| out.println(ex); |
| out.println(DELIMITER); |
| if (golden != null) { |
| golden.addSpawnExec(ex); |
| } |
| } |
| } |
| |
| public static void main(String[] args) throws Exception { |
| OptionsParser op = OptionsParser.builder().optionsClasses(ParserOptions.class).build(); |
| op.parseAndExitUponError(args); |
| |
| ParserOptions options = op.getOptions(ParserOptions.class); |
| List<String> remainingArgs = op.getResidue(); |
| |
| if (!remainingArgs.isEmpty()) { |
| System.err.println("Unexpected options: " + String.join(" ", remainingArgs)); |
| System.exit(1); |
| } |
| |
| if (options.logPath == null || options.logPath.isEmpty()) { |
| System.err.println("--log_path needs to be specified."); |
| System.exit(1); |
| } |
| if (options.outputPath != null && options.outputPath.size() > options.logPath.size()) { |
| System.err.println("Too many --output_path values."); |
| System.exit(1); |
| } |
| |
| String logPath = options.logPath.get(0); |
| String secondPath = null; |
| String output1 = null; |
| String output2 = null; |
| |
| if (options.logPath.size() > 1) { |
| if (options.logPath.size() > 2) { |
| System.err.println("Too many --log_path: at most two files are currently supported."); |
| System.exit(1); |
| } |
| secondPath = options.logPath.get(1); |
| if (options.outputPath == null || options.outputPath.size() != 2) { |
| System.err.println( |
| "Exactly two --output_path values expected, one for each of --log_path values."); |
| System.exit(1); |
| } |
| output1 = options.outputPath.get(0); |
| output2 = options.outputPath.get(1); |
| } else { |
| if (options.outputPath != null && !options.outputPath.isEmpty()) { |
| output1 = options.outputPath.get(0); |
| } |
| } |
| |
| ReorderingParser.Golden golden = null; |
| if (secondPath != null) { |
| golden = new ReorderingParser.Golden(); |
| } |
| |
| try (InputStream input = new FileInputStream(logPath)) { |
| Parser parser = new FilteringLogParser(input, options.restrictToRunner); |
| |
| if (output1 == null) { |
| output(parser, System.out, golden); |
| } else { |
| try (OutputStream output = new FileOutputStream(output1)) { |
| output(parser, output, golden); |
| } |
| } |
| } |
| |
| if (secondPath != null) { |
| try (InputStream file2 = new FileInputStream(secondPath); |
| OutputStream output = new FileOutputStream(output2)) { |
| Parser parser = new FilteringLogParser(file2, options.restrictToRunner); |
| // ReorderingParser will read the whole golden on initialization, |
| // so it is safe to close after. |
| parser = new ReorderingParser(golden, parser); |
| output(parser, output, null); |
| } |
| } |
| } |
| } |
| |