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  }