blob: d6a6291652986fbce83010769224a52f42f43ffd [file] [log] [blame]
// 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);
}
}
}
}