blob: 58524b5b057f888a106e0e63fa624049cf5a94bd [file] [log] [blame]
<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>