Skip to content

Commit 84fe9a9

Browse files
KSemenenkoclaude
andcommitted
feat(site): rebuild the benchmark section as a visual comparison
Replace the select-and-table instrument with a dashboard built from the same verified session projection: - headline counts (databases measured, operations timed, documents per engine, concurrent clients); - radio chip controls for cluster size, ranking metric, database family and test, with per-topology engine counts; - a ranked bar chart with p50/p95/p99 per request on a log scale, naming engines without a value by their actual disposition instead of listing em dashes; - a family-grouped matrix of every test shaded relative to the fastest engine per column, with clickable column headers; - a 1-node to 3-node comparison for engines recorded in both topologies; - a collapsed results table that keeps the complete planned row inventory, exact values, job/artifact links and each engine's setup. The per-worker hash/PID provenance dump is removed from the page; the original artifacts stay linked. Client process CPU/allocation/RSS are no longer ranking metrics. Website browser tests follow the new DOM: radio selection, ranked order against the oracle, family filters, arrow keys on the cluster group and matrix header selection. Verified locally: KeyLoad.WebsiteTests build with -warnaserror and dotnet format verify; governance verify; session source closure verify; content-only build markers; asset budgets (JS 38,052 / 40,960 gzip, CSS 11,613 / 20,480); in-browser check of 1,056 metric x test x topology x family combinations against the published projection of run 40415138954. The TUnit website suite needs the GitHub capture and runs only in the Website workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9c30d28 commit 84fe9a9

20 files changed

Lines changed: 699 additions & 295 deletions

‎scripts/Features/BenchmarkComparisons/session-source-closure.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const entries = [
1111
'scripts/Features/BenchmarkComparisons/session-source-closure.mjs',
1212
...['bootstrap', 'cluster-scene', 'scene-geometry', 'scene-lifecycle', 'scene-observers',
1313
'contracts', 'build-site', 'measured-build', 'isolated-contracts', 'isolated-http', 'isolated-loader',
14-
'isolated-projection', 'isolated-measurements', 'isolated-controls', 'isolated-lab', 'isolated-view',
14+
'isolated-projection', 'isolated-measurements', 'isolated-controls', 'isolated-lab', 'isolated-view', 'isolated-charts',
1515
'measurements', 'measurement-loader'].map(name => feature + name + '.mjs'),
1616
];
1717
// Only literal relative imports are executable dependencies. Platform built-ins carry no repository source.

‎scripts/Features/BenchmarkComparisons/site-isolated-dependencies.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ site/Features/BenchmarkComparisons/bootstrap.mjs
4141
site/Features/BenchmarkComparisons/build-site.mjs
4242
site/Features/BenchmarkComparisons/cluster-scene.mjs
4343
site/Features/BenchmarkComparisons/contracts.mjs
44+
site/Features/BenchmarkComparisons/isolated-charts.mjs
4445
site/Features/BenchmarkComparisons/isolated-contracts.mjs
4546
site/Features/BenchmarkComparisons/isolated-controls.mjs
4647
site/Features/BenchmarkComparisons/isolated-http.mjs

