| // 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(); | 
 |   } | 
 |  | 
 | } |