blob: 834a0ccf94ac943ef76b809a73d9b449a9a6f535 [file] [log] [blame]
// 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.common.base.Joiner;
import com.google.common.base.StandardSystemProperty;
import com.google.devtools.build.lib.profiler.statistics.SkylarkStatistics;
import com.google.devtools.build.lib.profiler.statistics.TasksStatistics;
import com.google.devtools.build.lib.util.LongArrayList;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Map;
/**
* Formats {@link SkylarkStatistics} as HTML tables and histogram charts.
*/
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 = "starlarkData";
private static final String JS_TABLE_VAR = JS_DATA_VAR + "Table";
private final SkylarkStatistics stats;
private final boolean printHistograms;
public SkylarkHtml(PrintStream out, SkylarkStatistics stats) {
this(out, stats, true);
}
public SkylarkHtml(PrintStream out, SkylarkStatistics stats, boolean printHistograms) {
super(out);
this.stats = stats;
this.printHistograms = printHistograms;
}
/**
* Prints all CSS definitions and JavaScript code. May be a large amount of output.
*/
void printHtmlHead() {
lnOpen("style", "type", "text/css", "<!--");
lnPrint("div.starlark-histogram {");
lnPrint(" width: 95%; margin: 0 auto; display: none;");
lnPrint("}");
lnPrint("div.starlark-chart {");
lnPrint(" width: 100%; height: 200px; margin: 0 auto 2em;");
lnPrint("}");
lnPrint("div.starlark-table {");
lnPrint(" width: 95%; margin: 0 auto;");
lnPrint("}");
lnPrint("-->");
close(); // style
lnOpen("script", "type", "text/javascript");
lnPrintf("var %s = {};\n", JS_DATA_VAR);
lnPrintf("var %s = {};\n", JS_TABLE_VAR);
lnPrint("var histogramData;");
if (printHistograms) {
lnPrint("var options = {");
down();
lnPrint("isStacked: true,");
lnPrint("legend: { position: 'none' },");
lnPrint("hAxis: { },");
lnPrint("histogram: { lastBucketPercentile: 5 },");
lnPrint("vAxis: { title: '# calls', viewWindowMode: 'pretty', gridlines: { count: -1 } }");
up();
lnPrint("};");
}
lnPrint("function selectHandler(category) {");
down();
lnPrint("return function() {");
down();
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);", 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;");
lnPrint("var histData = histogramData[category][key];");
lnPrint("var fnOptions = JSON.parse(JSON.stringify(options));");
lnPrint("fnOptions.title = loc + '#' + func;");
lnPrint("var chartDiv = document.getElementById(category+'-chart');");
lnPrint("var chart = new google.visualization.Histogram(chartDiv);");
lnPrint("histogramDiv.style.display = 'block';");
lnPrint("chart.draw(histData, fnOptions);");
} else {
lnPrint("var chartDiv = document.getElementById(category+'-chart');");
lnPrint("chartDiv.innerHTML = '<h3>' + loc + '#' + func + '</h3>';");
lnPrint("chartDiv.style.height = 'auto';");
lnPrint("histogramDiv.style.display = 'block';");
}
up();
lnPrint("}");
up();
lnPrint("};");
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.getCompiledUserFunctionStatistics(),
stats.getCompiledUserFunctionSelfStatistics(),
"compiled",
stats.getCompiledUserTotalNanos());
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('#compiled-close').onclick = function() {");
lnPrint(" document.querySelector('#compiled-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();
printHistogramData(stats.getBuiltinFunctionDurations(), "builtin");
printHistogramData(stats.getUserFunctionDurations(), "user");
printHistogramData(stats.getCompiledUserFunctionDurations(), "compiled");
up();
lnPrint("}");
}
private void printHistogramData(Map<String, LongArrayList> functionDurations, String category) {
lnPrintf("'%s': {", category);
down();
for (Map.Entry<String, LongArrayList> entry : functionDurations.entrySet()) {
String function = entry.getKey();
LongArrayList durations = entry.getValue();
lnPrintf("'%s': google.visualization.arrayToDataTable(", function);
lnPrint("[['duration']");
for (int index = 0; index < durations.size(); index++) {
printf(",[%f]", durations.get(index) / 1000000.);
}
lnPrint("], false),");
}
up();
lnPrint("},");
}
private void printStatsJs(
Map<String, TasksStatistics> taskStatistics,
Map<String, TasksStatistics> taskSelfStatistics,
String category,
long totalNanos) {
String tmpVar = category + JS_DATA_VAR;
lnPrintf("var statsDiv = document.getElementById('%s_function_stats');", category);
if (taskStatistics.isEmpty()) {
lnPrint(
"statsDiv.innerHTML = '<i>No relevant function calls to display. Some minor"
+ " builtin functions may have been ignored because their names could not be used"
+ " as variables in JavaScript.</i>'");
} else {
lnPrintf("var %s = new google.visualization.DataTable();", tmpVar);
lnPrintf("%s.addColumn('string', 'Location');", tmpVar);
lnPrintf("%s.addColumn('string', 'Function');", tmpVar);
lnPrintf("%s.addColumn('number', 'count');", tmpVar);
lnPrintf("%s.addColumn('number', 'min');", tmpVar);
lnPrintf("%s.addColumn('number', 'mean');", tmpVar);
lnPrintf("%s.addColumn('number', 'mean self');", tmpVar);
lnPrintf("%s.addColumn('number', 'median');", tmpVar);
lnPrintf("%s.addColumn('number', 'median self');", tmpVar);
lnPrintf("%s.addColumn('number', 'max');", tmpVar);
lnPrintf("%s.addColumn('number', 'max self');", tmpVar);
lnPrintf("%s.addColumn('number', 'std dev');", tmpVar);
lnPrintf("%s.addColumn('number', 'self');", tmpVar);
lnPrintf("%s.addColumn('number', 'self (%%)');", tmpVar);
lnPrintf("%s.addColumn('number', 'total');", tmpVar);
lnPrintf("%s.addColumn('number', 'relative (%%)');", tmpVar);
lnPrintf("%s.addRows([", tmpVar);
down();
for (Map.Entry<String, TasksStatistics> entry : taskStatistics.entrySet()) {
String function = entry.getKey();
TasksStatistics stats = entry.getValue();
TasksStatistics selfStats = taskSelfStatistics.get(function);
double relativeTotal = (double) stats.totalNanos / totalNanos;
double relativeSelf = (double) selfStats.totalNanos / stats.totalNanos;
String[] split = stats.name.split("#");
String location;
String name;
if (split.length > 1) {
location = split[0];
name = split[1];
} else {
location = "(unknown)";
name = split[0];
}
lnPrintf("[{v:'%s', f:'%s'}, ", location, abbreviatePath(location));
printf("'%s', ", name);
printf("%d, ", stats.count);
printf("%.3f, ", stats.minimumMillis());
printf("%.3f, ", stats.meanMillis());
printf("%.3f, ", selfStats.meanMillis());
printf("%.3f, ", stats.medianMillis());
printf("%.3f, ", selfStats.medianMillis());
printf("%.3f, ", stats.maximumMillis());
printf("%.3f, ", selfStats.maximumMillis());
printf("%.3f, ", stats.standardDeviationMillis);
printf("%.3f, ", selfStats.totalMillis());
printf("{v:%.4f, f:'%.3f %%'}, ", relativeSelf, relativeSelf * 100);
printf("%.3f,", stats.totalMillis());
printf("{v:%.4f, f:'%.3f %%'},", relativeTotal, relativeTotal * 100);
printf("],");
}
lnPrint("]);");
up();
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'));",
JS_TABLE_VAR,
category,
category);
lnPrintf(
"%s.%s.draw(%s.%s, {showRowNumber: true, width: '100%%', height: '100%%'});",
JS_TABLE_VAR,
category,
JS_DATA_VAR,
category);
}
}
/**
* Prints two sections for histograms and tables of statistics for user-defined and built-in
* Skylark functions.
*/
void printHtmlBody() {
lnPrint("<a name='starlark_stats'/>");
lnElement("h3", "Starlark Statistics");
lnElement("p", "All duration columns in milliseconds, except where noted otherwise.");
lnElement("h4", "User-Defined function execution time");
lnOpen("div", "class", "starlark-histogram", "id", "user-histogram");
lnElement("div", "class", "starlark-chart", "id", "user-chart");
lnElement("button", "id", "user-close", "Hide");
lnClose(); // div user-histogram
lnElement("div", "class", "starlark-table", "id", "user_function_stats");
lnElement("h4", "Compiled function execution time");
lnOpen("div", "class", "starlark-histogram", "id", "compiled-histogram");
lnElement("div", "class", "starlark-chart", "id", "compiled-chart");
lnElement("button", "id", "user-close", "Hide");
lnClose(); // div compiled-histogram
lnElement("div", "class", "starlark-table", "id", "compiled_function_stats");
lnElement("h4", "Builtin function execution time");
lnOpen("div", "class", "starlark-histogram", "id", "builtin-histogram");
lnElement("div", "class", "starlark-chart", "id", "builtin-chart");
lnElement("button", "id", "builtin-close", "Hide");
lnClose(); // div builtin-histogram
lnElement("div", "class", "starlark-table", "id", "builtin_function_stats");
}
/**
* Computes a string keeping the structure of the input but reducing the amount of characters on
* elements at the front if necessary.
*
* <p>Reduces the length of function location strings by keeping at least the last element fully
* intact and at most {@link #NUM_LOCATION_CHARS_UNABBREVIATED} from other
* elements from the end. Elements before are abbreviated with their first two characters.
*
* <p>Example:
* "//source/tree/with/very/descriptive/and/long/hierarchy/of/directories/longfilename.bzl:42"
* becomes: "//so/tr/wi/ve/de/an/lo/hierarch/of/directories/longfilename.bzl:42"
*
* <p>There is no fixed length to the result as the last element is kept and the location may
* have many elements.
*
* @param location Either a sequence of path elements separated by
* {@link StandardSystemProperty#FILE_SEPARATOR} and preceded by some root element
* (e.g. "/", "C:\") or path elements separated by "." and having no root element.
*/
private String abbreviatePath(String location) {
String[] elements;
int lowestAbbreviateIndex;
String root;
String separator = StandardSystemProperty.FILE_SEPARATOR.value();
if (location.contains(separator)) {
elements = location.split(separator);
// must take care to preserve file system roots (e.g. "/", "C:\"), keep separate
lowestAbbreviateIndex = 1;
root = location.substring(0, location.indexOf(separator) + 1);
} else {
// must be java class name for a builtin function
elements = location.split("\\.");
lowestAbbreviateIndex = 0;
root = "";
separator = ".";
}
String last = elements[elements.length - 1];
int remaining = NUM_LOCATION_CHARS_UNABBREVIATED - last.length();
// start from the next to last element of the location and add until "remaining" many
// chars added, abbreviate rest with first 2 characters
for (int index = elements.length - 2; index >= lowestAbbreviateIndex; index--) {
String element = elements[index];
if (remaining > 0) {
int length = Math.min(remaining, element.length());
element = element.substring(0, length);
remaining -= length;
} else {
element = element.substring(0, Math.min(2, element.length()));
}
elements[index] = element;
}
return root + Joiner.on(separator).join(Arrays.asList(elements).subList(1, elements.length));
}
}