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
7 changes: 7 additions & 0 deletions bench/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ column-by-column; an unknown `kind` or a missing dimension field fails the inges
`compression_size` records include `uncompressed_bytes`, the Arrow memory size after the
source Parquet file is decoded. The column is nullable so historical rows remain valid.

`string-bench` shares the two compression tables with `compress-bench`. Its records use the
dataset `strings/<column>`, the string encoder (`onpair-12`, `fsst`, ...) as `dataset_variant`,
and `vortex-file-compressed` as `format`; `encode` is the Vortex file write and `decode` the scan.
Its `uncompressed_bytes` is the column's canonical Arrow `Utf8View` size. The site lists these
rows in the `String Encoding` groups and compares each encoder with `fsst`, so the prefix and the
`fsst` label are part of the contract.

`random_access_time` records include `open_mode`. The value is `cached` for a reused accessor
or `reopen` when each timed take includes a new accessor. Historical rows use `cached`.
The `reopen` mode does not clear the OS page cache.
Expand Down
2 changes: 1 addition & 1 deletion bench/web/components/SummaryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function SummaryCard({ summary }: { summary?: Summary | undefined }) {
</div>
<div className="compression-size-panel compression-size-parquet-panel">
<div className="compression-size-parquet-header">
<span className="compression-size-group">Vs Parquet</span>
<span className="compression-size-group">Vs {summary.baseline ?? 'Parquet'}</span>
<span className="compression-size-heading">Min</span>
<span className="compression-size-heading">Mean</span>
<span className="compression-size-heading">Max</span>
Expand Down
6 changes: 6 additions & 0 deletions bench/web/lib/benchmark-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ describe('groupDocPath', () => {
expect(groupDocPath({ k: 'CompressionTimeGroup' })).toBe('benchmarks/compress-bench/README.md');
// Two measurement kinds of one benchmark, so one doc.
expect(groupDocPath({ k: 'CompressionSizeGroup' })).toBe('benchmarks/compress-bench/README.md');
expect(groupDocPath({ k: 'CompressionTimeGroup', suite: 'strings' })).toBe(
'benchmarks/string-bench/README.md',
);
expect(groupDocPath({ k: 'CompressionSizeGroup', suite: 'strings' })).toBe(
'benchmarks/string-bench/README.md',
);
expect(groupDocPath({ k: 'RandomAccessGroup', storage: 'nvme' })).toBe(
'benchmarks/random-access-bench/README.md',
);
Expand Down
5 changes: 4 additions & 1 deletion bench/web/lib/benchmark-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const QUERY_DOCS: Readonly<Record<string, string>> = {
/** The compression benchmark, whose two measurement kinds share one doc. */
const COMPRESS_BENCH_DOC = 'benchmarks/compress-bench/README.md';

/** The string encoding benchmark, which shares the compression tables. */
const STRING_BENCH_DOC = 'benchmarks/string-bench/README.md';

/** The random-access benchmark. */
const RANDOM_ACCESS_BENCH_DOC = 'benchmarks/random-access-bench/README.md';

Expand Down Expand Up @@ -79,7 +82,7 @@ export function groupDocPath(key: GroupKey): string | null {
switch (key.k) {
case 'CompressionTimeGroup':
case 'CompressionSizeGroup':
return COMPRESS_BENCH_DOC;
return key.suite === 'strings' ? STRING_BENCH_DOC : COMPRESS_BENCH_DOC;
case 'RandomAccessGroup':
return RANDOM_ACCESS_BENCH_DOC;
case 'QueryGroup':
Expand Down
19 changes: 19 additions & 0 deletions bench/web/lib/compression-suite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

/**
* Which benchmark suite wrote a compression row: `compress-bench`, or `string-bench`,
* which shares the compression tables under the `strings/` dataset prefix with the
* string encoder in `dataset_variant`.
*/
export type CompressionSuite = 'compress' | 'strings';

/** Dataset prefix `string-bench` gives every row it writes. */
export const STRING_DATASET_PREFIX = 'strings/';

/** The `LIKE` pattern matching every `string-bench` dataset. */
export const STRING_DATASET_PATTERN = `${STRING_DATASET_PREFIX}%`;

export function compressionSuite(dataset: string): CompressionSuite {
return dataset.startsWith(STRING_DATASET_PREFIX) ? 'strings' : 'compress';
}
4 changes: 4 additions & 0 deletions bench/web/lib/descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ function staticDescription(name: string): string | null {
'Compares compressed file sizes and compression ratios across different encoding ' +
'strategies'
);
case 'String Encoding':
return 'Measures Vortex file write and scan throughput for each string encoder';
case 'String Encoding Size':
return 'Compares Vortex file sizes for each string encoder';
case 'Clickbench':
return (
"ClickHouse's analytical benchmark suite testing real-world query patterns on web " +
Expand Down
90 changes: 90 additions & 0 deletions bench/web/lib/groups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,96 @@ describe.skipIf(!databaseAvailable)('summary math fidelity (testcontainers Postg
).toBeNull();
});

it('splits string-bench rows into their own groups, summaries, and chart bundles', async () => {
const sha = 'a'.repeat(40);
await insertCommit(sha, '2026-04-23T12:00:00Z');
await insertCompTimePair(sha, 'encode', 100, 200);
await insertCompSizePair(sha, 100, 200);
// `string-bench` writes `strings/<column>` with the encoder in `dataset_variant`.
for (const [encoder, encodeNs, decodeNs, bytes] of [
['fsst', 400, 100, 300],
['onpair-12', 200, 50, 200],
] as const) {
await getPool().query(
`INSERT INTO compression_times
(measurement_id, commit_sha, dataset, dataset_variant, format, op,
value_ns, all_runtimes_ns)
VALUES ($1, $2, 'strings/fineweb/text', $3, 'vortex-file-compressed', 'encode', $5,
'{1}'::bigint[]),
($4, $2, 'strings/fineweb/text', $3, 'vortex-file-compressed', 'decode', $6,
'{1}'::bigint[])`,
[nextId(), sha, encoder, nextId(), encodeNs, decodeNs],
);
await getPool().query(
`INSERT INTO compression_sizes
(measurement_id, commit_sha, dataset, dataset_variant, format,
value_bytes, uncompressed_bytes)
VALUES ($1, $2, 'strings/fineweb/text', $3, 'vortex-file-compressed', $4, 1200)`,
[nextId(), sha, encoder, bytes],
);
}

const groups = await collectGroups();
expect(groups.map((group) => group.name)).toEqual([
'Compression',
'Compression Size',
'String Encoding',
'String Encoding Size',
]);
const byName = new Map(groups.map((group) => [group.name, group]));
expect(groupKeyFromSlug(byName.get('Compression')!.slug)).toEqual({
k: 'CompressionTimeGroup',
});
expect(byName.get('Compression')!.charts.map((chart) => chart.name)).toEqual(['tpch-lineitem']);
expect(byName.get('Compression')!.summary).toMatchObject({
type: 'compression',
rankings: [
{ name: 'vortex-file-compressed', operation: 'encode', ratio: 2 },
{ name: 'parquet', operation: 'encode', ratio: 1 },
],
});
expect(groupKeyFromSlug(byName.get('String Encoding')!.slug)).toEqual({
k: 'CompressionTimeGroup',
suite: 'strings',
});
expect(byName.get('String Encoding')!.charts).toEqual(
['fsst', 'onpair-12'].map((encoder) => ({
name: `fineweb/text/${encoder}`,
slug: chartKeyToSlug({
k: 'CompressionTime',
dataset: 'strings/fineweb/text',
dataset_variant: encoder,
}),
})),
);
expect(byName.get('String Encoding')!.summary).toMatchObject({
type: 'compression',
rankings: [
{ name: 'onpair-12', operation: 'encode', ratio: 2, throughputGbS: 6 },
{ name: 'fsst', operation: 'encode', ratio: 1, throughputGbS: 3 },
{ name: 'onpair-12', operation: 'decode', ratio: 2, throughputGbS: 24 },
{ name: 'fsst', operation: 'decode', ratio: 1, throughputGbS: 12 },
],
});
expect(byName.get('String Encoding Size')!.summary).toMatchObject({
type: 'compressionSize',
baseline: 'FSST',
rankings: [
{ name: 'onpair-12', ratio: 2 / 3, compressionRatio: 6 },
{ name: 'fsst', ratio: 1, compressionRatio: 4 },
],
});

const bundle = await collectGroupCharts(
{ k: 'CompressionSizeGroup', suite: 'strings' },
parseCommitWindow(null),
);
expect(bundle?.charts.map((chart) => chart.name)).toEqual([
'fineweb/text/fsst',
'fineweb/text/onpair-12',
]);
});

it('ranks random access over every chart, not the alphabetically first one', async () => {
// The bug this pins: the old summary walked the group's chart links and
// published the first populated chart's raw times under the group-wide
Expand Down
90 changes: 60 additions & 30 deletions bench/web/lib/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import type { PoolClient } from 'pg';
import { BRANCH_COMMITS, factOnReadBranch } from './branch';
import { getPool } from './db';
import { randomAccessStorage } from './random-access';
import {
compressionSuite,
type CompressionSuite,
STRING_DATASET_PREFIX,
} from './compression-suite';
import { groupDescription } from './descriptions';
import type { FilterUniverse } from './chart-format';
import { type ChartKind, compareCodeUnits, FAMILIES, type GroupKind } from './families';
Expand Down Expand Up @@ -710,9 +715,13 @@ function chartInGroup(group: GroupKey, key: ChartKey): boolean {
key.storage === group.storage
);
case 'CompressionTimeGroup':
return key.k === 'CompressionTime';
return (
key.k === 'CompressionTime' && compressionSuite(key.dataset) === (group.suite ?? 'compress')
);
case 'CompressionSizeGroup':
return key.k === 'CompressionSize';
return (
key.k === 'CompressionSize' && compressionSuite(key.dataset) === (group.suite ?? 'compress')
);
case 'RandomAccessGroup':
return key.k === 'RandomAccess' && randomAccessStorage(key.dataset) === group.storage;
case 'VectorSearchGroup':
Expand Down Expand Up @@ -1148,6 +1157,8 @@ export interface GroupChartsResponse {
const GROUP_ORDER: readonly string[] = [
'Compression',
'Compression Size',
'String Encoding',
'String Encoding Size',
'Random Access (NVMe)',
'Random Access (S3)',
'Clickbench',
Expand Down Expand Up @@ -1421,7 +1432,50 @@ async function collectQueryGroups(): Promise<Group[]> {

type DatasetVariantRow = { dataset: string; dataset_variant: string | null };

/** The single `Compression` group, or `[]` if the fact table is empty. */
/** Group display names per compression suite and family. */
const COMPRESSION_GROUP_NAMES: Record<
'CompressionTimeGroup' | 'CompressionSizeGroup',
Record<CompressionSuite, string>
> = {
CompressionTimeGroup: { compress: 'Compression', strings: 'String Encoding' },
CompressionSizeGroup: { compress: 'Compression Size', strings: 'String Encoding Size' },
};

/**
* Split one compression table's `(dataset, dataset_variant)` rows into the `compress-bench`
* group and the `string-bench` group, dropping a group with no rows. String charts drop the
* shared `strings/` prefix from their names.
*/
function compressionGroups(
rows: readonly DatasetVariantRow[],
groupKind: 'CompressionTimeGroup' | 'CompressionSizeGroup',
): Group[] {
const chartKind = groupKind === 'CompressionTimeGroup' ? 'CompressionTime' : 'CompressionSize';
const suites: CompressionSuite[] = ['compress', 'strings'];
return suites.flatMap((suite) => {
const charts: ChartLink[] = rows
.filter((row) => compressionSuite(row.dataset) === suite)
.map((row) => {
const dataset =
suite === 'strings' ? row.dataset.slice(STRING_DATASET_PREFIX.length) : row.dataset;
return {
name: row.dataset_variant !== null ? `${dataset}/${row.dataset_variant}` : dataset,
slug: chartKeyToSlug({
k: chartKind,
dataset: row.dataset,
dataset_variant: row.dataset_variant,
}),
};
});
if (charts.length === 0) {
return [];
}
const key: GroupKey = suite === 'strings' ? { k: groupKind, suite } : { k: groupKind };
return [{ name: COMPRESSION_GROUP_NAMES[groupKind][suite], slug: groupKeyToSlug(key), charts }];
});
}

/** The `Compression` and `String Encoding` groups, or `[]` if the fact table is empty. */
async function collectCompressionTimeGroup(): Promise<Group[]> {
const text = `
SELECT dataset, dataset_variant
Expand All @@ -1430,21 +1484,10 @@ async function collectCompressionTimeGroup(): Promise<Group[]> {
ORDER BY dataset, dataset_variant NULLS FIRST
`;
const rows = (await getPool().query<DatasetVariantRow>(text)).rows;
if (rows.length === 0) {
return [];
}
const charts: ChartLink[] = rows.map((row) => ({
name: row.dataset_variant !== null ? `${row.dataset}/${row.dataset_variant}` : row.dataset,
slug: chartKeyToSlug({
k: 'CompressionTime',
dataset: row.dataset,
dataset_variant: row.dataset_variant,
}),
}));
return [{ name: 'Compression', slug: groupKeyToSlug({ k: 'CompressionTimeGroup' }), charts }];
return compressionGroups(rows, 'CompressionTimeGroup');
}

/** The single `Compression Size` group, or `[]` if the fact table is empty. */
/** The `Compression Size` and `String Encoding Size` groups, or `[]` if the fact table is empty. */
async function collectCompressionSizeGroup(): Promise<Group[]> {
const text = `
SELECT dataset, dataset_variant
Expand All @@ -1453,20 +1496,7 @@ async function collectCompressionSizeGroup(): Promise<Group[]> {
ORDER BY dataset, dataset_variant NULLS FIRST
`;
const rows = (await getPool().query<DatasetVariantRow>(text)).rows;
if (rows.length === 0) {
return [];
}
const charts: ChartLink[] = rows.map((row) => ({
name: row.dataset_variant !== null ? `${row.dataset}/${row.dataset_variant}` : row.dataset,
slug: chartKeyToSlug({
k: 'CompressionSize',
dataset: row.dataset,
dataset_variant: row.dataset_variant,
}),
}));
return [
{ name: 'Compression Size', slug: groupKeyToSlug({ k: 'CompressionSizeGroup' }), charts },
];
return compressionGroups(rows, 'CompressionSizeGroup');
}

/** One random-access group per storage backend, omitting empty sections. */
Expand Down
15 changes: 15 additions & 0 deletions bench/web/lib/slug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const GROUP_KEYS: GroupKey[] = [
{ k: 'QueryGroup', dataset: 'tpch', dataset_variant: null, scale_factor: '1', storage: 'nvme' },
{ k: 'CompressionTimeGroup' },
{ k: 'CompressionSizeGroup' },
{ k: 'CompressionTimeGroup', suite: 'strings' },
{ k: 'CompressionSizeGroup', suite: 'strings' },
{ k: 'RandomAccessGroup', storage: 'nvme' },
{ k: 'RandomAccessGroup', storage: 's3' },
{ k: 'VectorSearchGroup', dataset: 'cohere', layout: 'partitioned' },
Expand Down Expand Up @@ -84,6 +86,19 @@ describe('group slug round-trip', () => {
expect(chartPrefix).not.toBe(groupPrefix);
});

it('keeps the compress-bench group slugs from before the string-bench split', () => {
expect(groupKeyToSlug({ k: 'CompressionTimeGroup' })).toBe(
`ctg.${Buffer.from('{"k":"CompressionTimeGroup"}').toString('base64url')}`,
);
});

it.each(['compress', '', null, 1])('rejects invalid compression group suite %s', (suite) => {
const slug =
'ctg.' +
Buffer.from(JSON.stringify({ k: 'CompressionTimeGroup', suite })).toString('base64url');
expect(() => groupKeyFromSlug(slug)).toThrow();
});

it('resolves legacy random-access group slugs to NVMe', () => {
const legacy = `rag.${Buffer.from('{"k":"RandomAccessGroup"}').toString('base64url')}`;
expect(groupKeyFromSlug(legacy)).toEqual({ k: 'RandomAccessGroup', storage: 'nvme' });
Expand Down
18 changes: 13 additions & 5 deletions bench/web/lib/slug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ export type GroupKey =
scale_factor: string | null;
storage: string;
}
| { k: 'CompressionTimeGroup' }
| { k: 'CompressionSizeGroup' }
// `suite` is absent for the `compress-bench` group, so its slug predates the split.
| { k: 'CompressionTimeGroup'; suite?: 'strings' }
| { k: 'CompressionSizeGroup'; suite?: 'strings' }
| { k: 'RandomAccessGroup'; storage: RandomAccessStorage }
| { k: 'VectorSearchGroup'; dataset: string; layout: string };

Expand Down Expand Up @@ -240,7 +241,7 @@ export function groupKeyToSlug(key: GroupKey): string {
break;
case 'CompressionTimeGroup':
case 'CompressionSizeGroup':
ordered = { k: key.k };
ordered = key.suite === undefined ? { k: key.k } : { k: key.k, suite: key.suite };
break;
}
return `${prefix}.${encodePayload(JSON.stringify(ordered))}`;
Expand Down Expand Up @@ -277,7 +278,14 @@ export function groupKeyFromSlug(slug: string): GroupKey {
return { k: kind, storage };
}
case 'CompressionTimeGroup':
case 'CompressionSizeGroup':
return { k: kind };
case 'CompressionSizeGroup': {
if (obj.suite === undefined) {
return { k: kind };
}
if (obj.suite !== 'strings') {
throw new Error('compression group suite must be strings when present');
}
return { k: kind, suite: 'strings' };
}
}
}
Loading
Loading