rattle-0.2: html/rattle.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.writers.length === 0;
}
function run(i) {
if (i === undefined)
return environmentThis.built;
else
return environmentThis.built === i;
}
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 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],
builtLast: x[3],
changed: x[4],
filesWritten: x[5],
filesRead: x[6],
readers: x[7],
writers: x[8],
hazards: x[9] // depends and rdepends that violate consistency
}));
return ans;
}
function profileRoot(profile) {
const [s, search] = createSearch(profile);
const t = createTabs([["Summary", () => reportSummary(profile)]
// , ["Command plot", () => reportCmdPlot(profile)]
// , ["Commands", () => reportCmdTable(profile, search)]
,
["Commands", () => 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://github.com/ndmitchell/rattle", style: "font-size: 20px; text-decoration: none; color: #3131a7; font-weight: bold;" }, "Rattle profile report"),
React.createElement("span", { style: "color:gray;white-space:pre;" },
" - generated at ",
generated,
" by Rattle 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);
}
// Stuff that Shake generates and injects in
function untraced(p) {
return 0;
}
/////////////////////////////////////////////////////////////////////
// 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 showBool(b) {
return b === 1 ? "True" : "False";
}
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 showFile(p) {
if (p[1]) {
return React.createElement("li", null,
React.createElement("b", null, p[0]));
}
else {
return React.createElement("li", null, p[0]);
}
}
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, "Built last run:"),
" ",
showBool(p.builtLast)),
React.createElement("li", null,
React.createElement("b", null, "Changed:"),
" ",
showBool(p.changed)),
React.createElement("li", null,
React.createElement("b", null, "Execution time:"),
showTime(p.execution)),
React.createElement("li", null,
React.createElement("b", null, "Cmds that wrote files I read:"),
React.createElement("ol", null, p.writers.map(d => React.createElement("li", null, f(d))))),
React.createElement("li", null,
React.createElement("b", null, "Cmds that read files I wrote:"),
React.createElement("ul", null, p.readers.map(d => React.createElement("li", null, f(d))))),
React.createElement("li", null,
React.createElement("b", null, "Cmds that I have a hazard with:"),
React.createElement("ul", null, p.hazards.map(d => React.createElement("li", null, f(d))))),
React.createElement("li", null,
React.createElement("b", null, "Files I wrote (bold files changed last run):"),
React.createElement("ul", null, p.filesWritten.map(f => showFile(f)))),
React.createElement("li", null,
React.createElement("b", null, "Files I read (bold files changed last run):"),
React.createElement("ul", null, p.filesRead.map(f => showFile(f)))));
$(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.writers.length === 0);
const waiting = profile.map(x => x.writers.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].readers) {
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 reportRuleTable(profile, search) {
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: "time", label: "Time", width: 75, alignRight: true, show: showTime }
];
return newTable(columns, search.map(s => ruleData(s)), "time", true);
}
function ruleData(search) {
return search.mapProfiles((ps, name) => ({
name,
count: ps.length,
leaf: ps.every(p => p.writers.length === 0),
run: ps.map(p => p.built).minimum(),
time: ps.map(p => p.execution).sum()
}));
}
function reportSummary(profile) {
let sumExecution = 0; // build time in total
const countTrace = profile.length;
// start both are -1 because the end command will have run in the previous step
for (const p of profile) {
sumExecution += p.execution;
}
return React.createElement("div", null,
React.createElement("h2", null, "Totals"),
React.createElement("ul", null,
React.createElement("li", null,
React.createElement("b", null, "Rules:"),
" ",
showInt(profile.length),
" ",
React.createElement("span", { class: "note" }, "number of commands."))),
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, "Speculative critical path:"),
" ",
showTime(speculativeCriticalPath(profile)),
" ",
React.createElement("span", { class: "note" }, "how long it would take on infinite CPUs."))));
}
function speculativeCriticalPath(profile) {
const criticalPath = []; // the critical path to any element
let maxCriticalPath = 0;
for (const p of profile) {
let cost = 0;
for (const d of p.writers)
cost = Math.max(cost, criticalPath[d]);
cost += p.execution;
maxCriticalPath = Math.max(cost, maxCriticalPath);
criticalPath[p.index] = cost;
}
return maxCriticalPath;
}