/* * 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.io.Files; import com.google.common.io.LineProcessor; import groovy.json.JsonOutput; import groovy.json.JsonSlurper; import org.gradle.StartParameter; import org.gradle.internal.UncheckedException; import org.gradle.internal.concurrent.Stoppable; import org.gradle.internal.progress.BuildOperationListenerManager; import org.gradle.util.GFileUtils; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import static org.gradle.internal.Cast.uncheckedCast; /** * 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 */ public class BuildOperationTrace implements Stoppable { public static final String SYSPROP = "org.gradle.internal.operations.trace"; private final String basePath; private final OutputStream logOutputStream; public BuildOperationTrace(StartParameter startParameter, BuildOperationListenerManager listenerManager) { Map<String, String> sysProps = startParameter.getSystemPropertiesArgs(); this.basePath = sysProps.get(SYSPROP); if (basePath == null) { 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); } listenerManager.addListener(new SerializingBuildOperationListener(logOutputStream)); } @Override public void stop() { if (logOutputStream != null) { try { logOutputStream.close(); final List<BuildOperationRecord> roots = readLogToTreeRoots(logFile(basePath)); writeDetailTree(roots); writeSummaryTree(roots); } catch (IOException e) { throw UncheckedException.throwAsUncheckedException(e); } } } private void writeDetailTree(List<BuildOperationRecord> roots) throws IOException { String rawJson = JsonOutput.toJson(BuildOperationTree.serialize(roots)); String prettyJson = JsonOutput.prettyPrint(rawJson); Files.asCharSink(file(basePath, "-tree.json"), Charsets.UTF_8).write(prettyJson); } private void writeSummaryTree(final List<BuildOperationRecord> roots) throws IOException { Files.asCharSink(file(basePath, "-tree.txt"), Charsets.UTF_8).writeLines(new Iterable<String>() { @Override public Iterator<String> iterator() { final Deque<Queue<BuildOperationRecord>> stack = new ArrayDeque<Queue<BuildOperationRecord>>(Collections.singleton(new ArrayDeque<BuildOperationRecord>(roots))); final StringBuilder stringBuilder = new StringBuilder(); return new Iterator<String>() { @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<BuildOperationRecord> children = stack.peek(); BuildOperationRecord record = children.poll(); stringBuilder.setLength(0); for (int i = 0; i < stack.size() - 1; ++i) { stringBuilder.append(" "); } if (!record.children.isEmpty()) { stack.addFirst(new ArrayDeque<BuildOperationRecord>(record.children)); } stringBuilder.append(record.displayName); if (record.details != null) { stringBuilder.append(" "); stringBuilder.append(JsonOutput.toJson(record.details)); } if (record.result != null) { stringBuilder.append(" "); stringBuilder.append(JsonOutput.toJson(record.result)); } stringBuilder.append(" ["); stringBuilder.append(record.endTime - record.startTime); stringBuilder.append("ms]"); stringBuilder.append(" ("); stringBuilder.append(record.id); stringBuilder.append(")"); return stringBuilder.toString(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }); } public static BuildOperationTree read(String basePath) throws FileNotFoundException { List<BuildOperationRecord> roots = readLogToTreeRoots(logFile(basePath)); return new BuildOperationTree(roots); } private static List<BuildOperationRecord> readLogToTreeRoots(File logFile) { try { final JsonSlurper slurper = new JsonSlurper(); final List<BuildOperationRecord> roots = new ArrayList<BuildOperationRecord>(); final Map<Object, SerializedOperationStart> pending = new HashMap<Object, SerializedOperationStart>(); final Map<Object, List<BuildOperationRecord>> childrens = new HashMap<Object, List<BuildOperationRecord>>(); Files.asCharSource(logFile, Charsets.UTF_8).readLines(new LineProcessor<Void>() { @Override public boolean processLine(String line) throws IOException { Map<String, ?> map = uncheckedCast(slurper.parseText(line)); if (map.containsKey("startTime")) { SerializedOperationStart serialized = new SerializedOperationStart(map); pending.put(serialized.id, serialized); childrens.put(serialized.id, new LinkedList<BuildOperationRecord>()); } else { SerializedOperationFinish finish = new SerializedOperationFinish(map); SerializedOperationStart start = pending.remove(finish.id); assert start != null; List<BuildOperationRecord> children = childrens.remove(finish.id); assert children != null; Map<String, ?> detailsMap = uncheckedCast(start.details); Map<String, ?> resultMap = uncheckedCast(finish.result); BuildOperationRecord record = new BuildOperationRecord( start.id, start.parentId, start.displayName, start.startTime, finish.endTime, detailsMap == null ? null : Collections.unmodifiableMap(detailsMap), start.detailsClassName, resultMap == null ? null : Collections.unmodifiableMap(resultMap), finish.resultClassName, finish.failureMsg, children ); if (start.parentId == null) { roots.add(record); } else { List<BuildOperationRecord> parentChildren = childrens.get(start.parentId); assert parentChildren != null; parentChildren.add(record); } } return true; } @Override public Void getResult() { return null; } }); assert pending.isEmpty(); return roots; } catch (Exception e) { throw UncheckedException.throwAsUncheckedException(e); } } private static File logFile(String basePath) { return file(basePath, "-log.txt"); } private static File file(String base, String suffix) { return new File((base == null || base.trim().isEmpty() ? "operations" : base) + suffix).getAbsoluteFile(); } }