| // Copyright 2014 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.lib.profiler; | |
| import static com.google.devtools.build.lib.profiler.ProfilerTask.CRITICAL_PATH; | |
| import static com.google.devtools.build.lib.profiler.ProfilerTask.TASK_COUNT; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.base.Predicate; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.ListMultimap; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.collect.Maps; | |
| import com.google.common.collect.MultimapBuilder.ListMultimapBuilder; | |
| import com.google.common.collect.Ordering; | |
| import com.google.common.collect.Sets; | |
| import com.google.devtools.build.lib.util.Preconditions; | |
| import com.google.devtools.build.lib.util.VarInt; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import java.io.BufferedInputStream; | |
| import java.io.DataInputStream; | |
| import java.io.IOException; | |
| import java.io.PrintStream; | |
| import java.io.UnsupportedEncodingException; | |
| import java.nio.ByteBuffer; | |
| import java.util.ArrayDeque; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.Comparator; | |
| import java.util.Deque; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.regex.Pattern; | |
| import java.util.zip.Inflater; | |
| import java.util.zip.InflaterInputStream; | |
| /** | |
| * Holds parsed profile file information and provides various ways of | |
| * accessing it (mostly through different dictionaries or sorted lists). | |
| * | |
| * <p>Class should not be instantiated directly but through the use of the | |
| * ProfileLoader.loadProfile() method. | |
| */ | |
| public class ProfileInfo { | |
| /** | |
| * Immutable container for the aggregated stats. | |
| */ | |
| public static final class AggregateAttr { | |
| public final int count; | |
| public final long totalTime; | |
| AggregateAttr(int count, long totalTime) { | |
| this.count = count; | |
| this.totalTime = totalTime; | |
| } | |
| } | |
| /** | |
| * Immutable compact representation of the Map<ProfilerTask, AggregateAttr>. | |
| */ | |
| static final class CompactStatistics { | |
| final byte[] content; | |
| CompactStatistics(byte[] content) { | |
| this.content = content; | |
| } | |
| /** | |
| * Create compact task statistic instance using provided array. | |
| * Array length must exactly match ProfilerTask value space. | |
| * Each statistic is stored in the array according to the ProfilerTask | |
| * value ordinal() number. Absent statistics are represented by null. | |
| */ | |
| CompactStatistics(AggregateAttr[] stats) { | |
| Preconditions.checkArgument(stats.length == TASK_COUNT); | |
| ByteBuffer sink = ByteBuffer.allocate(TASK_COUNT * (1 + 5 + 10)); | |
| for (int i = 0; i < TASK_COUNT; i++) { | |
| if (stats[i] != null && stats[i].count > 0) { | |
| sink.put((byte) i); | |
| VarInt.putVarInt(stats[i].count, sink); | |
| VarInt.putVarLong(stats[i].totalTime, sink); | |
| } | |
| } | |
| content = sink.position() > 0 ? Arrays.copyOf(sink.array(), sink.position()) : null; | |
| } | |
| boolean isEmpty() { return content == null; } | |
| /** | |
| * Converts instance back into AggregateAttr[TASK_COUNT]. See | |
| * constructor documentation for more information. | |
| */ | |
| AggregateAttr[] toArray() { | |
| AggregateAttr[] stats = new AggregateAttr[TASK_COUNT]; | |
| if (!isEmpty()) { | |
| ByteBuffer source = ByteBuffer.wrap(content); | |
| while (source.hasRemaining()) { | |
| byte id = source.get(); | |
| int count = VarInt.getVarInt(source); | |
| long time = VarInt.getVarLong(source); | |
| stats[id] = new AggregateAttr(count, time); | |
| } | |
| } | |
| return stats; | |
| } | |
| /** | |
| * Returns AggregateAttr instance for the given ProfilerTask value. | |
| */ | |
| AggregateAttr getAttr(ProfilerTask task) { | |
| if (isEmpty()) { return ZERO; } | |
| ByteBuffer source = ByteBuffer.wrap(content); | |
| byte id = (byte) task.ordinal(); | |
| while (source.hasRemaining()) { | |
| if (id == source.get()) { | |
| int count = VarInt.getVarInt(source); | |
| long time = VarInt.getVarLong(source); | |
| return new AggregateAttr(count, time); | |
| } else { | |
| VarInt.getVarInt(source); | |
| VarInt.getVarLong(source); | |
| } | |
| } | |
| return ZERO; | |
| } | |
| /** | |
| * Returns cumulative time stored in this instance across whole | |
| * ProfilerTask dimension. | |
| */ | |
| long getTotalTime() { | |
| if (isEmpty()) { return 0; } | |
| ByteBuffer source = ByteBuffer.wrap(content); | |
| long totalTime = 0; | |
| while (source.hasRemaining()) { | |
| source.get(); | |
| VarInt.getVarInt(source); | |
| totalTime += VarInt.getVarLong(source); | |
| } | |
| return totalTime; | |
| } | |
| } | |
| public static final Ordering<Task> TASK_DURATION_ORDERING = | |
| new Ordering<Task>() { | |
| @Override | |
| public int compare(Task o1, Task o2) { | |
| return Long.compare(o1.durationNanos, o2.durationNanos); | |
| } | |
| }; | |
| /** | |
| * Container for the profile record information. | |
| * | |
| * <p> TODO(bazel-team): (2010) Current Task instance heap size is 72 bytes. And there are | |
| * millions of them. Consider trimming some attributes. | |
| */ | |
| public final class Task implements Comparable<Task> { | |
| public final long threadId; | |
| public final int id; | |
| public final int parentId; | |
| public final long startTime; | |
| public final long durationNanos; | |
| public final ProfilerTask type; | |
| final CompactStatistics stats; | |
| // Contains statistic for a task and all subtasks. Populated only for root tasks. | |
| CompactStatistics aggregatedStats = null; | |
| // Subtasks are stored as an array for performance and memory utilization | |
| // reasons (we can easily deal with millions of those objects). | |
| public Task[] subtasks = NO_TASKS; | |
| final int descIndex; | |
| // Reference to the related task (e.g. ACTION_GRAPH->ACTION task relation). | |
| private Task relatedTask; | |
| Task( | |
| long threadId, | |
| int id, | |
| int parentId, | |
| long startTime, | |
| long durationNanos, | |
| ProfilerTask type, | |
| int descIndex, | |
| CompactStatistics stats) { | |
| this.threadId = threadId; | |
| this.id = id; | |
| this.parentId = parentId; | |
| this.startTime = startTime; | |
| this.durationNanos = durationNanos; | |
| this.type = type; | |
| this.descIndex = descIndex; | |
| this.stats = stats; | |
| relatedTask = null; | |
| } | |
| public String getDescription() { | |
| return descriptionList.get(descIndex); | |
| } | |
| public boolean hasStats() { | |
| return !stats.isEmpty(); | |
| } | |
| public boolean isFake() { | |
| return id < 0; | |
| } | |
| public long getInheritedDuration() { | |
| return stats.getTotalTime(); | |
| } | |
| public AggregateAttr[] getStatAttrArray() { | |
| Preconditions.checkNotNull(stats); | |
| return stats.toArray(); | |
| } | |
| private void combineStats(int[] counts, long[] duration) { | |
| int ownIndex = type.ordinal(); | |
| if (parentId != 0) { | |
| // Parent task already accounted for this task total duration. We need to adjust | |
| // for the inherited duration. | |
| duration[ownIndex] -= getInheritedDuration(); | |
| } | |
| AggregateAttr[] ownStats = stats.toArray(); | |
| for (int i = 0; i < TASK_COUNT; i++) { | |
| AggregateAttr attr = ownStats[i]; | |
| if (attr != null) { | |
| counts[i] += attr.count; | |
| duration[i] += attr.totalTime; | |
| } | |
| } | |
| for (Task task : subtasks) { | |
| task.combineStats(counts, duration); | |
| } | |
| } | |
| /** | |
| * Calculates aggregated statistics covering all subtasks (including | |
| * nested ones). Must be called only for parent tasks. | |
| */ | |
| void calculateRootStats() { | |
| Preconditions.checkState(parentId == 0); | |
| int[] counts = new int[TASK_COUNT]; | |
| long[] duration = new long[TASK_COUNT]; | |
| combineStats(counts, duration); | |
| AggregateAttr[] statArray = ProfileInfo.createEmptyStatArray(); | |
| for (int i = 0; i < TASK_COUNT; i++) { | |
| statArray[i] = new AggregateAttr(counts[i], duration[i]); | |
| } | |
| this.aggregatedStats = new CompactStatistics(statArray); | |
| } | |
| @Override | |
| public boolean equals(Object o) { | |
| return (o instanceof ProfileInfo.Task) && ((Task) o).id == this.id; | |
| } | |
| @Override | |
| public int hashCode() { | |
| return this.id; | |
| } | |
| @Override | |
| public String toString() { | |
| return type + "(" + id + "," + getDescription() + ")"; | |
| } | |
| /** | |
| * Produce a nicely indented tree of the task and its subtasks with execution time. | |
| * | |
| * <p>Execution times are in milliseconds. | |
| * | |
| * <p>Example: | |
| * | |
| * <pre> | |
| * 636779 SKYLARK_USER_FN (259.593 ms) /path/file.bzl:42#function [ | |
| * 636810 SKYLARK_USER_FN (257.768 ms) /path/file.bzl:133#_other_function [ | |
| * 636974 SKYLARK_BUILTIN_FN (254.596 ms) some.package.PackageFactory$9#genrule [] | |
| * 2 subtree(s) omitted] | |
| * ] | |
| * </pre> | |
| * | |
| * @param durationThresholdMillis Tasks with a shorter duration than this threshold will be | |
| * skipped | |
| * @return whether this task took longer than the threshold and was thus printed | |
| */ | |
| public boolean printTaskTree(PrintStream out, long durationThresholdMillis) { | |
| return printTaskTree(out, "", TimeUnit.MILLISECONDS.toNanos(durationThresholdMillis)); | |
| } | |
| /** | |
| * @see #printTaskTree(PrintStream, long) | |
| */ | |
| private boolean printTaskTree( | |
| PrintStream out, String indent, final long durationThresholdNanos) { | |
| if (durationNanos < durationThresholdNanos) { | |
| return false; | |
| } | |
| out.printf("%s%6d %s", indent, id, type); | |
| out.printf(" (%5.3f ms) ", durationNanos / 1000000.0); | |
| out.print(getDescription()); | |
| out.print(" ["); | |
| ImmutableList<Task> sortedSubTasks = | |
| TASK_DURATION_ORDERING | |
| .reverse() | |
| .immutableSortedCopy( | |
| Iterables.filter( | |
| Arrays.asList(subtasks), | |
| new Predicate<Task>() { | |
| @Override | |
| public boolean apply(Task task) { | |
| return task.durationNanos >= durationThresholdNanos; | |
| } | |
| })); | |
| String sep = ""; | |
| for (Task task : sortedSubTasks) { | |
| out.print(sep); | |
| out.println(); | |
| task.printTaskTree(out, indent + " ", durationThresholdNanos); | |
| sep = ","; | |
| } | |
| if (!sortedSubTasks.isEmpty()) { | |
| out.println(); | |
| out.print(indent); | |
| } | |
| int skipped = subtasks.length - sortedSubTasks.size(); | |
| if (skipped > 0) { | |
| out.printf("%d subtree(s) omitted", skipped); | |
| } | |
| out.print("]"); | |
| if (indent.equals("")) { | |
| out.println(); | |
| } | |
| return true; | |
| } | |
| /** | |
| * Tasks records by default sorted by their id. Since id was obtained using | |
| * AtomicInteger, this comparison will correctly sort tasks in time-ascending | |
| * order regardless of their origin thread. | |
| */ | |
| @Override | |
| public int compareTo(Task task) { | |
| return this.id - task.id; | |
| } | |
| } | |
| /** | |
| * Represents node on critical build path | |
| */ | |
| public static final class CriticalPathEntry { | |
| public final Task task; | |
| public final long duration; | |
| public final long cumulativeDuration; | |
| public final CriticalPathEntry next; | |
| private long criticalTime = 0L; | |
| public CriticalPathEntry(Task task, long duration, CriticalPathEntry next) { | |
| this.task = task; | |
| this.duration = duration; | |
| this.next = next; | |
| this.cumulativeDuration = | |
| duration + (next != null ? next.cumulativeDuration : 0); | |
| } | |
| private void setCriticalTime(long duration) { | |
| criticalTime = duration; | |
| } | |
| public long getCriticalTime() { | |
| return criticalTime; | |
| } | |
| /** | |
| * @return true when this is just an action element on the critical path as logged by | |
| * {@link com.google.devtools.build.lib.runtime.BuildSummaryStatsModule} and is thus a | |
| * pre-processed and -analyzed critical path element | |
| */ | |
| public boolean isComponent() { | |
| return task.type == ProfilerTask.CRITICAL_PATH_COMPONENT; | |
| } | |
| } | |
| /** | |
| * Helper class to create space-efficient task multimap, used to associate | |
| * array of tasks with specific key. | |
| */ | |
| private abstract static class TaskMapCreator<K> implements Comparator<Task> { | |
| @Override | |
| public abstract int compare(Task a, Task b); | |
| public abstract K getKey(Task task); | |
| public Map<K, Task[]> createTaskMap(List<Task> taskList) { | |
| // Created map usually will end up with thousands of entries, so we | |
| // preinitialize it to the 10000. | |
| Map<K, Task[]> taskMap = Maps.newHashMapWithExpectedSize(10000); | |
| if (taskList.isEmpty()) { | |
| return taskMap; | |
| } | |
| Task[] taskArray = taskList.toArray(new Task[taskList.size()]); | |
| Arrays.sort(taskArray, this); | |
| K key = getKey(taskArray[0]); | |
| int start = 0; | |
| for (int i = 0; i < taskArray.length; i++) { | |
| K currentKey = getKey(taskArray[i]); | |
| if (!key.equals(currentKey)) { | |
| taskMap.put(key, Arrays.copyOfRange(taskArray, start, i)); | |
| key = currentKey; | |
| start = i; | |
| } | |
| } | |
| if (start < taskArray.length) { | |
| taskMap.put(key, Arrays.copyOfRange(taskArray, start, taskArray.length)); | |
| } | |
| return taskMap; | |
| } | |
| } | |
| /** | |
| * An interface to pass back profile loading and aggregation messages. | |
| */ | |
| public interface InfoListener { | |
| void info(String text); | |
| void warn(String text); | |
| } | |
| private static final Task[] NO_TASKS = new Task[0]; | |
| private static final AggregateAttr ZERO = new AggregateAttr(0, 0); | |
| public final String comment; | |
| private boolean corruptedOrIncomplete = false; | |
| // TODO(bazel-team): (2010) In one case, this list took 277MB of heap. Ideally it should be | |
| // replaced with a trie. | |
| private final List<String> descriptionList; | |
| private final Map<Task, Task> parallelBuilderCompletionQueueTasks; | |
| public final Map<Long, Task[]> tasksByThread; | |
| public final List<Task> allTasksById; | |
| public List<Task> rootTasksById; // Not final due to the late initialization. | |
| public final List<Task> phaseTasks; | |
| private ListMultimap<String, Task> userFunctions; | |
| private ListMultimap<String, Task> compiledUserFunctions; | |
| private ListMultimap<String, Task> builtinFunctions; | |
| public final Map<Task, Task[]> actionDependencyMap; | |
| // Used to create fake Action tasks if ACTIONG_GRAPH task does not have | |
| // corresponding ACTION task. For action dependency calculations we will | |
| // create fake ACTION tasks and assign them negative ids. | |
| private int fakeActionId = 0; | |
| private ProfileInfo(String comment) { | |
| this.comment = comment; | |
| descriptionList = Lists.newArrayListWithExpectedSize(10000); | |
| tasksByThread = Maps.newHashMap(); | |
| parallelBuilderCompletionQueueTasks = Maps.newHashMap(); | |
| allTasksById = Lists.newArrayListWithExpectedSize(50000); | |
| phaseTasks = Lists.newArrayList(); | |
| actionDependencyMap = Maps.newHashMapWithExpectedSize(10000); | |
| } | |
| private void addTask(Task task) { | |
| allTasksById.add(task); | |
| } | |
| /** | |
| * Returns true if profile datafile was corrupted or incomplete | |
| * and false otherwise. | |
| */ | |
| public boolean isCorruptedOrIncomplete() { | |
| return corruptedOrIncomplete; | |
| } | |
| /** | |
| * Returns number of missing actions which were faked in order to complete | |
| * action graph. | |
| */ | |
| public int getMissingActionsCount() { | |
| return -fakeActionId; | |
| } | |
| /** | |
| * Initializes minimum internal data structures necessary to obtain individual | |
| * task statistic. This method is sufficient to initialize data for dumping. | |
| */ | |
| public void calculateStats() { | |
| if (allTasksById.isEmpty()) { | |
| return; | |
| } | |
| Collections.sort(allTasksById); | |
| Map<Integer, Task[]> subtaskMap = new TaskMapCreator<Integer>() { | |
| @Override | |
| public int compare(Task a, Task b) { | |
| return a.parentId != b.parentId ? a.parentId - b.parentId : a.compareTo(b); | |
| } | |
| @Override | |
| public Integer getKey(Task task) { return task.parentId; } | |
| }.createTaskMap(allTasksById); | |
| for (Task task : allTasksById) { | |
| Task[] subtasks = subtaskMap.get(task.id); | |
| if (subtasks != null) { | |
| task.subtasks = subtasks; | |
| } | |
| } | |
| rootTasksById = Arrays.asList(subtaskMap.get(0)); | |
| for (Task task : rootTasksById) { | |
| task.calculateRootStats(); | |
| if (task.type == ProfilerTask.PHASE) { | |
| if (!phaseTasks.isEmpty()) { | |
| phaseTasks.get(phaseTasks.size() - 1).relatedTask = task; | |
| } | |
| phaseTasks.add(task); | |
| } | |
| } | |
| } | |
| /** | |
| * Collects all Skylark function tasks. Must be called before calling | |
| * {@link #getSkylarkUserFunctionTasks} and {@link #getSkylarkBuiltinFunctionTasks}. | |
| */ | |
| private void calculateSkylarkStatistics() { | |
| userFunctions = ListMultimapBuilder.treeKeys().arrayListValues().build(); | |
| compiledUserFunctions = ListMultimapBuilder.treeKeys().arrayListValues().build(); | |
| builtinFunctions = ListMultimapBuilder.treeKeys().arrayListValues().build(); | |
| for (Task task : allTasksById) { | |
| if (task.type == ProfilerTask.SKYLARK_BUILTIN_FN) { | |
| builtinFunctions.put(task.getDescription(), task); | |
| } else if (task.type == ProfilerTask.SKYLARK_USER_FN) { | |
| userFunctions.put(task.getDescription(), task); | |
| } else if (task.type == ProfilerTask.SKYLARK_USER_COMPILED_FN) { | |
| compiledUserFunctions.put(task.getDescription(), task); | |
| } | |
| } | |
| } | |
| /** | |
| * {@link #calculateSkylarkStatistics} must have been called before. | |
| * @return The {@link Task}s profiled for each user-defined Skylark function name. | |
| */ | |
| public ListMultimap<String, Task> getSkylarkUserFunctionTasks() { | |
| if (userFunctions == null) { | |
| calculateSkylarkStatistics(); | |
| } | |
| return userFunctions; | |
| } | |
| /** | |
| * {@link #calculateSkylarkStatistics} must have been called before. | |
| * @return The {@link Task}s profiled for each user-defined Skylark function name. | |
| */ | |
| public ListMultimap<String, Task> getCompiledSkylarkUserFunctionTasks() { | |
| if (compiledUserFunctions == null) { | |
| calculateSkylarkStatistics(); | |
| } | |
| return compiledUserFunctions; | |
| } | |
| /** | |
| * {@link #calculateSkylarkStatistics} must have been called before. | |
| * @return The {@link Task}s profiled for each builtin Skylark function name. | |
| */ | |
| public ListMultimap<String, Task> getSkylarkBuiltinFunctionTasks() { | |
| if (builtinFunctions == null) { | |
| calculateSkylarkStatistics(); | |
| } | |
| return builtinFunctions; | |
| } | |
| /** | |
| * Analyzes task relationships and dependencies. Used for the detailed profile | |
| * analysis. | |
| */ | |
| public void analyzeRelationships() { | |
| tasksByThread.putAll(new TaskMapCreator<Long>() { | |
| @Override | |
| public int compare(Task a, Task b) { | |
| return a.threadId != b.threadId ? (a.threadId < b.threadId ? -1 : 1) : a.compareTo(b); | |
| } | |
| @Override | |
| public Long getKey(Task task) { return task.threadId; } | |
| }.createTaskMap(rootTasksById)); | |
| buildDependencyMap(); | |
| } | |
| /** | |
| * Calculates cumulative time attributed to the specific task type. | |
| * Expects to be called only for root (parentId = 0) tasks. | |
| * calculateStats() must have been called first. | |
| */ | |
| public AggregateAttr getStatsForType(ProfilerTask type, Collection<Task> tasks) { | |
| long totalTime = 0; | |
| int count = 0; | |
| for (Task task : tasks) { | |
| if (task.parentId > 0) { | |
| throw new IllegalArgumentException("task " + task.id + " is not a root task"); | |
| } | |
| AggregateAttr attr = task.aggregatedStats.getAttr(type); | |
| count += attr.count; | |
| totalTime += attr.totalTime; | |
| if (task.type == type) { | |
| count++; | |
| totalTime += (task.durationNanos - task.getInheritedDuration()); | |
| } | |
| } | |
| return new AggregateAttr(count, totalTime); | |
| } | |
| /** | |
| * Returns list of all root tasks related to (in other words, started during) | |
| * the specified phase task. | |
| */ | |
| public List<Task> getTasksForPhase(Task phaseTask) { | |
| Preconditions.checkArgument(phaseTask.type == ProfilerTask.PHASE, | |
| "Unsupported task type %s", phaseTask.type); | |
| // Algorithm below takes into account fact that rootTasksById list is sorted | |
| // by the task id and task id values are monotonically increasing with time | |
| // (this property is guaranteed by the profiler). Thus list is effectively | |
| // sorted by the startTime. We are trying to select a sublist that includes | |
| // all tasks that were started later than the given task but earlier than | |
| // its completion time. | |
| int startIndex = Collections.binarySearch(rootTasksById, phaseTask); | |
| Preconditions.checkState(startIndex >= 0, | |
| "Phase task %s is not a root task", phaseTask.id); | |
| int endIndex = (phaseTask.relatedTask != null) | |
| ? Collections.binarySearch(rootTasksById, phaseTask.relatedTask) | |
| : rootTasksById.size(); | |
| Preconditions.checkState(endIndex >= startIndex, | |
| "Failed to find end of the phase marked by the task %s", phaseTask.id); | |
| return rootTasksById.subList(startIndex, endIndex); | |
| } | |
| /** | |
| * Returns task with "Build artifacts" description - corresponding to the | |
| * execution phase. Usually used to location ACTION_GRAPH task tree. | |
| */ | |
| public Task getPhaseTask(ProfilePhase phase) { | |
| for (Task task : phaseTasks) { | |
| if (task.getDescription().equals(phase.description)) { | |
| return task; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Returns duration of the given phase in ns. | |
| */ | |
| public long getPhaseDuration(Task phaseTask) { | |
| Preconditions.checkArgument(phaseTask.type == ProfilerTask.PHASE, | |
| "Unsupported task type %s", phaseTask.type); | |
| long duration; | |
| if (phaseTask.relatedTask != null) { | |
| duration = phaseTask.relatedTask.startTime - phaseTask.startTime; | |
| } else { | |
| Task lastTask = rootTasksById.get(rootTasksById.size() - 1); | |
| duration = lastTask.startTime + lastTask.durationNanos - phaseTask.startTime; | |
| } | |
| Preconditions.checkState(duration >= 0); | |
| return duration; | |
| } | |
| /** | |
| * Builds map of dependencies between ACTION tasks based on dependencies | |
| * between ACTION_GRAPH tasks | |
| */ | |
| private Task buildActionTaskTree(Task actionGraphTask, List<Task> actionTasksByDescription) { | |
| Task actionTask = actionGraphTask.relatedTask; | |
| if (actionTask == null) { | |
| actionTask = actionTasksByDescription.get(actionGraphTask.descIndex); | |
| if (actionTask == null) { | |
| // If we cannot find ACTION task that corresponds to the ACTION_GRAPH task, | |
| // most likely scenario is that we dealing with either aborted or failed | |
| // build. In this case we will find or create fake zero-duration action | |
| // task and still reconstruct dependency graph. | |
| actionTask = new Task(-1, --fakeActionId, 0, 0, 0, | |
| ProfilerTask.ACTION, actionGraphTask.descIndex, new CompactStatistics((byte[]) null)); | |
| actionTask.calculateRootStats(); | |
| actionTasksByDescription.set(actionGraphTask.descIndex, actionTask); | |
| } | |
| actionGraphTask.relatedTask = actionTask; | |
| } | |
| if (actionGraphTask.subtasks.length != 0) { | |
| List<Task> list = Lists.newArrayListWithCapacity(actionGraphTask.subtasks.length); | |
| for (Task task : actionGraphTask.subtasks) { | |
| if (task.type == ProfilerTask.ACTION_GRAPH) { | |
| list.add(buildActionTaskTree(task, actionTasksByDescription)); | |
| } | |
| } | |
| if (!list.isEmpty()) { | |
| Task[] actionPrerequisites = list.toArray(new Task[list.size()]); | |
| Arrays.sort(actionPrerequisites); | |
| actionDependencyMap.put(actionTask, actionPrerequisites); | |
| } | |
| } | |
| return actionTask; | |
| } | |
| /** | |
| * Builds map of dependencies between ACTION tasks based on dependencies | |
| * between ACTION_GRAPH tasks. Root of that dependency tree would be | |
| * getBuildPhaseTask(). | |
| * | |
| * <p> Also marks related ACTION and ACTION_SUBMIT tasks. | |
| */ | |
| private void buildDependencyMap() { | |
| Task analysisPhaseTask = getPhaseTask(ProfilePhase.ANALYZE); | |
| Task executionPhaseTask = getPhaseTask(ProfilePhase.EXECUTE); | |
| if ((executionPhaseTask == null) || (analysisPhaseTask == null)) { | |
| return; | |
| } | |
| // Association between ACTION_GRAPH tasks and ACTION tasks can be established through | |
| // description id. So we create appropriate xref list. | |
| List<Task> actionTasksByDescription = Lists.newArrayList(new Task[descriptionList.size()]); | |
| for (Task task : getTasksForPhase(executionPhaseTask)) { | |
| if (task.type == ProfilerTask.ACTION) { | |
| actionTasksByDescription.set(task.descIndex, task); | |
| } | |
| } | |
| List<Task> list = new ArrayList<>(); | |
| for (Task task : getTasksForPhase(analysisPhaseTask)) { | |
| if (task.type == ProfilerTask.ACTION_GRAPH) { | |
| list.add(buildActionTaskTree(task, actionTasksByDescription)); | |
| } | |
| } | |
| Task[] actionPrerequisites = list.toArray(new Task[list.size()]); | |
| Arrays.sort(actionPrerequisites); | |
| actionDependencyMap.put(executionPhaseTask, actionPrerequisites); | |
| // Scan through all execution phase tasks to identify ACTION_SUBMIT tasks and associate | |
| // them with ACTION task counterparts. ACTION_SUBMIT tasks are not necessarily root | |
| // tasks so we need to scan ALL tasks. | |
| for (Task task : allTasksById.subList(executionPhaseTask.id, allTasksById.size())) { | |
| if (task.type == ProfilerTask.ACTION_SUBMIT) { | |
| Task actionTask = actionTasksByDescription.get(task.descIndex); | |
| if (actionTask != null) { | |
| task.relatedTask = actionTask; | |
| actionTask.relatedTask = task; | |
| } | |
| } else if (task.type == ProfilerTask.ACTION_BUILDER) { | |
| Task actionTask = actionTasksByDescription.get(task.descIndex); | |
| if (actionTask != null) { | |
| parallelBuilderCompletionQueueTasks.put(actionTask, task); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Calculates critical path for the specific action | |
| * excluding specified nested task types (e.g. VFS-related time) and not | |
| * accounting for overhead related to the Blaze scheduler. | |
| */ | |
| private CriticalPathEntry computeCriticalPathForAction( | |
| Set<ProfilerTask> ignoredTypes, Set<Task> ignoredTasks, | |
| Task actionTask, Map<Task, CriticalPathEntry> cache, Deque<Task> stack) { | |
| // Loop check is expensive for the Deque (and we don't want to use hash sets because adding | |
| // and removing elements was shown to be very expensive). To avoid quadratic costs we're | |
| // checking for infinite loop only when deque's size equal to the power of 2 and >= 32. | |
| if ((stack.size() & 0x1F) == 0 && Integer.bitCount(stack.size()) == 1) { | |
| if (stack.contains(actionTask)) { | |
| // This situation will appear if build has ended with the | |
| // IllegalStateException thrown by the | |
| // ParallelBuilder.getNextCompletedAction(), warning user about | |
| // possible cycle in the dependency graph. But the exception text | |
| // is more friendly and will actually identify the loop. | |
| // Do not use Preconditions class below due to the very expensive | |
| // toString() calls used in the message. | |
| throw new IllegalStateException ("Dependency graph contains loop:\n" | |
| + actionTask + " in the\n" + Joiner.on('\n').join(stack)); | |
| } | |
| } | |
| stack.addLast(actionTask); | |
| CriticalPathEntry entry; | |
| try { | |
| entry = cache.get(actionTask); | |
| long entryDuration = 0; | |
| if (entry == null) { | |
| Task[] actionPrerequisites = actionDependencyMap.get(actionTask); | |
| if (actionPrerequisites != null) { | |
| for (Task task : actionPrerequisites) { | |
| CriticalPathEntry candidate = | |
| computeCriticalPathForAction(ignoredTypes, ignoredTasks, task, cache, stack); | |
| if (entry == null || entryDuration < candidate.cumulativeDuration) { | |
| entry = candidate; | |
| entryDuration = candidate.cumulativeDuration; | |
| } | |
| } | |
| } | |
| if (actionTask.type == ProfilerTask.ACTION) { | |
| long duration = actionTask.durationNanos; | |
| if (ignoredTasks.contains(actionTask)) { | |
| duration = 0L; | |
| } else { | |
| for (ProfilerTask type : ignoredTypes) { | |
| duration -= actionTask.aggregatedStats.getAttr(type).totalTime; | |
| } | |
| } | |
| entry = new CriticalPathEntry(actionTask, duration, entry); | |
| cache.put(actionTask, entry); | |
| } | |
| } | |
| } finally { | |
| stack.removeLast(); | |
| } | |
| return entry; | |
| } | |
| /** | |
| * Returns the critical path information from the {@code CriticalPathComputer} recorded stats. | |
| * This code does not have the "Critical" column (Time difference if we removed this node from | |
| * the critical path). | |
| */ | |
| public CriticalPathEntry getCriticalPathNewVersion() { | |
| for (Task task : rootTasksById) { | |
| if (task.type == CRITICAL_PATH) { | |
| CriticalPathEntry entry = null; | |
| for (Task shared : task.subtasks) { | |
| entry = new CriticalPathEntry(shared, shared.durationNanos, entry); | |
| } | |
| return entry; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Calculates critical path for the given action graph excluding | |
| * specified tasks (usually ones that belong to the "real" critical path). | |
| */ | |
| public CriticalPathEntry getCriticalPath(Set<ProfilerTask> ignoredTypes) { | |
| Task actionTask = getPhaseTask(ProfilePhase.EXECUTE); | |
| if (actionTask == null) { | |
| return null; | |
| } | |
| Map <Task, CriticalPathEntry> cache = Maps.newHashMapWithExpectedSize(1000); | |
| CriticalPathEntry result = computeCriticalPathForAction(ignoredTypes, | |
| new HashSet<Task>(), actionTask, cache, | |
| new ArrayDeque<Task>()); | |
| if (result != null) { | |
| return result; | |
| } | |
| return getCriticalPathNewVersion(); | |
| } | |
| /** | |
| * Calculates critical path time that will be saved by eliminating specific | |
| * entry from the critical path | |
| */ | |
| public void analyzeCriticalPath(Set<ProfilerTask> ignoredTypes, CriticalPathEntry path) { | |
| // With light critical path we do not need to analyze since it is already preprocessed | |
| // by blaze build. | |
| if (path == null || path.isComponent()) { | |
| return; | |
| } | |
| for (CriticalPathEntry entry = path; entry != null; entry = entry.next) { | |
| Map <Task, CriticalPathEntry> cache = Maps.newHashMapWithExpectedSize(1000); | |
| entry.setCriticalTime(path.cumulativeDuration - | |
| computeCriticalPathForAction(ignoredTypes, Sets.newHashSet(entry.task), | |
| getPhaseTask(ProfilePhase.EXECUTE), cache, new ArrayDeque<Task>()) | |
| .cumulativeDuration); | |
| } | |
| } | |
| /** | |
| * Return the next critical path entry for the task or null if there is none. | |
| */ | |
| public CriticalPathEntry getNextCriticalPathEntryForTask(CriticalPathEntry path, Task task) { | |
| for (CriticalPathEntry entry = path; entry != null; entry = entry.next) { | |
| if (entry.task.id == task.id) { | |
| return entry; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Returns time action waited in the execution queue (difference between | |
| * ACTION task start time and ACTION_SUBMIT task start time). | |
| */ | |
| public long getActionWaitTime(Task actionTask) { | |
| // Light critical path does not record wait time. | |
| if (actionTask.type == ProfilerTask.CRITICAL_PATH_COMPONENT) { | |
| return 0; | |
| } | |
| Preconditions.checkArgument(actionTask.type == ProfilerTask.ACTION); | |
| if (actionTask.relatedTask != null) { | |
| Preconditions.checkState(actionTask.relatedTask.type == ProfilerTask.ACTION_SUBMIT); | |
| long time = actionTask.startTime - actionTask.relatedTask.startTime; | |
| Preconditions.checkState(time >= 0); | |
| return time; | |
| } else { | |
| return 0L; // submission time is not available. | |
| } | |
| } | |
| /** | |
| * Returns time action waited in the parallel builder completion queue | |
| * (difference between ACTION task end time and ACTION_BUILDER start time). | |
| */ | |
| public long getActionQueueTime(Task actionTask) { | |
| // Light critical path does not record queue time. | |
| if (actionTask.type == ProfilerTask.CRITICAL_PATH_COMPONENT) { | |
| return 0; | |
| } | |
| Preconditions.checkArgument(actionTask.type == ProfilerTask.ACTION); | |
| Task related = parallelBuilderCompletionQueueTasks.get(actionTask); | |
| if (related != null) { | |
| Preconditions.checkState(related.type == ProfilerTask.ACTION_BUILDER); | |
| long time = related.startTime - (actionTask.startTime + actionTask.durationNanos); | |
| Preconditions.checkState(time >= 0); | |
| return time; | |
| } else { | |
| return 0L; // queue task is not available. | |
| } | |
| } | |
| /** | |
| * Searches for the task by its description. Linear in the number of tasks. | |
| * @param description a regular expression pattern which will be matched against the task | |
| * description | |
| * @return an Iterable of Tasks matching the description | |
| */ | |
| public Iterable<Task> findTasksByDescription(final Pattern description) { | |
| return Iterables.filter( | |
| allTasksById, | |
| new Predicate<Task>() { | |
| @Override | |
| public boolean apply(Task task) { | |
| return description.matcher(task.getDescription()).find(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Returns an empty array used to store task statistics. Array index | |
| * corresponds to the ProfilerTask ordinal() value associated with the | |
| * given statistic. Absent statistics are stored as null. | |
| * <p> | |
| * In essence, it is a fast equivalent of Map<ProfilerTask, AggregateAttr>. | |
| */ | |
| public static AggregateAttr[] createEmptyStatArray() { | |
| return new AggregateAttr[TASK_COUNT]; | |
| } | |
| /** | |
| * Loads and parses Blaze profile file. | |
| * | |
| * @param profileFile profile file path | |
| * | |
| * @return ProfileInfo object with some fields populated (call calculateStats() | |
| * and analyzeRelationships() to populate the remaining fields) | |
| * @throws UnsupportedEncodingException if the file format is invalid | |
| * @throws IOException if the file can't be read | |
| */ | |
| public static ProfileInfo loadProfile(Path profileFile) | |
| throws IOException { | |
| // It is extremely important to wrap InflaterInputStream using | |
| // BufferedInputStream because majority of reads would be done using | |
| // readInt()/readLong() methods and InflaterInputStream is very inefficient | |
| // in handling small read requests (performance difference with 1MB buffer | |
| // used below is almost 10x). | |
| DataInputStream in = new DataInputStream( | |
| new BufferedInputStream(new InflaterInputStream( | |
| profileFile.getInputStream(), new Inflater(false), 65536), 1024 * 1024)); | |
| if (in.readInt() != Profiler.MAGIC) { | |
| in.close(); | |
| throw new UnsupportedEncodingException("Invalid profile datafile format"); | |
| } | |
| if (in.readInt() != Profiler.VERSION) { | |
| in.close(); | |
| throw new UnsupportedEncodingException("Incompatible profile datafile version"); | |
| } | |
| String fileComment = in.readUTF(); | |
| // Read list of used record types | |
| int typeCount = in.readInt(); | |
| boolean hasUnknownTypes = false; | |
| Set<String> supportedTasks = new HashSet<>(); | |
| for (ProfilerTask task : ProfilerTask.values()) { | |
| supportedTasks.add(task.toString()); | |
| } | |
| List<ProfilerTask> typeList = new ArrayList<>(); | |
| for (int i = 0; i < typeCount; i++) { | |
| String name = in.readUTF(); | |
| if (supportedTasks.contains(name)) { | |
| typeList.add(ProfilerTask.valueOf(name)); | |
| } else { | |
| hasUnknownTypes = true; | |
| typeList.add(ProfilerTask.UNKNOWN); | |
| } | |
| } | |
| ProfileInfo info = new ProfileInfo(fileComment); | |
| // Read record until we encounter end marker (-1). | |
| // TODO(bazel-team): Maybe this still should handle corrupted(truncated) files. | |
| try { | |
| int size; | |
| while ((size = in.readInt()) != Profiler.EOF_MARKER) { | |
| byte[] backingArray = new byte[size]; | |
| in.readFully(backingArray); | |
| ByteBuffer buffer = ByteBuffer.wrap(backingArray); | |
| long threadId = VarInt.getVarLong(buffer); | |
| int id = VarInt.getVarInt(buffer); | |
| int parentId = VarInt.getVarInt(buffer); | |
| long startTime = VarInt.getVarLong(buffer); | |
| long duration = VarInt.getVarLong(buffer); | |
| int descIndex = VarInt.getVarInt(buffer) - 1; | |
| if (descIndex == -1) { | |
| String desc = in.readUTF(); | |
| descIndex = info.descriptionList.size(); | |
| info.descriptionList.add(desc); | |
| } | |
| ProfilerTask type = typeList.get(buffer.get()); | |
| byte[] stats = null; | |
| if (buffer.hasRemaining()) { | |
| // Copy aggregated stats. | |
| int offset = buffer.position(); | |
| stats = Arrays.copyOfRange(backingArray, offset, size); | |
| if (hasUnknownTypes) { | |
| while (buffer.hasRemaining()) { | |
| byte attrType = buffer.get(); | |
| if (typeList.get(attrType) == ProfilerTask.UNKNOWN) { | |
| // We're dealing with unknown aggregated type - update stats array to | |
| // use ProfilerTask.UNKNOWN.ordinal() value. | |
| stats[buffer.position() - 1 - offset] = (byte) ProfilerTask.UNKNOWN.ordinal(); | |
| } | |
| VarInt.getVarInt(buffer); | |
| VarInt.getVarLong(buffer); | |
| } | |
| } | |
| } | |
| ProfileInfo.Task task = info.new Task(threadId, id, parentId, startTime, duration, type, | |
| descIndex, new CompactStatistics(stats)); | |
| info.addTask(task); | |
| } | |
| } catch (IOException e) { | |
| info.corruptedOrIncomplete = true; | |
| } finally { | |
| in.close(); | |
| } | |
| return info; | |
| } | |
| /** | |
| * Loads and parses Blaze profile file, and reports what it is doing. | |
| * | |
| * @param profileFile profile file path | |
| * @param reporter for progress messages and warnings | |
| * | |
| * @return ProfileInfo object with most fields populated | |
| * (call analyzeRelationships() to populate the remaining fields) | |
| * @throws UnsupportedEncodingException if the file format is invalid | |
| * @throws IOException if the file can't be read | |
| */ | |
| public static ProfileInfo loadProfileVerbosely(Path profileFile, InfoListener reporter) | |
| throws IOException { | |
| reporter.info("Loading " + profileFile.getPathString()); | |
| ProfileInfo profileInfo = ProfileInfo.loadProfile(profileFile); | |
| if (profileInfo.isCorruptedOrIncomplete()) { | |
| reporter.warn("Profile file is incomplete or corrupted - not all records were parsed"); | |
| } | |
| reporter.info(profileInfo.comment + ", " + profileInfo.allTasksById.size() + " record(s)"); | |
| return profileInfo; | |
| } | |
| /* | |
| * Sorts and aggregates Blaze profile file, and reports what it is doing. | |
| */ | |
| public static void aggregateProfile(ProfileInfo profileInfo, InfoListener reporter) { | |
| reporter.info("Aggregating task statistics"); | |
| profileInfo.calculateStats(); | |
| } | |
| } |