diff options
author | Steven Knight <knight@baldmt.com> | 2009-12-18 08:03:50 (GMT) |
---|---|---|
committer | Steven Knight <knight@baldmt.com> | 2009-12-18 08:03:50 (GMT) |
commit | 66d0823dd960afe9cf45c910d2fcedea133c6fef (patch) | |
tree | 6e1f3e3dd9a03cf38faa62500cd224b751d27905 /timings | |
parent | a158bdde183073bb22007d08b4a7cfa1163f082a (diff) | |
download | SCons-66d0823dd960afe9cf45c910d2fcedea133c6fef.zip SCons-66d0823dd960afe9cf45c910d2fcedea133c6fef.tar.gz SCons-66d0823dd960afe9cf45c910d2fcedea133c6fef.tar.bz2 |
Move the timings-specific pieces of the buildbot infrastructure into
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).
Diffstat (limited to 'timings')
-rw-r--r-- | timings/changelog.html | 204 | ||||
-rw-r--r-- | timings/graph.html | 411 | ||||
-rw-r--r-- | timings/index.html | 196 | ||||
-rw-r--r-- | timings/js/common.js | 96 | ||||
-rw-r--r-- | timings/js/coordinates.js | 125 | ||||
-rw-r--r-- | timings/js/plotter.js | 336 |
6 files changed, 1368 insertions, 0 deletions
diff --git a/timings/changelog.html b/timings/changelog.html new file mode 100644 index 0000000..586ebad --- /dev/null +++ b/timings/changelog.html @@ -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 index 0000000..e418069 --- /dev/null +++ b/timings/graph.html @@ -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 index 0000000..de102ea --- /dev/null +++ b/timings/index.html @@ -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> </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> </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 index 0000000..80510b3 --- /dev/null +++ b/timings/js/common.js @@ -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 index 0000000..69cb4c2 --- /dev/null +++ b/timings/js/coordinates.js @@ -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 index 0000000..86fb230 --- /dev/null +++ b/timings/js/plotter.js @@ -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')); +}; |