mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-12-27 17:15:18 +00:00
421 lines
13 KiB
JavaScript
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"));
|
|
}
|