Perf data visualization

Change-Id: Ia8924ba332689007e7b590b7d66f9f647aa67500
diff --git a/tools/perf/benchmark_data.json b/tools/perf/benchmark_data.json
new file mode 100644
index 0000000..9d18466
--- /dev/null
+++ b/tools/perf/benchmark_data.json
@@ -0,0 +1,24 @@
+[{
+  "author": "Christoffer Adamsen",
+  "hash": "e6a33325a4a4bac599ffce1f75cab8505a35d02a",
+  "submitted": "Thu Jun 06 13:07:44 2024 +0200",
+  "title": "Allow compiling CompileDumpCompatR8 in isolation",
+  "benchmarks": {
+    "NowInAndroidApp": {
+      "benchmark_name": "NowInAndroidApp",
+      "results": [
+        { "code_size": 42, "runtime": 1 },
+        { "code_size": 42, "runtime": 2 },
+        { "code_size": 42, "runtime": 3 }
+      ]
+    },
+    "TiviApp": {
+      "benchmark_name": "TiviApp",
+      "results": [
+        { "code_size": 84, "runtime": 4 },
+        { "code_size": 84, "runtime": 5 },
+        { "code_size": 84, "runtime": 6 }
+      ]
+    }
+  }
+}]
\ No newline at end of file
diff --git a/tools/perf/index.html b/tools/perf/index.html
new file mode 100644
index 0000000..32bee4a
--- /dev/null
+++ b/tools/perf/index.html
@@ -0,0 +1,268 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>R8 perf</title>
+</head>
+<body>
+  <select id="benchmark-selector"></select>
+  <div>
+      <canvas id="myChart"></canvas>
+  </div>
+  <div>
+    <div style="float: left; width: 50%">
+      <button type="button" id="show-more-left" disabled>⇐</button>
+      <button type="button" id="show-less-left">⇒</button>
+    </div>
+    <div style="float: left; text-align: right; width: 50%">
+      <button type="button" id="show-less-right">⇐</button>
+      <button type="button" id="show-more-right" disabled>⇒</button>
+    </div>
+  </div>
+  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
+  <script type="module">
+    import commits from "./benchmark_data.json" with { type: "json" };
+
+    // Amend the commits with their unique index.
+    for (var i = 0; i < commits.length; i++) {
+      commits[i].index = i;
+    }
+
+    // Utility methods.
+    Array.prototype.first = function() {
+        return this[0];
+    };
+    Array.prototype.avg = function() {
+        return this.reduce(function(x, y) { return x + y; }, 0) / this.length;
+    };
+    Array.prototype.min = function() {
+        return this.reduce(function(x, y) { return x === null ? y : Math.min(x, y); }, null) / this.length;
+    };
+
+    // DOM references.
+    const benchmarkSelector = document.getElementById('benchmark-selector')
+    const canvas = document.getElementById('myChart');
+    const showMoreLeft = document.getElementById('show-more-left');
+    const showLessLeft = document.getElementById('show-less-left');
+    const showLessRight = document.getElementById('show-less-right');
+    const showMoreRight = document.getElementById('show-more-right');
+
+    // Initialize benchmark selector.
+    const benchmarks = new Set();
+    for (const commit of commits.values()) {
+      for (const benchmark in commit.benchmarks) {
+          benchmarks.add(benchmark)
+      }
+    }
+    var selectedBenchmark = window.location.hash.substring(1)
+    if (!benchmarks.has(selectedBenchmark)) {
+      selectedBenchmark = benchmarks.values().next().value;
+    }
+    for (const benchmark of benchmarks.values()) {
+      const opt = document.createElement('option');
+      opt.value = benchmark;
+      opt.innerHTML = benchmark;
+      benchmarkSelector.appendChild(opt);
+      if (benchmark == selectedBenchmark) {
+        benchmarkSelector.selectedIndex = benchmarkSelector.options.length - 1
+      }
+    }
+    benchmarkSelector.onchange = function(event) {
+      selectedBenchmark =
+          benchmarkSelector.options[benchmarkSelector.selectedIndex].value;
+      updateChart();
+      window.location.hash = selectedBenchmark;
+    };
+
+    // Chart data provider.
+    function getData(start = 0, end = commits.length) {
+      const filteredCommits =
+          commits
+              .slice(start, end)
+              .filter(
+                  commit =>
+                      selectedBenchmark in commit.benchmarks
+                          && commit.benchmarks[selectedBenchmark].results.length > 0);
+      const labels = filteredCommits.map((c, i) => c.index);
+      const codeSizeData =
+          filteredCommits.map(
+              (c, i) =>
+                  filteredCommits[i]
+                      .benchmarks[selectedBenchmark]
+                      .results
+                      .first()
+                      .code_size);
+      const runtimeData =
+          filteredCommits.map(
+              (c, i) =>
+                  filteredCommits[i]
+                      .benchmarks[selectedBenchmark]
+                      .results
+                      .map(result => result.runtime)
+                      .min());
+      const runtimeScatterData = [];
+      for (const commit of filteredCommits.values()) {
+        const runtimes =
+            commit.benchmarks[selectedBenchmark].results.map(result => result.runtime)
+        for (const runtime of runtimes.values()) {
+          runtimeScatterData.push({ x: commit.index, y: runtime });
+        }
+      }
+
+      return {
+        labels: labels,
+        datasets: [{
+            type: 'line',
+            label: 'Code size',
+            data: codeSizeData,
+            tension: 0.1
+          },
+          {
+            type: 'line',
+            label: 'Runtime',
+            data: runtimeData,
+            tension: 0.1,
+            yAxisID: 'y2'
+          },
+          {
+            type: 'scatter',
+            label: 'Runtime',
+            data: runtimeScatterData,
+            yAxisID: 'y2'
+          }
+        ],
+      };
+    }
+
+    // Chart options.
+    const options = {
+      onHover: (event, chartElement) =>
+          event.native.target.style.cursor =
+              chartElement[0] ? 'pointer' : 'default',
+      plugins: {
+        tooltip: {
+          callbacks: {
+            title: (context) => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              return commit.title;
+            },
+            footer: (context) => {
+              const elementInfo = context[0];
+              var commit;
+              if (elementInfo.dataset.type == 'line') {
+                commit = commits[elementInfo.dataIndex];
+              } else {
+                console.assert(elementInfo.dataset.type == 'scatter');
+                commit = commits[elementInfo.raw.x];
+              }
+              return `Author: ${commit.author}\n`
+                  + `Submitted: ${commit.submitted}\n`
+                  + `Hash: ${commit.hash}`;
+            }
+          }
+        }
+      },
+      responsive: true,
+      scales: {
+        x: {},
+        y: {
+          position: 'left',
+          title: {
+            display: true,
+            text: 'Code size (bytes)'
+          }
+        },
+        y2: {
+          position: 'right',
+          title: {
+            display: true,
+            text: 'Runtime (ms)'
+          }
+        }
+      }
+    };
+
+    // Create chart.
+    const myChart = new Chart(canvas, {
+      data: getData(),
+      options: options
+    });
+
+    // Setup click handler.
+    canvas.onclick = function (event) {
+      const points =
+          myChart.getElementsAtEventForMode(
+              event, 'nearest', { intersect: true }, true);
+      if (points.length > 0) {
+        const point = points[0];
+        const commit = commits[point.index];
+        window.open('https://r8.googlesource.com/r8/+/' + commit.hash, '_blank');
+      }
+    };
+
+    // Setup chart navigation.
+    var left = 0;
+    var right = commits.length;
+
+    showMoreLeft.onclick = function (event) {
+      if (left == 0) {
+        return;
+      }
+      const currentSize = right - left;
+      left = left - currentSize;
+      if (left < 0) {
+        left = 0;
+      }
+      updateChart();
+    };
+
+    showLessLeft.onclick = function (event) {
+      const currentSize = right - left;
+      left = left + Math.floor(currentSize / 2);
+      if (left >= right) {
+        left = right - 1;
+      }
+      updateChart();
+    };
+
+    showLessRight.onclick = function (event) {
+      if (right == 0) {
+        return;
+      }
+      const currentSize = right - left;
+      right = right - Math.floor(currentSize / 2);
+      if (right < left) {
+        right = left;
+      }
+      updateChart();
+    };
+
+    showMoreRight.onclick = function (event) {
+      const currentSize = right - left;
+      right = right + currentSize;
+      if (right > commits.length) {
+        right = commits.length;
+      }
+      updateChart();
+    };
+
+    function updateChart() {
+      console.assert(left <= right);
+      const newData = getData(left, right);
+      Object.assign(myChart.data, newData);
+      myChart.update();
+      showMoreLeft.disabled = left == 0;
+      showLessLeft.disabled = left == right - 1;
+      showLessRight.disabled = left == right - 1;
+      showMoreRight.disabled = right == commits.length;
+    }
+  </script>
+</body>
+</html>
\ No newline at end of file