test(benchmark): print a readable summary table
CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m5s
CI / build (push) Successful in 1m38s
CI / policy (pull_request) Successful in 10s
CI / security (push) Successful in 1m19s
CI / test (push) Successful in 3m29s
CI / validate (pull_request) Successful in 1m42s
CI / benchmark (push) Successful in 4m13s
CI / security (pull_request) Successful in 56s
CI / build (pull_request) Successful in 1m42s
CI / test (pull_request) Successful in 3m8s
CI / images (push) Successful in 1m15s
CI / required (push) Successful in 1s
CI / benchmark (pull_request) Successful in 3m37s
CI / images (pull_request) Successful in 1m11s
CI / required (pull_request) Successful in 1s

Print the calibration and a Markdown table with one aligned row per
scenario as soon as it finishes: both planning times, mean, budget, ratio,
multiplier, outcome, placements, determinism and budget status. Write
directly to stdout so Vitest adds no per-line headers. The complete TSV
report still follows the table.

Refs #281, #285

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nessarandClaude Opus 5.5 committed 2026-10-17 02:09:54 +02:00
1 parent cfdb28c90a
commit 3bbe989eca
4 files changed
+127 -28

No files matched your search

+11 -5
View File
@@ -20,11 +20,17 @@ speed, so a fixed wall-clock limit was flaky.
Before the protocol, in the same process, the spec times the frozen engine
(`src/app/services/frozen-engine/leave-planner.core.frozen.mjs`, the engine at
a871f8f) on the `two years` scenario, best of three, and prints
`calibration\tfrozen two years best of 3\t<ms>`. Each scenario's budget is its
multiplier times that calibration; the mean `planningMs` of its two measured
repetitions must not exceed it. A scenario without a multiplier fails too.
`#286` tightens the multipliers once the speed target is met.
a871f8f) on the `two years` scenario, best of three, and prints the result.
Each scenario's budget is its multiplier times that calibration; the mean
`planningMs` of its two measured repetitions must not exceed it. A scenario
without a multiplier fails too. `#286` tightens the multipliers once the speed
target is met.
The job log shows a Markdown table with one row per scenario, printed as soon
as that scenario finishes. Each row gives both measured planning times, their
mean, the budget, the ratio to the calibration, the multiplier, the outcome,
placements, determinism and `ok`, `OVER` or `no budget`. The complete
tab-separated report follows the table.
| Scenario | Mean planning (s) | Ratio to calibration | Multiplier |
| --------------------------- | ----------------- | -------------------- | ---------- |
+34 -1
View File
@@ -1,4 +1,9 @@
import { BenchmarkMeasurement, budgetViolations } from './leave-planner-benchmark';
import {
BenchmarkMeasurement,
benchmarkSummaryHeader,
benchmarkSummaryRow,
budgetViolations,
} from './leave-planner-benchmark';
const measure = (scenario: string, planningMs: number) =>
({ scenario, planningMs }) as BenchmarkMeasurement;
@@ -28,3 +33,31 @@ describe('benchmark budget gate', () => {
]);
});
});
describe('benchmark summary table', () => {
const runs = (planningMs: number[]) =>
planningMs.map(
(ms, index) =>
({
scenario: 'a',
planningMs: ms,
outcome: 'Plan',
lastCompletedStage: 'dates',
placements: 3,
deterministic: index ? 'yes' : 'baseline',
}) as BenchmarkMeasurement,
);
it('aligns rows with the header', () => {
const [header, separator] = benchmarkSummaryHeader().split('\n');
const row = benchmarkSummaryRow(runs([150, 250]), 100, { a: 2 });
expect(row).toHaveLength(header.length);
expect(separator).toHaveLength(header.length);
expect(row).toContain('0.15 / 0.25 s');
expect(row).toContain('Plan (dates)');
expect(row).toMatch(/\| ok +\|$/);
});
it('marks scenarios over budget or without a budget', () => {
expect(benchmarkSummaryRow(runs([300, 320]), 100, { a: 2 })).toMatch(/\| OVER +\|$/);
expect(benchmarkSummaryRow(runs([1]), 100, {})).toMatch(/\| no budget \|$/);
});
});
@@ -3,10 +3,10 @@ import { calculatePlanningAsyncInternal } from '../services/frozen-engine/leave-
import { createPlanningCalculation } from '../services/leave-planner.service';
import {
BENCHMARK_BUDGET_MULTIPLIERS,
BENCHMARK_REPORT_HEADER,
benchmarkReport,
benchmarkReportRows,
benchmarkScenarios,
benchmarkSummaryHeader,
benchmarkSummaryRow,
budgetViolations,
runLeavePlannerBenchmark,
} from './leave-planner-benchmark';
@@ -78,10 +78,15 @@ describe('leave planner benchmark protocol', () => {
await calculatePlanningAsyncInternal({ ...request, calculationDate: '2024-01-01' });
calibrationMs = Math.min(calibrationMs, performance.now() - start);
}
console.info(`calibration\tfrozen two years best of 3\t${calibrationMs.toFixed(1)}`);
console.info(BENCHMARK_REPORT_HEADER);
// Direct writes skip Vitest's per-call "stdout |" headers and appear immediately.
const print = (text: string) => process.stdout.write(`${text}\n`);
print(
`\nCalibration: frozen engine on "two years", best of 3 = ${calibrationMs.toFixed(1)} ms` +
'\nBudget = limit x calibration; ratio = mean / calibration.\n',
);
print(benchmarkSummaryHeader());
const measurements = await runLeavePlannerBenchmark(2, (rows) =>
console.info(benchmarkReportRows(rows)),
print(benchmarkSummaryRow(rows, calibrationMs)),
);
for (const [name, placements] of [
['two years', 60],
@@ -114,7 +119,7 @@ describe('leave planner benchmark protocol', () => {
loaded[0].preparationMs + loaded[0].planningMs,
);
const report = benchmarkReport(measurements);
console.info(report);
print(`\nRaw report (TSV):\n${report}\n`);
expect(report.split('\n')).toHaveLength(15);
expect(report).toContain('planning_ms\twall_ms');
expect(report).toContain('planning_work_ms\tpeak_main_thread_heap_bytes');
+71 -16
View File
@@ -277,12 +277,12 @@ async function measureRepetition(
}
}
export const BENCHMARK_REPORT_HEADER =
'scenario\trepetition\tpreparation_ms\tplanning_ms\twall_ms\toutcome\tlast_stage\tplacements\tdeterministic\tmax_ui_delay_ms\tplanning_work_ms\tpeak_main_thread_heap_bytes';
export function benchmarkReportRows(measurements: BenchmarkMeasurement[]): string {
return measurements
.map((item) =>
export function benchmarkReport(measurements: BenchmarkMeasurement[]): string {
const header =
'scenario\trepetition\tpreparation_ms\tplanning_ms\twall_ms\toutcome\tlast_stage\tplacements\tdeterministic\tmax_ui_delay_ms\tplanning_work_ms\tpeak_main_thread_heap_bytes';
return [
header,
...measurements.map((item) =>
[
item.scenario,
item.repetition,
@@ -297,15 +297,8 @@ export function benchmarkReportRows(measurements: BenchmarkMeasurement[]): strin
item.planningWorkMs?.toFixed(1) ?? '',
item.peakMainThreadHeapBytes ?? '',
].join('\t'),
)
.join('\n');
}
export function benchmarkReport(measurements: BenchmarkMeasurement[]): string {
// Empty input yields the header alone, with no trailing newline.
return measurements.length
? [BENCHMARK_REPORT_HEADER, benchmarkReportRows(measurements)].join('\n')
: BENCHMARK_REPORT_HEADER;
),
].join('\n');
}
// Measured on the development machine (ratio to the frozen two-years run), tightened by #286.
@@ -330,7 +323,7 @@ export function budgetViolations(
byScenario.set(scenario, [...(byScenario.get(scenario) ?? []), planningMs]);
const violations: string[] = [];
for (const [scenario, times] of byScenario) {
const mean = times.reduce((sum, value) => sum + value, 0) / times.length;
const mean = average(times);
const multiplier = multipliers[scenario];
if (multiplier === undefined) {
violations.push(`${scenario}: no budget multiplier defined`);
@@ -344,3 +337,65 @@ export function budgetViolations(
}
return violations;
}
function average(values: number[]): number {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
// Fixed widths keep rows printed as each scenario finishes aligned with the header.
const SUMMARY_COLUMNS: [title: string, width: number, alignRight: boolean][] = [
['Scenario', 27, false],
['Planning (runs)', 17, true],
['Mean', 9, true],
['Budget', 9, true],
['Ratio', 6, true],
['Limit', 6, true],
['Outcome', 16, false],
['Placements', 10, true],
['Deterministic', 13, false],
['Status', 9, false],
];
function summaryLine(cells: string[]): string {
return `| ${cells
.map((cell, index) => {
const [, width, alignRight] = SUMMARY_COLUMNS[index];
return alignRight ? cell.padStart(width) : cell.padEnd(width);
})
.join(' | ')} |`;
}
/** Markdown table header for {@link benchmarkSummaryRow}. */
export function benchmarkSummaryHeader(): string {
return [
summaryLine(SUMMARY_COLUMNS.map(([title]) => title)),
`|${SUMMARY_COLUMNS.map(([, width, alignRight]) => (alignRight ? `${'-'.repeat(width + 1)}:` : `:${'-'.repeat(width + 1)}`)).join('|')}|`,
].join('\n');
}
/** One aligned table row for one scenario's measured repetitions, judged like {@link budgetViolations}. */
export function benchmarkSummaryRow(
scenarioMeasurements: BenchmarkMeasurement[],
calibrationMs: number,
multipliers: Record<string, number> = BENCHMARK_BUDGET_MULTIPLIERS,
): string {
const [first] = scenarioMeasurements;
const seconds = (ms: number) => (ms / 1000).toFixed(2);
const mean = average(scenarioMeasurements.map(({ planningMs }) => planningMs));
const multiplier = multipliers[first.scenario];
const budget = multiplier === undefined ? undefined : multiplier * calibrationMs;
return summaryLine([
first.scenario,
`${scenarioMeasurements.map(({ planningMs }) => seconds(planningMs)).join(' / ')} s`,
`${seconds(mean)} s`,
budget === undefined ? '-' : `${seconds(budget)} s`,
(mean / calibrationMs).toFixed(2),
multiplier === undefined ? '-' : String(multiplier),
first.lastCompletedStage ? `${first.outcome} (${first.lastCompletedStage})` : first.outcome,
String(first.placements),
scenarioMeasurements.slice(1).every(({ deterministic }) => deterministic === 'yes')
? 'yes'
: 'no',
budget === undefined ? 'no budget' : mean > budget ? 'OVER' : 'ok',
]);
}