| <script lang="ts"> | |
| import type { Remote } from "comlink"; | |
| import { createEventDispatcher } from "svelte"; | |
| import { | |
| derived, | |
| writable, | |
| type Readable, | |
| type Writable, | |
| } from "svelte/store"; | |
| import { readBenchmarks } from "../files.js"; | |
| import { | |
| ChartDataTransforms, | |
| type Mapper, | |
| } from "../transforms/data-transforms.js"; | |
| import { Transforms } from "../transforms/metric-transforms.js"; | |
| import { buildMapper } from "../transforms/standard-mappers.js"; | |
| import type { Data, Series } from "../types/chart.js"; | |
| import type { Metrics } from "../types/data.js"; | |
| import type { | |
| Controls, | |
| DatasetSelection, | |
| FileMetadataEvent, | |
| MetricSelection, | |
| StatInfo, | |
| } from "../types/events.js"; | |
| import type { FileMetadata } from "../types/files.js"; | |
| import type { StatService } from "../workers/service.js"; | |
| import { Session, type IndexedWrapper } from "../wrappers/session.js"; | |
| import Chart from "./Chart.svelte"; | |
| import Group from "./Group.svelte"; | |
| export let fileEntries: FileMetadata[]; | |
| export let service: Remote<StatService>; | |
| // State | |
| let eventDispatcher = createEventDispatcher<FileMetadataEvent>(); | |
| let session: Session; | |
| let mapper: Mapper<number>; | |
| let metrics: Metrics<number>; | |
| let series: Series[]; | |
| let chartData: Data; | |
| let classGroups: Record<string, IndexedWrapper[]>; | |
| let showControls: boolean; | |
| let size: number; | |
| let activeSeries: Promise<Series[]>; | |
| // Stores | |
| let buckets: Writable<number> = writable(100); | |
| let normalizeMetrics: Writable<boolean> = writable(false); | |
| let activeDragDrop: Writable<boolean> = writable(false); | |
| let suppressed: Writable<Set<string>> = writable(new Set()); | |
| let suppressedMetrics: Writable<Set<string>> = writable(new Set()); | |
| let activeStats: Writable<StatInfo[]> = writable([]); | |
| let active: Readable<Set<string>> = derived(activeStats, ($activeStats) => { | |
| const datasets = []; | |
| for (let i = 0; i < $activeStats.length; i += 1) { | |
| const activeStat = $activeStats[i]; | |
| datasets.push(activeStat.name); | |
| } | |
| return new Set(datasets); | |
| }); | |
| // Events | |
| let datasetHandler = function (event: CustomEvent<DatasetSelection[]>) { | |
| const selections: DatasetSelection[] = event.detail; | |
| for (let i = 0; i < selections.length; i += 1) { | |
| const selection = selections[i]; | |
| if (!selection.enabled) { | |
| $suppressed.add(selection.name); | |
| } else { | |
| $suppressed.delete(selection.name); | |
| } | |
| } | |
| $suppressed = $suppressed; | |
| }; | |
| let metricsHandler = function (event: CustomEvent<MetricSelection[]>) { | |
| const selections: MetricSelection[] = event.detail; | |
| for (let i = 0; i < selections.length; i += 1) { | |
| const selection = selections[i]; | |
| if (!selection.enabled) { | |
| $suppressedMetrics.add(selection.name); | |
| } else { | |
| $suppressedMetrics.delete(selection.name); | |
| } | |
| } | |
| $suppressedMetrics = $suppressedMetrics; | |
| }; | |
| let statHandler = function (event: CustomEvent<StatInfo[]>) { | |
| const statistics = event.detail; | |
| for (let i = 0; i < statistics.length; i += 1) { | |
| const statInfo = statistics[i]; | |
| if (!statInfo.enabled) { | |
| const index = $activeStats.findIndex( | |
| (entry) => entry.name == statInfo.name && entry.type == statInfo.type | |
| ); | |
| if (index >= 0) { | |
| $activeStats.splice(index, 1); | |
| } | |
| } else { | |
| $activeStats.push(statInfo); | |
| } | |
| $activeStats = $activeStats; | |
| } | |
| }; | |
| let controlsHandler = function (event: CustomEvent<Controls>) { | |
| const controls: Controls = event.detail; | |
| $buckets = controls.buckets; | |
| }; | |
| $: { | |
| session = new Session(fileEntries); | |
| mapper = buildMapper($buckets); | |
| metrics = Transforms.buildMetrics(session, $suppressed, $suppressedMetrics); | |
| showControls = Array.isArray(metrics.sampled) && metrics.sampled.length > 0; | |
| activeSeries = service.pSeries(metrics, $active); | |
| series = ChartDataTransforms.mapToSeries( | |
| metrics, | |
| mapper, | |
| $normalizeMetrics | |
| ); | |
| chartData = ChartDataTransforms.mapToDataset(series); | |
| classGroups = session.classGroups; | |
| size = session.fileNames.size; | |
| } | |
| // Helpers | |
| function onDropFile(event: DragEvent) { | |
| handleFileDragDrop(event); // async | |
| $activeDragDrop = false; | |
| event.preventDefault(); | |
| } | |
| function onDragOver(event: DragEvent) { | |
| $activeDragDrop = true; | |
| event.preventDefault(); | |
| } | |
| function onDragLeave(event: DragEvent) { | |
| $activeDragDrop = false; | |
| event.preventDefault(); | |
| } | |
| async function handleFileDragDrop(event: DragEvent) { | |
| const items = [...event.dataTransfer?.items || []]; | |
| if (items) { | |
| let newFiles = await Promise.all( | |
| items | |
| .filter( | |
| (item) => { | |
| const file = item.getAsFile() | |
| return item.kind === "file" && file && file.name.endsWith(".json") | |
| } | |
| ) | |
| .map(async (item) => { | |
| // At this point, we know the file had some JSON content. | |
| const file = item.getAsFile()!!; | |
| const benchmarks = await readBenchmarks(file); | |
| const entry: FileMetadata = { | |
| enabled: true, | |
| file: file, | |
| container: benchmarks, | |
| }; | |
| return entry; | |
| }) | |
| ); | |
| // Deep copy & notify | |
| eventDispatcher("entries", [...fileEntries, ...newFiles]); | |
| } | |
| } | |
| </script> | |
| {#if size <= 0} | |
| <article | |
| id="drop" | |
| class="drop" | |
| class:active={$activeDragDrop} | |
| on:drop={onDropFile} | |
| on:dragover={onDragOver} | |
| on:dragleave={onDragLeave} | |
| > | |
| <h5>Drag and drop benchmark results to get started.</h5> | |
| </article> | |
| {:else} | |
| <article | |
| id="drop" | |
| class="drop" | |
| class:active={$activeDragDrop} | |
| on:drop={onDropFile} | |
| on:dragover={onDragOver} | |
| on:dragleave={onDragLeave} | |
| > | |
| {#if showControls} | |
| <div class="toolbar"> | |
| <div class="control"> | |
| <label for="normalize"> | |
| <input | |
| type="checkbox" | |
| id="normalize" | |
| name="normalize" | |
| data-tooltip="Normalize Metrics" | |
| on:change={(_) => { | |
| $normalizeMetrics = !$normalizeMetrics; | |
| }} | |
| /> | |
| ≃ | |
| </label> | |
| </div> | |
| </div> | |
| {/if} | |
| <h5>Benchmarks</h5> | |
| {#each Object.entries(classGroups) as [className, wrappers]} | |
| <Group | |
| {className} | |
| datasetGroup={wrappers} | |
| suppressedMetrics={$suppressedMetrics} | |
| on:datasetSelections={datasetHandler} | |
| on:metricSelections={metricsHandler} | |
| on:info={statHandler} | |
| /> | |
| {/each} | |
| </article> | |
| {#if series.length > 0} | |
| <Chart | |
| data={chartData} | |
| showHistogramControls={showControls} | |
| on:controls={controlsHandler} | |
| /> | |
| {/if} | |
| {#await activeSeries} | |
| <article aria-busy="true"></article> | |
| {:then chartData} | |
| {#if chartData.length > 0} | |
| <Chart | |
| data={ChartDataTransforms.mapToDataset(chartData)} | |
| isExperimental={true} | |
| /> | |
| {/if} | |
| {/await} | |
| {/if} | |
| <style> | |
| .toolbar { | |
| padding: 0; | |
| margin: 2rem; | |
| display: flex; | |
| flex-direction: row; | |
| justify-content: flex-end; | |
| } | |
| .active { | |
| outline: beige; | |
| outline-style: dashed; | |
| } | |
| </style> |