// 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; import java.util.Map.Entry; /** * 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 = "skylarkData"; 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.skylark-histogram {"); lnPrint(" width: 95%; margin: 0 auto; display: none;"); lnPrint("}"); lnPrint("div.skylark-chart {"); lnPrint(" width: 100%; height: 200px; margin: 0 auto 2em;"); lnPrint("}"); lnPrint("div.skylark-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 (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 (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='skylark_stats'/>"); lnElement("h3", "Skylark Statistics"); lnElement("p", "All duration columns in milliseconds, except where noted otherwise."); lnElement("h4", "User-Defined function execution time"); lnOpen("div", "class", "skylark-histogram", "id", "user-histogram"); lnElement("div", "class", "skylark-chart", "id", "user-chart"); lnElement("button", "id", "user-close", "Hide"); lnClose(); // div user-histogram lnElement("div", "class", "skylark-table", "id", "user_function_stats"); lnElement("h4", "Compiled function execution time"); lnOpen("div", "class", "skylark-histogram", "id", "compiled-histogram"); lnElement("div", "class", "skylark-chart", "id", "compiled-chart"); lnElement("button", "id", "user-close", "Hide"); lnClose(); // div compiled-histogram lnElement("div", "class", "skylark-table", "id", "compiled_function_stats"); lnElement("h4", "Builtin function execution time"); lnOpen("div", "class", "skylark-histogram", "id", "builtin-histogram"); lnElement("div", "class", "skylark-chart", "id", "builtin-chart"); lnElement("button", "id", "builtin-close", "Hide"); lnClose(); // div builtin-histogram lnElement("div", "class", "skylark-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)); } }