blob: aa1042b8b5cb1b79c4b2fe5d4ca74daa5dede107 [file] [log] [blame]
function StackedAreaChart({
xSeries,
marginTop = 30, // top margin, in pixels
marginRight = 100, // right margin, in pixels
marginBottom = 60, // bottom margin, in pixels
marginLeft = 50, // left margin, in pixels
} = {}) {
const width = 756;
const height = 189;
const svg = d3.create("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", [0, 0, width, height]);
const xRange = [marginLeft, width - marginRight]; // [left, right]
const yRange = [height - marginBottom, marginTop]; // [bottom, top]
// Add empty axes first.
svg.append("g")
.classed("x axis", true)
.attr("transform", `translate(0,${height-marginBottom})`)
svg.append("g")
.classed("y axis", true)
.attr("transform", `translate(${marginLeft},0)`)
const update = function(data, mutTime) {
const seriesKeys = Object.keys(data[0]).filter(s => s !== xSeries);
let seriesColors = new Array();
if (seriesKeys.length > 3) {
const colorFn = d3.interpolateViridis;
for (let i = 0; i < seriesKeys.length; i++) {
seriesColors.push(colorFn((i / 10) - Math.floor(i/10)));
}
} else {
seriesColors = ["#253443", "#007d9c", "#50b7e0"];
if (seriesKeys.length < 3) {
seriesColors = seriesColors.slice(seriesKeys.length-1);
}
}
const seriesScale = d3.scaleOrdinal()
.domain(seriesKeys)
.range(seriesColors);
const yStack = (d3.stack().keys(seriesKeys))(data);
const xDomain = d3.extent(d3.map(data, p => p[xSeries]));
const yDomain = d3.extent(d3.map(yStack[yStack.length-1], p => p[1]));
yDomain[0] = 0;
const xScale = d3.scaleLinear(xDomain, xRange);
const yScale = d3.scaleLinear(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).tickFormat(x => `${x.toFixed(1)} s`);
svg.selectAll("g.x.axis")
.style("font-size", "11px")
.call(xAxis);
const yAxis = d3.axisLeft(yScale).ticks(5).tickFormat(x => `${x.toFixed(0)} MiB`);
svg.selectAll("g.y.axis")
.style("font-size", "11px")
.call(yAxis);
const area = d3.area()
.curve(d3.curveLinear)
.x(d => xScale(d.data[xSeries]))
.y0(d => yScale(d[0]))
.y1(d => yScale(d[1]));
svg.selectAll("path.series")
.data(yStack)
.join("path")
.classed("series", true)
.attr("d", area)
.style("fill", d => seriesScale(d.key));
svg.selectAll("text.label")
.data(seriesKeys)
.join("text")
.classed("label", true)
.attr("text-anchor", "left")
.attr("font-size", "12px")
.attr("x", width-marginRight+20)
.attr("y", d => (seriesKeys.length-1-seriesKeys.indexOf(d))*24+60)
.attr("fill", "currentColor")
.attr("display", (() => {
if (seriesKeys.length <= 3) {
return "inherit";
}
return "none";
})())
.text(d => d);
svg.selectAll("rect.legend")
.data(seriesKeys)
.join("rect")
.classed("legend", true)
.attr("stroke", "none")
.attr("x", width-marginRight+7)
.attr("y", d => (seriesKeys.length-1-seriesKeys.indexOf(d))*24+51)
.attr("width", 10)
.attr("height", 10)
.attr("display", (() => {
if (seriesKeys.length <= 3) {
return "inherit";
}
return "none";
})())
.attr("fill", d => seriesScale(d));
svg.selectAll("text.duration")
.data([xDomain[1]])
.join("text")
.classed("duration", true)
.attr("text-anchor", "left")
.attr("font-size", "10px")
.attr("x", width-marginRight+5)
.attr("y", height-marginBottom+10)
.attr("fill", "currentColor")
.attr("font-weight", "bold")
.text(d => `Total: ${d.toFixed(2)} s`);
svg.selectAll("text.results")
.data([[(xDomain[1]-mutTime)/xDomain[1]*100, yDomain[1]]])
.join("text")
.classed("results", true)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("x", marginLeft + (width-marginLeft-marginRight)/2)
.attr("y", height-marginBottom+37)
.attr("fill", "currentColor")
.attr("font-weight", "bold")
.text(d => `GC CPU = ${d[0].toFixed(1)}%, Peak Mem = ${d[1].toFixed(1)} MiB`);
const peakLive = d3.max(d3.map(data, p => p["Live Heap"]));
const otherMem = d3.max(d3.map(data, p => p["Other Mem."]));
svg.selectAll("text.subresults")
.data([[peakLive, otherMem]])
.join("text")
.classed("subresults", true)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("x", marginLeft + (width-marginLeft-marginRight)/2)
.attr("y", height-marginBottom+51)
.attr("fill", "currentColor")
.text(d => {
let base = "";
if (d[0]) {
base += `Peak Live Mem = ${d[0].toFixed(1)} MiB`;
}
if (d[1]) {
base += `, Other Mem = ${d[1].toFixed(1)} MiB`;
}
if (base !== "") {
base = "(" + base + ")";
}
return base;
});
}
return [svg.node(), update];
}
function gcModel(workload, config) {
let otherMem = config["otherMem"];
if (typeof(otherMem) !== 'number') {
otherMem = document.getElementById(config["otherMem"]).value;
}
let gogc = config["GOGC"];
if (typeof(gogc) !== 'number') {
gogc = document.getElementById(config["GOGC"]).value;
}
let memoryLimit = config["memoryLimit"];
if (typeof(memoryLimit) !== 'number') {
memoryLimit = document.getElementById(config["memoryLimit"]).value;
}
let initialLive = 0;
if ("initialLive" in config) {
initialLive = config["initialLive"];
}
let trackLive = false;
if ("trackLive" in config) {
trackLive = config["trackLive"];
if (typeof(trackLive) !== 'boolean') {
trackLive = document.getElementById(config["trackLive"]).checked;
}
}
let fixedWindow = Infinity;
if ("fixedWindow" in config) {
fixedWindow = config["fixedWindow"];
}
const data = new Array();
// State.
const minHeapGoal = 4; // MiB
let t = 0;
let liveHeap = initialLive;
let newHeap = 0;
let liveFromCycle = new Array();
liveFromCycle.push(initialLive);
liveFromCycle.push(0);
const computeHeapGoal = (liveHeap) => {
let heapGoal = liveHeap*(1.0 + (gogc / 100));
if (gogc === Infinity) {
heapGoal = Infinity;
}
if (heapGoal+otherMem > memoryLimit) {
heapGoal = memoryLimit - otherMem
}
if (gogc !== Infinity && heapGoal < minHeapGoal) {
heapGoal = minHeapGoal
}
if (heapGoal < liveHeap + 0.0625) {
heapGoal = liveHeap + 0.0625
}
return heapGoal
}
let heapGoal = computeHeapGoal(minHeapGoal / (1 + gogc/100)); // Fake a live heap for minHeapGoal.
if (initialLive !== 0) {
heapGoal = computeHeapGoal(initialLive);
}
let n = 0;
const emit = function() {
const datum = {"t": t};
// The series will be automatically stacked, so for the best
// possible presentation, we should make sure to put in
// "other mem" first, then "live," then "new."
// This is roughly in order of "least dynamic" series
// to "most dynamic" which helps make the graph easier to
// interpret.
if (otherMem !== 0) {
datum["Other Mem."] = otherMem;
}
if (trackLive) {
for (let i = 0; i < liveFromCycle.length; i++) {
datum[`Live Heap From GC ${i+1}`] = liveFromCycle[i];
}
} else {
datum["Live Heap"] = liveHeap;
datum["New Heap"] = newHeap;
}
data.push(datum)
}
// Emit points.
emit();
let nextLive = 0;
let nextWillLive = 0;
let nextWillDie = 0;
let totalMutTime = 0;
for (const work of workload) {
let left = work.duration;
let lastLive = liveHeap + nextLive;
const willLive = work.duration * work.allocRate * work.newSurvivalRate;
const willDie = lastLive * work.oldDeathRate;
while (left > 0) {
if (t >= fixedWindow) {
break;
} else if (t + left > fixedWindow) {
left = fixedWindow - t;
}
let alloc = left * work.allocRate;
let endCycle = false;
if (liveHeap+newHeap+alloc > heapGoal) {
alloc = heapGoal-liveHeap-newHeap;
endCycle = true;
}
newHeap += alloc;
// Calculate mutator time.
const mutTime = alloc / work.allocRate;
left -= mutTime;
t += mutTime;
totalMutTime += mutTime;
nextLive += (willLive - willDie) * (mutTime / work.duration);
// For tracking per-GC live memory.
nextWillLive += willLive * (mutTime / work.duration);
nextWillDie += willDie * (mutTime / work.duration);
liveFromCycle[liveFromCycle.length-1] = newHeap;
if (endCycle) {
emit();
liveHeap += nextLive;
for (let i = 0; i < liveFromCycle.length; i++) {
const live = liveFromCycle[i];
if (live > 0) {
if (live > nextWillDie) {
liveFromCycle[i] -= nextWillDie;
nextWillDie = 0;
break;
}
nextWillDie -= live;
liveFromCycle[i] = 0;
}
}
liveFromCycle[liveFromCycle.length-1] = nextWillLive;
nextLive = 0;
nextWillLive = 0;
nextWillDie = 0;
newHeap = 0;
const gcTime = liveHeap / work.scanRate + config.fixedCost;
t += gcTime;
emit();
heapGoal = computeHeapGoal(liveHeap)
liveFromCycle.push(newHeap);
}
}
emit();
}
if (trackLive) {
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < liveFromCycle.length; j++) {
const key = `Live Heap From GC ${j+1}`;
if (!(key in data[i])) {
data[i][key] = 0;
}
}
}
}
return [data, totalMutTime];
}
const graphs = document.querySelectorAll('.gc-guide-graph');
for (let i = 0; i < graphs.length; i++) {
const workload = JSON.parse(graphs[i].getAttribute("data-workload"));
const config = JSON.parse(graphs[i].getAttribute("data-config"));
const [chart, update] = StackedAreaChart({xSeries: "t"});
const setupSlider = function(parameter, f, fmt) {
if (typeof(config[parameter]) !== 'number') {
const id = config[parameter];
const slider = document.getElementById(id);
const display = document.getElementById(id+"-display");
const value = f(slider.value);
if (display) {
display.innerHTML = fmt(value);
}
config[parameter] = value;
slider.oninput = function() {
const value = f(this.value);
if (display) {
display.innerHTML = fmt(value);
}
config[parameter] = value;
const [data, mutTime] = gcModel(workload, config);
update(data, mutTime);
}
}
};
const setupCheckbox = function(parameter) {
if (parameter in config && typeof(config[parameter]) !== 'boolean') {
const id = config[parameter];
const checkbox = document.getElementById(id);
config[parameter] = checkbox.checked;
checkbox.oninput = function() {
config[parameter] = checkbox.checked;
const [data, mutTime] = gcModel(workload, config);
update(data, mutTime);
}
}
};
setupSlider("otherMem", x => parseInt(x), x => `${x} MiB`);
setupSlider("GOGC", x => {
const v = Math.round(Math.pow(2, parseFloat(x)))
if (v >= 1024) {
return Infinity;
}
return v;
}, x => {
if (x === Infinity) {
return "off";
}
return `${x}`;
});
setupSlider("memoryLimit", x => parseFloat(x), x => `${x.toFixed(1)} MiB`);
setupCheckbox("trackLive");
const [data, mutTime] = gcModel(workload, config);
update(data, mutTime);
graphs[i].appendChild(chart);
}