github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/docs/js/write-throughput.js (about) 1 // TODO(travers): support multiple time-seriies on the summary chart, once we 2 // have data available. 3 const writeThroughputWorkload = "write/values=1024"; 4 5 /* 6 * Returns the full URL to the write-throughput summary JSON file. 7 */ 8 function writeThroughputSummaryURL() { 9 return "https://pebble-benchmarks.s3.amazonaws.com/write-throughput/summary.json"; 10 } 11 12 /* 13 * Returns the full URL to a write-throughput summary detail file, given the 14 * filename. 15 */ 16 function writeThroughputDetailURL(filename) { 17 return `https://pebble-benchmarks.s3.amazonaws.com/write-throughput/${filename}`; 18 } 19 20 /* 21 * Renders the appropriate detail view given the array of data and the date 22 * extract. 23 * 24 * This function works by using the provided date to "bisect" into the data 25 * array and pull out the corresponding datapoint. 26 */ 27 function bisectAndRenderWriteThroughputDetail(data, detailDate) { 28 const bisect = d3.bisector(d => parseTime(d.date)).left; 29 let i = bisect(data, detailDate, 1); 30 31 let workload = data[i]; 32 let date = workload.date; 33 let name = workload.name; 34 let opsSec = workload.opsSec; 35 let filename = workload.summaryPath; 36 37 fetchWriteThroughputSummaryData(filename) 38 .then( 39 d => renderWriteThroughputSummaryDetail(name, date, opsSec, d), 40 _ => renderWriteThroughputSummaryDetail(name, date, opsSec, null), 41 ); 42 } 43 44 /* 45 * Renders the write-throughput summary view, given the correspnding data. 46 * 47 * This function generates a time-series similar to the YCSB benchmark data. 48 * The x-axis represents the day on which the becnhmark was run, and the y-axis 49 * represents the calculated "max sustainable throughput" in ops-second. 50 * 51 * Clicking on an individual day renders the detail view for the given day, 52 * allowing the user to drill down into the per-worker performance. 53 */ 54 function renderWriteThroughputSummary(allData) { 55 const svg = d3.select(".chart.write-throughput"); 56 57 // Filter on the appropriate time-series. 58 const dataKey = "write/values=1024"; 59 const data = allData[dataKey]; 60 61 // Set up axes. 62 63 const margin = {top: 25, right: 60, bottom: 25, left: 60}; 64 let maxY = d3.max(data, d => d.opsSec); 65 66 const width = styleWidth(svg) - margin.left - margin.right; 67 const height = styleHeight(svg) - margin.top - margin.bottom; 68 69 const x = d3.scaleTime() 70 .domain([minDate, max.date]) 71 .range([0, width]); 72 const x2 = d3.scaleTime() 73 .domain([minDate, max.date]) 74 .range([0, width]); 75 76 const y = d3.scaleLinear() 77 .domain([0, maxY * 1.1]) 78 .range([height, 0]); 79 80 const z = d3.scaleOrdinal(d3.schemeCategory10); 81 82 const xAxis = d3.axisBottom(x) 83 .ticks(5); 84 85 const yAxis = d3.axisLeft(y) 86 .ticks(5); 87 88 const g = svg 89 .append("g") 90 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 91 92 g.append("g") 93 .attr("class", "axis axis--x") 94 .attr("transform", "translate(0," + height + ")") 95 .call(xAxis); 96 97 g.append("g") 98 .attr("class", "axis axis--y") 99 .call(yAxis); 100 101 g.append("text") 102 .attr("class", "chart-title") 103 .attr("x", margin.left + width / 2) 104 .attr("y", 0) 105 .style("text-anchor", "middle") 106 .style("font", "8pt sans-serif") 107 .text(dataKey); 108 109 // Create a rectangle that can be used to clip the data. This avoids having 110 // the time-series spill across the y-axis when panning and zooming. 111 112 const defs = svg.append("defs"); 113 114 defs.append("clipPath") 115 .attr("id", dataKey) 116 .append("rect") 117 .attr("x", 0) 118 .attr("y", -margin.top) 119 .attr("width", width) 120 .attr("height", margin.top + height + 10); 121 122 // Plot time-series. 123 124 const view = g.append("g") 125 .attr("class", "view") 126 .attr("clip-path", "url(#" + dataKey + ")"); 127 128 const line = d3.line() 129 .x(d => x(parseTime(d.date))) 130 .y(d => y(d.opsSec)); 131 132 const path = view.selectAll(".line1") 133 .data([data]) 134 .enter() 135 .append("path") 136 .attr("class", "line1") 137 .attr("d", line) 138 .style("stroke", z(0)); 139 140 // Hover to show labels. 141 142 const lineHover = g 143 .append("line") 144 .attr("class", "hover") 145 .style("fill", "none") 146 .style("stroke", "#f99") 147 .style("stroke-width", "1px"); 148 149 const dateHover = g 150 .append("text") 151 .attr("class", "hover") 152 .attr("fill", "#f22") 153 .attr("text-anchor", "middle") 154 .attr("alignment-baseline", "hanging") 155 .attr("transform", "translate(0, 0)"); 156 157 const opsHover = g 158 .append("text") 159 .attr("class", "hover") 160 .attr("fill", "#f22") 161 .attr("text-anchor", "middle") 162 .attr("transform", "translate(0, 0)"); 163 164 const marker = g 165 .append("circle") 166 .attr("class", "hover") 167 .attr("r", 3) 168 .style("opacity", "0") 169 .style("stroke", "#f22") 170 .style("fill", "#f22"); 171 172 svg.node().updateMouse = function (mouse, date, hover) { 173 const mousex = mouse[0]; 174 const bisect = d3.bisector(d => parseTime(d.date)).left; 175 const i = bisect(data, date, 1); 176 const v = 177 i === data.length 178 ? data[i - 1] 179 : mousex - x(parseTime(data[i - 1].date)) < x(parseTime(data[i].date)) - mousex 180 ? data[i - 1] 181 : data[i]; 182 const noData = mousex < x(parseTime(data[0].date)); 183 184 let lineY = height; 185 if (!noData) { 186 lineY = pathGetY(path.node(), mousex); 187 } 188 189 let val, valY, valFormat; 190 val = v.opsSec; 191 valY = y(val); 192 valFormat = d3.format(",.0f"); 193 194 lineHover 195 .attr("x1", mousex) 196 .attr("x2", mousex) 197 .attr("y1", lineY) 198 .attr("y2", height); 199 marker.attr("transform", "translate(" + x(parseTime(v.date)) + "," + valY + ")"); 200 dateHover 201 .attr("transform", "translate(" + mousex + "," + (height + 8) + ")") 202 .text(formatTime(date)); 203 opsHover 204 .attr("transform", "translate(" + x(parseTime(v.date)) + "," + (valY - 7) + ")") 205 .text(valFormat(val)); 206 }; 207 208 // Panning and zooming. 209 210 const updateZoom = function (t) { 211 x.domain(t.rescaleX(x2).domain()); 212 g.select(".axis--x").call(xAxis); 213 g.selectAll(".line1").attr("d", line); 214 }; 215 svg.node().updateZoom = updateZoom; 216 217 const zoom = d3.zoom() 218 .extent([[0, 0], [width, 1]]) 219 .scaleExtent([0.25, 2]) // [45, 360] days 220 .translateExtent([[-width * 3, 0], [width, 1]]) // [today-360, today] 221 .on("zoom", function () { 222 const t = d3.event.transform; 223 if (!d3.event.sourceEvent) { 224 updateZoom(t); 225 return; 226 } 227 228 d3.selectAll(".chart").each(function () { 229 if (this.updateZoom != null) { 230 this.updateZoom(t); 231 } 232 }); 233 234 d3.selectAll(".chart").each(function () { 235 this.__zoom = t.translate(0, 0); 236 }); 237 }); 238 239 svg.call(zoom); 240 svg.call(zoom.transform, d3.zoomTransform(svg.node())); 241 242 svg.append("rect") 243 .attr("class", "mouse") 244 .attr("cursor", "move") 245 .attr("fill", "none") 246 .attr("pointer-events", "all") 247 .attr("width", width) 248 .attr("height", height + margin.top + margin.bottom) 249 .attr("transform", "translate(" + margin.left + "," + 0 + ")") 250 .on("mousemove", function () { 251 const mouse = d3.mouse(this); 252 const date = x.invert(mouse[0]); 253 254 d3.selectAll(".chart").each(function () { 255 if (this.updateMouse != null) { 256 this.updateMouse(mouse, date, 1); 257 } 258 }); 259 }) 260 .on("mouseover", function () { 261 d3.selectAll(".chart") 262 .selectAll(".hover") 263 .style("opacity", 1.0); 264 }) 265 .on("mouseout", function () { 266 d3.selectAll(".chart") 267 .selectAll(".hover") 268 .style("opacity", 0); 269 }) 270 .on("click", function(d) { 271 // Use the date corresponding to the clicked data point to bisect 272 // into the workload data to pluck out the correct datapoint. 273 const mouse = d3.mouse(this); 274 let detailDate = d3.timeDay.floor(x.invert(mouse[0])); 275 bisectAndRenderWriteThroughputDetail(data, detailDate); 276 }); 277 } 278 279 function fetchWriteThroughputSummaryData(file) { 280 return fetch(writeThroughputDetailURL(file)) 281 .then(response => response.json()) 282 .then(data => { 283 for (let key in data) { 284 let csvData = data[key].rawData; 285 data[key].data = d3.csvParseRows(csvData, function (d, i) { 286 return { 287 elapsed: +d[0], 288 opsSec: +d[1], 289 passed: d[2] === 'true', 290 size: +d[3], 291 levels: +d[4], 292 }; 293 }); 294 delete data[key].rawData; 295 } 296 return data; 297 }); 298 } 299 300 /* 301 * Renders the write-throughput detail view, given the correspnding data, and 302 * the particular workload and date on which it was run. 303 * 304 * This function generates a series with the x-axis representing the elapsed 305 * time since the start of the benchmark, and the measured write load at that 306 * point in time (in ops/second). Each series is a worker that participated in 307 * the benchmark on the selected date. 308 */ 309 function renderWriteThroughputSummaryDetail(workload, date, opsSec, rawData) { 310 const svg = d3.select(".chart.write-throughput-detail"); 311 312 // Remove anything that was previously on the canvas. This ensures that a 313 // user clicking multiple times does not keep adding data to the canvas. 314 svg.selectAll("*").remove(); 315 316 const margin = {top: 25, right: 60, bottom: 25, left: 60}; 317 let maxX = 0; 318 let maxY = 0; 319 for (let key in rawData) { 320 let run = rawData[key]; 321 maxX = Math.max(maxX, d3.max(run.data, d => d.elapsed)); 322 maxY = Math.max(maxY, d3.max(run.data, d => d.opsSec)); 323 } 324 325 const width = styleWidth(svg) - margin.left - margin.right; 326 const height = styleHeight(svg) - margin.top - margin.bottom; 327 328 // Panning and zooming. 329 // These callbacks are defined as they are called from the panning / 330 // zooming functions elsewhere, however, they are simply no-ops on this 331 // chart, as they x-axis is a measure of "elapsed time" rather than a date. 332 333 svg.node().updateMouse = function (mouse, date, hover) {} 334 svg.node().updateZoom = function () {}; 335 336 // Set up axes. 337 338 const x = d3.scaleLinear() 339 .domain([0, 8.5 * 3600]) 340 .range([0, width]); 341 342 const y = d3.scaleLinear() 343 .domain([0, maxY * 1.1]) 344 .range([height, 0]); 345 346 const z = d3.scaleOrdinal(d3.schemeCategory10); 347 348 const xAxis = d3.axisBottom(x) 349 .ticks(5) 350 .tickFormat(d => Math.floor(d / 3600) + "h"); 351 352 const yAxis = d3.axisLeft(y) 353 .ticks(5); 354 355 const g = svg 356 .append("g") 357 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 358 359 g.append("g") 360 .attr("class", "axis axis--x") 361 .attr("transform", "translate(0," + height + ")") 362 .call(xAxis); 363 364 g.append("g") 365 .attr("class", "axis axis--y") 366 .call(yAxis); 367 368 // If we get no data, we just render an empty chart. 369 if (rawData == null) { 370 g.append("text") 371 .attr("class", "chart-title") 372 .attr("x", margin.left + width / 2) 373 .attr("y", height / 2) 374 .style("text-anchor", "middle") 375 .style("font", "8pt sans-serif") 376 .text("Data unavailable"); 377 return; 378 } 379 380 g.append("text") 381 .attr("class", "chart-title") 382 .attr("x", margin.left + width / 2) 383 .attr("y", 0) 384 .style("text-anchor", "middle") 385 .style("font", "8pt sans-serif") 386 .text("Ops/sec over time"); 387 388 // Plot data. 389 390 const view = g.append("g") 391 .attr("class", "view"); 392 393 let values = []; 394 for (let key in rawData) { 395 values.push({ 396 id: key, 397 values: rawData[key].data, 398 }); 399 } 400 401 const line = d3.line() 402 .x(d => x(d.elapsed)) 403 .y(d => y(d.opsSec)); 404 405 const path = view.selectAll(".line1") 406 .data(values) 407 .enter() 408 .append("path") 409 .attr("class", "line1") 410 .attr("d", d => line(d.values)) 411 .style("stroke", d => z(d.id)); 412 413 // Draw a horizontal line for the calculated ops/sec average. 414 415 view.append("path") 416 .attr("d", d3.line()([[x(0), y(opsSec)], [x(maxX), y(opsSec)]])) 417 .attr("stroke", "black") 418 .attr("stroke-width", "2") 419 .style("stroke-dasharray", ("2, 5")); 420 }