‎site/Features/BenchmarkComparisons/build-site.mjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ export const BUILD = Object.freeze({
1515
sessionAssets: ['session-contract.mjs', 'session-plan-contract.mjs', 'session-process-identity.mjs',
1616
'session-report-validation.mjs', 'session-aggregate-contract.mjs'],
1717
measuredAssets: ['isolated-contracts.mjs', 'isolated-http.mjs', 'isolated-loader.mjs', 'isolated-measurements.mjs',
18-
'isolated-view.mjs', 'isolated-controls.mjs', 'isolated-lab.mjs', 'measurements.mjs', 'measurement-loader.mjs'],
18+
'isolated-charts.mjs', 'isolated-view.mjs', 'isolated-controls.mjs', 'isolated-lab.mjs', 'measurements.mjs',
19+
'measurement-loader.mjs'],
1920
commonAssets: ['contracts.mjs', 'bootstrap.mjs', 'cluster-scene.mjs', 'scene-geometry.mjs',
2021
'scene-lifecycle.mjs', 'scene-observers.mjs', 'styles.css', 'brand.css', 'tokens.css', 'scene.css',
2122
'assets/agent-context.png'],
@@ -154,9 +155,8 @@ async function emitContentHtml(output, siteRevision) {
154155
html = html.replaceAll(BUILD.documentationBlob, `/blob/${siteRevision}/docs`)
155156
.replaceAll(BUILD.documentationTree, `/tree/${siteRevision}/docs`);
156157
html = replaceRequired(html, ' data-isolated-catalog="data/isolated-catalog.json"', '');
157-
html = replaceRequired(html, '<p class="section-intro">Each database and topology has its own Linux runner. Separate TUnit tests use one live database session; the initial million-record load is measured separately, with no warmup or reset.</p>',
158+
html = replaceRequired(html, '<p class="section-intro">One million documents per engine, then 100,000 timed operations per test from 16 concurrent clients. Each database runs alone on its own GitHub Actions runner, on one or three nodes.</p>',
158159
'<p class="section-intro">Verified execution comparison results will appear here after a completed benchmark run.</p>');
159-
html = replaceRequired(html, '<div class="qualification-note"><span class="qualification-mark" aria-hidden="true">i</span><p><strong>Early development.</strong> Cluster qualification is in progress. These measurements describe the recorded run conditions; they do not establish production readiness, a winner or power-loss durability.</p></div>', '');
160160
html = replaceRequired(html, '<noscript><p>Enable JavaScript to explore the measurements. The original aggregate and authenticated GitHub run remain available:</p><!-- KEYLOAD_ISOLATED_EVIDENCE --></noscript>', '');
161161
const instrument = / <div class="instrument">[\s\S]*? <\/div>\n <\/section>/;
162162
if (!instrument.test(html)) throw new Error(ERRORS.html);

‎site/Features/BenchmarkComparisons/index.html‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -269,21 +269,20 @@ <h2 id="engine-title">A grain per connection. Parallel operations.</h2>
269269

270270
<section id="benchmarks" class="benchmark-section" aria-labelledby="benchmark-title" data-isolated-catalog="data/isolated-catalog.json">
271271
<header class="section-head">
272-
<div><p class="eyebrow">topology comparisons</p><h2 id="benchmark-title">Performance with 1 and 3 nodes.</h2></div>
273-
<p class="section-intro">Each database and topology has its own Linux runner. Separate TUnit tests use one live database session; the initial million-record load is measured separately, with no warmup or reset.</p>
272+
<div><p class="eyebrow">Benchmarks</p><h2 id="benchmark-title">Every engine.<br>The same workload.</h2></div>
273+
<p class="section-intro">One million documents per engine, then 100,000 timed operations per test from 16 concurrent clients. Each database runs alone on its own GitHub Actions runner, on one or three nodes.</p>
274274
</header>
275-
<div class="qualification-note"><span class="qualification-mark" aria-hidden="true">i</span><p><strong>Early development.</strong> Cluster qualification is in progress. These measurements describe the recorded run conditions; they do not establish production readiness, a winner or power-loss durability.</p></div>
276275
<noscript><p>Enable JavaScript to explore the measurements. The original aggregate and authenticated GitHub run remain available:</p><!-- KEYLOAD_ISOLATED_EVIDENCE --></noscript>
277276
<div class="instrument">
278-
<div class="instrument-controls" role="group" aria-label="measurement controls">
279-
<label class="control" for="isolated-scenario">Scenario<select id="isolated-scenario"></select></label>
280-
<label class="control" for="isolated-node-count">Nodes<select id="isolated-node-count"></select></label>
281-
<label class="control" for="isolated-target">Engine<select id="isolated-target"></select></label>
282-
<label class="control" for="isolated-metric">Metric<select id="isolated-metric"></select></label>
283-
<label class="control" for="isolated-repetition">Repetition<select id="isolated-repetition"></select></label>
277+
<div id="isolated-summary" class="bench-kpis"></div>
278+
<div class="bench-controls" role="group" aria-label="Benchmark view">
279+
<fieldset id="isolated-node-count" class="bench-seg"><legend>Cluster</legend></fieldset>
280+
<fieldset id="isolated-metric" class="bench-chips"><legend>Rank by</legend></fieldset>
281+
<fieldset id="isolated-type" class="bench-chips"><legend>Database type</legend></fieldset>
282+
<fieldset id="isolated-scenario" class="bench-chips"><legend>Test</legend></fieldset>
284283
</div>
285284
<div class="measurement-workspace">
286-
<div id="isolated-results" aria-live="polite"><p class="empty-state">Loading qualified measurements…</p></div>
285+
<div id="isolated-results" aria-live="polite"><p class="empty-state">Loading benchmark results…</p></div>
287286
<p id="isolated-error" class="load-error" role="alert" hidden></p>
288287
<button id="isolated-retry" class="retry-button" type="button" hidden>Retry measurements</button>
289288
<p id="isolated-announcement" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></p>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { ENGINE_TYPES } from './isolated-contracts.mjs';
2+
3+
const INTEGER = new Intl.NumberFormat('en', { maximumFractionDigits: 0 });
4+
const ONE = new Intl.NumberFormat('en', { maximumFractionDigits: 1 });
5+
const TWO = new Intl.NumberFormat('en', { maximumFractionDigits: 2 });
6+
const SIGNED = new Intl.NumberFormat('en', { maximumFractionDigits: 0, signDisplay: 'exceptZero' });
7+
const LATENCIES = Object.freeze(['p50', 'p95', 'p99']);
8+
const HEAT_STEPS = 5;
9+
10+
export function el(document, tag, className, text) {
11+
const node = document.createElement(tag);
12+
if (className) node.className = className;
13+
if (text !== undefined && text !== null) node.textContent = text;
14+
return node;
15+
}
16+
17+
// Chart labels round for reading; the table view keeps the recorded precision.
18+
export function friendly(value, unit) {
19+
if (!Number.isFinite(value)) return '—';
20+
if (unit === 'ms') return value >= 1000 ? ONE.format(value / 1000) + ' s'
21+
: (value >= 10 ? INTEGER : value >= 1 ? ONE : TWO).format(value) + ' ms';
22+
if (unit === 's') return (value >= 100 ? INTEGER : value >= 10 ? ONE : TWO).format(value) + ' s';
23+
if (unit === '%') return TWO.format(value) + '%';
24+
return (value >= 100 ? INTEGER : ONE).format(value) + (unit ? ' ' + unit : '');
25+
}
26+
27+
export function compact(value, unit) {
28+
if (!Number.isFinite(value) || unit !== 'ops/s') return friendly(value, unit);
29+
return value >= 1e6 ? ONE.format(value / 1e6) + 'M' : value >= 1e4 ? ONE.format(value / 1e3) + 'k' : INTEGER.format(value);
30+
}
31+
32+
const position = (node, name, percent) => node.style.setProperty(name, Math.min(100, Math.max(0, percent)).toFixed(2) + '%');
33+
34+
export function engineName(document, name, tag = 'span') {
35+
const node = el(document, tag, 'engine-name');
36+
node.append(el(document, 'strong', null, name), el(document, 'small', null, ENGINE_TYPES[name]));
37+
return node;
38+
}
39+
40+
export function latencyScale(rows) {
41+
const values = rows.flatMap(row => [row.p50, row.p99]).filter(value => Number.isFinite(value) && value > 0);
42+
if (values.length === 0) return null;
43+
const low = Math.floor(Math.log10(Math.min(...values)));
44+
const span = Math.max(1, Math.ceil(Math.log10(Math.max(...values))) - low);
45+
return { ticks: Array.from({ length: span + 1 }, (_, index) => 10 ** (low + index)),
46+
at: value => (Math.log10(Math.max(value, 10 ** low)) - low) / span * 100 };
47+
}
48+
49+
function latency(document, row, scale) {
50+
const cell = el(document, 'span', 'rank-lat');
51+
const track = el(document, 'span', 'lat-track');
52+
const range = el(document, 'span', 'lat-range');
53+
position(range, '--from', scale.at(row.p50));
54+
position(range, '--to', scale.at(row.p99));
55+
track.append(range);
56+
for (const key of LATENCIES) {
57+
const dot = el(document, 'span', 'lat-dot lat-' + key);
58+
position(dot, '--at', scale.at(row[key]));
59+
dot.title = `${row.name} · ${key} ${friendly(row[key], 'ms')}`;
60+
track.append(dot);
61+
}
62+
cell.append(track, el(document, 'small', null, `p50 ${friendly(row.p50, 'ms')} · p99 ${friendly(row.p99, 'ms')}`));
63+
return cell;
64+
}
65+
66+
export function rankChart(document, rows, metric) {
67+
const scale = latencyScale(rows);
68+
const chart = el(document, 'div', 'rank' + (scale ? ' has-latency' : ''));
69+
const head = el(document, 'div', 'rank-head');
70+
head.setAttribute('aria-hidden', 'true');
71+
head.append(el(document, 'span'), el(document, 'span', null, 'Engine'), el(document, 'span', null, `${metric.title} · ${metric.unit}`));
72+
if (scale) {
73+
const ticks = el(document, 'span', 'lat-ticks', 'Latency per request · log scale');
74+
for (const tick of scale.ticks) position(ticks.appendChild(el(document, 'i', null, friendly(tick, 'ms'))), '--at', scale.at(tick));
75+
head.append(ticks);
76+
}
77+
const list = el(document, 'ol', 'rank-list');
78+
list.setAttribute('aria-label', metric.title + ' ranking');
79+
const maximum = Math.max(0, ...rows.map(row => row.value));
80+
rows.forEach((row, index) => {
81+
const item = el(document, 'li', 'rank-row' + (row.name === 'KeyLoad' ? ' is-keyload' : ''));
82+
item.dataset.workerId = row.id;
83+
item.dataset.value = String(row.value);
84+
const bar = el(document, 'span', 'rank-bar');
85+
bar.style.setProperty('--ratio', String(maximum > 0 ? row.value / maximum : 0));
86+
const [number, unit] = metric.unit === 'ops/s' ? [friendly(row.value), metric.unit] : [friendly(row.value, metric.unit), ''];
87+
bar.append(el(document, 'span', 'rank-fill'), el(document, 'b', null, number));
88+
if (unit) bar.lastChild.append(el(document, 'small', null, ' ' + unit));
89+
item.append(el(document, 'span', 'rank-no', String(index + 1)), engineName(document, row.name), bar);
90+
if (scale) item.append(latency(document, row, scale));
91+
list.append(item);
92+
});
93+
chart.append(head, list);
94+
return chart;
95+
}
96+
97+
function heat(cell, value, column) {
98+
if (!Number.isFinite(value)) {
99+
cell.className = 'heat-none';
100+
return;
101+
}
102+
const score = column.higher ? value / column.best : column.best / value;
103+
cell.className = 'heat-' + Math.min(HEAT_STEPS - 1, Math.floor(score * HEAT_STEPS)) + (score === 1 ? ' is-best' : '');
104+
}
105+
106+
export function matrix(document, model) {
107+
const table = el(document, 'table', 'matrix');
108+
table.append(el(document, 'caption', null, model.caption));
109+
const header = el(document, 'tr');
110+
header.append(el(document, 'th', null, 'Engine'));
111+
for (const column of model.columns) {
112+
const th = el(document, 'th', column.value === model.selected ? 'is-selected' : null);
113+
th.scope = 'col';
114+
const button = el(document, 'button', null, column.label);
115+
button.type = 'button';
116+
button.dataset.scenario = column.value;
117+
button.append(el(document, 'small', null, column.group ?? column.unit));
118+
th.append(button);
119+
header.append(th);
120+
}
121+
table.appendChild(el(document, 'thead')).append(header);
122+
for (const group of model.groups) {
123+
const body = table.appendChild(el(document, 'tbody'));
124+
const label = body.appendChild(el(document, 'tr', 'matrix-group')).appendChild(el(document, 'th', null, group.type));
125+
label.colSpan = model.columns.length + 1;
126+
label.scope = 'rowgroup';
127+
for (const row of group.rows) {
128+
const tr = body.appendChild(el(document, 'tr', row.name === 'KeyLoad' ? 'is-keyload' : null));
129+
tr.append(el(document, 'th', null, row.name));
130+
tr.firstChild.scope = 'row';
131+
model.columns.forEach((column, index) => {
132+
const value = row.values[index];
133+
const td = tr.appendChild(el(document, 'td', null, compact(value, column.unit)));
134+
heat(td, value, column);
135+
if (Number.isFinite(value)) td.title = `${row.name} · ${column.title} · ${friendly(value, column.unit)}`;
136+
});
137+
}
138+
}
139+
return table;
140+
}
141+
142+
export function scaling(document, pairs, metric) {
143+
const list = el(document, 'ul', 'scale-list');
144+
const maximum = Math.max(0, ...pairs.flatMap(pair => [pair.one, pair.three]));
145+
for (const pair of pairs) {
146+
const item = el(document, 'li', 'scale-row');
147+
const track = el(document, 'span', 'scale-track');
148+
const range = track.appendChild(el(document, 'span', 'scale-range'));
149+
const [one, three] = [pair.one, pair.three].map(value => maximum > 0 ? value / maximum * 100 : 0);
150+
position(range, '--from', Math.min(one, three));
151+
position(range, '--to', Math.max(one, three));
152+
for (const [className, at, value, nodes] of [['dot-one', one, pair.one, 1], ['dot-three', three, pair.three, 3]]) {
153+
const dot = track.appendChild(el(document, 'span', 'scale-dot ' + className));
154+
position(dot, '--at', at);
155+
dot.title = `${pair.name} · ${nodes} node${nodes === 1 ? '' : 's'} · ${friendly(value, metric.unit)}`;
156+
}
157+
const change = (pair.three - pair.one) / pair.one * 100;
158+
item.append(engineName(document, pair.name), track,
159+
el(document, 'span', 'scale-values', `${friendly(pair.one, metric.unit)} → ${friendly(pair.three, metric.unit)}`),
160+
el(document, 'b', 'scale-change', SIGNED.format(change).replace('-', '−') + '%'));
161+
list.append(item);
162+
}
163+
return list;
164+
}

‎site/Features/BenchmarkComparisons/isolated-contracts.mjs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,30 @@
22
export const ISOLATED = Object.freeze({ repository: 'managedcode/KeyLoad', ref: 'refs/heads/main', workflow: 'Benchmarks',
33
error: 'E_ISOLATED_EVIDENCE', failed: 'failed', failure: 'failure', success: 'success' });
44
export const DOM = Object.freeze({
5-
scenario: 'isolated-scenario', nodes: 'isolated-node-count', target: 'isolated-target', metric: 'isolated-metric',
6-
repetition: 'isolated-repetition', results: 'isolated-results', error: 'isolated-error', retry: 'isolated-retry',
5+
summary: 'isolated-summary', scenario: 'isolated-scenario', nodes: 'isolated-node-count', metric: 'isolated-metric',
6+
type: 'isolated-type', results: 'isolated-results', error: 'isolated-error', retry: 'isolated-retry',
77
announcement: 'isolated-announcement',
88
});
9+
// Database families group engines for the type filter and the overview matrix; they are presentation labels only.
10+
export const ENGINE_TYPES = Object.freeze({
11+
'KeyLoad': 'Multi-model', 'SurrealDB': 'Multi-model', 'PostgreSQL + pgvector': 'Relational', 'SQL Server': 'Relational',
12+
'MySQL': 'Relational', 'MongoDB': 'Document', 'Redis': 'Key-value', 'OpenSearch': 'Search', 'Neo4j': 'Graph',
13+
'HelixDB': 'Graph', 'TimescaleDB': 'Time series', 'InfluxDB': 'Time series', 'ClickHouse': 'Analytics',
14+
'Qdrant': 'Vector', 'RabbitMQ': 'Queue', 'KurrentDB': 'Event store',
15+
});
16+
export const TYPE_ORDER = Object.freeze(['Multi-model', 'Relational', 'Document', 'Key-value', 'Search', 'Graph',
17+
'Time series', 'Analytics', 'Vector', 'Queue', 'Event store']);
18+
export const SCENARIOS = Object.freeze([
19+
['load-million', 'Load 1M'], ['index', 'Index build'], ['sequential-read', 'Sequential read'],
20+
['random-read', 'Random read'], ['search', 'Search'], ['verify-final', 'Full readback'], ['mixed/create', 'Create'],
21+
['mixed/delete', 'Delete'], ['mixed/update', 'Update'], ['mixed/read', 'Read'], ['mixed/search', 'Search'],
22+
].map(([value, label]) => Object.freeze({ value, label, mixed: value.startsWith('mixed/') })));
23+
// Index build and final readback record elapsed time only; every other scenario has per-request measurements.
24+
export const durationOnly = scenario => scenario === 'index' || scenario === 'verify-final';
25+
export const scenarioTitle = value => {
26+
const scenario = SCENARIOS.find(item => item.value === value);
27+
return scenario.mixed ? 'Mixed workload · ' + scenario.label.toLowerCase() : scenario.label;
28+
};
929
export function assertIsolated(condition) {
1030
if (!condition) {
1131
const error = new TypeError('Benchmark session evidence is unavailable or invalid.');

0 commit comments

Comments
 (0)