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>
139 lines
5.8 KiB
TypeScript
139 lines
5.8 KiB
TypeScript
import { vi } from 'vitest';
|
|
import { calculatePlanningAsyncInternal } from '../services/frozen-engine/leave-planner.core.frozen.mjs';
|
|
import { createPlanningCalculation } from '../services/leave-planner.service';
|
|
import {
|
|
BENCHMARK_BUDGET_MULTIPLIERS,
|
|
benchmarkReport,
|
|
benchmarkScenarios,
|
|
benchmarkSummaryHeader,
|
|
benchmarkSummaryRow,
|
|
budgetViolations,
|
|
runLeavePlannerBenchmark,
|
|
} from './leave-planner-benchmark';
|
|
|
|
describe('leave planner benchmark protocol', () => {
|
|
beforeEach(() => vi.setSystemTime(new Date('2024-01-01T12:00:00Z')));
|
|
afterEach(() => vi.useRealTimers());
|
|
// Warm-up and two repetitions include explanation delivery after planning.
|
|
it('keeps seven deterministic scenarios including two and three years and a later earned consumer', async () => {
|
|
const scenarios = benchmarkScenarios();
|
|
expect(scenarios).toHaveLength(7);
|
|
for (const [name, end] of [
|
|
['two years', '2026-01-01'],
|
|
['three years', '2027-01-01'],
|
|
]) {
|
|
const scenario = scenarios.find((item) => item.name === name)!;
|
|
expect(scenario.request.horizon).toEqual({ start: '2024-01-01', end });
|
|
expect(
|
|
scenario.request.balances.some(
|
|
(item) => String(item.availableFrom) >= '2025-01-01',
|
|
),
|
|
).toBe(true);
|
|
expect(scenario.request.deadlineMs).toBeUndefined();
|
|
}
|
|
const typedObligations = scenarios.find(
|
|
(scenario) => scenario.name === 'typed obligations',
|
|
);
|
|
const cpRule = typedObligations?.request.rules?.find((rule) => rule.id === 'cp-window');
|
|
const rttRule = typedObligations?.request.rules?.find((rule) => rule.id === 'rtt-window');
|
|
expect(cpRule).toMatchObject({ placeType: 'cp', placeDays: 2 });
|
|
expect(rttRule).toMatchObject({ placeType: 'rtt', placeDays: 2 });
|
|
expect(cpRule?.type).toBe('time_window');
|
|
expect(rttRule?.type).toBe('time_window');
|
|
if (cpRule && 'startDate' in cpRule && rttRule && 'startDate' in rttRule)
|
|
expect(
|
|
String(cpRule.startDate) <= String(rttRule.endDate) &&
|
|
String(rttRule.startDate) <= String(cpRule.endDate),
|
|
).toBe(true);
|
|
const chain = scenarios.find((scenario) => scenario.name === 'chained earned entitlements');
|
|
expect(
|
|
chain?.request.rules
|
|
?.filter(({ type }) => type !== 'optimization')
|
|
.map((rule) => rule.id),
|
|
).toEqual(['wednesday-holidays', 'thursday-consumer']);
|
|
|
|
const browserWindow = globalThis.window;
|
|
vi.stubGlobal('window', undefined);
|
|
try {
|
|
const typedOutcome = await createPlanningCalculation(typedObligations!.request).outcome;
|
|
expect(typedOutcome.kind).toBe('Plan');
|
|
if (typedOutcome.kind === 'Plan') {
|
|
expect(
|
|
typedOutcome.placements.filter((placement) =>
|
|
placement.ruleIds.includes('cp-window'),
|
|
),
|
|
).toHaveLength(2);
|
|
expect(
|
|
typedOutcome.placements.filter((placement) =>
|
|
placement.ruleIds.includes('rtt-window'),
|
|
),
|
|
).toHaveLength(2);
|
|
}
|
|
// Frozen engine (a871f8f) on 'two years', best of three: the runner-speed yardstick.
|
|
const calibrationRequest = scenarios.find((item) => item.name === 'two years')!.request;
|
|
let calibrationMs = Infinity;
|
|
for (let run = 0; run < 3; run++) {
|
|
const request = structuredClone(calibrationRequest);
|
|
const start = performance.now();
|
|
await calculatePlanningAsyncInternal({ ...request, calculationDate: '2024-01-01' });
|
|
calibrationMs = Math.min(calibrationMs, performance.now() - start);
|
|
}
|
|
// 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) =>
|
|
print(benchmarkSummaryRow(rows, calibrationMs)),
|
|
);
|
|
for (const [name, placements] of [
|
|
['two years', 60],
|
|
['three years', 90],
|
|
] as const) {
|
|
const longer = measurements.filter((measurement) => measurement.scenario === name);
|
|
expect(longer).toHaveLength(2);
|
|
expect(longer.map((item) => item.outcome)).toEqual(['Plan', 'Plan']);
|
|
expect(longer.map((item) => item.lastCompletedStage)).toEqual(['dates', 'dates']);
|
|
expect(longer.map((item) => item.placements)).toEqual([placements, placements]);
|
|
expect(longer.map((item) => item.deterministic)).toEqual(['baseline', 'yes']);
|
|
}
|
|
const chained = measurements.find(
|
|
(measurement) => measurement.scenario === 'chained earned entitlements',
|
|
);
|
|
const loaded = measurements.filter(
|
|
(measurement) => measurement.scenario === 'loaded year',
|
|
);
|
|
expect(chained?.outcome).toBe('Plan');
|
|
expect(chained?.placements).toBeGreaterThanOrEqual(3);
|
|
expect(loaded).toHaveLength(2);
|
|
expect(loaded[0].outcome).toBe('Plan');
|
|
expect(loaded[0].deterministic).toBe('baseline');
|
|
expect(loaded[1].deterministic).toBe('yes');
|
|
expect(loaded[0].planningMs).toBeGreaterThanOrEqual(0);
|
|
expect(loaded[0].planningWorkMs).toBeGreaterThanOrEqual(0);
|
|
expect(loaded[0].planningWorkMs).toBeLessThanOrEqual(loaded[0].planningMs);
|
|
expect(loaded[0].maxUiDelayMs).toBeGreaterThanOrEqual(0);
|
|
expect(loaded[0].wallMs).toBeGreaterThanOrEqual(
|
|
loaded[0].preparationMs + loaded[0].planningMs,
|
|
);
|
|
const report = benchmarkReport(measurements);
|
|
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');
|
|
expect(report).toContain('\tbaseline\t');
|
|
const violations = budgetViolations(
|
|
measurements,
|
|
calibrationMs,
|
|
BENCHMARK_BUDGET_MULTIPLIERS,
|
|
);
|
|
if (violations.length)
|
|
throw new Error(`Benchmark over budget:\n${violations.join('\n')}`);
|
|
} finally {
|
|
vi.stubGlobal('window', browserWindow);
|
|
}
|
|
}, 570_000);
|
|
});
|