shake-0.9: html/shake.js
/*jsl:option explicit*/
"use strict";
//////////////////////////////////////////////////////////////////////
// UTILITIES
function showTime(x)
{
function digits(x){var s = String(x); return s.length === 1 ? "0" + s : s;}
if (x >= 3600)
{
var x = Math.round(x / 60);
return Math.floor(x / 60) + "h" + digits(x % 60) + "m";
}
else if (x >= 60)
{
var x = Math.round(x);
return Math.floor(x / 60) + "m" + digits(x % 60) + "s";
}
else
return x.toFixed(2) + "s";
}
function showPerc(x){return (x*100).toFixed(2) + "%";}
function plural(n,not1,is1){
return n === 1
? (is1 === undefined ? "" : is1)
: (not1 === undefined ? "s" : not1);
}
function listEq(xs, ys)
{
if (xs.length !== ys.length) return false;
for (var i = 0; i < xs.length; i++)
{
if (xs[i] !== ys[i])
return false;
}
return true;
}
// Return the n top entries, grouping as necessary by words
// huffman :: Int -> [{sum :: Double, name :: String}] -> [{sum :: Double, count :: Int, name :: String}]
function huffman(resultSize, xs)
{
var val = {sum: 0, count: 0, children: {}};
for (var i = 0; i < xs.length; i++)
{
var val2 = val;
var ys = xs[i].name.split(' ');
for (var j = 0; j < ys.length; j++)
{
if (val2.children[ys[j]])
val2 = val2.children[ys[j]];
else
{
var t = {sum: 0, count: 0, children: {}};
val2.children[ys[j]] = t;
val2 = t;
}
val2.sum += xs[i].sum;
val2.count++;
}
}
function compress(val) // return the number of items
{
var n = 0;
for (var j in val.children)
n += compress(val.children[j]);
if (n === 1)
val.children = {};
return Math.max(1,n);
}
compress(val);
function flatten(path, val, res)
{
for (var i in val.children)
{
var ii = val.children[i];
var pp = path.slice(0);
pp.push(i);
res.push({sum:ii.sum, count:ii.count, name:pp, free:path.length===0});
flatten(pp,ii,res);
}
}
var flat = [];
flatten([], val, flat);
while(true)
{
// order everything
flat.sort(function(a,b){var i = b.sum - a.sum; return i !== 0 ? i : b.count - a.count;});
// now, if something in the first n is not free, mark it free and delete anyone who relies on it
// then repeat, if nothing is not free, break
var cont = false;
for (var i = 0; i < Math.min(resultSize, flat.length); i++)
{
if (!flat[i].free)
{
cont = true;
flat[i].free = true;
for (var j = 0; j < flat.length; j++)
{
if (listEq(flat[i].name.slice(0,flat[i].name.length-1), flat[j].name))
{
flat[j].count -= flat[i].count;
flat[j].sum -= flat[i].sum;
}
}
break;
}
}
if (!cont) break;
}
for (var i = 0; i < flat.length; i++)
flat[i].name = flat[i].name.join(" ");
return flat.slice(0, resultSize);
}
//////////////////////////////////////////////////////////////////////
// DATA MANIPULATION/ADDITION
// Summary statistics
var countLast = 0; // :: Int, number of rules run in the last run
var highestRun = 0; // :: Int, highest run you have seen (add 1 to get the count of runs)
var sumExecution = 0; // :: Seconds, build time in total
var maxExecution = 0; // :: Seconds, longest build rule
var countTrace = 0, countTraceLast = 0; // :: Int, traced commands run
var sumTrace = 0, sumTraceLast = 0; // :: Seconds, time running traced commands
var maxTrace = 0; // :: Seconds, lonest traced command
var maxTraceStopLast = 0; // :: Seconds, time the last traced command stopped
var _ = function(){
// Fold over shake to produce the summary
for (var i = 0; i < shake.length; i++)
{
var isLast = shake[i].built === 0;
countLast += isLast ? 1 : 0;
sumExecution += shake[i].execution;
maxExecution = Math.max(maxExecution, shake[i].execution);
highestRun = Math.max(highestRun, shake[i].changed); // changed is always greater or equal to built
var traces = shake[i].traces;
if (!traces) continue;
for (var j = 0; j < traces.length; j++)
{
var time = traces[j].stop - traces[j].start;
countTrace += 1;
countTraceLast += isLast ? 1 : 0;
sumTrace += time;
sumTraceLast += isLast ? time : 0;
maxTrace = Math.max(maxTrace, time);
maxTraceStopLast = Math.max(maxTraceStopLast, isLast ? traces[j].stop : 0);
}
}
}();
// Mutate the shake data, adding in rdeps, being the 1-level reverse dependencies
function addRdeps()
{
// find the reverse dependencies
var rdeps = [];
for (var i = 0; i < shake.length; i++)
rdeps[i] = {};
for (var i = 0; i < shake.length; i++)
{
var deps = shake[i].depends;
for (var j = 0, n = deps.length; j < n; j++)
rdeps[deps[j]][i] = true;
}
for (var i = 0; i < rdeps.length; i++)
{
var ans = [];
for (var j in rdeps[i])
ans.push(Number(j));
shake[i].rdeps = ans;
}
}
// Given an array of indices, calculate the cost to rebuild if all of them change
// You must call addRdeps and addCost first
function calcRebuildCosts(xs)
{
var seen = {};
var tot = 0;
function f(i)
{
if (seen[i]) return;
seen[i] = true;
tot += shake[i].execution;
var deps = shake[i].rdeps;
for (var j = 0, n = deps.length; j < n; j++)
f(deps[j]);
}
if (xs.length === 1 && shake[xs[0]].depends.length === 1)
tot = shake[shake[xs[0]].depends[0]].cost + shake[xs[0]].execution;
else
{
for (var i = 0, n = xs.length; i < n; i++)
f(xs[i]);
}
return tot;
}
// Mutate the shake data, adding in cost, being the cost to rebuild if this item changes
function addCost()
{
for (var i = 0; i < shake.length; i++)
shake[i].cost = calcRebuildCosts([i]);
}
//////////////////////////////////////////////////////////////////////
// REPORTS
function reportSummary()
{
var res =
"<ul>" +
"<li><strong>Runs:</strong> This database has tracked " + (highestRun+1) + " run" + plural(highestRun+1) + ".</li>" +
"<li><strong>Rules:</strong> There are " + shake.length + " rules (" + countLast + " rebuilt in the last run).</li>" +
"<li><strong>Commands:</strong> Building required " + countTrace + " traced commands (" + countTraceLast + " in the last run).</li>" +
"<li><strong>Build time:</strong> The total (unparallelised) build time is " + showTime(sumExecution) + " of which " + showTime(sumTrace) + " is traced commands.</li>" +
"<li><strong>Longest steps:</strong> The longest rule takes " + showTime(maxExecution) + ", and the longest traced command takes " + showTime(maxTrace) + ".</li>" +
"<li><strong>Parallelism:</strong> Last run gave an average parallelism of " + (maxTraceStopLast === 0 ? 0 : sumTraceLast / maxTraceStopLast).toFixed(2) + " times over " + showTime(maxTraceStopLast) + ".</li>" +
"</ul>";
return res;
}
function reportParallelismGraph()
{
var buckets = [];
var countBuckets = 100;
for (var i = 0; i <= countBuckets; i++)
buckets.push(0); // fill with 1 more element, but the last bucket will always be 0
for (var i = 0; i < shake.length; i++)
{
var traces = shake[i].traces;
if (!traces || shake[i].built !== 0) continue;
for (var j = 0; j < traces.length; j++)
{
var start = traces[j].start * countBuckets / maxTraceStopLast;
var stop = traces[j].stop * countBuckets / maxTraceStopLast;
if (Math.floor(start) === Math.floor(stop))
buckets[Math.floor(start)] += stop - start;
else
{
for (var k = Math.ceil(start); k < Math.floor(stop); k++)
buckets[k]++;
buckets[Math.floor(start)] += Math.ceil(start) - start;
buckets[Math.floor(stop)] += stop - Math.floor(stop);
}
}
}
var plotvals = [];
for (var i = 0; i < countBuckets; i++)
plotvals.push([i, buckets[i]]);
return [{color: '#5EB95E', data: plotvals}];
}
function reportExpensiveRules()
{
var top = shake.slice(0).sort(function(a,b){return b.execution-a.execution;}).slice(0,15);
var res = "";
for (var i = 0; i < top.length; i++)
{
res += "<tr>" +
"<td><div class='progress progress-success' style='height: 10px'>" +
"<div class='bar' style='width:" + (top[i].execution * 40 / top[0].execution) + "px;'></div></div></td>" +
"<td>" + showTime(top[i].execution) + "</td>" +
"<td>" + showPerc(top[i].execution / sumExecution) + "</td>" +
"<td>" + top[i].name + "</td>" +
"</tr>";
}
return res;
}
function reportExpensiveCommands()
{
var tooList = [];
for (var i = 0; i < shake.length; i++)
{
var traces = shake[i].traces;
if (!traces) continue;
for (var j = 0; j < traces.length; j++)
tooList.push({name: traces[j].command, sum: traces[j].stop - traces[j].start});
}
var toolList = huffman(15, tooList);
var res = "";
for (var i = 0; i < toolList.length; i++)
{
res += "<tr>" +
"<td><div class='progress progress-success' style='height: 10px'>" +
"<div class='bar' style='width:" + (toolList[i].sum * 40 / toolList[0].sum) + "px;'></div></div></td>" +
"<td>" + showTime(toolList[i].sum) + "</td>" +
"<td>" + showPerc(toolList[i].sum / sumExecution) + "</td>" +
"<td>" + toolList[i].count + " ×</td>" +
"<td>" + toolList[i].name + "</td>" +
"</tr>";
}
return res;
}
function reportRebuildCost()
{
$("#rebuild-cost input").on('input', function(){
try {
var regex = new RegExp($(this).val());
$(this).parent().removeClass("error").find("span").text("");
} catch(e) {
$(this).parent().addClass("error").find("span").text(e);
return;
}
var result1 = [];
var resultN = {};
for (var i = 0; i < shake.length; i++)
{
var res = regex.exec(shake[i].name);
if (res === null) continue;
if (res.length === 1)
result1.push(shake[i]);
else
{
var extra = "";
for (var j = 1; j < res.length; j++)
extra += (extra === "" ? "" : " ") + res[j];
if (resultN[extra] === undefined)
resultN[extra] = {name: extra, deps: {}, count: 0};
resultN[extra].deps[i] = true;
resultN[extra].count++;
}
}
for (var i in resultN)
{
var xs = [];
for (var j in resultN[i].deps)
xs.push(Number(j));
result1.push({name: resultN[i].name + " (" + resultN[i].count + ")", cost: calcRebuildCosts(xs)});
}
var top = result1.sort(function(a,b){return b.cost - a.cost;}).slice(0, 15);
var res = "";
for (var i = 0; i < top.length; i++)
{
res += "<tr>" +
"<td><div class='progress progress-success' style='height: 10px'>" +
"<div class='bar' style='width:" + (top[i].cost * 40 / top[0].cost) + "px;'></div></div></td>" +
"<td>" + showTime(top[i].cost) + "</td>" +
"<td>" + showPerc(top[i].cost / sumExecution) + "</td>" +
"<td>" + top[i].name + "</td></tr>";
}
$("#rebuild-details tbody").empty().append(res);
});
// Put them in order
var top = shake.slice(0).sort(function(a,b){return b.cost-a.cost;}).slice(0,15);
var res = "";
for (var i = 0; i < top.length; i++)
{
res += "<tr>" +
"<td><div class='progress progress-success' style='height: 10px'>" +
"<div class='bar' style='width:" + (top[i].cost * 40 / top[0].cost) + "px;'></div></div></td>" +
"<td>" + showTime(top[i].cost) + "</td>" +
"<td>" + showPerc(top[i].cost / sumExecution) + "</td>" +
"<td>" + top[i].name + "</td></tr>";
}
return res;
}
//////////////////////////////////////////////////////////////////////
// LOADING
function tickFormatter(i)
{
return showTime(maxTraceStopLast * i / 100);
}
function load()
{
$('#summary').append(reportSummary());
$.plot($('#shakeplot'), reportParallelismGraph(), {yaxis : {min: 0}, xaxis : {tickFormatter: tickFormatter}});
// Put everything not initially visible behind a timeout
window.setTimeout(function(){
$("#rule-details tbody").append(reportExpensiveRules());
$("#cmd-details tbody").append(reportExpensiveCommands());
addRdeps();
addCost();
$("#rebuild-details tbody").append(reportRebuildCost());
});
}
function reportUnchanged()
{
for (var i = 0; i < shake.length; i++)
{
if (shake[i].changed !== shake[i].built)
console.log(shake[i].name);
}
}
/*
data Query = Query
{ruleFilter :: RegExp
,ruleType :: Int
,commandFilter :: RegExp
,advancedFilter :: Function(Record -> Bool)
,viewRule :: RuleType
}
*/
var RuleType =
{ByName: 0
,ThisDependsOn: 1
,ThisDependsOnTransitive: 2
,DependsOnThis: 3
,DependsOnThisTransitive: 4
}
function readFun(s)
{
try {return Function("x","return " + s);}
catch (e){return undefined;}
}
function getQuery() // :: IO Query
{
var query = "";
function addQuery(s)
{
query += (query !== "" && s !== "" ? " && " : "") + s;
}
var ruleFilter = $("#rule-filter").val();
var ruleType = $("#rule-type").val();
clearError("#rule-filter");
try {RegExp(ruleFilter);}
catch (e)
{
setError("#rule-filter", e);
return;
}
if (ruleFilter !== "") addQuery(ruleType + "(/" + ruleFilter + "/)");
var unchanged = $("#unchanged-filter").val().toLowerCase();
if (unchanged !== "") addQuery((unchanged[0] === "t" ? "" : "!") + "unchanged()");
var runs = $("#runs-filter").val();
if (runs !== "") addQuery("run() == " + runs);
var leaf = $("#leaf-filter").val().toLowerCase();
if (leaf !== "") addQuery((leaf[0] === "t" ? "" : "!") + "leaf()");
if (query === "") query = "true";
$("#advanced-filter").val(query);
return;
return {ruleFilter: RegExp($("#rule-filter").val())
,ruleType: Number($("#rule-type").val())
,commandFilter: RegExp($("#command-filter").val())
,advancedFilter: readFun($("#advanced-filter").val())
,viewRule: $("#view-rule").is(":checked")};
}
function onQuery(f) // :: IO () -> IO ()
{
$("#details-output input").live('input', f);
$("#details-output select").change(f);
}
function runQuery(x) // :: Query -> Report
{
var res = [];
if (x.viewRule)
{
var want = [];
for (var i = 0, n = shake.length; i < n; i++)
{
if (x.ruleFilter.test(shake[i].name))
want.push(i);
}
var add = function(i){
var o = shake[i];
res.push({rule: o.name, time: o.execution, cost: o.cost, unchanged: o.changed !== o.built, run: o.built, leaf: o.depends.length === 0});
}
var find = function(key){
var seen = {};
for (var i = 0; i < want.length; i++)
{
var deps = shake[want[i]][key];
for (var j = 0; j < deps.length; j++)
{
if (deps[j] in seen) continue;
seen[deps[j]] = true;
add(deps[j]);
}
}
}
var findTransitive = function(key, dirFwd){
var want2 = {};
for (var i = 0; i < want.length; i++)
want2[want[i]] = true;
for (var i = 0; i < shake.length; i++)
{
var j = dirFwd ? i : shake.length - 1 - i;
if (!(j in want2)) continue;
add(j);
var ds = shake[j][key];
for (var k = 0; k < ds.length; k++)
want2[ds[k]] = true;
}
}
switch (x.ruleType)
{
case RuleType.ByName:
for (var i = 0; i < want.length; i++)
add(want[i]);
break;
case RuleType.ThisDependsOn:
find("depends");
break;
case RuleType.DependsOnThis:
find("rdeps");
break;
case RuleType.ThisDependsOnTransitive:
findTransitive("depends", false);
break;
case RuleType.DependsOnThisTransitive:
findTransitive("rdeps", true);
break;
}
if (x.advancedFilter !== undefined)
{
var res2 = [];
for (var i = 0; i < res.length; i++)
{
if (x.advancedFilter(res[i]) !== false)
res2.push(res[i]);
}
res = res2;
}
}
else
{
}
return res;
}
var invertOrder = {rule: 0, run: 0};
var keyName = {rule: "Rule", time: "Time", cost: "Cost", unchanged: "Unchanged", run: "Runs since", leaf:"Leaf"};
var keyShow =
{time: function(t){return showTime(t) + "</td><td>" + showPerc(t / sumExecution);}
,cost: function(t){return showTime(t) + "</td><td>" + showPerc(t / sumExecution);}
};
var sortOrder = ["time"];
function sortByOrder(a,b)
{
for (var i = 0; i < sortOrder.length; i++)
{
var s = sortOrder[i];
if (!(s in a)) continue;
var aa = a[s];
var bb = b[s];
if (aa == bb) continue;
return aa > bb == (s in invertOrder) ? 1 : -1;
}
}
function setTable(xs) // :: Report -> IO ()
{
$("#details-output > tbody").empty();
if (xs.length === 0) return;
var x0 = xs[0];
var ord = (sortOrder[0] in x0) && typeof x0[sortOrder[0]] === "number" && x0[sortOrder[0]] > 0 ? sortOrder[0] : "";
var top = xs.sort(sortByOrder).slice(0,50);
var s = "";
for (var i = 0; i < top.length; i++)
{
s += "<tr>";
var x = top[i];
if (ord === "")
s += "<td><div style='width:40px'></div></td>";
else
s += "<td><div class='progress progress-success' style='height: 10px'><div class='bar' style='width:" + (x[ord] * 40 / top[0][ord]) + "px;'></div><div></td>";
for (var j in x)
s += "<td>" + (j in keyShow ? keyShow[j](x[j]) : x[j]) + "</td>";
s += "</tr>";
}
$("#details-output > tbody").append(s);
}
$(function(){
addRdeps();
addCost();
//var f = function(){setTable(runQuery(getQuery()));};
var f = function(){getQuery();};
onQuery(f);
window.setTimeout(f);
$("#advanced-apply").click(function(){
setTable(runQuery2($("#advanced-filter").val()));
});
$("#advanced-filter").keyup(function(e){
if (e.which === 13 /* enter */)
$("#advanced-apply").click();
});
});
function clearError(element)
{
$(element).removeClass("erroroneous");
$(element).next(".error").remove();
}
function setError(element, message)
{
$(element).addClass("erroroneous");
$(element).after("<span class='error' title='" + message + "'><abbrev>Error!</span>");
}
/*
query is a record with the search fields in
report is the output of a table
setQuery :: Query -> IO ()
getQuery :: IO Query
setTable :: Report -> IO ()
runQuery :: Query -> Report
Report is just a set of records, with predefined formatting for certain fields
and field priorities
Include things like, "unchanged", lastRun=1, cost-self, cost-children
*/
/////////////////////////////////////////////////////////////////////
// FILTER AND GROUP EXECUTION ENVIRONMENT
// These are global variables mutated/queried by query execution
var queryKey = 0;
var queryVal = {};
var queryGroup = "";
function runQuery2(s)
{
var res = [];
var f = Function("return " + s);
for (queryKey = 0; queryKey < shake.length; queryKey++)
{
queryGroup = "";
queryVal = shake[queryKey];
if (f())
{
var o = queryVal;
res.push({rule: o.name, time: o.execution, cost: o.cost, unchanged: o.changed !== o.built, run: o.built, leaf: o.depends.length === 0});
}
}
return res;
}
function /* export */ addGroup(x)
{
queryGroup += (queryGroup === "" ? "" : " ") + x;
}
function /* export */ leaf()
{
return queryVal.depends.length === 0;
}
function /* export */ run()
{
return queryVal.built;
}
function /* export */ unchanged()
{
return queryVal.changed !== queryVal.built;
}
function /* export */ byName(r)
{
var res = r.exec(queryVal.name);
if (res === null)
return false;
if (res.length !== 1)
{
for (var i = 1; i < res.length; i++)
query_addGroup(res[i]);
}
return true;
}
function findCache(f)
{
var cache = {};
return function(r){
var s = cache[r.source];
if (s === undefined)
{
s = f(r);
cache[r.source] = s;
}
return queryKey in s;
}
}
function findDirect(key)
{
return findCache(function(r){
var want = {};
for (var i = 0; i < shake.length; i++)
{
if (r.test(shake[i].name))
{
var deps = shake[i][key];
for (var j = 0; j < deps.length; j++)
want[deps[j]] = true;
}
}
return want;
});
}
function findTransitive(key, dirFwd)
{
return findCache(function(r){
var want = {};
for (var i = 0; i < shake.length; i++)
{
var j = dirFwd ? i : shake.length - 1 - i;
if ((j in want) || r.test(shake[j].name))
{
want[j] = true;
var deps = shake[j][key];
for (var k = 0; k < deps.length; k++)
want[deps[k]] = true;
}
}
return want;
});
}
var /* export */ dependsOnThis = findDirect("rdeps");
var /* export */ thisDependsOn = findDirect("depends");
var /* export */ dependsOnThisTransitive = findTransitive("depends", false);
var /* export */ thisDependsOnTransitive = findTransitive("rdeps", true);