// Copyright 2015 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.statistics; import com.google.common.base.Predicate; import com.google.devtools.build.lib.actions.MiddlemanAction; import com.google.devtools.build.lib.profiler.ProfileInfo; import com.google.devtools.build.lib.profiler.ProfileInfo.CriticalPathEntry; import com.google.devtools.build.lib.profiler.ProfileInfo.Task; import com.google.devtools.build.lib.profiler.ProfilerTask; import com.google.devtools.build.lib.util.Pair; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; /** * Keeps a predefined list of {@link CriticalPathEntry}'s cumulative durations and allows * iterating over pairs of their descriptions and relative durations. */ //TODO(bazel-team): Add remote vs build stats recorded by Logging.CriticalPathStats public final class CriticalPathStatistics implements Iterable<Pair<String, Double>> { private static final EnumSet<ProfilerTask> FILTER_NONE = EnumSet.noneOf(ProfilerTask.class); /** Always filter out ACTION_LOCK and WAIT tasks to simulate unlimited resource critical path. * @see #optimalPath */ private static final EnumSet<ProfilerTask> DEFAULT_FILTER = EnumSet.of(ProfilerTask.ACTION_LOCK, ProfilerTask.WAIT); private static final List<Pair<String, EnumSet<ProfilerTask>>> FILTERS = Arrays.asList( Pair.of("the builder overhead", EnumSet.allOf(ProfilerTask.class)), Pair.of( "the VFS calls", ProfilerTask.allSatisfying( new Predicate<ProfilerTask>() { @Override public boolean apply(ProfilerTask task) { return DEFAULT_FILTER.contains(task) || task.name().startsWith("VFS_"); } })), typeFilter("the dependency checking", ProfilerTask.ACTION_CHECK), typeFilter("the execution setup", ProfilerTask.ACTION_EXECUTE), typeFilter("local execution", ProfilerTask.SPAWN, ProfilerTask.LOCAL_EXECUTION), typeFilter("the include scanner", ProfilerTask.SCANNER), typeFilter( "Remote execution (cumulative)", ProfilerTask.REMOTE_EXECUTION, ProfilerTask.PROCESS_TIME, ProfilerTask.LOCAL_PARSE, ProfilerTask.UPLOAD_TIME, ProfilerTask.REMOTE_QUEUE, ProfilerTask.REMOTE_SETUP, ProfilerTask.FETCH), typeFilter(" file uploads", ProfilerTask.UPLOAD_TIME, ProfilerTask.REMOTE_SETUP), typeFilter(" file fetching", ProfilerTask.FETCH), typeFilter(" process time", ProfilerTask.PROCESS_TIME), typeFilter(" remote queueing", ProfilerTask.REMOTE_QUEUE), typeFilter(" remote execution parse", ProfilerTask.LOCAL_PARSE), typeFilter(" other remote activities", ProfilerTask.REMOTE_EXECUTION)); private final List<Long> criticalPathDurations; /** * The actual critical path. */ private final CriticalPathEntry totalPath; /** * Unlimited resource critical path. Essentially, we assume that if we remove all scheduling * delays caused by resource semaphore contention, each action execution time would not change * (even though load now would be substantially higher - so this assumption might be incorrect * but it is the path excluding scheduling delays). */ private final CriticalPathEntry optimalPath; private final long workerWaitTime; private final long mainThreadWaitTime; public CriticalPathStatistics(ProfileInfo info) { totalPath = info.getCriticalPath(FILTER_NONE); info.analyzeCriticalPath(FILTER_NONE, totalPath); optimalPath = info.getCriticalPath(DEFAULT_FILTER); info.analyzeCriticalPath(DEFAULT_FILTER, optimalPath); if (totalPath == null || totalPath.isComponent()) { this.workerWaitTime = 0; this.mainThreadWaitTime = 0; criticalPathDurations = Collections.emptyList(); return; } // Worker thread pool scheduling delays for the actual critical path. long workerWaitTime = 0; long mainThreadWaitTime = 0; for (CriticalPathEntry entry = totalPath; entry != null; entry = entry.next) { workerWaitTime += info.getActionWaitTime(entry.task); mainThreadWaitTime += info.getActionQueueTime(entry.task); } this.workerWaitTime = workerWaitTime; this.mainThreadWaitTime = mainThreadWaitTime; criticalPathDurations = getCriticalPathDurations(info); } /** * @return the critical path obtained by not filtering out any {@link ProfilerTask} */ public CriticalPathEntry getTotalPath() { return totalPath; } /** * @return the critical path obtained by filtering out any lock and wait tasks (see * {@link #DEFAULT_FILTER}) */ public CriticalPathEntry getOptimalPath() { return optimalPath; } /** * @see ProfileInfo#getActionWaitTime(Task) * @return the sum of all action wait times on the total critical path */ public long getWorkerWaitTime() { return workerWaitTime; } /** * @see ProfileInfo#getActionQueueTime(Task) * @return the mainThreadWaitTime */ public long getMainThreadWaitTime() { return mainThreadWaitTime; } /** * Constructs a filtered Iterable from a critical path. * * <p>Ignores all fake (task id < 0) path entries and * {@link com.google.devtools.build.lib.actions.MiddlemanAction}-related entries. */ public Iterable<CriticalPathEntry> getMiddlemanFilteredPath(final CriticalPathEntry path) { return new Iterable<CriticalPathEntry>() { @Override public Iterator<CriticalPathEntry> iterator() { return new Iterator<CriticalPathEntry>() { private CriticalPathEntry nextEntry = path; @Override public boolean hasNext() { return nextEntry != null; } @Override public CriticalPathEntry next() { CriticalPathEntry current = nextEntry; do { nextEntry = nextEntry.next; } while (nextEntry != null && (nextEntry.task.isFake() || isMiddleMan(nextEntry.task))); return current; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } @Override public Iterator<Pair<String, Double>> iterator() { return new Iterator<Pair<String, Double>>() { Iterator<Long> durations = criticalPathDurations.iterator(); Iterator<Pair<String, EnumSet<ProfilerTask>>> filters = FILTERS.iterator(); boolean overheadFilter = true; @Override public boolean hasNext() { return durations.hasNext(); } @Override public Pair<String, Double> next() { long duration = durations.next(); String description = filters.next().first; double relativeDuration; if (overheadFilter) { overheadFilter = false; relativeDuration = (double) duration / totalPath.cumulativeDuration; } else { relativeDuration = (double) (optimalPath.cumulativeDuration - duration) / optimalPath.cumulativeDuration; } return Pair.of(description, relativeDuration); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } /** * Extracts all {@link CriticalPathEntry} durations for the filters defined by {@link #FILTERS}. */ private static List<Long> getCriticalPathDurations(ProfileInfo info) { List<Long> list = new ArrayList<>(FILTERS.size()); for (Pair<String, EnumSet<ProfilerTask>> filter : FILTERS) { list.add(info.getCriticalPath(filter.second).cumulativeDuration); } return list; } /** * Returns set of profiler tasks to be filtered from critical path. * Also always filters out ACTION_LOCK and WAIT tasks to simulate * unlimited resource critical path (see comments inside formatExecutionPhaseStatistics() * method). */ private static Pair<String, EnumSet<ProfilerTask>> typeFilter( String description, ProfilerTask... tasks) { EnumSet<ProfilerTask> filter = EnumSet.of(ProfilerTask.ACTION_LOCK, ProfilerTask.WAIT); Collections.addAll(filter, tasks); return Pair.of(description, filter); } /** * @return Whether the task is {@link MiddlemanAction}-related. */ private static boolean isMiddleMan(Task task) { String description = task.getDescription(); return description.startsWith(MiddlemanAction.MIDDLEMAN_MNEMONIC + " ") || description.startsWith("TargetCompletionMiddleman"); } /** * Aggregates statistics related to {@link MiddlemanAction}s on the critical path. */ public static final class MiddleManStatistics { public final int count; public final long duration; public final long criticalTime; private MiddleManStatistics(int count, long duration, long criticalTime) { this.count = count; this.duration = duration; this.criticalTime = criticalTime; } /** * Summarizes middleman actions on the critical path. * @return null if path is null, else the aggregate statistics */ public static MiddleManStatistics create(@Nullable CriticalPathEntry path) { if (path == null) { return null; } int middleManCount = 0; long middleManDuration = 0; long middleManCritTime = 0; for (CriticalPathEntry entry = path; entry != null; entry = entry.next) { Task task = entry.task; if (!task.isFake() && isMiddleMan(task)) { // Aggregate middleman actions. middleManCount++; middleManDuration += entry.duration; middleManCritTime += entry.getCriticalTime(); } } return new MiddleManStatistics(middleManCount, middleManDuration, middleManCritTime); } } }