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);