ceremonyclient/pebble/docs/js/write-throughput.js
Cassandra Heart 2e2a1e4789
v1.2.0 (#31)
2024-01-03 01:31:42 -06:00

421 lines
13 KiB
JavaScript

// TODO(travers): support multiple time-seriies on the summary chart, once we
// have data available.
const writeThroughputWorkload = "write/values=1024";
/*
* Returns the full URL to the write-throughput summary JSON file.
*/
function writeThroughputSummaryURL() {
return "https://pebble-benchmarks.s3.amazonaws.com/write-throughput/summary.json";
}
/*
* Returns the full URL to a write-throughput summary detail file, given the
* filename.
*/
function writeThroughputDetailURL(filename) {
return `https://pebble-benchmarks.s3.amazonaws.com/write-throughput/${filename}`;
}
/*
* Renders the appropriate detail view given the array of data and the date
* extract.
*
* This function works by using the provided date to "bisect" into the data
* array and pull out the corresponding datapoint.
*/
function bisectAndRenderWriteThroughputDetail(data, detailDate) {
const bisect = d3.bisector(d => parseTime(d.date)).left;
let i = bisect(data, detailDate, 1);
let workload = data[i];
let date = workload.date;
let name = workload.name;
let opsSec = workload.opsSec;
let filename = workload.summaryPath;
fetchWriteThroughputSummaryData(filename)
.then(
d => renderWriteThroughputSummaryDetail(name, date, opsSec, d),
_ => renderWriteThroughputSummaryDetail(name, date, opsSec, null),
);
}
/*
* Renders the write-throughput summary view, given the correspnding data.
*
* This function generates a time-series similar to the YCSB benchmark data.
* The x-axis represents the day on which the becnhmark was run, and the y-axis
* represents the calculated "max sustainable throughput" in ops-second.
*
* Clicking on an individual day renders the detail view for the given day,
* allowing the user to drill down into the per-worker performance.
*/
function renderWriteThroughputSummary(allData) {
const svg = d3.select(".chart.write-throughput");
// Filter on the appropriate time-series.
const dataKey = "write/values=1024";
const data = allData[dataKey];
// Set up axes.
const margin = {top: 25, right: 60, bottom: 25, left: 60};
let maxY = d3.max(data, d => d.opsSec);
const width = styleWidth(svg) - margin.left - margin.right;
const height = styleHeight(svg) - margin.top - margin.bottom;
const x = d3.scaleTime()
.domain([minDate, max.date])
.range([0, width]);
const x2 = d3.scaleTime()
.domain([minDate, max.date])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, maxY * 1.1])
.range([height, 0]);
const z = d3.scaleOrdinal(d3.schemeCategory10);
const xAxis = d3.axisBottom(x)
.ticks(5);
const yAxis = d3.axisLeft(y)
.ticks(5);
const g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
g.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
g.append("text")
.attr("class", "chart-title")
.attr("x", margin.left + width / 2)
.attr("y", 0)
.style("text-anchor", "middle")
.style("font", "8pt sans-serif")
.text(dataKey);
// Create a rectangle that can be used to clip the data. This avoids having
// the time-series spill across the y-axis when panning and zooming.
const defs = svg.append("defs");
defs.append("clipPath")
.attr("id", dataKey)
.append("rect")
.attr("x", 0)
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top + height + 10);
// Plot time-series.
const view = g.append("g")
.attr("class", "view")
.attr("clip-path", "url(#" + dataKey + ")");
const line = d3.line()
.x(d => x(parseTime(d.date)))
.y(d => y(d.opsSec));
const path = view.selectAll(".line1")
.data([data])
.enter()
.append("path")
.attr("class", "line1")
.attr("d", line)
.style("stroke", z(0));
// Hover to show labels.
const lineHover = g
.append("line")
.attr("class", "hover")
.style("fill", "none")
.style("stroke", "#f99")
.style("stroke-width", "1px");
const dateHover = g
.append("text")
.attr("class", "hover")
.attr("fill", "#f22")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "hanging")
.attr("transform", "translate(0, 0)");
const opsHover = g
.append("text")
.attr("class", "hover")
.attr("fill", "#f22")
.attr("text-anchor", "middle")
.attr("transform", "translate(0, 0)");
const marker = g
.append("circle")
.attr("class", "hover")
.attr("r", 3)
.style("opacity", "0")
.style("stroke", "#f22")
.style("fill", "#f22");
svg.node().updateMouse = function (mouse, date, hover) {
const mousex = mouse[0];
const bisect = d3.bisector(d => parseTime(d.date)).left;
const i = bisect(data, date, 1);
const v =
i === data.length
? data[i - 1]
: mousex - x(parseTime(data[i - 1].date)) < x(parseTime(data[i].date)) - mousex
? data[i - 1]
: data[i];
const noData = mousex < x(parseTime(data[0].date));
let lineY = height;
if (!noData) {
lineY = pathGetY(path.node(), mousex);
}
let val, valY, valFormat;
val = v.opsSec;
valY = y(val);
valFormat = d3.format(",.0f");
lineHover
.attr("x1", mousex)
.attr("x2", mousex)
.attr("y1", lineY)
.attr("y2", height);
marker.attr("transform", "translate(" + x(parseTime(v.date)) + "," + valY + ")");
dateHover
.attr("transform", "translate(" + mousex + "," + (height + 8) + ")")
.text(formatTime(date));
opsHover
.attr("transform", "translate(" + x(parseTime(v.date)) + "," + (valY - 7) + ")")
.text(valFormat(val));
};
// Panning and zooming.
const updateZoom = function (t) {
x.domain(t.rescaleX(x2).domain());
g.select(".axis--x").call(xAxis);
g.selectAll(".line1").attr("d", line);
};
svg.node().updateZoom = updateZoom;
const zoom = d3.zoom()
.extent([[0, 0], [width, 1]])
.scaleExtent([0.25, 2]) // [45, 360] days
.translateExtent([[-width * 3, 0], [width, 1]]) // [today-360, today]
.on("zoom", function () {
const t = d3.event.transform;
if (!d3.event.sourceEvent) {
updateZoom(t);
return;
}
d3.selectAll(".chart").each(function () {
if (this.updateZoom != null) {
this.updateZoom(t);
}
});
d3.selectAll(".chart").each(function () {
this.__zoom = t.translate(0, 0);
});
});
svg.call(zoom);
svg.call(zoom.transform, d3.zoomTransform(svg.node()));
svg.append("rect")
.attr("class", "mouse")
.attr("cursor", "move")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("width", width)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + 0 + ")")
.on("mousemove", function () {
const mouse = d3.mouse(this);
const date = x.invert(mouse[0]);
d3.selectAll(".chart").each(function () {
if (this.updateMouse != null) {
this.updateMouse(mouse, date, 1);
}
});
})
.on("mouseover", function () {
d3.selectAll(".chart")
.selectAll(".hover")
.style("opacity", 1.0);
})
.on("mouseout", function () {
d3.selectAll(".chart")
.selectAll(".hover")
.style("opacity", 0);
})
.on("click", function(d) {
// Use the date corresponding to the clicked data point to bisect
// into the workload data to pluck out the correct datapoint.
const mouse = d3.mouse(this);
let detailDate = d3.timeDay.floor(x.invert(mouse[0]));
bisectAndRenderWriteThroughputDetail(data, detailDate);
});
}
function fetchWriteThroughputSummaryData(file) {
return fetch(writeThroughputDetailURL(file))
.then(response => response.json())
.then(data => {
for (let key in data) {
let csvData = data[key].rawData;
data[key].data = d3.csvParseRows(csvData, function (d, i) {
return {
elapsed: +d[0],
opsSec: +d[1],
passed: d[2] === 'true',
size: +d[3],
levels: +d[4],
};
});
delete data[key].rawData;
}
return data;
});
}
/*
* Renders the write-throughput detail view, given the correspnding data, and
* the particular workload and date on which it was run.
*
* This function generates a series with the x-axis representing the elapsed
* time since the start of the benchmark, and the measured write load at that
* point in time (in ops/second). Each series is a worker that participated in
* the benchmark on the selected date.
*/
function renderWriteThroughputSummaryDetail(workload, date, opsSec, rawData) {
const svg = d3.select(".chart.write-throughput-detail");
// Remove anything that was previously on the canvas. This ensures that a
// user clicking multiple times does not keep adding data to the canvas.
svg.selectAll("*").remove();
const margin = {top: 25, right: 60, bottom: 25, left: 60};
let maxX = 0;
let maxY = 0;
for (let key in rawData) {
let run = rawData[key];
maxX = Math.max(maxX, d3.max(run.data, d => d.elapsed));
maxY = Math.max(maxY, d3.max(run.data, d => d.opsSec));
}
const width = styleWidth(svg) - margin.left - margin.right;
const height = styleHeight(svg) - margin.top - margin.bottom;
// Panning and zooming.
// These callbacks are defined as they are called from the panning /
// zooming functions elsewhere, however, they are simply no-ops on this
// chart, as they x-axis is a measure of "elapsed time" rather than a date.
svg.node().updateMouse = function (mouse, date, hover) {}
svg.node().updateZoom = function () {};
// Set up axes.
const x = d3.scaleLinear()
.domain([0, 8.5 * 3600])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, maxY * 1.1])
.range([height, 0]);
const z = d3.scaleOrdinal(d3.schemeCategory10);
const xAxis = d3.axisBottom(x)
.ticks(5)
.tickFormat(d => Math.floor(d / 3600) + "h");
const yAxis = d3.axisLeft(y)
.ticks(5);
const g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
g.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
// If we get no data, we just render an empty chart.
if (rawData == null) {
g.append("text")
.attr("class", "chart-title")
.attr("x", margin.left + width / 2)
.attr("y", height / 2)
.style("text-anchor", "middle")
.style("font", "8pt sans-serif")
.text("Data unavailable");
return;
}
g.append("text")
.attr("class", "chart-title")
.attr("x", margin.left + width / 2)
.attr("y", 0)
.style("text-anchor", "middle")
.style("font", "8pt sans-serif")
.text("Ops/sec over time");
// Plot data.
const view = g.append("g")
.attr("class", "view");
let values = [];
for (let key in rawData) {
values.push({
id: key,
values: rawData[key].data,
});
}
const line = d3.line()
.x(d => x(d.elapsed))
.y(d => y(d.opsSec));
const path = view.selectAll(".line1")
.data(values)
.enter()
.append("path")
.attr("class", "line1")
.attr("d", d => line(d.values))
.style("stroke", d => z(d.id));
// Draw a horizontal line for the calculated ops/sec average.
view.append("path")
.attr("d", d3.line()([[x(0), y(opsSec)], [x(maxX), y(opsSec)]]))
.attr("stroke", "black")
.attr("stroke-width", "2")
.style("stroke-dasharray", ("2, 5"));
}