diff options
author | Klaas Boesche <klaasb@google.com> | 2015-10-09 13:24:28 +0000 |
---|---|---|
committer | Kristina Chodorow <kchodorow@google.com> | 2015-10-09 14:42:49 +0000 |
commit | 6133c3f93d2664f8923ec027443041e595eadead (patch) | |
tree | 377504d2d345d6aa9c43cd16f7694f265b9bc258 /src/main/java/com/google/devtools/build/lib | |
parent | 3c74af02c9ab519fa9551bf518c834f77babd8a4 (diff) |
Add combine option for multiple profile file stats
Add the --combine option to produce a single aggregated statistics output for
multiple profile files. Outputs neither Skylark histograms nor the task chart.
--
MOS_MIGRATED_REVID=105051164
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib')
11 files changed, 732 insertions, 208 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/output/HtmlCreator.java b/src/main/java/com/google/devtools/build/lib/profiler/output/HtmlCreator.java index ab703ac2fd..885c965ee4 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/output/HtmlCreator.java +++ b/src/main/java/com/google/devtools/build/lib/profiler/output/HtmlCreator.java @@ -21,6 +21,7 @@ import com.google.devtools.build.lib.profiler.chart.Chart; import com.google.devtools.build.lib.profiler.chart.ChartCreator; import com.google.devtools.build.lib.profiler.chart.DetailedChartCreator; import com.google.devtools.build.lib.profiler.chart.HtmlChartVisitor; +import com.google.devtools.build.lib.profiler.statistics.MultiProfileStatistics; import com.google.devtools.build.lib.profiler.statistics.PhaseStatistics; import com.google.devtools.build.lib.profiler.statistics.PhaseSummaryStatistics; import com.google.devtools.build.lib.profiler.statistics.SkylarkStatistics; @@ -40,6 +41,7 @@ public final class HtmlCreator extends HtmlPrinter { private final Optional<Chart> chart; private final HtmlChartVisitor chartVisitor; private final Optional<SkylarkHtml> skylarkStats; + private final Optional<MultiProfilePhaseHtml> multiFileStats; private final String title; private final PhaseHtml phases; @@ -48,25 +50,21 @@ public final class HtmlCreator extends HtmlPrinter { String title, Optional<Chart> chart, Optional<SkylarkHtml> skylarkStats, - int htmlPixelsPerSecond, - PhaseHtml phases) { + Optional<MultiProfilePhaseHtml> multiFileStats, + PhaseHtml phases, + int htmlPixelsPerSecond) { super(out); this.title = title; this.chart = chart; this.skylarkStats = skylarkStats; this.phases = phases; + this.multiFileStats = multiFileStats; chartVisitor = new HtmlChartVisitor(out, htmlPixelsPerSecond); } - public HtmlCreator( - PrintStream out, - String title, - Optional<SkylarkHtml> skylarkStats, - int htmlPixelsPerSecond, - PhaseHtml phases) { - this(out, title, Optional.<Chart>absent(), skylarkStats, htmlPixelsPerSecond, phases); - } - + /** + * Output the HTML depending on which statistics should be printed. + */ private void print() { htmlFrontMatter(); if (chart.isPresent()) { @@ -77,22 +75,34 @@ public final class HtmlCreator extends HtmlPrinter { element("h2", "Statistics"); phases.print(); + if (multiFileStats.isPresent()) { + multiFileStats.get().printHtmlBody(); + } if (skylarkStats.isPresent()) { skylarkStats.get().printHtmlBody(); } htmlBackMatter(); } + /** + * Print opening tags, CSS and JavaScript + */ private void htmlFrontMatter() { lnOpen("html"); lnOpen("head"); lnElement("title", title); + + printVisualizationJs(); + if (chart.isPresent()) { chartVisitor.printCss(chart.get().getSortedTypes()); } phases.printCss(); + if (multiFileStats.isPresent()) { + multiFileStats.get().printHtmlHead(); + } if (skylarkStats.isPresent()) { skylarkStats.get().printHtmlHead(); } @@ -108,6 +118,30 @@ public final class HtmlCreator extends HtmlPrinter { } /** + * Print code for loading the Google Visualization JS library. + * + * <p>Used for the charts and tables for {@link SkylarkHtml} and {@link MultiProfilePhaseHtml}. + * Also adds a callback on load of the library which draws the charts and tables. + */ + private void printVisualizationJs() { + lnElement("script", "type", "text/javascript", "src", "https://www.google.com/jsapi"); + lnOpen("script", "type", "text/javascript"); + lnPrint("google.load(\"visualization\", \"1.1\", {packages:[\"corechart\",\"table\"]});"); + lnPrint("google.setOnLoadCallback(drawVisualization);"); + lnPrint("function drawVisualization() {"); + down(); + if (skylarkStats.isPresent()) { + skylarkStats.get().printVisualizationCallbackJs(); + } + if (multiFileStats.isPresent()) { + multiFileStats.get().printVisualizationCallbackJs(); + } + up(); + lnPrint("}"); + lnClose(); // script + } + + /** * Writes the HTML profiling information. * * @throws IOException @@ -140,8 +174,49 @@ public final class HtmlCreator extends HtmlPrinter { } chart = Optional.of(chartCreator.create()); } - new HtmlCreator(out, info.comment, chart, skylarkStats, htmlPixelsPerSecond, phaseHtml) + new HtmlCreator( + out, + info.comment, + chart, + skylarkStats, + Optional.<MultiProfilePhaseHtml>absent(), + phaseHtml, + htmlPixelsPerSecond) .print(); } } + + /** + * Writes the HTML profiling information for multiple files. + * + * <p>Does not print a {@link Chart} or even multiple charts and no Skylark histograms. + */ + public static void create( + PrintStream out, + MultiProfileStatistics statistics, + boolean detailed, + int htmlPixelsPerSecond, + int vfsStatsLimit) { + PhaseHtml phaseHtml = + new PhaseHtml( + out, + statistics.getSummaryStatistics(), + statistics.getSummaryPhaseStatistics(), + vfsStatsLimit); + Optional<SkylarkHtml> skylarkStats; + if (detailed) { + skylarkStats = Optional.of(new SkylarkHtml(out, statistics.getSkylarkStatistics(), false)); + } else { + skylarkStats = Optional.absent(); + } + new HtmlCreator( + out, + "Statistics from multiple profile files", + Optional.<Chart>absent(), + skylarkStats, + Optional.of(new MultiProfilePhaseHtml(out, statistics)), + phaseHtml, + htmlPixelsPerSecond) + .print(); + } } diff --git a/src/main/java/com/google/devtools/build/lib/profiler/output/MultiProfilePhaseHtml.java b/src/main/java/com/google/devtools/build/lib/profiler/output/MultiProfilePhaseHtml.java new file mode 100644 index 0000000000..cbde62b4b4 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/profiler/output/MultiProfilePhaseHtml.java @@ -0,0 +1,110 @@ +// 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.output; + +import com.google.devtools.build.lib.profiler.ProfilePhase; +import com.google.devtools.build.lib.profiler.ProfilerTask; +import com.google.devtools.build.lib.profiler.statistics.MultiProfileStatistics; +import com.google.devtools.build.lib.profiler.statistics.PhaseStatistics; +import com.google.devtools.build.lib.util.TimeUtilities; +import com.google.devtools.build.lib.vfs.Path; + +import java.io.PrintStream; +import java.util.EnumMap; + +/** + * Formats the per file phase statistics from {@link MultiProfileStatistics} into HTML tables. + */ +public final class MultiProfilePhaseHtml extends HtmlPrinter { + + private final MultiProfileStatistics statistics; + + public MultiProfilePhaseHtml(PrintStream out, MultiProfileStatistics statistics) { + super(out); + this.statistics = statistics; + } + + /** + * Prints CSS definitions and JavaScript code. + */ + void printHtmlHead() { + lnOpen("style", "type", "text/css", "<!--"); + lnPrint("div.profiles-table {"); + lnPrint(" width: 95%; margin: 0 auto; height: auto;"); + lnPrint("}"); + lnPrint("-->"); + close(); // style + } + + /** + * Prints the table data and JS for each phase and file. + * + * <p>Code must be added to the callback that is run when the Visualization library has loaded. + */ + public void printVisualizationCallbackJs() { + lnPrint("var multiData;"); + lnPrint("var statsDiv;"); + lnPrint("var profileTable;"); + for (ProfilePhase phase : statistics.getSummaryStatistics()) { + lnPrintf("statsDiv = document.getElementById('profile_file_stats_%s');", phase.nick); + lnPrint("multiData = new google.visualization.DataTable();"); + lnPrint("multiData.addColumn('string', 'File');"); + lnPrint("multiData.addColumn('number', 'total');"); + PhaseStatistics summaryPhaseStatistics = statistics.getSummaryPhaseStatistics(phase); + for (ProfilerTask taskType : summaryPhaseStatistics) { + lnPrintf("multiData.addColumn('number', '%s %%');", taskType.name()); + } + lnPrint("multiData.addRows(["); + down(); + for (Path file : statistics) { + EnumMap<ProfilePhase, PhaseStatistics> phases = statistics.getPhaseStatistics(file); + PhaseStatistics phaseStatistics = phases.get(phase); + lnPrintf("['%s', ", file); + long phaseDuration = phaseStatistics.getPhaseDurationNanos(); + printf("{v:%d, f:'%s'}, ", phaseDuration, TimeUtilities.prettyTime(phaseDuration)); + for (ProfilerTask taskType : summaryPhaseStatistics) { + if (phaseStatistics.wasExecuted(taskType)) { + double relative = phaseStatistics.getTotalRelativeDuration(taskType); + printf("{v:%.4f, f:'%.3f %%'}, ", relative, relative * 100); + } else { + print("0, "); + } + } + print("],"); + } + lnPrint("]);"); + up(); + lnPrint("profileTable = new google.visualization.Table(statsDiv);"); + lnPrint("profileTable.draw(multiData, {showRowNumber: true, width: '100%%'});"); + } + } + + /** + * Prints divs for the tables of statistics for profile files and their phases. + */ + void printHtmlBody() { + lnPrint("<a name='profile_file_stats'/>"); + lnElement("h3", "Profile File Statistics"); + lnOpen("div", "class", "profiles-tables", "id", "profile_file_stats"); + for (ProfilePhase phase : statistics.getSummaryStatistics()) { + lnOpen("div"); + lnElement("h4", phase.nick); + lnElement("div", "class", "profiles-table", "id", "profile_file_stats_" + phase.nick); + lnClose(); + } + lnClose(); // div + } +} + + diff --git a/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseHtml.java b/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseHtml.java index 231143177d..039ea28de7 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseHtml.java +++ b/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseHtml.java @@ -25,7 +25,6 @@ import com.google.devtools.build.lib.util.TimeUtilities; import java.io.PrintStream; import java.util.Arrays; import java.util.EnumMap; -import java.util.Map.Entry; /** * Output {@link PhaseSummaryStatistics}, {@link PhaseStatistics} and {@link PhaseVfsStatistics} @@ -88,7 +87,7 @@ public final class PhaseHtml extends HtmlPrinter { for (ProfilePhase phase : Arrays.asList(ProfilePhase.INIT, ProfilePhase.LOAD, ProfilePhase.ANALYZE)) { PhaseStatistics statistics = phaseStatistics.get(phase); - if (!statistics.wasExecuted()) { + if (statistics == null || !statistics.wasExecuted()) { continue; } printPhaseStatistics(statistics); @@ -159,7 +158,7 @@ public final class PhaseHtml extends HtmlPrinter { for (ProfilerTask type : stats) { int numPrinted = 0; - for (Entry<Stat, String> stat : stats.getSortedStatistics(type)) { + for (Stat stat : stats.getSortedStatistics(type)) { lnOpen("tr"); if (vfsStatsLimit != -1 && numPrinted++ == vfsStatsLimit) { open("td", "class", "center", "colspan", "4"); @@ -169,9 +168,9 @@ public final class PhaseHtml extends HtmlPrinter { break; } element("td", type.name()); - element("td", stat.getKey().count); - element("td", TimeUtilities.prettyTime(stat.getKey().duration)); - element("td", "class", "left", stat.getValue()); + element("td", stat.getCount()); + element("td", TimeUtilities.prettyTime(stat.getDuration())); + element("td", "class", "left", stat.path); close(); // tr } } diff --git a/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseText.java b/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseText.java index e246eb1c71..f3b71098bf 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseText.java +++ b/src/main/java/com/google/devtools/build/lib/profiler/output/PhaseText.java @@ -26,7 +26,6 @@ import com.google.devtools.build.lib.util.TimeUtilities; import java.io.PrintStream; import java.util.Arrays; import java.util.EnumMap; -import java.util.Map.Entry; /** * Output {@link PhaseSummaryStatistics}, {@link PhaseStatistics} and {@link PhaseVfsStatistics} @@ -206,7 +205,7 @@ public final class PhaseText extends TextPrinter { for (ProfilerTask type : stats) { int numPrinted = 0; - for (Entry<Stat, String> stat : stats.getSortedStatistics(type)) { + for (Stat stat : stats.getSortedStatistics(type)) { if (vfsStatsLimit != -1 && numPrinted++ == vfsStatsLimit) { lnPrintf("... %d more ...", stats.getStatisticsCount(type) - vfsStatsLimit); break; @@ -214,9 +213,9 @@ public final class PhaseText extends TextPrinter { lnPrintf( "%15s %10d %10s %s", type.name(), - stat.getKey().count, - TimeUtilities.prettyTime(stat.getKey().duration), - stat.getValue()); + stat.getCount(), + TimeUtilities.prettyTime(stat.getDuration()), + stat.path); } } } diff --git a/src/main/java/com/google/devtools/build/lib/profiler/output/SkylarkHtml.java b/src/main/java/com/google/devtools/build/lib/profiler/output/SkylarkHtml.java index c0e9ee14fa..769e4e3b7f 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/output/SkylarkHtml.java +++ b/src/main/java/com/google/devtools/build/lib/profiler/output/SkylarkHtml.java @@ -33,6 +33,8 @@ public final class SkylarkHtml extends HtmlPrinter { * How many characters from the end of the location of a Skylark function to display. */ private static final int NUM_LOCATION_CHARS_UNABBREVIATED = 40; + private static final String JS_DATA_VAR = "skylarkData"; + private static final String JS_TABLE_VAR = JS_DATA_VAR + "Table"; private final SkylarkStatistics stats; private final boolean printHistograms; @@ -64,48 +66,11 @@ public final class SkylarkHtml extends HtmlPrinter { lnPrint("-->"); close(); // style - lnElement("script", "type", "text/javascript", "src", "https://www.google.com/jsapi"); lnOpen("script", "type", "text/javascript"); - lnPrint("google.load(\"visualization\", \"1.1\", {packages:[\"corechart\",\"table\"]});"); - lnPrint("google.setOnLoadCallback(drawVisualization);"); - - String dataVar = "data"; - String tableVar = dataVar + "Table"; - lnPrintf("var %s = {};\n", dataVar); - lnPrintf("var %s = {};\n", tableVar); + lnPrintf("var %s = {};\n", JS_DATA_VAR); + lnPrintf("var %s = {};\n", JS_TABLE_VAR); lnPrint("var histogramData;"); - lnPrint("function drawVisualization() {"); - down(); - printStatsJs( - stats.getUserFunctionStatistics(), - stats.getUserFunctionSelfStatistics(), - "user", - dataVar, - tableVar, - stats.getUserTotalNanos()); - printStatsJs( - stats.getBuiltinFunctionStatistics(), - stats.getBuiltinFunctionSelfStatistics(), - "builtin", - dataVar, - tableVar, - stats.getBuiltinTotalNanos()); - - if (printHistograms) { - printHistogramData(); - - lnPrint("document.querySelector('#user-close').onclick = function() {"); - lnPrint(" document.querySelector('#user-histogram').style.display = 'none';"); - lnPrint("};"); - lnPrint("document.querySelector('#builtin-close').onclick = function() {"); - lnPrint(" document.querySelector('#builtin-histogram').style.display = 'none';"); - lnPrint("};"); - } - - up(); - lnPrint("};"); - if (printHistograms) { lnPrint("var options = {"); down(); @@ -122,11 +87,11 @@ public final class SkylarkHtml extends HtmlPrinter { down(); lnPrint("return function() {"); down(); - printf("var selection = %s[category].getSelection();", tableVar); + printf("var selection = %s[category].getSelection();", JS_TABLE_VAR); lnPrint("if (selection.length < 1) return;"); lnPrint("var item = selection[0];"); - lnPrintf("var loc = %s[category].getValue(item.row, 0);", dataVar); - lnPrintf("var func = %s[category].getValue(item.row, 1);", dataVar); + lnPrintf("var loc = %s[category].getValue(item.row, 0);", JS_DATA_VAR); + lnPrintf("var func = %s[category].getValue(item.row, 1);", JS_DATA_VAR); lnPrint("var histogramDiv = document.getElementById(category+'-histogram');"); if (printHistograms) { lnPrint("var key = loc + '#' + func;"); @@ -151,6 +116,34 @@ public final class SkylarkHtml extends HtmlPrinter { lnClose(); // script } + /** + * Prints the data for the tables of Skylark function statistics and - if needed - the + * histogram data. + */ + void printVisualizationCallbackJs() { + printStatsJs( + stats.getUserFunctionStatistics(), + stats.getUserFunctionSelfStatistics(), + "user", + stats.getUserTotalNanos()); + printStatsJs( + stats.getBuiltinFunctionStatistics(), + stats.getBuiltinFunctionSelfStatistics(), + "builtin", + stats.getBuiltinTotalNanos()); + + if (printHistograms) { + printHistogramData(); + + lnPrint("document.querySelector('#user-close').onclick = function() {"); + lnPrint(" document.querySelector('#user-histogram').style.display = 'none';"); + lnPrint("};"); + lnPrint("document.querySelector('#builtin-close').onclick = function() {"); + lnPrint(" document.querySelector('#builtin-histogram').style.display = 'none';"); + lnPrint("};"); + } + } + private void printHistogramData() { lnPrint("histogramData = {"); down(); @@ -182,10 +175,8 @@ public final class SkylarkHtml extends HtmlPrinter { Map<String, TasksStatistics> taskStatistics, Map<String, TasksStatistics> taskSelfStatistics, String category, - String dataVar, - String tableVar, long totalNanos) { - String tmpVar = category + dataVar; + String tmpVar = category + JS_DATA_VAR; lnPrintf("var statsDiv = document.getElementById('%s_function_stats');", category); if (taskStatistics.isEmpty()) { lnPrint( @@ -239,18 +230,18 @@ public final class SkylarkHtml extends HtmlPrinter { } lnPrint("]);"); up(); - lnPrintf("%s.%s = %s;", dataVar, category, tmpVar); - lnPrintf("%s.%s = new google.visualization.Table(statsDiv);", tableVar, category); + lnPrintf("%s.%s = %s;", JS_DATA_VAR, category, tmpVar); + lnPrintf("%s.%s = new google.visualization.Table(statsDiv);", JS_TABLE_VAR, category); lnPrintf( "google.visualization.events.addListener(%s.%s, 'select', selectHandler('%s'));", - tableVar, + JS_TABLE_VAR, category, category); lnPrintf( "%s.%s.draw(%s.%s, {showRowNumber: true, width: '100%%', height: '100%%'});", - tableVar, + JS_TABLE_VAR, category, - dataVar, + JS_DATA_VAR, category); } } diff --git a/src/main/java/com/google/devtools/build/lib/profiler/statistics/MultiProfileStatistics.java b/src/main/java/com/google/devtools/build/lib/profiler/statistics/MultiProfileStatistics.java new file mode 100644 index 0000000000..a4e8959f44 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/profiler/statistics/MultiProfileStatistics.java @@ -0,0 +1,124 @@ +// 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.devtools.build.lib.profiler.ProfileInfo; +import com.google.devtools.build.lib.profiler.ProfileInfo.InfoListener; +import com.google.devtools.build.lib.profiler.ProfilePhase; +import com.google.devtools.build.lib.vfs.Path; + +import java.io.IOException; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Builds and aggregates statistics for multiple profile files. + */ +public final class MultiProfileStatistics implements Iterable<Path> { + private final PhaseSummaryStatistics summaryStatistics; + private final EnumMap<ProfilePhase, PhaseStatistics> summaryPhaseStatistics; + private final Map<Path, EnumMap<ProfilePhase, PhaseStatistics>> filePhaseStatistics; + private final SkylarkStatistics skylarkStatistics; + + private int missingActionsCount; + private boolean generateVfsStatistics; + + public MultiProfileStatistics( + Path workingDirectory, + String workSpaceName, + List<String> files, + InfoListener listener, + boolean generateVfsStatistics) { + summaryStatistics = new PhaseSummaryStatistics(); + summaryPhaseStatistics = new EnumMap<>(ProfilePhase.class); + filePhaseStatistics = new HashMap<>(); + skylarkStatistics = new SkylarkStatistics(); + this.generateVfsStatistics = generateVfsStatistics; + for (String file : files) { + loadProfileFile(workingDirectory, workSpaceName, file, listener); + } + } + + public PhaseSummaryStatistics getSummaryStatistics() { + return summaryStatistics; + } + + public EnumMap<ProfilePhase, PhaseStatistics> getSummaryPhaseStatistics() { + return summaryPhaseStatistics; + } + + public PhaseStatistics getSummaryPhaseStatistics(ProfilePhase phase) { + return summaryPhaseStatistics.get(phase); + } + + public SkylarkStatistics getSkylarkStatistics() { + return skylarkStatistics; + } + + public int getMissingActionsCount() { + return missingActionsCount; + } + + public EnumMap<ProfilePhase, PhaseStatistics> getPhaseStatistics(Path file) { + return filePhaseStatistics.get(file); + } + + @Override + public Iterator<Path> iterator() { + return filePhaseStatistics.keySet().iterator(); + } + + /** + * Loads a single profile file and adds the statistics to the previously collected ones. + */ + private void loadProfileFile( + Path workingDirectory, String workSpaceName, String file, InfoListener listener) { + ProfileInfo info; + Path profileFile = workingDirectory.getRelative(file); + try { + info = ProfileInfo.loadProfileVerbosely(profileFile, listener); + ProfileInfo.aggregateProfile(info, listener); + } catch (IOException e) { + listener.warn("Ignoring file " + file + " - cannot load: " + e.getMessage()); + return; + } + + summaryStatistics.addProfileInfo(info); + + EnumMap<ProfilePhase, PhaseStatistics> fileStatistics = new EnumMap<>(ProfilePhase.class); + filePhaseStatistics.put(profileFile, fileStatistics); + + for (ProfilePhase phase : ProfilePhase.values()) { + PhaseStatistics filePhaseStat = + new PhaseStatistics(phase, info, workSpaceName, generateVfsStatistics); + fileStatistics.put(phase, filePhaseStat); + + PhaseStatistics summaryPhaseStats; + if (summaryPhaseStatistics.containsKey(phase)) { + summaryPhaseStats = summaryPhaseStatistics.get(phase); + } else { + summaryPhaseStats = new PhaseStatistics(phase, generateVfsStatistics); + summaryPhaseStatistics.put(phase, summaryPhaseStats); + } + summaryPhaseStats.add(filePhaseStat); + } + + skylarkStatistics.addProfileInfo(info); + + missingActionsCount += info.getMissingActionsCount(); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseStatistics.java b/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseStatistics.java index 3f6e326adf..96602a9d39 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseStatistics.java +++ b/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseStatistics.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.build.lib.profiler.statistics; +import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.Iterators; import com.google.devtools.build.lib.profiler.ProfileInfo; @@ -31,39 +32,92 @@ import java.util.List; public final class PhaseStatistics implements Iterable<ProfilerTask> { private final ProfilePhase phase; - private final long phaseDurationNanos; - private final long totalDurationNanos; - private final EnumMap<ProfilerTask, AggregateAttr> aggregateTaskStatistics; + private long phaseDurationNanos; + private long totalDurationNanos; + private final EnumMap<ProfilerTask, Long> taskDurations; + private final EnumMap<ProfilerTask, Long> taskCounts; private final PhaseVfsStatistics vfsStatistics; - private final boolean wasExecuted; + private boolean wasExecuted; + private int count; - public PhaseStatistics(ProfilePhase phase, ProfileInfo info, String workSpaceName) { + public PhaseStatistics(ProfilePhase phase, boolean generateVfsStatistics) { this.phase = phase; - this.aggregateTaskStatistics = new EnumMap<>(ProfilerTask.class); - Task phaseTask = info.getPhaseTask(phase); - vfsStatistics = new PhaseVfsStatistics(workSpaceName, phase, info); - if (phaseTask == null) { - wasExecuted = false; - totalDurationNanos = 0; - phaseDurationNanos = 0; + this.taskDurations = new EnumMap<>(ProfilerTask.class); + this.taskCounts = new EnumMap<>(ProfilerTask.class); + if (generateVfsStatistics) { + vfsStatistics = new PhaseVfsStatistics(phase); } else { + vfsStatistics = null; + } + } + + public PhaseStatistics(ProfilePhase phase, ProfileInfo info, String workSpaceName, boolean vfs) { + this(phase, vfs); + addProfileInfo(workSpaceName, info); + } + + /** + * Add statistics from {@link ProfileInfo} to the ones already accumulated for this phase. + */ + public void addProfileInfo(String workSpaceName, ProfileInfo info) { + Task phaseTask = info.getPhaseTask(phase); + if (phaseTask != null) { + if (vfsStatistics != null) { + vfsStatistics.addProfileInfo(workSpaceName, info); + } wasExecuted = true; - phaseDurationNanos = info.getPhaseDuration(phaseTask); + long infoPhaseDuration = info.getPhaseDuration(phaseTask); + phaseDurationNanos += infoPhaseDuration; List<Task> taskList = info.getTasksForPhase(phaseTask); - long duration = phaseDurationNanos; + long duration = infoPhaseDuration; for (Task task : taskList) { // Tasks on the phaseTask thread already accounted for in the phaseDuration. if (task.threadId != phaseTask.threadId) { duration += task.durationNanos; } } - totalDurationNanos = duration; + totalDurationNanos += duration; for (ProfilerTask type : ProfilerTask.values()) { - aggregateTaskStatistics.put(type, info.getStatsForType(type, taskList)); + AggregateAttr attr = info.getStatsForType(type, taskList); + long totalTime = Math.max(0, attr.totalTime); + long count = Math.max(0, attr.count); + add(taskCounts, type, count); + add(taskDurations, type, totalTime); } + count++; } } + /** + * Add statistics accumulated in another PhaseStatistics object to this one. + */ + public void add(PhaseStatistics other) { + Preconditions.checkArgument( + phase == other.phase, "Should not combine statistics from different phases"); + if (other.wasExecuted) { + if (vfsStatistics != null && other.vfsStatistics != null) { + vfsStatistics.add(other.vfsStatistics); + } + wasExecuted = true; + phaseDurationNanos += other.phaseDurationNanos; + totalDurationNanos += other.totalDurationNanos; + for (ProfilerTask type : other) { + long otherCount = other.getCount(type); + long otherDuration = other.getTotalDurationNanos(type); + add(taskCounts, type, otherCount); + add(taskDurations, type, otherDuration); + } + count++; + } + } + + /** + * @return how many executions of this phase were accumulated + */ + public int getPhaseCount() { + return count; + } + public ProfilePhase getProfilePhase() { return phase; } @@ -76,7 +130,7 @@ public final class PhaseStatistics implements Iterable<ProfilerTask> { * @return true if no {@link ProfilerTask}s have been executed in this phase, false otherwise */ public boolean isEmpty() { - return aggregateTaskStatistics.isEmpty(); + return taskCounts.isEmpty(); } /** @@ -98,14 +152,19 @@ public final class PhaseStatistics implements Iterable<ProfilerTask> { * @return true if a task of the given {@link ProfilerTask} type was executed in this phase */ public boolean wasExecuted(ProfilerTask taskType) { - return aggregateTaskStatistics.get(taskType).count != 0; + Long count = taskCounts.get(taskType); + return count != null && count != 0; } /** * @return the sum of all task durations of the given type */ public long getTotalDurationNanos(ProfilerTask taskType) { - return aggregateTaskStatistics.get(taskType).totalTime; + Long duration = taskDurations.get(taskType); + if (duration == null) { + return 0; + } + return duration; } /** @@ -113,8 +172,9 @@ public final class PhaseStatistics implements Iterable<ProfilerTask> { */ public double getMeanDuration(ProfilerTask taskType) { if (wasExecuted(taskType)) { - AggregateAttr stats = aggregateTaskStatistics.get(taskType); - return (double) stats.totalTime / stats.count; + double duration = taskDurations.get(taskType); + long count = taskCounts.get(taskType); + return duration / count; } return 0; } @@ -124,17 +184,30 @@ public final class PhaseStatistics implements Iterable<ProfilerTask> { * phase duration */ public double getTotalRelativeDuration(ProfilerTask taskType) { - if (wasExecuted(taskType)) { - return (double) aggregateTaskStatistics.get(taskType).totalTime / totalDurationNanos; + Long duration = taskDurations.get(taskType); + if (duration == null || duration == 0) { + return 0; } - return 0; + // sanity check for broken profile files + Preconditions.checkState( + totalDurationNanos != 0, + "Profiler tasks of type %s have non-zero duration %s in phase %s but the phase itself has" + + " zero duration. Most likely the profile file is broken.", + taskType, + duration, + phase); + return (double) duration / totalDurationNanos; } /** * @return how many tasks of the given type were executed in this phase */ - public int getCount(ProfilerTask taskType) { - return aggregateTaskStatistics.get(taskType).count; + public long getCount(ProfilerTask taskType) { + Long count = taskCounts.get(taskType); + if (count == null) { + return 0; + } + return count; } /** @@ -144,14 +217,26 @@ public final class PhaseStatistics implements Iterable<ProfilerTask> { @Override public Iterator<ProfilerTask> iterator() { return Iterators.filter( - aggregateTaskStatistics.keySet().iterator(), + taskCounts.keySet().iterator(), new Predicate<ProfilerTask>() { @Override public boolean apply(ProfilerTask taskType) { - - return getTotalDurationNanos(taskType) != 0 && getCount(taskType) != 0; + return getTotalDurationNanos(taskType) > 0 && wasExecuted(taskType); } }); } + + /** + * Helper method to sum up long values within an {@link EnumMap}. + */ + private static <T extends Enum<T>> void add(EnumMap<T, Long> map, T key, long value) { + long previous; + if (map.containsKey(key)) { + previous = map.get(key); + } else { + previous = 0; + } + map.put(key, previous + value); + } } diff --git a/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseSummaryStatistics.java b/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseSummaryStatistics.java index 965cd78828..59032b13ef 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseSummaryStatistics.java +++ b/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseSummaryStatistics.java @@ -26,21 +26,31 @@ import java.util.NoSuchElementException; */ public final class PhaseSummaryStatistics implements Iterable<ProfilePhase> { - private final long totalDurationNanos; + private long totalDurationNanos; private final EnumMap<ProfilePhase, Long> durations; - public PhaseSummaryStatistics(ProfileInfo info) { + public PhaseSummaryStatistics() { durations = new EnumMap<>(ProfilePhase.class); - long totalDuration = 0; + totalDurationNanos = 0; + } + + public PhaseSummaryStatistics(ProfileInfo info) { + this(); + addProfileInfo(info); + } + + /** + * Add a summary of the {@link ProfilePhase}s durations from a {@link ProfileInfo}. + */ + public void addProfileInfo(ProfileInfo info) { for (ProfilePhase phase : ProfilePhase.values()) { ProfileInfo.Task phaseTask = info.getPhaseTask(phase); if (phaseTask != null) { long phaseDuration = info.getPhaseDuration(phaseTask); - totalDuration += phaseDuration; + totalDurationNanos += phaseDuration; durations.put(phase, phaseDuration); } } - totalDurationNanos = totalDuration; } /** diff --git a/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseVfsStatistics.java b/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseVfsStatistics.java index 4c52c47eb5..58d454f368 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseVfsStatistics.java +++ b/src/main/java/com/google/devtools/build/lib/profiler/statistics/PhaseVfsStatistics.java @@ -13,10 +13,12 @@ // limitations under the License. package com.google.devtools.build.lib.profiler.statistics; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimaps; -import com.google.common.collect.Ordering; -import com.google.common.collect.TreeMultimap; +import com.google.common.base.Supplier; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Table; +import com.google.common.collect.Table.Cell; +import com.google.common.collect.Tables; import com.google.devtools.build.lib.profiler.ProfileInfo; import com.google.devtools.build.lib.profiler.ProfileInfo.Task; import com.google.devtools.build.lib.profiler.ProfilePhase; @@ -28,7 +30,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Objects; +import java.util.SortedSet; /** * Compute and store statistics of all {@link ProfilerTask}s that begin with VFS_ in sorted order. @@ -36,34 +39,98 @@ import java.util.Map.Entry; public final class PhaseVfsStatistics implements Iterable<ProfilerTask> { /** - * Pair of duration and count for sorting by duration first and count in case of tie + * Duration, count and path for sorting by duration first and count in case of tie. Path for + * easy returning of a {@link SortedSet}. */ - public static class Stat implements Comparable<Stat> { - public long duration; - public long count; + public static final class Stat implements Comparable<Stat> { + private long duration; + private long count; + public final String path; + public Stat(String path) { + this.path = path; + } + + public Stat(Stat other) { + this.duration = other.duration; + this.count = other.count; + this.path = other.path; + } + + public long getDuration() { + return duration; + } + + public long getCount() { + return count; + } + + private void add(Stat other) { + this.duration += other.duration; + this.count += other.count; + } + + private void add(long duration) { + this.duration += duration; + this.count++; + } + + /** + * Order first by duration, then count, then path + */ @Override public int compareTo(Stat o) { - return this.duration == o.duration - ? Long.compare(this.count, o.count) - : Long.compare(this.duration, o.duration); + return ComparisonChain.start() + .compare(duration, o.duration) + .compare(count, o.count) + .compare(path, o.path) + .result(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Stat) { + return compareTo((Stat) obj) == 0; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(duration, count, path); } } private final ProfilePhase phase; - private final EnumMap<ProfilerTask, TreeMultimap<Stat, String>> sortedStatistics; - private final String workSpaceName; + private final Table<ProfilerTask, String, Stat> statistics; - public PhaseVfsStatistics(final String workSpaceName, ProfilePhase phase, ProfileInfo info) { - this.workSpaceName = workSpaceName; + public PhaseVfsStatistics(ProfilePhase phase) { this.phase = phase; - this.sortedStatistics = Maps.newEnumMap(ProfilerTask.class); + this.statistics = + Tables.newCustomTable( + new EnumMap<ProfilerTask, Map<String, Stat>>(ProfilerTask.class), + new Supplier<Map<String, Stat>>() { + @Override + public Map<String, Stat> get() { + return new HashMap<>(); + } + }); + } + + public PhaseVfsStatistics(final String workSpaceName, ProfilePhase phase, ProfileInfo info) { + this(phase); + addProfileInfo(workSpaceName, info); + } + /** + * Accumulate statistics from another {@link ProfileInfo} in this object. + */ + public void addProfileInfo(final String workSpaceName, ProfileInfo info) { Task phaseTask = info.getPhaseTask(phase); if (phaseTask == null) { return; } - collectVfsEntries(info.getTasksForPhase(phaseTask)); + collectVfsEntries(workSpaceName, info.getTasksForPhase(phaseTask)); } public ProfilePhase getProfilePhase() { @@ -71,63 +138,66 @@ public final class PhaseVfsStatistics implements Iterable<ProfilerTask> { } public boolean isEmpty() { - return sortedStatistics.isEmpty(); + return statistics.isEmpty(); } - public Iterable<Entry<Stat, String>> getSortedStatistics(ProfilerTask taskType) { - return sortedStatistics.get(taskType).entries(); + /** + * Builds a new {@link ImmutableSortedSet} of the path statistics for the given + * {@link ProfilerTask}. + * + * <p>{@link Stat}s are sorted by their natural order. + */ + public ImmutableSortedSet<Stat> getSortedStatistics(ProfilerTask taskType) { + return ImmutableSortedSet.copyOf(statistics.row(taskType).values()); } public int getStatisticsCount(ProfilerTask taskType) { - return sortedStatistics.get(taskType).size(); + return statistics.row(taskType).size(); } @Override public Iterator<ProfilerTask> iterator() { - return sortedStatistics.keySet().iterator(); + return statistics.rowKeySet().iterator(); } /** - * Group into VFS operations and build maps from path to duration. + * Add statistics from another PhaseVfsStatistics aggregation to this one. */ - private void collectVfsEntries(List<Task> taskList) { - EnumMap<ProfilerTask, Map<String, Stat>> stats = Maps.newEnumMap(ProfilerTask.class); + public void add(PhaseVfsStatistics other) { + for (Cell<ProfilerTask, String, Stat> cell : other.statistics.cellSet()) { + Stat stat = statistics.get(cell.getRowKey(), cell.getColumnKey()); + if (stat == null) { + stat = new Stat(cell.getValue()); + statistics.put(cell.getRowKey(), stat.path, stat); + } else { + stat.add(cell.getValue()); + } + } + } + + /** + * Add the VFS operations from the list of tasks to the {@link #statistics} table + */ + private void collectVfsEntries(String workSpaceName, List<Task> taskList) { for (Task task : taskList) { - collectVfsEntries(Arrays.asList(task.subtasks)); + collectVfsEntries(workSpaceName, Arrays.asList(task.subtasks)); if (!task.type.name().startsWith("VFS_")) { continue; } - Map<String, Stat> statsForType = stats.get(task.type); - if (statsForType == null) { - statsForType = new HashMap<>(); - stats.put(task.type, statsForType); - } - - String path = currentPathMapping(task.getDescription()); + String path = pathMapping(workSpaceName, task.getDescription()); - Stat stat = statsForType.get(path); + Stat stat = statistics.get(task.type, path); if (stat == null) { - stat = new Stat(); + stat = new Stat(path); + statistics.put(task.type, path, stat); } - stat.duration += task.durationNanos; - stat.count++; - statsForType.put(path, stat); - } - // Reverse the maps to get maps from duration to path. We use a TreeMultimap to sort by - // duration and because durations are not unique. - for (ProfilerTask type : stats.keySet()) { - Map<String, Stat> statsForType = stats.get(type); - TreeMultimap<Stat, String> sortedStats = - TreeMultimap.create(Ordering.natural().reverse(), Ordering.natural()); - - Multimaps.invertFrom(Multimaps.forMap(statsForType), sortedStats); - sortedStatistics.put(type, sortedStats); + stat.add(task.durationNanos); } } - private String currentPathMapping(String input) { + private String pathMapping(String workSpaceName, String input) { if (workSpaceName.isEmpty()) { return input; } else { diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java index 30eeb76111..b21d7ca20d 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java @@ -26,6 +26,7 @@ import com.google.devtools.build.lib.profiler.ProfilerTask; import com.google.devtools.build.lib.profiler.output.HtmlCreator; import com.google.devtools.build.lib.profiler.output.PhaseText; import com.google.devtools.build.lib.profiler.statistics.CriticalPathStatistics; +import com.google.devtools.build.lib.profiler.statistics.MultiProfileStatistics; import com.google.devtools.build.lib.profiler.statistics.PhaseStatistics; import com.google.devtools.build.lib.profiler.statistics.PhaseSummaryStatistics; import com.google.devtools.build.lib.runtime.BlazeCommand; @@ -42,6 +43,7 @@ import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsProvider; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.PrintStream; import java.util.EnumMap; @@ -75,6 +77,16 @@ public final class ProfileCommand implements BlazeCommand { ) public boolean chart; + @Option( + name = "combine", + defaultValue = "null", + help = + "If present, the statistics of all given profile files will be combined and output" + + " in text/--html format to the file named in the argument. Does not output HTML" + + " task charts." + ) + public String combine; + @Option(name = "dump", abbrev='d', converter = DumpConverter.class, @@ -178,65 +190,102 @@ public final class ProfileCommand implements BlazeCommand { env.getReporter().handle(Event.warn( null, "This information is intended for consumption by Blaze developers" + " only, and may change at any time. Script against it at your own risk")); - - for (String name : options.getResidue()) { - Path profileFile = runtime.getWorkingDirectory().getRelative(name); - try { - ProfileInfo info = ProfileInfo.loadProfileVerbosely( - profileFile, getInfoListener(env)); - - if (opts.dumpMode == null || !opts.dumpMode.contains("unsorted")) { - ProfileInfo.aggregateProfile(info, getInfoListener(env)); - } - - if (opts.taskTree != null) { - printTaskTree(out, name, info, opts.taskTree, opts.taskTreeThreshold); - continue; - } - - if (opts.dumpMode != null) { - dumpProfile(info, out, opts.dumpMode); - continue; - } - - PhaseSummaryStatistics phaseSummaryStatistics = new PhaseSummaryStatistics(info); - EnumMap<ProfilePhase, PhaseStatistics> phaseStatistics = - new EnumMap<>(ProfilePhase.class); - for (ProfilePhase phase : ProfilePhase.values()) { - phaseStatistics.put( - phase, new PhaseStatistics(phase, info, runtime.getWorkspaceName())); - } - + if (opts.combine != null && opts.dumpMode == null) { + MultiProfileStatistics statistics = + new MultiProfileStatistics( + runtime.getWorkingDirectory(), + runtime.getWorkspaceName(), + options.getResidue(), + getInfoListener(env), + opts.vfsStatsLimit > 0); + Path outputFile = runtime.getWorkingDirectory().getRelative(opts.combine); + try (PrintStream output = + new PrintStream(new BufferedOutputStream(outputFile.getOutputStream()))) { if (opts.html) { - Path htmlFile = - profileFile.getParentDirectory().getChild(profileFile.getBaseName() + ".html"); - - env.getReporter().handle(Event.info("Creating HTML output in " + htmlFile)); - + env.getReporter().handle(Event.info("Creating HTML output in " + outputFile)); HtmlCreator.create( - info, - htmlFile, - phaseSummaryStatistics, - phaseStatistics, - opts.htmlDetails, - opts.htmlPixelsPerSecond, - opts.vfsStatsLimit, - opts.chart, - opts.htmlHistograms); + output, statistics, opts.htmlDetails, opts.htmlPixelsPerSecond, opts.vfsStatsLimit); } else { - CriticalPathStatistics critPathStats = new CriticalPathStatistics(info); + env.getReporter().handle(Event.info("Creating text output in " + outputFile)); new PhaseText( - out, - phaseSummaryStatistics, - phaseStatistics, - Optional.of(critPathStats), - info.getMissingActionsCount(), + output, + statistics.getSummaryStatistics(), + statistics.getSummaryPhaseStatistics(), + Optional.<CriticalPathStatistics>absent(), + statistics.getMissingActionsCount(), opts.vfsStatsLimit) .print(); } } catch (IOException e) { - env.getReporter().handle(Event.error( - null, "Failed to process file " + name + ": " + e.getMessage())); + env + .getReporter() + .handle( + Event.error( + "Failed to write to output file " + outputFile + ":" + e.getMessage())); + } + } else { + for (String name : options.getResidue()) { + Path profileFile = runtime.getWorkingDirectory().getRelative(name); + try { + ProfileInfo info = ProfileInfo.loadProfileVerbosely(profileFile, getInfoListener(env)); + + if (opts.dumpMode == null || !opts.dumpMode.contains("unsorted")) { + ProfileInfo.aggregateProfile(info, getInfoListener(env)); + } + + if (opts.taskTree != null) { + printTaskTree(out, name, info, opts.taskTree, opts.taskTreeThreshold); + continue; + } + + if (opts.dumpMode != null) { + dumpProfile(info, out, opts.dumpMode); + continue; + } + + PhaseSummaryStatistics phaseSummaryStatistics = new PhaseSummaryStatistics(info); + EnumMap<ProfilePhase, PhaseStatistics> phaseStatistics = + new EnumMap<>(ProfilePhase.class); + for (ProfilePhase phase : ProfilePhase.values()) { + phaseStatistics.put( + phase, + new PhaseStatistics( + phase, info, runtime.getWorkspaceName(), opts.vfsStatsLimit > 0)); + } + + if (opts.html) { + Path htmlFile = + profileFile.getParentDirectory().getChild(profileFile.getBaseName() + ".html"); + + env.getReporter().handle(Event.info("Creating HTML output in " + htmlFile)); + + HtmlCreator.create( + info, + htmlFile, + phaseSummaryStatistics, + phaseStatistics, + opts.htmlDetails, + opts.htmlPixelsPerSecond, + opts.vfsStatsLimit, + opts.chart, + opts.htmlHistograms); + } else { + CriticalPathStatistics critPathStats = new CriticalPathStatistics(info); + new PhaseText( + out, + phaseSummaryStatistics, + phaseStatistics, + Optional.of(critPathStats), + info.getMissingActionsCount(), + opts.vfsStatsLimit) + .print(); + } + } catch (IOException e) { + System.out.println(e); + env + .getReporter() + .handle(Event.error("Failed to analyze profile file(s): " + e.getMessage())); + } } } } diff --git a/src/main/java/com/google/devtools/build/lib/util/TimeUtilities.java b/src/main/java/com/google/devtools/build/lib/util/TimeUtilities.java index 13de97cc82..2a1fbebc72 100644 --- a/src/main/java/com/google/devtools/build/lib/util/TimeUtilities.java +++ b/src/main/java/com/google/devtools/build/lib/util/TimeUtilities.java @@ -14,6 +14,8 @@ package com.google.devtools.build.lib.util; +import java.util.concurrent.TimeUnit; + /** * Various utility methods operating on time values. */ @@ -38,4 +40,14 @@ public class TimeUtilities { } return String.format("%.3f s", ms / 1000.0); } + + /** + * Convert nanoseconds to milliseconds. + * + * <p>This is different from the methods in {@link TimeUnit} in that it returns and accepts a + * double. + */ + public static double nanosToMillis(double timeNanos) { + return timeNanos / 1000000; + } } |