Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/app/(bench)/bench/chart/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { notFound, permanentRedirect, redirect } from 'next/navigation';
import { cache } from 'react';

import { Chart } from '@/bench/components/Chart';
import { ChartUnit } from '@/bench/components/ChartUnit';
import { Footer } from '@/bench/components/Footer';
import { BenchHeader } from '@/bench/components/BenchHeader';
import {
canonicalFilterCsv,
chartUnitLabel,
parseFilterCsv,
singleSearchParam,
unitKindLabel,
} from '@/bench/lib/chart-format';
import { cachedDefaultChartPayload, cachedFilterUniverse } from '@/bench/lib/data-cache';
import { chartPayload, type ChartResponse } from '@/bench/lib/queries';
Expand Down Expand Up @@ -204,7 +205,7 @@ export default async function ChartPage({
<main>
<p className="chart-meta">
{'unit: '}
<code>{unitKindLabel(chart.unit_kind)}</code>
<ChartUnit slug={slug} initial={chartUnitLabel(chart)} />
{' · '}
{seriesCount} series · {commitCount} commit{commitCount !== 1 ? 's' : ''}
</p>
Expand Down
12 changes: 6 additions & 6 deletions src/app/(bench)/bench/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { OpenGroupsScript } from '@/bench/components/OpenGroupsScript';
import { groupAnchor, groupAnchors } from '@/bench/lib/anchor';
import { type FilterUniverse, parseFilterCsv, singleSearchParam } from '@/bench/lib/chart-format';
import { cachedFilterUniverse, cachedGroups } from '@/bench/lib/data-cache';
import { parseGroupFilter } from '@/bench/lib/group-filter';
import { NO_GROUP_FILTER_CHOICES, parseGroupFilter } from '@/bench/lib/group-filter';
import type { Group } from '@/bench/lib/queries';
import { settlesWithin } from '@/bench/lib/settles-within';
import { BENCH_URL, datasetJsonLd } from '@/bench/lib/site-files';
Expand Down Expand Up @@ -127,6 +127,7 @@ function HomeContent({ data: [params, groups, universe] }: { data: LandingData }
return (
<>
<BenchHeader
groupControls={groups.length > 0}
universe={universe}
initialEngines={initialEngines}
initialFormats={initialFormats}
Expand Down Expand Up @@ -167,11 +168,10 @@ function HomeContent({ data: [params, groups, universe] }: { data: LandingData }
anchor={anchors[i] ?? groupAnchor(group.name)}
startIndex={startIndex}
universe={universe}
initialHiddenSeries={
ownsInitialFilter ? (initialGroupFilter?.hiddenSeries ?? []) : []
}
initialShownSeries={
ownsInitialFilter ? (initialGroupFilter?.shownSeries ?? []) : []
initialFilter={
ownsInitialFilter && initialGroupFilter !== null
? initialGroupFilter
: NO_GROUP_FILTER_CHOICES
}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions src/bench/components/Chart.lazy-hydration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { FETCH_TIMEOUT_MS } from '@/bench/lib/chart-format';
import { PAGE_LEAVE_GRACE_MS } from '@/bench/lib/page-leave';
import { bundleQueue, hydrationQueue, resetPayloadCache } from '@/bench/lib/chart-store';
import { defined } from '@/lib/defined';
import { NO_GROUP_FILTER_CHOICES } from '@/bench/lib/group-filter';

vi.mock('@/bench/lib/chart-js', () => ({
loadChartJs: () => new Promise(() => {}),
Expand Down Expand Up @@ -607,8 +608,7 @@ describe('PR-5.0.97 group-bundle hydration', () => {
]}
startIndex={0}
universe={{ engines: [], formats: [] }}
initialHiddenSeries={[]}
initialShownSeries={[]}
initialFilter={NO_GROUP_FILTER_CHOICES}
/>,
);
});
Expand Down
69 changes: 68 additions & 1 deletion src/bench/components/Chart.loading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { Chart } from '@/bench/components/Chart';
import { FETCH_TIMEOUT_MS } from '@/bench/lib/chart-format';
import { ChartUnit } from '@/bench/components/ChartUnit';
import { chartUnitLabel, FETCH_TIMEOUT_MS } from '@/bench/lib/chart-format';
import { PAGE_LEAVE_GRACE_MS } from '@/bench/lib/page-leave';
import { fullHistoryQueue } from '@/bench/lib/chart-store';
import { loadChartJs } from '@/bench/lib/chart-js';
Expand Down Expand Up @@ -200,6 +201,72 @@ describe('Chart opt-in full-history loading', () => {
expect(chip?.textContent).toBe('all 3,572');
});

// The chart page's meta line names the axis's unit, so it follows the axis
// when the full history's older values need a larger unit.
it('the chart page unit follows the axis when full history loads', async () => {
class StubChart {
data: { labels: unknown[]; datasets: unknown[] };
options: Record<string, unknown>;
constructor(
_canvas: HTMLCanvasElement,
config: {
data: { labels: unknown[]; datasets: unknown[] };
options: Record<string, unknown>;
},
) {
this.data = { labels: config.data.labels ?? [], datasets: config.data.datasets ?? [] };
this.options = config.options ?? {};
}
isDatasetVisible(): boolean {
return true;
}
update(): void {}
destroy(): void {}
}
vi.mocked(loadChartJs).mockResolvedValueOnce(StubChart as never);
let resolveFull: (r: Response) => void = () => {};
responders.push({
match: (u) => u.includes('n=all'),
respond: () =>
new Promise<Response>((res) => {
resolveFull = res;
}),
});
const slug = 'qm.eyJrIjoidW5pdCJ9';
const unitHost = document.createElement('p');
container.after(unitHost);
const unitRoot = createRoot(unitHost);
await act(async () => {
unitRoot.render(<ChartUnit slug={slug} initial="ns" />);
});
const chip = await renderOpenGroup(slug);
await act(async () => {
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
});
expect(unitHost.textContent).toBe('ns');

const full = completePayload(3572);
full.series['vortex/nvme'] = full.series['vortex/nvme'].map((v) => v * 1_000_000);
await act(async () => {
chip?.click();
await Promise.resolve();
});
await act(async () => {
resolveFull(jsonResponse(full));
for (let i = 0; i < 4; i++) {
await Promise.resolve();
}
});
expect(unitHost.textContent).toBe(chartUnitLabel(full as never));
expect(unitHost.textContent).not.toBe('ns');
await act(async () => {
unitRoot.unmount();
});
unitHost.remove();
});

it('a failed full fetch surfaces a retry affordance', async () => {
let rejectFull: (e: unknown) => void = () => {};
responders.push({
Expand Down
10 changes: 9 additions & 1 deletion src/bench/components/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,15 @@ export function Chart({ slug, name, index, groupSlug, initialPayload }: ChartIsl
// biome-ignore lint/correctness/useExhaustiveDependencies: the filter state is the trigger; `applyFilters` reads the stores itself
useEffect(() => {
controllerRef.current?.applyFilters();
}, [globalFilter, groupState.hiddenSeries, groupState.shownSeries]);
}, [
globalFilter,
groupState.hiddenSeries,
groupState.shownSeries,
groupState.hiddenEngines,
groupState.hiddenFormats,
groupState.shownEngines,
groupState.shownFormats,
]);

// Broadcast the per-group Y override to non-sticky charts; `null` (the
// resting default and the post-Reset state) reverts to linear.
Expand Down
24 changes: 24 additions & 0 deletions src/bench/components/ChartUnit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

'use client';

import { useCallback, useSyncExternalStore } from 'react';

import { getChartUnitLabel, subscribeChartUnitLabel } from '@/bench/lib/chart-store';

/**
* The chart page's unit, in its meta line. The server renders the unit of the
* window it sent (`initial`); once the chart draws, this follows the unit the
* y-axis is drawn in, which changes when the full history loads and its older
* values need a larger unit.
*/
export function ChartUnit({ slug, initial }: { slug: string; initial: string }) {
const subscribe = useCallback((cb: () => void) => subscribeChartUnitLabel(slug, cb), [slug]);
const label = useSyncExternalStore(
subscribe,
() => getChartUnitLabel(slug) ?? initial,
() => initial,
);
return <code>{label}</code>;
}
6 changes: 3 additions & 3 deletions src/bench/components/GroupBody.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { groupBodiesReady, resetGroupBodies } from '@/bench/lib/group-bodies';
import { takeOpenGroupsBootstrap } from '@/bench/lib/open-groups';
import type { Group } from '@/bench/lib/queries';
import { NO_GROUP_FILTER_CHOICES } from '@/bench/lib/group-filter';

// The real island is covered by the `Chart.*` suites; here a bare card stands
// in for it, so the body's own mount, fetch and abort behavior is all that runs.
Expand All @@ -49,8 +50,7 @@ const PROPS: GroupBodyProps = {
],
startIndex: 5,
universe: { engines: ['datafusion', 'duckdb'], formats: ['parquet', 'vortex'] },
initialHiddenSeries: ['duckdb:parquet'],
initialShownSeries: [],
initialFilter: { ...NO_GROUP_FILTER_CHOICES, hiddenSeries: ['duckdb:parquet'] },
};

function jsonResponse(body: unknown): Response {
Expand All @@ -76,7 +76,7 @@ describe('GroupBody', () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
resetPayloadCache();
resetGroupBodies();
initGroupFilter('g', [], []);
initGroupFilter('g', NO_GROUP_FILTER_CHOICES);
urls = [];
signals = [];
responders = [];
Expand Down
16 changes: 7 additions & 9 deletions src/bench/components/GroupBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
noteGroupBodyOpen,
seedGroupFilterOnce,
} from '@/bench/lib/group-bodies';
import type { GroupFilterChoices } from '@/bench/lib/group-filter';
import type { ChartLink } from '@/bench/lib/queries';

/** Props for one group's body. */
Expand All @@ -27,10 +28,8 @@ export interface GroupBodyProps {
startIndex: number;
/** Engine and format values for the toolbar's macro chips. */
universe: FilterUniverse;
/** Series the page URL hides in this group (`?group=…&hide=…`). */
initialHiddenSeries: string[];
/** Series the page URL shows in this group (`?group=…&show=…`). */
initialShownSeries: string[];
/** The filter choices the page URL gives this group (`?group=…&hide=…`). */
initialFilter: GroupFilterChoices;
}

/**
Expand Down Expand Up @@ -58,20 +57,19 @@ export function GroupBody({
charts,
startIndex,
universe,
initialHiddenSeries,
initialShownSeries,
initialFilter,
}: GroupBodyProps) {
const base = useBenchBase();
const ref = useRef<HTMLDivElement>(null);
// Starts closed, so hydration matches the server's empty body; the layout
// effect below opens it before paint when the disclosure is already open.
const [open, setOpen] = useState(false);

// Restore a shared group permalink's series overrides, once per page load,
// Restore a shared group permalink's filter choices, once per page load,
// so a remount never overwrites the user's later changes.
useEffect(() => {
seedGroupFilterOnce(groupSlug, initialHiddenSeries, initialShownSeries);
}, [groupSlug, initialHiddenSeries, initialShownSeries]);
seedGroupFilterOnce(groupSlug, initialFilter);
}, [groupSlug, initialFilter]);

useLayoutEffect(() => {
const details = ref.current
Expand Down
51 changes: 43 additions & 8 deletions src/bench/components/GroupPermalink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { GroupPermalink, groupPermalinkUrl } from '@/bench/components/GroupPermalink';
import {
applyGroupMacro,
initGlobalFilter,
initGroupFilter,
noteGroupSeries,
resetGroup,
toggleGroupSeries,
} from '@/bench/lib/chart-store';
import { NO_GROUP_FILTER_CHOICES } from '@/bench/lib/group-filter';

describe('groupPermalinkUrl', () => {
it('joins origin, pathname, and the readable anchor fragment', () => {
const loc = { origin: 'https://bench.vortex.dev', pathname: '/' };
expect(groupPermalinkUrl('random-access', [], [], { ...loc, search: '' })).toBe(
'https://bench.vortex.dev/#random-access',
);
expect(
groupPermalinkUrl('random-access', NO_GROUP_FILTER_CHOICES, { ...loc, search: '' }),
).toBe('https://bench.vortex.dev/#random-access');
});

it('percent-encodes fragment-hostile anchor characters defensively', () => {
const loc = { origin: 'https://bench.vortex.dev', pathname: '/' };
expect(groupPermalinkUrl('a#b c', [], [], { ...loc, search: '' })).toBe(
expect(groupPermalinkUrl('a#b c', NO_GROUP_FILTER_CHOICES, { ...loc, search: '' })).toBe(
'https://bench.vortex.dev/#a%23b%20c',
);
});
Expand All @@ -35,10 +38,24 @@ describe('groupPermalinkUrl', () => {
const loc = {
origin: 'https://bench.vortex.dev',
pathname: '/',
search: '?engine=duckdb&group=old&hide=old-series&show=old-shown',
search:
'?engine=duckdb&group=old&hide=old-series&show=old-shown&hide-engine=old&hide-format=old&show-engine=old&show-format=old',
};
expect(groupPermalinkUrl('random-access', ['z series', 'a'], ['lance'], loc)).toBe(
'https://bench.vortex.dev/?engine=duckdb&group=random-access&hide=a&hide=z+series&show=lance#random-access',
expect(
groupPermalinkUrl(
'random-access',
{
hiddenSeries: ['z series', 'a'],
shownSeries: ['lance'],
hiddenEngines: ['duckdb'],
hiddenFormats: ['vortex', 'parquet'],
shownEngines: ['datafusion'],
shownFormats: ['lance'],
},
loc,
),
).toBe(
'https://bench.vortex.dev/?engine=duckdb&group=random-access&hide=a&hide=z+series&show=lance&hide-engine=duckdb&hide-format=parquet&hide-format=vortex&show-engine=datafusion&show-format=lance#random-access',
);
});
});
Expand Down Expand Up @@ -88,7 +105,8 @@ describe('GroupPermalink click behavior', () => {
vi.useRealTimers();
Reflect.deleteProperty(navigator, 'clipboard');
window.history.replaceState(null, '', '/');
initGroupFilter('random_access', [], []);
initGroupFilter('random_access', NO_GROUP_FILTER_CHOICES);
resetGroup('random_access');
});

/** Mount the link inside a real `<details>`/`<summary>` host, as on the
Expand Down Expand Up @@ -259,6 +277,23 @@ describe('GroupPermalink click behavior', () => {
]);
});

// The engine goes into the link as an engine, so it also hides the series of
// charts that have not loaded yet when someone opens the link.
it('copies an engine or format choice as that engine or format', async () => {
const { link } = mount();
noteGroupSeries('random_access', {
'datafusion:parquet': { engine: 'datafusion', format: 'parquet' },
'duckdb:parquet': { engine: 'duckdb', format: 'parquet' },
});
applyGroupMacro('random_access', 'engine', 'datafusion');

await click(link);

expect(copiedText).toEqual([
`${window.location.origin}/?group=random-access&hide-engine=datafusion#random-access`,
]);
});

it('preserves a local override that restores a globally hidden series', async () => {
const { link } = mount();
initGlobalFilter({ engines: [], formats: ['parquet', 'lance'] }, [], []);
Expand Down
Loading
Loading