399 строки
11 KiB
JavaScript
399 строки
11 KiB
JavaScript
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);
|
|
}
|