github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/docs/js/app.js (about) 1 // TODO(peter) 2 // - Save pan/zoom settings in query params 3 // 4 // TODO(travers): There exists an awkward ordering script loading issue where 5 // write-throughput.js is loaded first, but contains references to functions 6 // defined in this file. Work out a better way of modularizing this code. 7 8 const parseTime = d3.timeParse("%Y%m%d"); 9 const formatTime = d3.timeFormat("%b %d"); 10 const dateBisector = d3.bisector(d => d.date).left; 11 12 let minDate; 13 let max = { 14 date: new Date(), 15 perChart: {}, 16 opsSec: 0, 17 readBytes: 0, 18 writeBytes: 0, 19 readAmp: 0, 20 writeAmp: 0 21 }; 22 let usePerChartMax = false; 23 let detail; 24 let detailName; 25 let detailFormat; 26 27 let annotations = []; 28 29 function getMaxes(chartKey) { 30 return usePerChartMax ? max.perChart[chartKey] : max; 31 } 32 33 function styleWidth(e) { 34 const width = +e.style("width").slice(0, -2); 35 return Math.round(Number(width)); 36 } 37 38 function styleHeight(e) { 39 const height = +e.style("height").slice(0, -2); 40 return Math.round(Number(height)); 41 } 42 43 function pathGetY(path, x) { 44 // Walk along the path using binary search to locate the point 45 // with the supplied x value. 46 let start = 0; 47 let end = path.getTotalLength(); 48 while (start < end) { 49 const target = (start + end) / 2; 50 const pos = path.getPointAtLength(target); 51 if (Math.abs(pos.x - x) < 0.01) { 52 // Close enough. 53 return pos.y; 54 } else if (pos.x > x) { 55 end = target; 56 } else { 57 start = target; 58 } 59 } 60 return path.getPointAtLength(start).y; 61 } 62 63 // Pretty formatting of a number in human readable units. 64 function humanize(s) { 65 const iecSuffixes = [" B", " KB", " MB", " GB", " TB", " PB", " EB"]; 66 if (s < 10) { 67 return "" + s; 68 } 69 let e = Math.floor(Math.log(s) / Math.log(1024)); 70 let suffix = iecSuffixes[Math.floor(e)]; 71 let val = Math.floor(s / Math.pow(1024, e) * 10 + 0.5) / 10; 72 return val.toFixed(val < 10 ? 1 : 0) + suffix; 73 } 74 75 function dirname(path) { 76 return path.match(/.*\//)[0]; 77 } 78 79 function equalDay(d1, d2) { 80 return ( 81 d1.getYear() == d2.getYear() && 82 d1.getMonth() == d2.getMonth() && 83 d1.getDate() == d2.getDate() 84 ); 85 } 86 87 function computeSegments(data) { 88 return data.reduce(function(segments, d) { 89 if (segments.length == 0) { 90 segments.push([d]); 91 return segments; 92 } 93 94 const lastSegment = segments[segments.length - 1]; 95 const lastDatum = lastSegment[lastSegment.length - 1]; 96 const days = Math.round( 97 (d.date.getTime() - lastDatum.date.getTime()) / 98 (24 * 60 * 60 * 1000) 99 ); 100 if (days == 1) { 101 lastSegment.push(d); 102 } else { 103 segments.push([d]); 104 } 105 return segments; 106 }, []); 107 } 108 109 function computeGaps(segments) { 110 let gaps = []; 111 for (let i = 1; i < segments.length; ++i) { 112 const last = segments[i - 1]; 113 const cur = segments[i]; 114 gaps.push([last[last.length - 1], cur[0]]); 115 } 116 117 // If the last day is not equal to the current day, add a gap that 118 // spans to the current day. 119 const last = segments[segments.length - 1]; 120 const lastDay = last[last.length - 1]; 121 if (!equalDay(lastDay.date, max.date)) { 122 const maxDay = Object.assign({}, lastDay); 123 maxDay.date = max.date; 124 gaps.push([lastDay, maxDay]); 125 } 126 return gaps; 127 } 128 129 function renderChart(chart) { 130 const chartKey = chart.attr("data-key"); 131 const vals = data[chartKey]; 132 133 const svg = chart.html(""); 134 135 const margin = { top: 25, right: 60, bottom: 25, left: 60 }; 136 137 const width = styleWidth(svg) - margin.left - margin.right, 138 height = styleHeight(svg) - margin.top - margin.bottom; 139 140 const defs = svg.append("defs"); 141 const filter = defs 142 .append("filter") 143 .attr("id", "textBackground") 144 .attr("x", 0) 145 .attr("y", 0) 146 .attr("width", 1) 147 .attr("height", 1); 148 filter.append("feFlood").attr("flood-color", "white"); 149 filter.append("feComposite").attr("in", "SourceGraphic"); 150 151 defs 152 .append("clipPath") 153 .attr("id", chartKey) 154 .append("rect") 155 .attr("x", 0) 156 .attr("y", -margin.top) 157 .attr("width", width) 158 .attr("height", margin.top + height + 10); 159 160 const title = svg 161 .append("text") 162 .attr("class", "chart-title") 163 .attr("x", margin.left + width / 2) 164 .attr("y", 15) 165 .style("text-anchor", "middle") 166 .style("font", "8pt sans-serif") 167 .text(chartKey); 168 169 const g = svg 170 .append("g") 171 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 172 173 const x = d3.scaleTime().range([0, width]); 174 const x2 = d3.scaleTime().range([0, width]); 175 const y1 = d3.scaleLinear().range([height, 0]); 176 const z = d3.scaleOrdinal(d3.schemeCategory10); 177 const xFormat = formatTime; 178 179 x.domain([minDate, max.date]); 180 x2.domain([minDate, max.date]); 181 182 y1.domain([0, getMaxes(chartKey).opsSec]); 183 184 const xAxis = d3.axisBottom(x).ticks(5); 185 186 g 187 .append("g") 188 .attr("class", "axis axis--x") 189 .attr("transform", "translate(0," + height + ")") 190 .call(xAxis); 191 g 192 .append("g") 193 .attr("class", "axis axis--y") 194 .call(d3.axisLeft(y1).ticks(5)); 195 196 if (!vals) { 197 // That's all we can draw for an empty chart. 198 svg 199 .append("text") 200 .attr("x", margin.left + width / 2) 201 .attr("y", margin.top + height / 2) 202 .style("text-anchor", "middle") 203 .style("font", "8pt sans-serif") 204 .text("No data"); 205 return; 206 } 207 208 const view = g 209 .append("g") 210 .attr("class", "view") 211 .attr("clip-path", "url(#" + chartKey + ")"); 212 213 const triangle = d3 214 .symbol() 215 .type(d3.symbolTriangle) 216 .size(12); 217 view 218 .selectAll("path.annotation") 219 .data(annotations) 220 .enter() 221 .append("path") 222 .attr("class", "annotation") 223 .attr("d", triangle) 224 .attr("stroke", "#2b2") 225 .attr("fill", "#2b2") 226 .attr( 227 "transform", 228 d => "translate(" + (x(d.date) + "," + (height + 5) + ")") 229 ); 230 231 view 232 .selectAll("line.annotation") 233 .data(annotations) 234 .enter() 235 .append("line") 236 .attr("class", "annotation") 237 .attr("fill", "none") 238 .attr("stroke", "#2b2") 239 .attr("stroke-width", "1px") 240 .attr("stroke-dasharray", "1 2") 241 .attr("x1", d => x(d.date)) 242 .attr("x2", d => x(d.date)) 243 .attr("y1", 0) 244 .attr("y2", height); 245 246 // Divide the data into contiguous days so that we can avoid 247 // interpolating days where there is missing data. 248 const segments = computeSegments(vals); 249 const gaps = computeGaps(segments); 250 251 const line1 = d3 252 .line() 253 .x(d => x(d.date)) 254 .y(d => y1(d.opsSec)); 255 const path1 = view 256 .selectAll(".line1") 257 .data(segments) 258 .enter() 259 .append("path") 260 .attr("class", "line1") 261 .attr("d", line1) 262 .style("stroke", d => z(0)); 263 264 view 265 .selectAll(".line1-gaps") 266 .data(gaps) 267 .enter() 268 .append("path") 269 .attr("class", "line1-gaps") 270 .attr("d", line1) 271 .attr("opacity", 0.8) 272 .style("stroke", d => z(0)) 273 .style("stroke-dasharray", "1 2"); 274 275 let y2 = d3.scaleLinear().range([height, 0]); 276 let line2; 277 let path2; 278 if (detail) { 279 y2 = d3.scaleLinear().range([height, 0]); 280 y2.domain([0, detail(getMaxes(chartKey))]); 281 g 282 .append("g") 283 .attr("class", "axis axis--y") 284 .attr("transform", "translate(" + width + ",0)") 285 .call( 286 d3 287 .axisRight(y2) 288 .ticks(5) 289 .tickFormat(detailFormat) 290 ); 291 292 line2 = d3 293 .line() 294 .x(d => x(d.date)) 295 .y(d => y2(detail(d))); 296 path2 = view 297 .selectAll(".line2") 298 .data(segments) 299 .enter() 300 .append("path") 301 .attr("class", "line2") 302 .attr("d", line2) 303 .style("stroke", d => z(1)); 304 view 305 .selectAll(".line2-gaps") 306 .data(gaps) 307 .enter() 308 .append("path") 309 .attr("class", "line2-gaps") 310 .attr("d", line2) 311 .attr("opacity", 0.8) 312 .style("stroke", d => z(1)) 313 .style("stroke-dasharray", "1 2"); 314 } 315 316 const updateZoom = function(t) { 317 x.domain(t.rescaleX(x2).domain()); 318 g.select(".axis--x").call(xAxis); 319 g.selectAll(".line1").attr("d", line1); 320 g.selectAll(".line1-gaps").attr("d", line1); 321 if (detail) { 322 g.selectAll(".line2").attr("d", line2); 323 g.selectAll(".line2-gaps").attr("d", line2); 324 } 325 g 326 .selectAll("path.annotation") 327 .attr( 328 "transform", 329 d => "translate(" + (x(d.date) + "," + (height + 5) + ")") 330 ); 331 g 332 .selectAll("line.annotation") 333 .attr("x1", d => x(d.date)) 334 .attr("x2", d => x(d.date)); 335 }; 336 svg.node().updateZoom = updateZoom; 337 338 const hoverSeries = function(mouse) { 339 if (!detail) { 340 return 1; 341 } 342 const mousex = mouse[0]; 343 const mousey = mouse[1] - margin.top; 344 const path1Y = pathGetY(path1.node(), mousex); 345 const path2Y = pathGetY(path2.node(), mousex); 346 return Math.abs(mousey - path1Y) < Math.abs(mousey - path2Y) ? 1 : 2; 347 }; 348 349 // This is a bit funky: initDate() initializes the date range to 350 // [today-90,today]. We then allow zooming out by 4x which will 351 // give a maximum range of 360 days. We limit translation to the 352 // 360 day period. The funkiness is that it would be more natural 353 // to start at the maximum zoomed amount and then initialize the 354 // zoom. But that doesn't work because we want to maintain the 355 // existing zoom settings whenever we have to (re-)render(). 356 const zoom = d3 357 .zoom() 358 .scaleExtent([0.25, 2]) 359 .translateExtent([[-width * 3, 0], [width, 1]]) 360 .extent([[0, 0], [width, 1]]) 361 .on("zoom", function() { 362 const t = d3.event.transform; 363 if (!d3.event.sourceEvent) { 364 updateZoom(t); 365 return; 366 } 367 368 d3.selectAll(".chart").each(function() { 369 if (this.updateZoom != null) { 370 this.updateZoom(t); 371 } 372 }); 373 374 d3.selectAll(".chart").each(function() { 375 this.__zoom = t.translate(0, 0); 376 }); 377 378 const mouse = d3.mouse(this); 379 if (mouse) { 380 mouse[0] -= margin.left; // adjust for rect.mouse position 381 const date = x.invert(mouse[0]); 382 const hover = hoverSeries(mouse); 383 d3.selectAll(".chart.ycsb").each(function() { 384 this.updateMouse(mouse, date, hover); 385 }); 386 } 387 }); 388 389 svg.call(zoom); 390 svg.call(zoom.transform, d3.zoomTransform(svg.node())); 391 392 const lineHover = g 393 .append("line") 394 .attr("class", "hover") 395 .style("fill", "none") 396 .style("stroke", "#f99") 397 .style("stroke-width", "1px"); 398 399 const dateHover = g 400 .append("text") 401 .attr("class", "hover") 402 .attr("fill", "#f22") 403 .attr("text-anchor", "middle") 404 .attr("alignment-baseline", "hanging") 405 .attr("transform", "translate(0, 0)"); 406 407 const opsHover = g 408 .append("text") 409 .attr("class", "hover") 410 .attr("fill", "#f22") 411 .attr("text-anchor", "middle") 412 .attr("transform", "translate(0, 0)"); 413 414 const marker = g 415 .append("circle") 416 .attr("class", "hover") 417 .attr("r", 3) 418 .style("opacity", "0") 419 .style("stroke", "#f22") 420 .style("fill", "#f22"); 421 422 svg.node().updateMouse = function(mouse, date, hover) { 423 const mousex = mouse[0]; 424 const mousey = mouse[1]; 425 const i = dateBisector(vals, date, 1); 426 const v = 427 i == vals.length 428 ? vals[i - 1] 429 : mousex - x(vals[i - 1].date) < x(vals[i].date) - mousex 430 ? vals[i - 1] 431 : vals[i]; 432 const noData = mousex < x(vals[0].date); 433 434 let lineY = height; 435 if (!noData) { 436 if (hover == 1) { 437 lineY = pathGetY(path1.node(), mousex); 438 } else { 439 lineY = pathGetY(path2.node(), mousex); 440 } 441 } 442 443 let val, valY, valFormat; 444 if (hover == 1) { 445 val = v.opsSec; 446 valY = y1(val); 447 valFormat = d3.format(",.0f"); 448 } else { 449 val = detail(v); 450 valY = y2(val); 451 valFormat = detailFormat; 452 } 453 454 lineHover 455 .attr("x1", mousex) 456 .attr("x2", mousex) 457 .attr("y1", lineY) 458 .attr("y2", height); 459 marker.attr("transform", "translate(" + x(v.date) + "," + valY + ")"); 460 dateHover 461 .attr("transform", "translate(" + mousex + "," + (height + 8) + ")") 462 .text(xFormat(date)); 463 opsHover 464 .attr( 465 "transform", 466 "translate(" + x(v.date) + "," + (valY - 7) + ")" 467 ) 468 .text(valFormat(val)); 469 }; 470 471 const rect = svg 472 .append("rect") 473 .attr("class", "mouse") 474 .attr("cursor", "move") 475 .attr("fill", "none") 476 .attr("pointer-events", "all") 477 .attr("width", width) 478 .attr("height", height + margin.top + margin.bottom) 479 .attr("transform", "translate(" + margin.left + "," + 0 + ")") 480 .on("mousemove", function() { 481 const mouse = d3.mouse(this); 482 const date = x.invert(mouse[0]); 483 const hover = hoverSeries(mouse); 484 485 let resetTitle = true; 486 for (let i in annotations) { 487 if (Math.abs(mouse[0] - x(annotations[i].date)) <= 5) { 488 title 489 .style("font-size", "9pt") 490 .text(annotations[i].message); 491 resetTitle = false; 492 break; 493 } 494 } 495 if (resetTitle) { 496 title.style("font-size", "8pt").text(chartKey); 497 } 498 499 d3.selectAll(".chart").each(function() { 500 if (this.updateMouse != null) { 501 this.updateMouse(mouse, date, hover); 502 } 503 }); 504 }) 505 .on("mouseover", function() { 506 d3 507 .selectAll(".chart") 508 .selectAll(".hover") 509 .style("opacity", 1.0); 510 }) 511 .on("mouseout", function() { 512 d3 513 .selectAll(".chart") 514 .selectAll(".hover") 515 .style("opacity", 0); 516 }); 517 } 518 519 function renderYCSB() { 520 d3.selectAll(".chart.ycsb").each(function(d, i) { 521 renderChart(d3.select(this)); 522 }); 523 } 524 525 function initData() { 526 for (key in data) { 527 data[key] = d3.csvParseRows(data[key], function(d, i) { 528 return { 529 date: parseTime(d[0]), 530 opsSec: +d[1], 531 readBytes: +d[2], 532 writeBytes: +d[3], 533 readAmp: +d[4], 534 writeAmp: +d[5] 535 }; 536 }); 537 538 const vals = data[key]; 539 max.perChart[key] = { 540 opsSec: d3.max(vals, d => d.opsSec), 541 readBytes: d3.max(vals, d => d.readBytes), 542 writeBytes: d3.max(vals, d => d.writeBytes), 543 readAmp: d3.max(vals, d => d.readAmp), 544 writeAmp: d3.max(vals, d => d.writeAmp), 545 } 546 max.opsSec = Math.max(max.opsSec, max.perChart[key].opsSec); 547 max.readBytes = Math.max(max.readBytes, max.perChart[key].readBytes); 548 max.writeBytes = Math.max( 549 max.writeBytes, 550 max.perChart[key].writeBytes, 551 ); 552 max.readAmp = Math.max(max.readAmp, max.perChart[key].readAmp); 553 max.writeAmp = Math.max(max.writeAmp, max.perChart[key].writeAmp); 554 } 555 556 // Load the write-throughput data and merge with the existing data. We 557 // return a promise here to allow us to continue to make progress elsewhere. 558 return fetch(writeThroughputSummaryURL()) 559 .then(response => response.json()) 560 .then(wtData => { 561 for (let key in wtData) { 562 data[key] = wtData[key]; 563 } 564 }); 565 } 566 567 function initDateRange() { 568 max.date.setHours(0, 0, 0, 0); 569 minDate = new Date(new Date().setDate(max.date.getDate() - 90)); 570 } 571 572 function initAnnotations() { 573 d3.selectAll(".annotation").each(function() { 574 const annotation = d3.select(this); 575 const date = parseTime(annotation.attr("data-date")); 576 annotations.push({ date: date, message: annotation.text() }); 577 }); 578 } 579 580 function setQueryParams() { 581 var params = new URLSearchParams(); 582 if (detailName) { 583 params.set("detail", detailName); 584 } 585 if (usePerChartMax) { 586 params.set("max", "local"); 587 } 588 var search = "?" + params; 589 if (window.location.search != search) { 590 window.history.pushState(null, null, search); 591 } 592 } 593 594 function setDetail(name) { 595 detail = undefined; 596 detailFormat = undefined; 597 detailName = name; 598 599 switch (detailName) { 600 case "readBytes": 601 detail = d => d.readBytes; 602 detailFormat = humanize; 603 break; 604 case "writeBytes": 605 detail = d => d.writeBytes; 606 detailFormat = humanize; 607 break; 608 case "readAmp": 609 detail = d => d.readAmp; 610 detailFormat = d3.format(",.1f"); 611 break; 612 case "writeAmp": 613 detail = d => d.writeAmp; 614 detailFormat = d3.format(",.1f"); 615 break; 616 } 617 618 d3.selectAll(".toggle").classed("selected", false); 619 d3.select("#" + detailName).classed("selected", detail != null); 620 } 621 622 function initQueryParams() { 623 var params = new URLSearchParams(window.location.search.substring(1)); 624 setDetail(params.get("detail")); 625 usePerChartMax = params.get("max") == "local"; 626 d3.select("#localMax").classed("selected", usePerChartMax); 627 } 628 629 function toggleDetail(name) { 630 const link = d3.select("#" + name); 631 const selected = !link.classed("selected"); 632 link.classed("selected", selected); 633 if (selected) { 634 setDetail(name); 635 } else { 636 setDetail(null); 637 } 638 setQueryParams(); 639 renderYCSB(); 640 } 641 642 function toggleLocalMax() { 643 const link = d3.select("#localMax"); 644 const selected = !link.classed("selected"); 645 link.classed("selected", selected); 646 usePerChartMax = selected; 647 setQueryParams(); 648 renderYCSB(); 649 } 650 651 window.onload = function init() { 652 d3.selectAll(".toggle").each(function() { 653 const link = d3.select(this); 654 link.attr("href", 'javascript:toggleDetail("' + link.attr("id") + '")'); 655 }); 656 d3.selectAll("#localMax").each(function() { 657 const link = d3.select(this); 658 link.attr("href", 'javascript:toggleLocalMax()'); 659 }); 660 661 initData().then(_ => { 662 initDateRange(); 663 initAnnotations(); 664 initQueryParams(); 665 666 renderYCSB(); 667 renderWriteThroughputSummary(data); 668 669 // Use the max date to bisect into the workload data to pluck out the 670 // correct datapoint. 671 let workloadData = data[writeThroughputWorkload]; 672 bisectAndRenderWriteThroughputDetail(workloadData, max.date); 673 674 let lastUpdate; 675 for (let key in data) { 676 const max = d3.max(data[key], d => d.date); 677 if (!lastUpdate || lastUpdate < max) { 678 lastUpdate = max; 679 } 680 } 681 d3.selectAll(".updated") 682 .text("Last updated: " + d3.timeFormat("%b %e, %Y")(lastUpdate)); 683 }) 684 685 // By default, display each panel with its local max, which makes spotting 686 // regressions simpler. 687 toggleLocalMax(); 688 }; 689 690 window.onpopstate = function() { 691 initQueryParams(); 692 renderYCSB(); 693 }; 694 695 window.addEventListener("resize", renderYCSB);