| 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); | |
| } |