org.gradle.internal.operations.trace.BuildOperationTrace Maven / Gradle / Ivy
Show all versions of gradle-api Show documentation
/*
* Copyright 2017 the original author or authors.
*
* 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 org.gradle.internal.operations.trace;
import com.google.common.base.Charsets;
import com.google.common.base.StandardSystemProperty;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import com.google.common.io.LineProcessor;
import groovy.json.JsonGenerator;
import groovy.json.JsonOutput;
import groovy.json.JsonSlurper;
import org.gradle.StartParameter;
import org.gradle.api.NonNullApi;
import org.gradle.internal.UncheckedException;
import org.gradle.internal.concurrent.Stoppable;
import org.gradle.internal.operations.BuildOperationDescriptor;
import org.gradle.internal.operations.BuildOperationListener;
import org.gradle.internal.operations.BuildOperationListenerManager;
import org.gradle.internal.operations.OperationFinishEvent;
import org.gradle.internal.operations.OperationIdentifier;
import org.gradle.internal.operations.OperationProgressEvent;
import org.gradle.internal.operations.OperationStartEvent;
import org.gradle.internal.service.scopes.Scope;
import org.gradle.internal.service.scopes.ServiceScope;
import org.gradle.util.internal.GFileUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import static org.gradle.internal.Cast.uncheckedCast;
import static org.gradle.internal.Cast.uncheckedNonnullCast;
/**
* Writes files describing the build operation stream for a build.
* Can be enabled for any build with `-Dorg.gradle.internal.operations.trace=«path-base»`.
*
* Imposes no overhead when not enabled.
* Also used as the basis for asserting on the event stream in integration tests, via BuildOperationFixture.
*
* Three files are created:
*
* - «path-base»-log.txt: a chronological log of events, each line is a JSON object
* - «path-base»-tree.json: a JSON tree of the event structure
* - «path-base»-tree.txt: A simplified tree representation showing basic information
*
*
* Generally, the simplified tree view is best for browsing.
* The JSON tree view can be used for more detailed analysis — open in a JSON tree viewer, like Chrome.
*
* The «path-base» param is optional.
* If invoked as `-Dorg.gradle.internal.operations.trace`, a base value of "operations" will be used.
*
* The “trace” produced here is different to the trace produced by Gradle Profiler.
* There, the focus is analyzing the performance profile.
* Here, the focus is debugging/developing the information structure of build operations.
*
* @since 4.0
*/
@ServiceScope(Scope.CrossBuildSession.class)
public class BuildOperationTrace implements Stoppable {
public static final String SYSPROP = "org.gradle.internal.operations.trace";
/**
* A list of either details or result class names, delimited by {@link #FILTER_SEPARATOR},
* that will be captured by this trace. When enabled, only operations matching this filter
* will be captured. This enables capturing a build operation traces of larger builds that
* would otherwise be too large to capture.
*
* When this property is set, a complete build operation tree is not captured. In this
* case, only the log file will be written, not the formatted tree output files.
*/
public static final String FILTER_SYSPROP = SYSPROP + ".filter";
/**
* Delimiter for entries in {@link #FILTER_SYSPROP}.
*/
public static final String FILTER_SEPARATOR = ";";
private static final byte[] NEWLINE = "\n".getBytes();
private final boolean outputTree;
private final BuildOperationListener listener;
private final String basePath;
private final OutputStream logOutputStream;
private final JsonGenerator jsonGenerator = createJsonGenerator();
private final BuildOperationListenerManager buildOperationListenerManager;
public BuildOperationTrace(StartParameter startParameter, BuildOperationListenerManager buildOperationListenerManager) {
this.buildOperationListenerManager = buildOperationListenerManager;
Set filter = getFilter(startParameter);
if (filter != null) {
this.outputTree = false;
this.listener = new FilteringBuildOperationListener(new SerializingBuildOperationListener(this::write), filter);
} else {
this.outputTree = true;
this.listener = new SerializingBuildOperationListener(this::write);
}
this.basePath = getProperty(startParameter, SYSPROP);
if (this.basePath == null || basePath.equals(Boolean.FALSE.toString())) {
this.logOutputStream = null;
return;
}
try {
File logFile = logFile(basePath);
GFileUtils.mkdirs(logFile.getParentFile());
if (logFile.isFile()) {
GFileUtils.forceDelete(logFile);
}
//noinspection ResultOfMethodCallIgnored
logFile.createNewFile();
this.logOutputStream = new BufferedOutputStream(new FileOutputStream(logFile));
} catch (IOException e) {
throw UncheckedException.throwAsUncheckedException(e);
}
buildOperationListenerManager.addListener(listener);
}
private static String getProperty(StartParameter startParameter, String property) {
Map sysProps = startParameter.getSystemPropertiesArgs();
String basePath = sysProps.get(property);
if (basePath == null) {
basePath = System.getProperty(property);
}
return basePath;
}
@Nullable
private static Set getFilter(StartParameter startParameter) {
String filterProperty = getProperty(startParameter, FILTER_SYSPROP);
if (filterProperty == null) {
return null;
}
return new HashSet<>(Arrays.asList(filterProperty.split(FILTER_SEPARATOR)));
}
@Override
public void stop() {
buildOperationListenerManager.removeListener(listener);
if (logOutputStream != null) {
try {
synchronized (logOutputStream) {
logOutputStream.close();
}
if (outputTree) {
List roots = readLogToTreeRoots(logFile(basePath), false);
writeDetailTree(roots);
writeSummaryTree(roots);
}
} catch (IOException e) {
throw UncheckedException.throwAsUncheckedException(e);
}
}
}
private void write(SerializedOperation operation) {
Thread currentThread = Thread.currentThread();
ClassLoader previousClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(JsonOutput.class.getClassLoader());
try {
String json = jsonGenerator.toJson(operation.toMap());
try {
synchronized (logOutputStream) {
logOutputStream.write(json.getBytes(StandardCharsets.UTF_8));
logOutputStream.write(NEWLINE);
logOutputStream.flush();
}
} catch (IOException e) {
throw UncheckedException.throwAsUncheckedException(e);
}
} finally {
currentThread.setContextClassLoader(previousClassLoader);
}
}
private void writeDetailTree(List roots) throws IOException {
try {
String rawJson = jsonGenerator.toJson(BuildOperationTree.serialize(roots));
String prettyJson = JsonOutput.prettyPrint(rawJson);
Files.asCharSink(file(basePath, "-tree.json"), Charsets.UTF_8).write(prettyJson);
} catch (OutOfMemoryError e) {
System.err.println("Failed to write build operation trace JSON due to out of memory.");
}
}
private void writeSummaryTree(final List roots) throws IOException {
Files.asCharSink(file(basePath, "-tree.txt"), Charsets.UTF_8).writeLines(new Iterable() {
@Override
@Nonnull
public Iterator iterator() {
final Deque> stack = new ArrayDeque<>(Collections.singleton(new ArrayDeque<>(roots)));
final StringBuilder stringBuilder = new StringBuilder();
return new Iterator() {
@Override
public boolean hasNext() {
if (stack.isEmpty()) {
return false;
} else if (stack.peek().isEmpty()) {
stack.pop();
return hasNext();
} else {
return true;
}
}
@Override
public String next() {
Queue children = stack.element();
BuildOperationRecord record = children.remove();
stringBuilder.setLength(0);
int indents = stack.size() - 1;
for (int i = 0; i < indents; ++i) {
stringBuilder.append(" ");
}
if (!record.children.isEmpty()) {
stack.addFirst(new ArrayDeque<>(record.children));
}
stringBuilder.append(record.displayName);
if (record.details != null) {
stringBuilder.append(" ");
stringBuilder.append(jsonGenerator.toJson(record.details));
}
if (record.result != null) {
stringBuilder.append(" ");
stringBuilder.append(jsonGenerator.toJson(record.result));
}
stringBuilder.append(" [");
stringBuilder.append(record.endTime - record.startTime);
stringBuilder.append("ms]");
stringBuilder.append(" (");
stringBuilder.append(record.id);
stringBuilder.append(")");
if (!record.progress.isEmpty()) {
for (BuildOperationRecord.Progress progress : record.progress) {
stringBuilder.append(StandardSystemProperty.LINE_SEPARATOR.value());
for (int i = 0; i < indents; ++i) {
stringBuilder.append(" ");
}
stringBuilder.append("- ")
.append(progress.details).append(" [")
.append(progress.time - record.startTime)
.append("]");
}
}
return stringBuilder.toString();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
});
}
public static BuildOperationTree read(String basePath) {
File logFile = logFile(basePath);
List roots = readLogToTreeRoots(logFile, true);
return new BuildOperationTree(roots);
}
/**
* Reads a list of records that represent a partial build operation tree.
* Some operations may not contain all of their children.
* Some operations' parents may be missing from the tree.
* Operations with missing parents are placed at the root of the returned tree.
*
* @param basePath The same path used for {@link #SYSPROP} when the trace was recorded.
*/
public static BuildOperationTree readPartialTree(String basePath) {
File logFile = logFile(basePath);
List partialTree = readLogToTreeRoots(logFile, false);
return new BuildOperationTree(partialTree);
}
private static List readLogToTreeRoots(final File logFile, boolean completeTree) {
try {
final JsonSlurper slurper = new JsonSlurper();
final List roots = new ArrayList<>();
final Map