shake-0.19.9: html/shake.js
"use strict";
function bindPlot(element, data, options) {
const redraw = () => {
if ($(element).is(":visible"))
$.plot($(element), data.get(), options);
};
window.setTimeout(redraw, 1);
$(window).on("resize", redraw);
data.event(redraw);
}
function varLink(name) {
return React.createElement("a", { href: "https://hackage.haskell.org/package/shake/docs/Development-Shake.html#v:" + name },
React.createElement("tt", null, name));
}
function newTable(columns, data, sortColumn, sortDescend) {
const f = (x) => ({ name: x.field, label: x.label, width: x.width, cellClasses: x.alignRight ? "right" : "" });
const formatters = {};
for (const c of columns)
formatters[c.field] = c.show || ((x) => x);
const table = new DGTable({
adjustColumnWidthForSortArrow: false,
cellFormatter: (val, colname) => formatters[colname](val),
columns: columns.map(f),
width: DGTable.Width.SCROLL
});
$(table.el).css("height", "100%");
window.setTimeout(() => {
table.render();
table.tableHeightChanged();
if (sortColumn)
table.sort(sortColumn, sortDescend);
table.setRows(data.get(), true);
}, 1);
let toRender = false;
data.event(xs => {
table.setRows(xs, true);
if ($(table.el).is(":visible"))
table.render();
else
toRender = true;
});
$(window).on("resize", () => {
if ($(table.el).is(":visible")) {
table.tableHeightChanged();
if (toRender) {
table.render();
toRender = false;
}
}
});
return React.createElement("div", { style: "height:100%;width:100%;" }, table.el);
}
// These are global variables mutated/queried by query execution
let environmentAll; // All the profiles
let environmentThis; // The specific profile under test
let environmentGroup; // The group produced as a result
function group(x) {
environmentGroup.push(x);
return true;
}
function leaf() {
return environmentThis.depends.length === 0;
}
function run(i) {
if (i === undefined)
return environmentThis.built;
else
return environmentThis.built === i;
}
function changed() {
return environmentThis.changed === environmentThis.built;
}
function unchanged() {
return !unchanged();
}
function named(r, groupName) {
if (r === undefined)
return environmentThis.name;
const res = execRegExp(r, environmentThis.name);
if (res === null) {
if (groupName === undefined)
return false;
else {
group(groupName);
return true;
}
}
if (res.length !== 1) {
for (let i = 1; i < res.length; i++)
group(res[i]);
}
return true;
}
function command(r, groupName) {
const n = (environmentThis.traces || []).length;
if (r === undefined)
return n === 0 ? "" : environmentThis.traces[0].command;
for (const t of environmentThis.traces) {
const res = execRegExp(r, t.command);
if (res === null)
continue;
if (res.length !== 1) {
for (let j = 1; j < res.length; j++)
group(res[j]);
}
return true;
}
if (groupName === undefined)
return false;
else {
group(groupName);
return true;
}
}
function profileLoaded(profileRaw) {
$(document.body).empty().append(profileRoot(unraw(profileRaw)));
}
function unraw(xs) {
const ans = xs.map((x, i) => ({
index: i,
name: x[0],
execution: x[1],
built: x[2],
changed: x[3],
depends: x.length > 4 ? x[4] : [],
rdepends: [],
traces: x.length > 5 ? x[5].map(y => ({ command: y[0], start: y[1], stop: y[2] })) : []
}));
for (const p of ans)
for (const ds of p.depends)
for (const d of ds)
ans[d].rdepends.push(p.index);
return ans;
}
function profileRoot(profile) {
const [s, search] = createSearch(profile);
const t = createTabs([["Summary", () => reportSummary(profile)],
["Command plot", () => reportCmdPlot(profile)],
["Commands", () => reportCmdTable(profile, search)],
["Rules", () => reportRuleTable(profile, search)],
["Parallelizability", () => reportParallelism(profile)],
["Details", () => reportDetails(profile, search)]
// , ["Why rebuild", () => reportRebuild(profile, search)]
]);
return React.createElement("table", { class: "fill" },
React.createElement("tr", null,
React.createElement("td", { style: "padding-top: 8px; padding-bottom: 8px;" },
React.createElement("a", { href: "https://shakebuild.com/", style: "font-size: 20px; text-decoration: none; color: #3131a7; font-weight: bold;" }, "Shake profile report"),
React.createElement("span", { style: "color:gray;white-space:pre;" },
" - generated at ",
generated,
" by Shake v",
version))),
React.createElement("tr", null,
React.createElement("td", null, s)),
React.createElement("tr", null,
React.createElement("td", { height: "100%" }, t)));
}
function createTabs(xs) {
const bodies = xs.map(x => {
const el = React.createElement("div", { style: "padding:5px;width:100%;height:100%;min-width:150px;min-height:150px;overflow:auto;display:none;" });
const upd = lazy(() => $(el).append(x[1]()));
return pair(el, upd);
});
let lbls = [];
const f = (i) => () => {
bodies[i][1]();
lbls.map((x, j) => $(x).toggleClass("active", i === j));
bodies.map((x, j) => $(x[0]).toggle(i === j));
$(window).trigger("resize");
};
lbls = xs.map((x, i) => React.createElement("a", { onclick: f(i) }, x[0]));
f(0)();
return React.createElement("table", { class: "fill" },
React.createElement("tr", null,
React.createElement("td", null,
React.createElement("table", { width: "100%", style: "border-spacing:0px;" },
React.createElement("tr", { class: "tabstrip" },
React.createElement("td", { width: "20", class: "bottom" }, "\u00A0"),
React.createElement("td", { style: "padding:0px;" }, lbls),
React.createElement("td", { width: "100%", class: "bottom" }, "\u00A0"))))),
React.createElement("tr", { height: "100%" },
React.createElement("td", { style: "background-color:white;" }, bodies.map(fst))));
}
// A mapping from names (rule names or those matched from rule parts)
// to the indicies in profiles.
class Search {
constructor(profile, mapping) {
this.profile = profile;
if (mapping !== undefined)
this.mapping = mapping;
else {
this.mapping = {};
for (const p of profile)
this.mapping[p.name] = [p.index];
}
}
forEachProfiles(f) {
for (const s in this.mapping)
f(this.mapping[s].map(i => this.profile[i]), s);
}
forEachProfile(f) {
this.forEachProfiles((ps, group) => ps.forEach(p => f(p, group)));
}
mapProfiles(f) {
const res = [];
this.forEachProfiles((ps, group) => res.push(f(ps, group)));
return res;
}
mapProfile(f) {
const res = [];
this.forEachProfile((p, group) => res.push(f(p, group)));
return res;
}
}
function createSearch(profile) {
const caption = React.createElement("div", null,
"Found ",
profile.length,
" entries, not filtered or grouped.");
const input = React.createElement("input", { id: "search", type: "text", value: "", placeholder: "Filter and group", style: "width: 100%; font-size: 16px; border-radius: 8px; padding: 5px 10px; border: 2px solid #999;" });
const res = new Prop(new Search(profile));
$(input).on("change keyup paste", () => {
const s = $(input).val();
if (s === "") {
res.set(new Search(profile));
$(caption).text("Found " + profile.length + " entries, not filtered or grouped.");
}
else if (s.indexOf("(") === -1) {
const mapping = {};
let found = 0;
for (const p of profile) {
if (p.name.indexOf(s) !== -1) {
found++;
mapping[p.name] = [p.index];
}
}
res.set(new Search(profile, mapping));
$(caption).text("Substring filtered to " + found + " / " + profile.length + " entries, not grouped.");
}
else {
let f;
try {
f = new Function("return " + s);
}
catch (e) {
$(caption).text("Error compiling function, " + e);
return;
}
const mapping = {};
let groups = 0;
let found = 0;
environmentAll = profile;
for (const p of profile) {
environmentThis = p;
environmentGroup = [];
let bool;
try {
bool = f();
}
catch (e) {
$(caption).text("Error running function, " + e);
return;
}
if (bool) {
found++;
const name = environmentGroup.length === 0 ? p.name : environmentGroup.join(" ");
if (name in mapping)
mapping[name].push(p.index);
else {
groups++;
mapping[name] = [p.index];
}
}
}
res.set(new Search(profile, mapping));
$(caption).text("Function filtered to " + found + " / " + profile.length + " entries, " +
(groups === found ? "not grouped." : groups + " groups."));
}
});
const body = React.createElement("table", { width: "100%", style: "padding-bottom: 17px;" },
React.createElement("tr", null,
React.createElement("td", { width: "100%" }, input),
React.createElement("td", { style: "padding-left:6px;padding-right: 6px;" }, searchHelp(input))),
React.createElement("tr", null,
React.createElement("td", null, caption)));
return [body, res];
}
function searchHelp(input) {
const examples = [["Only the last run", "run(0)"],
["Named 'Main'", "named(\"Main\")"],
["Group by file extension", "named(/(\\.[_0-9a-z]+)$/)"],
["No dependencies (an input)", "leaf()"],
["Didn't change when it last rebuilt", "unchanged()"],
["Ran 'gcc'", "command(\"gcc\")"]
];
const f = (code) => () => {
$(input).val((i, x) => x + (x === "" ? "" : " && ") + code);
$(input).trigger("change");
};
const dropdown = React.createElement("div", { class: "dropdown", style: "display:none;" },
React.createElement("ul", { style: "padding-left:30px;" }, examples.map(([desc, code]) => React.createElement("li", null,
React.createElement("a", { onclick: f(code) },
React.createElement("tt", null, code)),
" ",
React.createElement("span", { class: "note" }, desc)))));
const arrow_down = React.createElement("span", { style: "vertical-align:middle;font-size:80%;" }, "\u25BC");
const arrow_up = React.createElement("span", { style: "vertical-align:middle;font-size:80%;display:none;" }, "\u25B2");
const show_inner = () => { $(dropdown).toggle(); $(arrow_up).toggle(); $(arrow_down).toggle(); };
return React.createElement("div", null,
React.createElement("button", { style: "white-space:nowrap;padding-top:5px;padding-bottom:5px;", onclick: show_inner },
React.createElement("b", { style: "font-size:150%;vertical-align:middle;" }, "+"),
"\u00A0 Filter and Group \u00A0",
arrow_down,
arrow_up),
dropdown);
}
function initProgress() {
$(function () {
$(".version").html("Generated by <a href='https://shakebuild.com'>Shake " + version + "</a>.");
$("#output").html("");
for (const x of progress) {
var actual = [];
var ideal = [];
// Start at t = 5 seconds, since the early progress jumps a lot
for (var t = 5; t < x.values.length; t++) {
var y = x.values[t];
actual.push([y.idealSecs, y.actualSecs]);
ideal.push([y.idealSecs, y.idealSecs]);
}
var ys = [{ data: ideal, color: "gray" }, { label: x.name, data: actual, color: "red" }];
var div = $("<div class='plot'>");
$("#output").append(div);
$.plot(div, ys, {
xaxis: {
transform: function (v) { return -v; },
inverseTransform: function (v) { return -v; }
}
});
}
});
}
// Stuff that Shake generates and injects in
function untraced(p) {
return Math.max(0, p.execution - p.traces.map(t => t.stop - t.start).sum());
}
/////////////////////////////////////////////////////////////////////
// BASIC UI TOOLKIT
class Prop {
constructor(val) { this.val = val; this.callback = () => { return; }; }
get() { return this.val; }
set(val) {
this.val = val;
this.callback(val);
}
event(next) {
const old = this.callback;
this.callback = val => { old(val); next(val); };
next(this.val);
}
map(f) {
const res = new Prop(f(this.get()));
this.event(a => res.set(f(a)));
return res;
}
}
jQuery.fn.enable = function (x) {
// Set the values to enabled/disabled
return this.each(function () {
if (x)
$(this).removeAttr("disabled");
else
$(this).attr("disabled", "disabled");
});
};
/////////////////////////////////////////////////////////////////////
// BROWSER HELPER METHODS
// Given "?foo=bar&baz=1" returns {foo:"bar",baz:"1"}
function uriQueryParameters(s) {
// From https://stackoverflow.com/questions/901115/get-querystring-values-with-jquery/3867610#3867610
const params = {};
const a = /\+/g; // Regex for replacing addition symbol with a space
const r = /([^&=]+)=?([^&]*)/g;
const d = (x) => decodeURIComponent(x.replace(a, " "));
const q = s.substring(1);
while (true) {
const e = r.exec(q);
if (!e)
break;
params[d(e[1])] = d(e[2]);
}
return params;
}
/////////////////////////////////////////////////////////////////////
// STRING FORMATTING
function showTime(x) {
function digits(x) { const s = String(x); return s.length === 1 ? "0" + s : s; }
if (x >= 3600) {
x = Math.round(x / 60);
return Math.floor(x / 60) + "h" + digits(x % 60) + "m";
}
else if (x >= 60) {
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 showInt(x) {
// From https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
// Show, with commas
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function showRun(run) {
return run === 0 ? "Latest run" : run + " run" + plural(run) + " ago";
}
function plural(n, not1 = "s", is1 = "") {
return n === 1 ? is1 : not1;
}
/////////////////////////////////////////////////////////////////////
// MISC
function compareFst(a, b) {
return a[0] - b[0];
}
function compareSnd(a, b) {
return a[1] - b[1];
}
function compareSndRev(a, b) {
return b[1] - a[1];
}
function pair(a, b) {
return [a, b];
}
function triple(a, b, c) {
return [a, b, c];
}
function fst([x, _]) {
return x;
}
function snd([_, x]) {
return x;
}
function execRegExp(r, s) {
if (typeof r === "string")
return s.indexOf(r) === -1 ? null : [];
else
return r.exec(s);
}
function cache(key, op) {
const store = {};
return k => {
const s = key(k);
if (!(s in store))
store[s] = op(k);
return store[s];
};
}
function lazy(thunk) {
let store = null;
let done = false;
return () => {
if (!done) {
store = thunk();
done = true;
}
return store;
};
}
Array.prototype.sum = function () {
let res = 0;
for (const x of this)
res += x;
return res;
};
Array.prototype.insertSorted = function (x, compare) {
let start = 0;
let stop = this.length - 1;
let middle = 0;
while (start <= stop) {
middle = Math.floor((start + stop) / 2);
if (compare(this[middle], x) > 0)
stop = middle - 1;
else
start = middle + 1;
}
this.splice(start, 0, x);
return this;
};
Array.prototype.concatLength = function () {
let res = 0;
for (const x of this)
res += x.length;
return res;
};
Array.prototype.sortOn = function (f) {
return this.map(x => pair(f(x), x)).sort(compareFst).map(snd);
};
Array.prototype.last = function () {
return this[this.length - 1];
};
Array.prototype.maximum = function (def) {
if (this.length === 0)
return def;
let res = this[0];
for (let i = 1; i < this.length; i++)
res = Math.max(res, this[i]);
return res;
};
Array.prototype.minimum = function (def) {
if (this.length === 0)
return def;
let res = this[0];
for (let i = 1; i < this.length; i++)
res = Math.min(res, this[i]);
return res;
};
// Use JSX with el instead of React.createElement
// Originally from https://gist.github.com/sergiodxa/a493c98b7884128081bb9a281952ef33
// our element factory
function createElement(type, props, ...children) {
const element = document.createElement(type);
for (const name in props || {}) {
if (name.substr(0, 2) === "on")
element.addEventListener(name.substr(2), props[name]);
else
element.setAttribute(name, props[name]);
}
for (const child of children.flat(10)) {
const c = typeof child === "object" ? child : document.createTextNode(child.toString());
element.appendChild(c);
}
return element;
}
// How .tsx gets desugared
const React = { createElement };
function reportCmdPlot(profile) {
// first find the end point
const runs = findRuns(profile);
if (runs.length === 0) {
return React.createElement("div", null,
React.createElement("h2", null, "No data found"),
React.createElement("p", null, "The Shake database contains no rules which ran traced commands."),
React.createElement("p", null,
"You can populate this information by using ",
varLink("cmd"),
" or wrapping your ",
React.createElement("tt", null, "IO"),
" actions in ",
varLink("traced"),
"."));
}
const combo = React.createElement("select", null,
runs.map(([run, time], i) => React.createElement("option", null,
showRun(run) + " (" + showTime(time) + ") ",
i === 0 ? "" : " - may be incomplete")),
";");
const warning = React.createElement("i", null);
const plot = React.createElement("div", { style: "width:100%; height:100%;" });
const plotData = new Prop([]);
bindPlot(plot, plotData, {
legend: { show: true, position: "nw", sorted: "reverse" },
series: { stack: true, lines: { fill: 1, lineWidth: 0 } },
yaxis: { min: 0 },
xaxis: { tickFormatter: showTime }
});
function setPlotData(runsIndex) {
const [run, end] = runs[runsIndex];
const profileRun = profile.filter(p => p.built === run);
// Make sure we max(0,) every step in the process, in case one does parallelism of threads
const missing = profileRun.map(untraced).sum();
$(warning).text(missing < 1 ? "" : "Warning: " + showTime(missing) + " of execution was not traced.");
const series = calcPlotData(end, profileRun, 100);
const res = [];
for (const s in series)
res.push({ label: s, data: series[s].map((x, i) => pair(end * i / 100, x)) });
plotData.set(res);
}
setPlotData(0);
$(combo).change(() => setPlotData(combo.selectedIndex));
return React.createElement("table", { class: "fill" },
React.createElement("tr", null,
React.createElement("td", { width: "100%", style: "text-align:center;" },
React.createElement("h2", null, "Number of commands executing over time")),
React.createElement("td", null, combo)),
React.createElement("tr", null,
React.createElement("td", { height: "100%", colspan: "2" }, plot)),
React.createElement("tr", null,
React.createElement("td", { colspan: "2", style: "text-align:center;" },
"Time since the start of building. ",
warning)));
}
// Find which runs had traced commands and when the last stopped, sort so most recent first
function findRuns(profile) {
const runs = {};
for (const p of profile) {
if (p.traces.length > 0) {
if (p.traces.length === 1 && p.traces[0].command === "")
continue; // the fake end command
const old = runs[p.built];
const end = p.traces.last().stop;
runs[p.built] = old === undefined ? end : Math.max(old, end);
}
}
const runsList = [];
for (const i in runs)
runsList.push(pair(Number(i), runs[i]));
runsList.sort(compareFst);
return runsList;
}
function calcPlotData(end, profile, buckets) {
const ans = {};
for (const p of profile) {
for (const t of p.traces) {
let xs;
if (t.command in ans)
xs = ans[t.command];
else {
xs = [];
for (let i = 0; i < buckets; i++)
xs.push(0); // fill with 1 more element, but the last bucket will always be 0
ans[t.command] = xs;
}
const start = t.start * buckets / end;
const stop = t.stop * buckets / end;
if (Math.floor(start) === Math.floor(stop))
xs[Math.floor(start)] += stop - start;
else {
for (let j = Math.ceil(start); j < Math.floor(stop); j++)
xs[j]++;
xs[Math.floor(start)] += Math.ceil(start) - start;
xs[Math.floor(stop)] += stop - Math.floor(stop);
}
}
}
return ans;
}
function reportCmdTable(profile, search) {
const columns = [{ field: "name", label: "Name", width: 200 },
{ field: "count", label: "Count", width: 65, alignRight: true, show: showInt },
{ field: "total", label: "Total", width: 75, alignRight: true, show: showTime },
{ field: "average", label: "Average", width: 75, alignRight: true, show: showTime },
{ field: "max", label: "Max", width: 75, alignRight: true, show: showTime }
];
return newTable(columns, search.map(cmdData), "total", true);
}
function cmdData(search) {
const res = {};
search.forEachProfile(p => {
for (const t of p.traces) {
const time = t.stop - t.start;
if (t.command === "")
continue; // do nothing
else if (!(t.command in res))
res[t.command] = { count: 1, total: time, max: time };
else {
const ans = res[t.command];
ans.count++;
ans.total += time;
ans.max = Math.max(ans.max, time);
}
}
});
const res2 = [];
for (const i in res)
res2.push({ name: i, average: res[i].total / res[i].count, ...res[i] });
return res2;
}
function reportDetails(profile, search) {
const result = React.createElement("div", { class: "details" });
const self = new Prop(0);
search.event(xs => self.set(xs.mapProfile((p, _) => p.index).maximum()));
const f = (i) => React.createElement("a", { onclick: () => self.set(i) }, profile[i].name);
self.event(i => {
const p = profile[i];
const content = React.createElement("ul", null,
React.createElement("li", null,
React.createElement("b", null, "Name:"),
" ",
p.name),
React.createElement("li", null,
React.createElement("b", null, "Built:"),
" ",
showRun(p.built)),
React.createElement("li", null,
React.createElement("b", null, "Changed:"),
" ",
showRun(p.changed)),
React.createElement("li", null,
React.createElement("b", null, "Execution time:"),
showTime(p.execution)),
React.createElement("li", null,
React.createElement("b", null, "Traced commands:"),
React.createElement("ol", null, p.traces.map(t => React.createElement("li", null,
t.command,
" took ",
showTime(t.stop - t.start))))),
React.createElement("li", null,
React.createElement("b", null, "Dependencies:"),
React.createElement("ol", null, p.depends.map(ds => React.createElement("li", null,
React.createElement("ul", null, ds.map(d => React.createElement("li", null, f(d)))))))),
React.createElement("li", null,
React.createElement("b", null, "Things that depend on me:"),
React.createElement("ul", null, p.rdepends.map(d => React.createElement("li", null, f(d))))));
$(result).empty().append(content);
});
return result;
}
function reportParallelism(profile) {
// now simulate for -j1 .. -j24
const plotData = [{ label: "Realistic (based on current dependencies)", data: [], color: "#3131a7" },
{ label: "Ideal (if no dependencies and perfect speedup)", data: [], color: "green" },
{ label: "Gap", data: [], color: "orange" }
];
let threads1;
for (let threads = 1; threads <= 24; threads++) {
const taken = simulateThreads(profile, threads)[0];
if (threads === 1)
threads1 = taken;
plotData[0].data.push([threads, taken]);
plotData[1].data.push([threads, threads1 / threads]);
plotData[2].data.push([threads, Math.max(0, taken - (threads1 / threads))]);
}
const plot = React.createElement("div", { style: "width:100%; height:100%;" });
bindPlot(plot, new Prop(plotData), {
xaxis: { tickDecimals: 0 },
yaxis: { min: 0, tickFormatter: showTime }
});
return React.createElement("table", { class: "fill" },
React.createElement("tr", null,
React.createElement("td", { style: "text-align:center;" },
React.createElement("h2", null, "Time to build at different number of threads"))),
React.createElement("tr", null,
React.createElement("td", { height: "100%" }, plot)),
React.createElement("tr", null,
React.createElement("td", { style: "text-align:center;" }, "Number of threads available.")));
}
// Simulate running N threads over the profile, return:
// [total time take, point at which each entry kicked off]
function simulateThreads(profile, threads) {
// How far are we through this simulation
let timestamp = 0;
// Who is currently running, with the highest seconds FIRST
const running = [];
const started = [];
// Things that are done
const ready = profile.filter(x => x.depends.length === 0);
const waiting = profile.map(x => x.depends.concatLength()); // number I am waiting on before I am done
function runningWait() {
const [ind, time] = running.pop();
timestamp = time;
for (const d of profile[ind].rdepends) {
waiting[d]--;
if (waiting[d] === 0)
ready.push(profile[d]);
}
}
while (true) {
// Queue up as many people as we can
while (running.length < threads && ready.length > 0) {
const p = ready.pop();
started[p.index] = timestamp;
running.insertSorted([p.index, timestamp + p.execution], compareSndRev);
}
if (running.length === 0) {
if (waiting.maximum(0) > 0)
throw new Error("Failed to run all tasks");
return [timestamp, started];
}
runningWait();
}
}
function reportRebuild(profile, search) {
const depth = [];
for (const p of profile) {
depth[p.index] = p.depends.flat().map(d => depth[d] + 1).maximum(0);
}
const ind = search.get().mapProfile((p, _) => p.index).sortOn(i => -depth[i])[0];
const p = profile[ind];
function f(p) {
const res = [];
while (p.depends.length !== 0) {
const ds = p.depends.flat().sortOn(i => -depth[i]);
res.push(React.createElement("li", null,
React.createElement("select", { style: "width:400px;" }, ds.slice(0, 1).map(x => React.createElement("option", null, profile[x].name)))));
p = profile[ds[0]];
}
return res;
}
return React.createElement("div", null,
React.createElement("h2", null, "Why did it rebuild?"),
React.createElement("p", null,
"Rule ",
p.name + " " + (p.built === 0 ? "rebuild in the last run" : "did not rebuild")),
React.createElement("ul", null, f(p)));
}
function reportRuleTable(profile, search) {
const [etimes, wtimes] = calcEWTimes(profile, 24);
const columns = [{ field: "name", label: "Name", width: 400 },
{ field: "count", label: "Count", width: 65, alignRight: true, show: showInt },
{ field: "leaf", label: "Leaf", width: 60, alignRight: true },
{ field: "run", label: "Run", width: 50, alignRight: true },
{ field: "changed", label: "Change", width: 60, alignRight: true },
{ field: "time", label: "Time", width: 75, alignRight: true, show: showTime },
{ field: "etime", label: "ETime", width: 75, alignRight: true, show: showTime },
{ field: "wtime", label: "WTime", width: 75, alignRight: true, show: showTime },
{ field: "untraced", label: "Untraced", width: 100, alignRight: true, show: showTime }
];
return newTable(columns, search.map(s => ruleData(etimes, wtimes, s)), "time", true);
}
// Calculate the exclusive time of each rule at some number of threads
function calcEWTimes(profile, threads) {
const [_, started] = simulateThreads(profile, threads);
const starts = started.map((s, i) => pair(i, s)).sort(compareSnd);
const costs = starts.map(([ind, start], i) => {
// find out who else runs before I finish
const execution = profile[ind].execution;
const end = start + execution;
let overlap = 0; // how much time I am overlapped for
let exclusive = 0; // how much time I am the only runner
let finisher = start; // the first overlapping person to finish
for (let j = i + 1; j < starts.length; j++) {
const [jInd, jStarts] = starts[j];
if (jStarts > end)
break;
overlap += Math.min(end - jStarts, profile[jInd].execution);
exclusive += Math.max(0, Math.min(jStarts, end) - finisher);
finisher = Math.max(finisher, jStarts + profile[jInd].execution);
}
exclusive += Math.max(0, end - finisher);
return triple(ind, execution === 0 ? 0 : execution * (execution / (execution + overlap)), exclusive);
});
const etimes = [];
const wtimes = [];
for (const [ind, etime, wtime] of costs) {
etimes[ind] = etime;
wtimes[ind] = wtime;
}
return [etimes, wtimes];
}
function ruleData(etimes, wtimes, search) {
return search.mapProfiles((ps, name) => ({
name,
count: ps.length,
leaf: ps.every(p => p.depends.length === 0),
run: ps.map(p => p.built).minimum(),
changed: ps.some(p => p.built === p.changed),
time: ps.map(p => p.execution).sum(),
etime: ps.map(p => etimes[p.index]).sum(),
wtime: ps.map(p => wtimes[p.index]).sum(),
untraced: ps.map(untraced).sum()
}));
}
function reportSummary(profile) {
let countLast = 0; // number of rules run in the last run
let highestRun = 0; // highest run you have seen (add 1 to get the count of runs)
let sumExecution = 0; // build time in total
let sumExecutionLast = 0; // build time in total
let countTrace = -1;
let countTraceLast = -1; // traced commands run
// start both are -1 because the end command will have run in the previous step
let maxTraceStopLast = 0; // time the last traced command stopped
for (const p of profile) {
sumExecution += p.execution;
highestRun = Math.max(highestRun, p.changed); // changed is always greater or equal to built
countTrace += p.traces.length;
if (p.built === 0) {
sumExecutionLast += p.execution;
countLast++;
countTraceLast += p.traces.length;
if (p.traces.length > 0)
maxTraceStopLast = Math.max(maxTraceStopLast, p.traces.last().stop);
}
}
return React.createElement("div", null,
React.createElement("h2", null, "Totals"),
React.createElement("ul", null,
React.createElement("li", null,
React.createElement("b", null, "Runs:"),
" ",
showInt(highestRun + 1),
" ",
React.createElement("span", { class: "note" }, "number of times Shake has been run.")),
React.createElement("li", null,
React.createElement("b", null, "Rules:"),
" ",
showInt(profile.length),
" (",
showInt(countLast),
" in last run) ",
React.createElement("span", { class: "note" }, "number of defined rules, e.g. individual files.")),
React.createElement("li", null,
React.createElement("b", null, "Traced:"),
" ",
showInt(countTrace),
" (",
showInt(countTraceLast),
" in last run)",
React.createElement("span", { class: "note" },
"number of calls to ",
varLink("cmd"),
" or ",
varLink("traced"),
"."))),
React.createElement("h2", null, "Performance"),
React.createElement("ul", null,
React.createElement("li", null,
React.createElement("b", null, "Build time:"),
" ",
showTime(sumExecution),
" ",
React.createElement("span", { class: "note" }, "how long a complete build would take single threaded.")),
React.createElement("li", null,
React.createElement("b", null, "Last build time:"),
" ",
showTime(maxTraceStopLast),
" ",
React.createElement("span", { class: "note" }, "how long the last build take.")),
React.createElement("li", null,
React.createElement("b", null, "Parallelism:"),
" ",
(maxTraceStopLast === 0 ? 0 : sumExecutionLast / maxTraceStopLast).toFixed(2),
" ",
React.createElement("span", { class: "note" }, "average number of commands executing simultaneously in the last build.")),
React.createElement("li", null,
React.createElement("b", null, "Speculative critical path:"),
" ",
showTime(speculativeCriticalPath(profile)),
" ",
React.createElement("span", { class: "note" }, "how long it would take on infinite CPUs.")),
React.createElement("li", null,
React.createElement("b", null, "Precise critical path:"),
" ",
showTime(preciseCriticalPath(profile)),
" ",
React.createElement("span", { class: "note" }, "critical path not speculatively executing."))));
}
function speculativeCriticalPath(profile) {
const criticalPath = []; // the critical path to any element
let maxCriticalPath = 0;
for (const p of profile) {
let cost = 0;
for (const ds of p.depends)
for (const d of ds)
cost = Math.max(cost, criticalPath[d]);
cost += p.execution;
maxCriticalPath = Math.max(cost, maxCriticalPath);
criticalPath[p.index] = cost;
}
return maxCriticalPath;
}
/*
Calculating a precise critical path, taking into account the deep dependeny structure, is non-obvious.
Dependencies have the type [{X}], e.g:
X = [{a,b},{c,d}]
That is r builds a and b, then after those both complete (assuming they don't change), it builds c and d,
then it is finished. Importantly, r doesn't start building c/d until after a and b have finished. This
detail extends the critical path.
To calculate the precise critical path, we simulate with the notion of demand and waiting.
*/
function preciseCriticalPath(profile) {
const waiting = profile.map(x => x.depends.concatLength()); // number I am waiting on before I am done
const demanded = []; // I have been demanded by someone
const oncomplete = []; // Completion functions
const complete = []; // Who is complete already
const running = [];
let timestamp = 0;
// demand dependency set N of a rule
function demandN(p, round) {
for (; round < p.depends.length; round++) {
let todo = p.depends[round].length; // Number before we continue
const step = () => {
todo--;
if (todo === 0)
demandN(p, round + 1);
};
for (const d of p.depends[round]) {
if (complete[d])
todo--;
else {
const old = oncomplete[d];
oncomplete[d] = !old ? step : () => { old(); step(); };
demand(profile[d]);
}
}
if (todo !== 0)
break;
// todo === 0, so continue (equivalent to calling step but tail recursive)
}
}
// demand a particular rule
function demand(p) {
if (demanded[p.index])
return;
demanded[p.index] = true;
if (waiting[p.index] === 0)
running.insertSorted([p.index, timestamp + p.execution], compareSndRev);
else
demandN(p, 0);
}
// We don't know the targets we ask for, so we approximate by saying the ones which nothing depends on
for (const p of profile) {
if (p.rdepends.length === 0)
demand(p);
}
while (running.length > 0) {
const [ind, time] = running.pop();
timestamp = time;
complete[ind] = true;
if (oncomplete[ind]) {
oncomplete[ind]();
delete oncomplete[ind];
}
for (const d of profile[ind].rdepends) {
waiting[d]--;
if (waiting[d] === 0 && demanded[d])
running.insertSorted([d, timestamp + profile[d].execution], compareSndRev);
}
}
for (let i = 0; i < profile.length; i++)
if (!complete[i])
throw new Error("Failed to run all tasks");
return timestamp;
}