Add a utility to plot benchmark results.
* This tool is useful when iterating locally, and compare benchmark runs over time.
Test: Tested this locally.
Change-Id: I7a79df0fa6d3e011056e427b136144fed4f6a032
diff --git a/development/plot-benchmarks/src/lib/Chart.svelte b/development/plot-benchmarks/src/lib/Chart.svelte
new file mode 100644
index 0000000..f7d382e
--- /dev/null
+++ b/development/plot-benchmarks/src/lib/Chart.svelte
@@ -0,0 +1,163 @@
+<script lang="ts">
+ import type { LegendItem } from "chart.js";
+ import { Chart } from "chart.js/auto";
+ import { onMount } from "svelte";
+ import type { Readable, Writable } from "svelte/store";
+ import { derived, writable } from "svelte/store";
+ import { chartData } from "../chart-transforms.js";
+ import { saveToClipboard } from "../clipboard.js";
+ import { LegendPlugin } from "../plugins.js";
+ import { expressionFilter } from "../regexp.js";
+ import type { Benchmarks } from "../schema.js";
+ import { benchmarksDataset } from "../transforms.js";
+ type FilterFn = (label: string) => boolean;
+
+ export let containers: Readable<Array<Benchmarks>>;
+
+ let canvas: HTMLCanvasElement;
+ let filter: Writable<FilterFn> = writable((_: string) => true);
+
+ let data = derived([containers, filter], ([$containers, $filter]) => {
+ return chartData(benchmarksDataset($containers, $filter));
+ });
+
+ let chart: Writable<Chart | undefined> = writable(null);
+ let legendLabels: Writable<Array<LegendItem> | undefined> = writable(null);
+
+ data.subscribe(($data) => {
+ if ($chart) {
+ $chart.data = $data;
+ $chart.update();
+ }
+ });
+
+ onMount(() => {
+ const onUpdate = (updated: Chart) => {
+ chart.set(updated);
+ legendLabels.set(
+ updated.options.plugins.legend.labels.generateLabels(updated)
+ );
+ };
+ const plugins = {
+ legend: {
+ display: false,
+ },
+ benchmark: {
+ onUpdate: onUpdate,
+ },
+ };
+ chart.set(
+ new Chart(canvas, {
+ type: "line",
+ data: $data,
+ plugins: [LegendPlugin],
+ options: {
+ plugins: plugins,
+ },
+ })
+ );
+ });
+
+ function onItemClick(item: LegendItem) {
+ return (_: Event) => {
+ // https://www.chartjs.org/docs/latest/samples/legend/html.html
+ $chart.setDatasetVisibility(
+ item.datasetIndex,
+ !$chart.isDatasetVisible(item.datasetIndex)
+ );
+ // Update chart
+ $chart.update();
+ };
+ }
+
+ function onFilterChanged(event: Event) {
+ const target = event.currentTarget as HTMLInputElement;
+ const value = target.value;
+ $filter = expressionFilter(value);
+ }
+
+ async function onCopy(_: Event) {
+ if ($chart) {
+ await saveToClipboard($chart);
+ }
+ }
+</script>
+
+<article>
+ <button
+ class="copy outline"
+ data-tooltip="Copy chart to clipboard."
+ on:click={onCopy}
+ >
+ ⎘
+ </button>
+ <canvas id="chart" class="chart" bind:this={canvas} />
+</article>
+
+{#if $legendLabels && $legendLabels.length >= 0}
+ <article>
+ <div class="filter">
+ <label for="metricFilter">
+ <input
+ type="text"
+ id="metricFilter"
+ name="metricFilter"
+ placeholder="Filter metrics (regular expressions)"
+ autocomplete="off"
+ on:input={onFilterChanged}
+ />
+ </label>
+ </div>
+ <div class="legend">
+ {#each $legendLabels as label}
+ <div
+ class="item"
+ on:click={onItemClick(label)}
+ on:keyup={onItemClick(label)}
+ >
+ <span
+ class="box"
+ style="background: {label.fillStyle}; border-color: {label.strokeStyle}; border-width: {label.lineWidth}px;"
+ />
+ <span
+ class="label"
+ style="text-decoration: {label.hidden ? 'line-through' : ''};"
+ >
+ {label.text}
+ </span>
+ </div>
+ {/each}
+ </div>
+ </article>
+{/if}
+
+<style>
+ .copy {
+ width: 4%;
+ position: relative;
+ padding: 0;
+ border: none;
+ /* Looked okay on my machine. */
+ right: -52rem;
+ top: -3rem;
+ }
+ .chart {
+ width: 100%;
+ }
+ .legend {
+ display: flex;
+ flex-direction: column;
+ row-gap: 3px;
+ }
+ .item {
+ display: flex;
+ flex-direction: row;
+ column-gap: 10px;
+ align-items: center;
+ }
+ .item .box {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ }
+</style>