Move the timings-specific pieces of the buildbot infrastructure into
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 18 Dec 2009 08:03:50 +0000 (08:03 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 18 Dec 2009 08:03:50 +0000 (08:03 +0000)
the trunk/timings directory.  We'll map them into the buildbot directory
using svn:externals.  This will let us keep all the pieces of a timing
configuration, including its buildbot pieces, in one place, and will
let us simplify the Master initialization (since it will be able to look
on-disk for the configurations for which it should set up buildbot steps,
instead of querying the SVN server).

git-svn-id: http://scons.tigris.org/svn/scons/trunk@4562 fdb21ef1-2011-0410-befe-b5e4ea1792b1

timings/changelog.html [new file with mode: 0644]
timings/graph.html [new file with mode: 0644]
timings/index.html [new file with mode: 0644]
timings/js/common.js [new file with mode: 0644]
timings/js/coordinates.js [new file with mode: 0644]
timings/js/plotter.js [new file with mode: 0644]

diff --git a/timings/changelog.html b/timings/changelog.html
new file mode 100644 (file)
index 0000000..586ebad
--- /dev/null
@@ -0,0 +1,204 @@
+<html>
+<head>
+<script src="js/common.js"></script>
+<style type="text/css">
+table {
+  border-collapse: collapse;
+}
+thead {
+  border-top: solid 1px gray;
+  border-left: solid 1px gray;
+}
+tbody {
+  border-top: solid 1px gray;
+  border-bottom: solid 1px gray;
+  border-left: solid 1px gray;
+}
+th {
+  text-align: center;
+  border-right: solid 1px gray;
+}
+td {
+  padding-left: 0.5em;
+  padding-top: 0.3em;
+  padding-bottom: 0.3em;
+  padding-right: 1.4em;
+  border-top: solid 1px gray;
+  vertical-align: top;
+  font-family: monospace;
+}
+form {
+  background-color: lightblue;
+  border: 1px solid gray;
+  padding: 2px;
+}
+iframe#content {
+  border: none;
+  width: 0px;
+  height: 0px;
+}
+/*
+form {
+  position: fixed;
+  left: 0px;
+  top: 0px;
+  width: 100%;
+}
+*/
+</style>
+</head>
+<body>
+<form name="ui">
+  SVN path: <input id="url" type="text" name="url" value="">
+  SVN revision range: <input id="range" type="text" name="range" value="">
+  <input id="mode_text" type="radio" name="mode" value="text">text
+  <input id="mode_html" type="radio" name="mode" value="html">html
+  <input type="submit" value="Show Changelog">
+</form>
+
+<script>
+params = ParseParams();
+
+function fix_text(str, n) {
+  if (str.length > n)
+    return str.substring(0, n);
+
+  for (var i = str.length; i < n; ++i)
+    str = str + ' ';
+  return str;
+}
+
+function get_entries() {
+  return content.contentDocument.getElementsByTagName("logentry");
+}
+
+function get_info(entry) {
+  var r = new Object;
+  r.rev = entry.getAttribute("revision");
+  r.author = entry.getElementsByTagName("author")[0].textContent;
+  r.msg = entry.getElementsByTagName("msg")[0].textContent;
+  r.paths = [];
+  var paths = entry.getElementsByTagName("path")
+  for (var i = 0; i < paths.length; ++i) {
+    r.paths.push({"action" : paths[i].getAttribute("action"),
+                  "value"  : paths[i].textContent});
+  }
+  return r;
+}
+
+function render_log_callback() {
+  if ("mode" in params && params.mode == "text") {
+    var out = document.createElement("PRE");
+    document.body.appendChild(out);
+
+    var entries = get_entries();
+    for (var i = 0; i < entries.length; ++i) {
+      var info = get_info(entries[i]);
+
+      var msg = info.msg;
+      msg = msg.replace(/\n/g, ' ' );
+      msg = msg.replace(/\t/g, ' ' );
+      while (msg.charAt(0) == ' ')
+        msg = msg.substring(1);
+      
+      var msg_clipped = msg.substring(0, 66);
+      if (msg_clipped.length < msg.length)
+        msg_clipped = msg_clipped + "...";
+
+      out.appendChild(document.createTextNode(
+          fix_text(info.rev, 6) + " " +
+          fix_text(info.author, 8) + " " +
+          msg_clipped + "\n"));
+    }
+  } else {
+    var table = document.createElement("TABLE");
+    table.setAttribute("class", "log");
+    document.body.appendChild(table);
+
+    var entries = get_entries();
+    for (var i = 0; i < entries.length; ++i) {
+      var info = get_info(entries[i]);
+
+      var tr = document.createElement("TR");
+      table.appendChild(tr);
+
+      var td, a;
+
+      // revision:
+      td = document.createElement("TD");
+      tr.appendChild(td);
+
+      a = document.createElement("A");
+      a.setAttribute("href", "http://scons.tigris.org/source/browse/scons?view=rev&revision=" + info.rev);
+      a.appendChild(document.createTextNode(info.rev));
+
+      td.appendChild(a);
+
+      // author:
+      td = document.createElement("TD");
+      tr.appendChild(td);
+
+      a = document.createElement("A");
+      a.setAttribute("href", "mailto:" + info.author);
+      a.appendChild(document.createTextNode(info.author));
+
+      td.appendChild(a);
+
+      // details:
+      td = document.createElement("TD");
+      tr.appendChild(td);
+
+      var p = document.createElement("PRE");
+      td.appendChild(p);
+
+      var s = info.msg;
+      p.appendChild(document.createTextNode(s));
+
+      for (var j = 0; j < info.paths.length; ++j) {
+        td.appendChild(document.createTextNode(info.paths[j]["action"] + " - "))
+        var a = document.createElement("A");
+        a.setAttribute("href", "http://scons.tigris.org/source/browse/scons" + info.paths[j]["value"] + "?r1=" + info.rev + "&r2=" + (info.rev - 1) + "&pathrev=" + info.rev);
+        a.appendChild(document.createTextNode(info.paths[j]["value"]));
+        td.appendChild(a);
+        td.appendChild(document.createElement("BR"));
+      }
+    }
+  }
+}
+
+function render_log() {
+  var svn_url = params["url"];
+  var svn_range = params["range"];
+  if (svn_url == undefined || svn_range == undefined)
+    return;
+
+  var url = "http://" + location.host + "/cgi-bin/svn-log?url=http://codf21.jail/svn/" +
+      unescape(svn_url) + "&range=" + unescape(svn_range);
+
+  // global 'content' variable: a hidden iframe used to fetch svn data.
+  content = document.createElement("IFRAME");
+  content.setAttribute("id", "content");
+  content.setAttribute("onload", "render_log_callback()");
+  content.setAttribute("src", url);
+  document.body.appendChild(content);
+
+  var el;
+  if ("mode" in params && params["mode"] == "text") {
+    el = document.getElementById("mode_text");
+  } else {
+    el = document.getElementById("mode_html");
+  }
+  el.setAttribute("checked", "1");
+
+  el = document.getElementById("url");
+  el.setAttribute("value", unescape(svn_url));
+
+  el = document.getElementById("range");
+  el.setAttribute("value", unescape(svn_range));
+}
+
+render_log()
+</script>
+</body>
+</html>
+
diff --git a/timings/graph.html b/timings/graph.html
new file mode 100644 (file)
index 0000000..e418069
--- /dev/null
@@ -0,0 +1,411 @@
+<html>
+
+<!--
+  Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
+  Use of this source code is governed by a BSD-style license that can be
+  found in the LICENSE file.
+-->
+
+<!--
+  A brief note on terminology as used here: a "graph" is a plotted screenful
+  of data, showing the results of one type of test: for example, the
+  page-load-time graph.  A "trace" is a single line on a graph, showing one
+  one for the test: for example, the reference build trace on the
+  page-load-time graph.
+
+  This page plots arbitrary numerical data loaded from files in a specific
+  format.  It uses two or more data files, all JSON-encoded:
+
+    graphs.dat: a list of objects, each with these properties: name (the name
+        of a graph) and units (the units for the data to be read by humans).
+        Schematically:
+          [{"name": <graph_name>, "units": <units>}, ...]
+
+    <graphname>-summary.dat: for each of the graphs listed in graphs.dat, the
+        corresponding summary file holds rows of data. Each row of data is an
+        object with several properties:
+          "rev": the revision number for this row of data
+          "traces": an object with several properties of its own. The name of
+              the property corresponds to a trace name, used only as an
+              internal identifier, and the property's value is an array of
+              its measurement and that measurement's standard deviation (or
+              other measurement error).
+        Schematically:
+          {"rev": <rev>,
+           "traces": {<trace_name1>: [<value1>, <stddev1>],
+                      <trace_name2>: [<value2>, <stddev2>], ...}
+          }
+-->
+<head>
+<style>
+body {
+  font-family: sans-serif;
+}
+div#output {
+  cursor: pointer;
+}
+div#switcher {
+  cursor: pointer;
+}
+div#switcher a {
+  border-top: 1px solid black;
+  border-left: 1px solid black;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+canvas.plot {
+  border: 1px solid black;
+}
+div.plot-coordinates {
+  font-family: monospace;
+}
+iframe {
+  display: none;
+  width: 100%;
+  height: 100%;
+  border: none;
+}
+div.selector {
+  border: solid 1px black;
+  cursor: pointer;
+  padding-left: 0.3em;
+  background-color: white;
+}
+div.selector:hover {
+  background-color: rgb(200,200,250);
+}
+div.selected {
+  border-left: none;
+}
+div#selectors {
+  width: 80px;
+  display: none;
+}
+#explain {
+  font-size: 0.75em;
+  font-style: italic;
+  color: rgb(100,100,100);
+}
+</style>
+
+<script src="js/common.js"></script>
+<script src="js/plotter.js"></script>
+<script src="js/coordinates.js"></script>
+<!-- <script src="config.js"></script> -->
+<script>
+var Config = {
+  'title': "TODO title",
+  'source': "http://scons.tigris.org/svn/scons/trunk",
+  'changeLinkPrefix': "changelog.html?mode=html&range=",
+  'builder': "TODO ",
+  'builderLink': "http://buildbot.scons.org:8010/",
+  'detailTabs': ['view-change'],
+};
+document.title = Config.title + ' - ' + Config.buildslave;
+
+var did_position_details = false;
+var units = 'thing-a-ma-bobs';
+var graph_list = [];
+var first_trace = '';
+
+var params = ParseParams();
+if (!('history' in params)) {
+  params.history = 150;
+  // make this option somewhat user discoverable :-/
+  window.location.href = MakeURL(params);
+}
+
+function jsonToJs(data) {
+  return eval('(' + data + ')')
+}
+
+function report_error(error) {
+  document.getElementById("output").innerHTML = "<p>" + error + "</p>";
+}
+
+function received_graph_list(data, error) {
+  if (error) {
+    report_error(error);
+    return;
+  }
+  graph_list = jsonToJs(data);
+
+  if (!('graph' in params) || params.graph == '') {
+    if (graph_list.length > 0)
+      params.graph = graph_list[0].name
+  }
+
+  // Add a selection tab for each graph, and find the units for the selected
+  // one while we're at it.
+  tabs = [];
+  for (var index = 0; index < graph_list.length; ++index) {
+    var graph = graph_list[index];
+    tabs.push(graph.name);
+    if (graph.name == params.graph)
+      units = graph.units;
+  }
+  initPlotSwitcher(tabs);
+
+  // Fetch the data for the selected graph.
+  fetch_summary();
+}
+
+function go_to(graph) {
+  params.graph = graph;
+  if (params.graph == '')
+    delete params.graph;
+  window.location.href = MakeURL(params);
+}
+
+function get_url() {
+  new_url = window.location.href;
+  new_url = new_url.replace(/50/, "150");
+  new_url = new_url.replace(/\&lookout/, "");
+  return new_url;
+}
+
+function on_clicked_plot(prev_cl, cl) {
+  if ('lookout' in params) {
+    window.open(get_url());
+    return;
+  }
+
+  // Define sources for detail tabs
+  if ('view-change' in Config.detailTabs) {
+    document.getElementById('view-change').
+        setAttribute('src', Config.changeLinkPrefix + prev_cl + ':' + cl);
+  }
+  if ('view-pages' in Config.detailTabs) {
+    document.getElementById('view-pages').
+        setAttribute('src', 'details.html?cl=' + cl + '&trace=' + first_trace);
+  }
+  if ('view-coverage' in Config.detailTabs) {
+    document.getElementById('view-coverage').
+        setAttribute('src', Config.coverageLinkPrefix + cl);
+  }
+
+  if (!did_position_details) {
+    position_details();
+    did_position_details = true;
+  }
+}
+
+function received_summary(data, error) {
+  if (error) {
+    report_error(error);
+    return;
+  }
+  // Parse the summary data file.
+  var rows = data.split('\n');
+  var max_rows = rows.length;
+  if (max_rows > params.history)
+    max_rows = params.history;
+
+  var allTraces = {};
+
+  // graphData[rev] = {trace1:[value, stddev], trace2:[value, stddev], ...}
+  var graphData = {};
+  for (var i = 0; i < max_rows; ++i) {
+    if (!rows[i].length)
+      continue;
+    var row = jsonToJs(rows[i]);
+    var traces = row['traces'];
+    var revision = parseInt(row['rev']);
+    graphData[revision] = traces;
+
+    // Collect unique trace names.
+    for (var traceName in traces)
+      allTraces[traceName] = 1;
+  }
+
+  // Build a list of all the trace names we've seen, in the order in which
+  // they appear in the data file. Although JS objects are not required by
+  // the spec to iterate their properties in order, in practice they do,
+  // because it causes compatibility problems otherwise.
+  var traceNames = [];
+  for (var traceName in allTraces)
+    traceNames.push(traceName);
+
+  first_trace = traceNames[0];
+
+  // Build and numerically sort a list of revision numbers.
+  var revisionNumbers = [];
+  for (var rev in graphData)
+    revisionNumbers.push(rev);
+  revisionNumbers.sort(
+      function(a, b) { return parseInt(a, 10) - parseInt(b, 10) });
+
+  // Build separate ordered lists of trace data.
+  var traceData = {};
+  for (var revIndex = 0; revIndex < revisionNumbers.length; ++revIndex) {
+    var rev = revisionNumbers[revIndex];
+    var revisionData = graphData[rev];
+    for (var nameIndex = 0; nameIndex < traceNames.length; ++nameIndex) {
+      var traceName = traceNames[nameIndex];
+      if (!traceData[traceName])
+        traceData[traceName] = [];
+      if (!revisionData[traceName])
+        traceData[traceName].push([NaN, NaN]);
+      else
+        traceData[traceName].push(revisionData[traceName]);
+    }
+  }
+  var plotData = [];
+  for (var traceName in traceData)
+    plotData.push(traceData[traceName]);
+
+  var plotter = new Plotter(revisionNumbers, plotData, traceNames, units,
+    document.getElementById("output"), true);
+  plotter.onclick = on_clicked_plot;
+  plotter.plot();
+}
+
+function fetch_summary() {
+  if ('graph' in params)
+    file = escape(params.graph) + ".dat"
+  else
+    file = "summary.dat"
+  Fetch(file, received_summary);
+}
+
+function fetch_graph_list() {
+  Fetch("graphs.dat", received_graph_list);
+}
+
+function initPlotSwitcher(tabs) {
+  var switcher = document.getElementById("switcher");
+  for(var i = 0; i < tabs.length; i++) {
+    var anchor = document.createElement("a");
+    anchor.appendChild(document.createTextNode(tabs[i] + " "));
+    anchor.addEventListener("click", goToClosure(tabs[i]), false);
+    switcher.appendChild(anchor);
+  }
+}
+
+function goToClosure(graph) {
+  return function(){go_to(graph)};
+}
+
+function position_details() {
+  var output = document.getElementById("output");
+
+  var win_height = window.innerHeight;
+
+  var details = document.getElementById("views");
+
+  var views = document.getElementById("views");
+  var selectors = document.getElementById("selectors");
+  selectors.style.display = "block";
+
+  var views_width = output.offsetWidth - selectors.offsetWidth;
+
+  views.style.border = "1px solid black";
+  views.style.width = views_width + "px";
+  views.style.height = (win_height - output.offsetHeight - output.offsetTop -
+                        30) + "px";
+
+  selectors.style.position = "absolute";
+  selectors.style.left = (views.offsetLeft + views_width + 1) + "px";
+  selectors.style.top = views.offsetTop + "px";
+
+  // Change to the first detail tab
+  for (var tab in Config.detailTabs) {
+    change_view(tab);
+    break; 
+  }
+}
+
+function change_view(target) {
+  for (var tab in Config.detailTabs) {
+    document.getElementById(tab).style.display = 
+        (tab == target ? "block" : "none");
+  }
+}
+
+function init() {
+  // We need to fill the graph list before parsing the params or fetching the
+  // data, so we have a default graph in case none was specified.
+  fetch_graph_list();
+}
+
+window.addEventListener("load", init, false);
+</script>
+</head>
+
+
+<body>
+<div id="header_lookout" align="center">
+  <font style='color: #0066FF; font-family: Arial, serif;
+               font-size: 20pt; font-weight: bold;'>
+    <script>
+      document.write("<a target=\"_blank\" href=\"");
+      document.write(get_url());
+      document.write("\">");
+      if ('header' in params && params.header != '') {
+        document.write(escape(params.header));
+      } else {
+        document.write(Config.title);
+      }
+      document.write("</a>");
+    </script>
+  </font>
+</div>
+
+<div id="header_text">
+Builds generated by the <a href="http://buildbot.chromium.org/">buildbot</a>
+are run through <b>
+<script>
+document.write(Config.title);
+</script>
+</b>and the results of that test are charted here.
+</div>
+
+<div id="explain">
+The vertical axis is measured values, and the horizontal
+axis is the revision number for the build being tested.
+</div>
+<p></p>
+<div id="switcher">
+
+</div>
+<div id="output"></div>
+<div id="details">
+  <div id="views">
+    <script>
+      for (var tab in Config.detailTabs) {
+        document.write("<iframe id=\"" + tab + "\"></iframe>");
+      }
+    </script>
+  </div>
+  <div id="selectors">
+    <script>
+      var firstTab = true;
+      for (var tab in Config.detailTabs) {
+        document.write("<div ");
+       if (firstTab) {
+         firstTab = false;
+       } else {
+         document.write("style=\"border-top: none\" ");
+       }
+       document.write("class=\"selector\" onclick=\"change_view('" 
+           + tab + "')\">" + Config.detailTabs[tab] + "</div>");
+      }
+    </script>
+  </div>
+</div>
+<pre id="log"></pre>
+<script>
+if ('lookout' in params) {
+  document.getElementById("switcher").style.display = "none";
+  document.getElementById("details").style.display = "none";
+  document.getElementById("header_text").style.display = "none";
+  document.getElementById("explain").style.display = "none";
+  if ('thumbnail' in params) {
+    document.getElementById("header_lookout").style.display = "none";
+  }
+} else {
+  document.getElementById("header_lookout").style.display = "none";
+}
+</script>
+</body>
+</html>
diff --git a/timings/index.html b/timings/index.html
new file mode 100644 (file)
index 0000000..de102ea
--- /dev/null
@@ -0,0 +1,196 @@
+<html>
+  <head>
+    <title>SCons Timings</title>
+    <script src="js/common.js"></script>
+    <script language="javascript">
+      var perf_url = DirName(window.location.href);
+      function DisplayGraph(name, heighth, width, thumbnail, graph) {
+        var params = ParseParams();
+        var url = perf_url + '/' + name + '/graph.html' +
+                  '?history=50' +
+                  '&lookout';
+        //var debug = url;
+        var debug = '';
+        if (typeof graph == 'undefined') {
+          if ('graph' in params) {
+            graph = params.graph;
+          }
+          url += '&header=';
+        } else if (graph != null) {
+          url += '&header=' + graph;
+        }
+        if (graph != null) {
+          url += '&graph=' + graph;
+        }
+        if (thumbnail) {
+          url += '&thumbnail';
+        }
+        document.write('<td>' + debug + '<iframe' +
+                       //' scrolling="no"' +
+                       ' height="' + heighth + '"' +
+                       ' width="' + width + '"' +
+                       ' src="' + url + '">' +
+                       '</iframe></td>');
+      }
+    </script>
+  </head>
+  <body>
+    <center>
+      <script language="javascript">
+        var params = ParseParams();
+        // TODO:  parameterize these in a .dat file
+        var builder_map = {
+            'Ubuntu': 'ubuntu-timings',
+        };
+        var graphs_map = {
+            'Performance': 'TimeSCons-elapsed',
+            'Memory': 'memory-final',
+        };
+        var test_map = {
+            'CPPPATH': 'CPPPATH',
+            'JTimer': 'JTimer',
+            'hundred': 'hundred',
+        };
+        if ('builder' in params) {
+          builder_list = params.builder.split(',');
+        } else {
+          builder_list = Keys(builder_map);
+        }
+        if ('graph' in params) {
+          graphs_list = param.graph.split(',');
+        } else {
+          graphs_list = Keys(graphs_map);
+        }
+        if ('test' in params) {
+          test_list = params.test.split(',');
+        } else {
+          test_list = Keys(test_map);
+        }
+        function write_builder_table() {
+          document.write('<table><tr>');
+          for (var tindex=0; tindex < test_list.length; tindex++) {
+            test = test_list[tindex];
+            test_name = test_map[test];
+            DisplayGraph(builder_id + '/' + test, 250, 400, false);
+            DisplayGraph(builder_id + '/' + test, 250, 400, false);
+            if ((tindex % 3) == 2) {
+              document.write('</tr><tr>');
+            }
+          }
+          document.write('</tr></table>');
+        }
+        function write_test_table() {
+          document.write('<table>');
+          row = [];
+          function write_row(row) {
+            document.write('<tr>');
+            for (var bidx in row) {
+              builder = row[bidx];
+              document.write('<th align="center"><p><br>' +
+                             builder + '</p></th>');
+            }
+            document.write('</tr><tr>');
+            for (var bidx in row) {
+              builder_id = builder_map[row[bidx]];
+              DisplayGraph(builder_id + '/' + test, 250, 400, true);
+            }
+            document.write('</tr>');
+          }
+          for (var bindex=0; bindex < builder_list.length; bindex++) {
+            builder = builder_list[bindex];
+            row.push(builder)
+            if ((bindex % 3) == 2) {
+              write_row(row);
+              row = [];
+            }
+          }
+          if (row.length > 0) {
+            write_row(row);
+          }
+          document.write('</table>');
+        }
+        function write_builders_header_row() {
+          document.write('<tr><th>&nbsp;</th>');
+          for (var bindex=0; bindex < builder_list.length; bindex++) {
+            builder = builder_list[bindex];
+            url = MakeURL({'builder':escape(builder)});
+            var s ='<th align="center" colspan="' +
+                   graphs_list.length +
+                   '">' +
+                   '<a href="' + url + '">' +
+                   builder +
+                   '</a></th>';
+            document.write(s);
+          }
+          document.write('</tr>');
+        }
+        function write_graphs_header_row() {
+          document.write('<tr><th>&nbsp;</th>');
+          for (var bindex=0; bindex < builder_list.length; bindex++) {
+            for (var gindex=0; gindex < graphs_list.length; gindex++) {
+              graph = graphs_list[gindex];
+              document.write('<th align="center">' + graph + '</th>');
+            }
+          }
+        }
+        function write_overview_table() {
+          document.write('<table>');
+          var width = document.body.scrollWidth / (builder_list.length + 2);
+          write_builders_header_row();
+          for (var tindex=0; tindex < test_list.length; tindex++) {
+            // Write a graphs header every four rows for navigability.
+            // TODO:  Use more sophisticated freezing of the header rows?
+            if ((tindex % 4) == 0) {
+              write_graphs_header_row();
+            }
+            test = test_list[tindex];
+            test_name = test_map[test];
+            document.write('<tr>');
+            url = MakeURL({'test':escape(test)});
+            document.write('<th valign="center"><a href="' + url + '">'
+                           + test_name + '</a></th>');
+            for (var bindex=0; bindex < builder_list.length; bindex++) {
+              builder = builder_list[bindex];
+              builder_id = builder_map[builder];
+              DisplayGraph(builder_id + '/' + test, 250, width, true, "TimeSCons-elapsed")
+              DisplayGraph(builder_id + '/' + test, 250, width, true, "memory-final")
+            }
+            document.write('<th valign="center"><a href="' + url + '">'
+                           + test_name + '</a></th>');
+            document.write('</tr>');
+          }
+          write_graphs_header_row();
+          write_builders_header_row();
+          document.write('</table>');
+        }
+        function write_header(header_string, url_string) {
+          document.write('<h1><a href="' + window.location.pathname + '">'
+                         + 'SCons Timings</a>');
+          if (header_string) {
+            document.write(':  ' + header_string);
+          }
+          if ('graph' in params) {
+            document.write(':  ' + escape(params.graph))
+          } else {
+            document.write(':  overview');
+          }
+          document.write('</h1>');
+        }
+        if (builder_list.length == 0) {
+          builder = builder_list[0];
+          builder_id = builder_map[builder];
+          write_header(builder, '&builder=' + escape(builder));
+          write_builder_table()
+        } else if (test_list.length == 1) {
+          test = test_list[0];
+          test_name = test_map[test];
+          write_header(test_name, '&test=' + escape(test));
+          write_test_table()
+        } else {
+          write_header('', '');
+          write_overview_table();
+        }
+      </script>
+    </center>
+  </body>
+</html>
diff --git a/timings/js/common.js b/timings/js/common.js
new file mode 100644 (file)
index 0000000..80510b3
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+  Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+  Use of this source code is governed by a BSD-style license that can be
+  found in the LICENSE file.
+*/
+
+/*
+  Common methods for performance-plotting JS.
+*/
+
+function Fetch(url, callback) {
+  var r = new XMLHttpRequest();
+  r.open("GET", url, true);
+  r.setRequestHeader("pragma", "no-cache");
+  r.setRequestHeader("cache-control", "no-cache");
+  r.onreadystatechange = function() {
+    if (r.readyState == 4) {
+      var error;
+      var text = r.responseText;
+      if (r.status != 200) {
+        error = url + ": " + r.status + ": " + r.statusText;
+      } else if (! text) {
+        error = url + ": null response";
+      }
+      callback(text, error);
+    }
+  }
+
+  r.send(null);
+}
+
+// Returns the keys of an object.
+function Keys(obj) {
+  result = [];
+  for (key in obj) {
+    result.push(key)
+  }
+  return result
+}
+
+// Returns the "directory name" portion of the string (URL),
+// stripping the last element.
+function DirName(s) {
+  elements = s.split('/')
+  elements.pop()
+  return elements.join('/')
+}
+
+// Returns an Object with properties given by the parameters specified in the
+// URL's query string.
+function ParseParams() {
+  var result = new Object();
+  var s = window.location.search.substring(1).split('&');
+  for (i = 0; i < s.length; ++i) {
+    var v = s[i].split('=');
+    result[v[0]] = unescape(v[1]);
+  }
+  return result;
+}
+
+// Creates the URL constructed from the current pathname and the given params.
+function MakeURL(params) {
+  var url = window.location.pathname;
+  var sep = '?';
+  for (p in params) {
+    if (!p)
+      continue;
+    url = url + sep + p + '=' + params[p];
+    sep = '&';
+  }
+  return url;
+}
+
+// Returns a string describing an object, recursively.  On the initial call,
+// |name| is optionally the name of the object and |indent| is not needed.
+function DebugDump(obj, opt_name, opt_indent) {
+  var name = opt_name || '';
+  var indent = opt_indent || '';
+  if (typeof obj == "object") {
+    var child = null;
+    var output = indent + name + "\n";
+
+    for (var item in obj) {
+      try {
+        child = obj[item];
+      } catch (e) {
+        child = "<Unable to Evaluate>";
+      }
+      output += DebugDump(child, item, indent + "  ");
+    }
+
+    return output;
+  } else {
+    return indent + name + ": " + obj + "\n";
+  }
+}
diff --git a/timings/js/coordinates.js b/timings/js/coordinates.js
new file mode 100644 (file)
index 0000000..69cb4c2
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+  Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+  Use of this source code is governed by a BSD-style license that can be
+  found in the LICENSE file.
+*/
+
+/**
+ * 'Understands' plot data positioning.
+ *  @constructor
+ *
+ * @param {Array} plotData data that will be displayed
+ */
+function Coordinates(plotData) {
+  this.plotData = plotData;
+  
+  height = window.innerHeight - 16;
+  width = window.innerWidth - 16;
+
+  this.widthMax = width;
+  this.heightMax = Math.min(400, height - 85);
+
+  this.xMinValue = -0.5;
+  this.xMaxValue = (this.plotData[0].length - 1)+ 0.5;
+  this.processYValues_();
+}
+
+Coordinates.prototype.processYValues_ = function () {
+  var merged = [];
+  for (var i = 0; i < this.plotData.length; i++)
+    for (var j = 0; j < this.plotData[i].length; j++)
+      merged.push(this.plotData[i][j][0]);
+  var max = Math.max.apply( Math, merged );
+  var min = Math.min.apply( Math, merged );
+
+  // If we have a missing value, find the real max and min the hard way.
+  if (isNaN(min)) {
+    for (var i = 0; i < merged.length; ++i) {
+      if (isNaN(min) || merged[i] < min)
+        min = merged[i];
+      if (isNaN(max) || merged[i] > max)
+        max = merged[i];
+    }
+  }
+  var yd = (max - min) / 10.0;
+  if (yd == 0)
+    yd = max / 10;
+  this.yMinValue = min - yd;
+  this.yMaxValue = max + yd;
+};
+
+/**
+ * Difference between horizontal max min values.
+ */
+Coordinates.prototype.xValueRange = function() {
+  return this.xMaxValue - this.xMinValue;
+};
+
+/**
+ * Difference between vertical max min values.
+ */
+Coordinates.prototype.yValueRange = function() {
+  return this.yMaxValue - this.yMinValue
+};
+
+/**
+ * Converts horizontal data value to pixel value on canvas.
+ * @param {number} value horizontal data value
+ */
+Coordinates.prototype.xPoints = function(value) {
+  return this.widthMax * ((value - this.xMinValue) / this.xValueRange());
+};
+
+/**
+ * Converts vertical data value to pixel value on canvas.
+ * @param {number} value vertical data value
+ */
+Coordinates.prototype.yPoints = function(value) {
+  /* Converts value to canvas Y position in pixels. */
+  return this.heightMax  - this.heightMax * (value - this.yMinValue) / 
+    this.yValueRange();
+};
+
+/**
+ * Converts X point on canvas to value it represents.
+ * @param {number} position horizontal point on canvas.
+ */
+Coordinates.prototype.xValue = function(position) {
+  /* Converts canvas X pixels to value. */
+  return position / this.widthMax * (this.xValueRange()) + this.xMinValue;
+};
+
+/**
+ * Converts Y point on canvas to value it represents.
+ * @param {number} position vertical point on canvas.
+ */
+Coordinates.prototype.yValue = function(position) {
+  /* Converts canvas Y pixels to value. 
+  position is point value is from top.
+  */
+  var position = this.heightMax - position;
+  var ratio = parseFloat(this.heightMax / position);
+  return  this.yMinValue + this.yValueRange() / ratio;
+};
+
+/**
+ * Converts canvas X pixel to data index.
+ * @param {number} xPosition horizontal point on canvas
+ */
+Coordinates.prototype.dataSampleIndex = function(xPosition) {
+  var xValue = this.xValue(xPosition);
+  var index;
+  if (xValue < 0) {
+    index = 0;
+  } else if (xValue > this.plotData[0].length - 1) {
+    index = this.plotData[0].length - 1;
+  } else {
+    index = xValue.toFixed(0);
+  }
+  return index;
+};
+
+Coordinates.prototype.log = function(val) {
+  document.getElementById('log').appendChild(
+    document.createTextNode(val + '\n'));
+};
diff --git a/timings/js/plotter.js b/timings/js/plotter.js
new file mode 100644 (file)
index 0000000..86fb230
--- /dev/null
@@ -0,0 +1,336 @@
+/*
+  Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+  Use of this source code is governed by a BSD-style license that can be
+  found in the LICENSE file.
+*/
+
+// Collection of classes used to plot data in a <canvas>.  Create a Plotter()
+// to generate a plot.
+
+// vertical marker for columns
+function Marker(color) {
+  var m = document.createElement("DIV");
+  m.setAttribute("class", "plot-cursor");
+  m.style.backgroundColor = color;
+  m.style.opacity = "0.3";
+  m.style.position = "absolute";
+  m.style.left = "-2px";
+  m.style.top = "-2px";
+  m.style.width = "0px";
+  m.style.height = "0px";
+  return m;
+}
+
+/**
+ * HorizontalMarker class
+ * Create a horizontal marker at the indicated mouse location.
+ * @constructor
+ *
+ * @param canvasRect {Object} The canvas bounds (in client coords).
+ * @param clientY {Number} The vertical mouse click location that spawned
+ *    the marker, in the client coordinate space.
+ * @param yValue {Number} The plotted value corresponding to the clientY
+ *     click location.
+ */
+function HorizontalMarker(canvasRect, clientY, yValue) {
+  // Add a horizontal line to the graph.
+  var m = document.createElement("DIV");
+  m.setAttribute("class", "plot-baseline");
+  m.style.backgroundColor = HorizontalMarker.COLOR;
+  m.style.opacity = "0.3";
+  m.style.position = "absolute";
+  m.style.left = canvasRect.offsetLeft;
+  var h = HorizontalMarker.HEIGHT;
+  m.style.top = (clientY - h/2).toFixed(0) + "px";
+  m.style.width = canvasRect.width + "px";
+  m.style.height = h + "px";
+  this.markerDiv_ = m;
+
+  this.value = yValue;
+}
+
+HorizontalMarker.HEIGHT = 5;
+HorizontalMarker.COLOR = "rgb(0,100,100)";
+
+// Remove the horizontal line from the graph.
+HorizontalMarker.prototype.remove_ = function() {
+  this.markerDiv_.parentNode.removeChild(this.markerDiv_);
+}
+
+/**
+ * Plotter class
+ * @constructor
+ *
+ * Draws a chart using CANVAS element. Takes array of lines to draw with
+ * deviations values for each data sample.
+ *
+ * @param {Array} clNumbers list of clNumbers for each data sample.
+ * @param {Array} plotData list of arrays that represent individual lines.
+ *                         The line itself is an Array of value and stdd.
+ * @param {Array} dataDescription list of data description for each line
+ *                         in plotData.
+ * @units {string} units name of measurement used to describe plotted data.
+ *
+ * Example of the plotData:
+ *  [
+ *    [line 1 data],
+ *    [line 2 data]
+ *  ].
+ *  Line data looks like  [[point one], [point two]].
+ *  And individual points are [value, deviation value]
+ */
+function Plotter(clNumbers, plotData, dataDescription, units, resultNode) {
+  this.clNumbers_ = clNumbers;
+  this.plotData_ = plotData;
+  this.dataDescription_ = dataDescription;
+  this.resultNode_ = resultNode;
+  this.units_ = units;
+  this.coordinates = new Coordinates(plotData);
+
+  // A color palette that's unambigous for normal and color-deficient viewers.
+  // Values are (red, green, blue) on a scale of 255.
+  // Taken from http://jfly.iam.u-tokyo.ac.jp/html/manuals/pdf/color_blind.pdf
+  this.colors = [[0, 114, 178],   // blue
+                 [230, 159, 0],   // orange
+                 [0, 158, 115],   // green
+                 [204, 121, 167], // purplish pink
+                 [86, 180, 233],  // sky blue
+                 [213, 94, 0],    // dark orange
+                 [0, 0, 0],       // black
+                 [240, 228, 66]   // yellow
+                ];
+}
+
+/**
+ * Does the actual plotting.
+ */
+Plotter.prototype.plot = function() {
+  var canvas = this.canvas();
+  this.coordinates_div_ = this.coordinates_();
+  this.ruler_div_ = this.ruler();
+  // marker for the result-point that the mouse is currently over
+  this.cursor_div_ = new Marker("rgb(100,80,240)");
+  // marker for the result-point for which details are shown
+  this.marker_div_ = new Marker("rgb(100,100,100)");
+  var ctx = canvas.getContext("2d");
+  for (var i = 0; i < this.plotData_.length; i++)
+    this.plotLine_(ctx, this.nextColor(i), this.plotData_[i]);
+
+  this.resultNode_.appendChild(canvas);
+  this.resultNode_.appendChild(this.coordinates_div_);
+
+  this.resultNode_.appendChild(this.ruler_div_);
+  this.resultNode_.appendChild(this.cursor_div_);
+  this.resultNode_.appendChild(this.marker_div_);
+  this.attachEventListeners(canvas);
+  this.canvasRectangle = {
+    "offsetLeft": canvas.offsetLeft,
+    "offsetTop":  canvas.offsetTop,
+    "width":      canvas.offsetWidth,
+    "height":     canvas.offsetHeight
+  };
+};
+
+Plotter.prototype.drawDeviationBar_ = function(context, strokeStyles, x, y,
+                                                deviationValue) {
+  context.strokeStyle = strokeStyles;
+  context.lineWidth = 1.0;
+  context.beginPath();
+  context.moveTo(x, (y + deviationValue));
+  context.lineTo(x, (y - deviationValue));
+  context.moveTo(x, (y - deviationValue));
+  context.closePath();
+  context.stroke();
+};
+
+Plotter.prototype.plotLine_ = function(ctx, strokeStyles, data) {
+  ctx.strokeStyle = strokeStyles;
+  ctx.lineWidth = 2.0;
+  ctx.beginPath();
+  var initial = true;
+  var deviationData = [];
+  for (var i = 0; i < data.length; i++) {
+    var x = this.coordinates.xPoints(i);
+    var value = data[i][0];
+    var stdd = data[i][1];
+    var y = 0.0;
+    var err = 0.0;
+    if (isNaN(value)) {
+      // Re-set 'initial' if we're at a gap in the data.
+      initial = true;
+    } else {
+      y = this.coordinates.yPoints(value);
+      // We assume that the stdd will only be NaN (missing) when the value is.
+      if (parseFloat(value) != 0.0)
+        err = y * parseFloat(stdd) / parseFloat(value);
+      if (initial)
+        initial = false;
+      else
+        ctx.lineTo(x, y);
+    }
+
+    ctx.moveTo(x, y);
+    deviationData.push([x, y, err])
+  }
+  ctx.closePath();
+  ctx.stroke();
+
+  for (var i = 0; i < deviationData.length; i++) {
+    this.drawDeviationBar_(ctx, strokeStyles, deviationData[i][0],
+                            deviationData[i][1], deviationData[i][2]);
+  }
+};
+
+Plotter.prototype.attachEventListeners = function(canvas) {
+  var self = this;
+  canvas.parentNode.addEventListener(
+    "mousemove", function(evt) { self.onMouseMove_(evt); }, false);
+  this.cursor_div_.addEventListener(
+    "click", function(evt) { self.onMouseClick_(evt); }, false);
+};
+
+Plotter.prototype.updateRuler_ = function(evt) {
+  var r = this.ruler_div_;
+  r.style.left = this.canvasRectangle.offsetLeft + "px";
+
+  r.style.top = this.canvasRectangle.offsetTop + "px";
+  r.style.width = this.canvasRectangle.width + "px";
+  var h = evt.clientY - this.canvasRectangle.offsetTop;
+  if (h > this.canvasRectangle.height)
+    h = this.canvasRectangle.height;
+  r.style.height = h + "px";
+};
+
+Plotter.prototype.updateCursor_ = function() {
+  var c = this.cursor_div_;
+  c.style.top = this.canvasRectangle.offsetTop + "px";
+  c.style.height = this.canvasRectangle.height + "px";
+  var w = this.canvasRectangle.width / this.clNumbers_.length;
+  var x = (this.canvasRectangle.offsetLeft +
+            w * this.current_index_).toFixed(0);
+  c.style.left = x + "px";
+  c.style.width = w + "px";
+};
+
+
+Plotter.prototype.onMouseMove_ = function(evt) {
+  var canvas = evt.currentTarget.firstChild;
+  var positionX = evt.clientX - this.canvasRectangle.offsetLeft;
+  var positionY = evt.clientY - this.canvasRectangle.offsetTop;
+
+  this.current_index_ = this.coordinates.dataSampleIndex(positionX);
+  var yValue = this.coordinates.yValue(positionY);
+
+  this.coordinates_td_.innerHTML =
+      "r" + this.clNumbers_[this.current_index_] + ": " +
+      this.plotData_[0][this.current_index_][0].toFixed(2) + " " +
+      this.units_ + " +/- " +
+      this.plotData_[0][this.current_index_][1].toFixed(2) + " " +
+      yValue.toFixed(2) + " " + this.units_;
+
+  // If there is a horizontal marker, also display deltas relative to it.
+  if (this.horizontal_marker_) {
+    var baseline = this.horizontal_marker_.value;
+    var delta = yValue - baseline
+    var fraction = delta / baseline; // allow division by 0
+
+    var deltaStr = (delta >= 0 ? "+" : "") + delta.toFixed(0) + " " +
+        this.units_;
+    var percentStr = (fraction >= 0 ? "+" : "") +
+        (fraction * 100).toFixed(3) + "%";
+
+    this.baseline_deltas_td_.innerHTML = deltaStr + ": " + percentStr;
+  }
+
+  this.updateRuler_(evt);
+  this.updateCursor_();
+};
+
+Plotter.prototype.onMouseClick_ = function(evt) {
+  // Shift-click controls the horizontal reference line.
+  if (evt.shiftKey) {
+    if (this.horizontal_marker_) {
+      this.horizontal_marker_.remove_();
+    }
+
+    var canvasY = evt.clientY - this.canvasRectangle.offsetTop;
+    this.horizontal_marker_ = new HorizontalMarker(this.canvasRectangle,
+        evt.clientY, this.coordinates.yValue(canvasY));
+
+    // Insert before cursor node, otherwise it catches clicks.
+    this.cursor_div_.parentNode.insertBefore(
+        this.horizontal_marker_.markerDiv_, this.cursor_div_);
+  } else {
+    var index = this.current_index_;
+    var m = this.marker_div_;
+    var c = this.cursor_div_;
+    m.style.top = c.style.top;
+    m.style.left = c.style.left;
+    m.style.width = c.style.width;
+    m.style.height = c.style.height;
+    if ("onclick" in this) {
+      var this_x = this.clNumbers_[index];
+      var prev_x = index > 0 ? (parseInt(this.clNumbers_[index-1]) + 1) :
+                                this_x;
+      this.onclick(prev_x, this_x);
+    }
+  }
+};
+
+Plotter.prototype.canvas = function() {
+  var canvas = document.createElement("CANVAS");
+  canvas.setAttribute("id", "_canvas");
+  canvas.setAttribute("class", "plot");
+  canvas.setAttribute("width", this.coordinates.widthMax);
+  canvas.setAttribute("height", this.coordinates.heightMax);
+  return canvas;
+};
+
+Plotter.prototype.ruler = function() {
+  ruler = document.createElement("DIV");
+  ruler.setAttribute("class", "plot-ruler");
+  ruler.style.borderBottom = "1px dotted black";
+  ruler.style.position = "absolute";
+  ruler.style.left = "-2px";
+  ruler.style.top = "-2px";
+  ruler.style.width = "0px";
+  ruler.style.height = "0px";
+  return ruler;
+};
+
+Plotter.prototype.coordinates_ = function() {
+  var coordinatesDiv = document.createElement("DIV");
+  var table_html =
+     "<table border=0 width='100%'><tbody><tr>" +
+     "<td colspan=2 class='legend'>Legend: ";
+  for (var i = 0; i < this.dataDescription_.length; i++) {
+    if (i > 0)
+      table_html += ", ";
+    table_html += "<span class='legend_item' style='color:" +
+      this.nextColor(i) + "'>" + this.dataDescription_[i] + "</span>";
+  }
+  table_html += "</td></tr><tr>" +
+     "<td class='plot-coordinates'><i>move mouse over graph</i></td>" +
+     "<td align=right style='color: " + HorizontalMarker.COLOR +
+     "'><i>Shift-click to place baseline</i></td>" +
+     "</tr></tbody></table>";
+  coordinatesDiv.innerHTML = table_html;
+
+  var tr = coordinatesDiv.firstChild.firstChild.childNodes[1];
+  this.coordinates_td_ = tr.childNodes[0];
+  this.baseline_deltas_td_ = tr.childNodes[1];
+
+  return coordinatesDiv;
+};
+
+Plotter.prototype.nextColor = function(i) {
+  var index = i % this.colors.length;
+  return "rgb(" + this.colors[index][0] + "," +
+                  this.colors[index][1] + "," +
+                  this.colors[index][2] + ")";
+};
+
+Plotter.prototype.log = function(val) {
+  document.getElementById('log').appendChild(
+    document.createTextNode(val + '\n'));
+};