floor-planner/scripts/benchmark-planner.mjs
2026-09-11 22:14:24 +01:00

108 lines
4.7 KiB
JavaScript

import { readFile, writeFile } from 'node:fs/promises';
import { createServer } from 'vite';
const server = await createServer({
server: { middlewareMode: true },
appType: 'custom',
optimizeDeps: { noDiscovery: true, include: [] },
});
try {
const [{ planFlooring }, { planSkirting }, { plannerCases }, metrics] = await Promise.all([
server.ssrLoadModule('/src/lib/flooring.ts'),
server.ssrLoadModule('/src/lib/skirting.ts'),
server.ssrLoadModule('/benchmarks/cases.ts'),
server.ssrLoadModule('/benchmarks/metrics.ts'),
]);
const cases = plannerCases();
const random = process.argv.includes('--random');
if (random) cases.forEach(({ project }) => (project.flooring.optimisation = 'random'));
const results = [];
// Warm up the geometry and planners before taking timings.
planFlooring(cases[0].project.rooms, cases[0].project.flooring, cases[0].project.tolerance);
for (const { name, project: p } of cases) {
const start = performance.now();
const flooring = planFlooring(p.rooms, p.flooring, p.tolerance);
const floorMs = performance.now() - start;
const skirtStart = performance.now();
const skirting = planSkirting(p.rooms, p.skirting, p.tolerance);
results.push({
name,
flooring: metrics.flooringMetrics(flooring, p.flooring),
skirting: metrics.skirtingMetrics(skirting),
floorMs: Math.round(floorMs * 10) / 10,
skirtMs: Math.round((performance.now() - skirtStart) * 10) / 10,
});
}
const baselinePath = new URL('../benchmarks/baseline.json', import.meta.url);
if (process.argv.includes('--write-baseline')) {
await writeFile(baselinePath, JSON.stringify(results, null, 2) + '\n');
console.log(`Recorded ${results.length} baseline cases.`);
} else {
const comparison = process.argv
.find((arg) => arg.startsWith('--compare='))
?.slice('--compare='.length);
const baseline = JSON.parse(await readFile(comparison ?? baselinePath, 'utf8'));
const sum = (rows, key, metric) => rows.reduce((total, row) => total + row[key][metric], 0);
const summary = [];
for (const kind of ['flooring', 'skirting']) {
const compare =
kind === 'flooring'
? (a, b) => metrics.compareFlooring(a, b, random)
: metrics.compareSkirting;
const changes = results.map((result) => {
const previous = baseline.find((row) => row.name === result.name);
if (!previous) throw new Error(`No baseline for ${result.name}`);
return { name: result.name, change: compare(result[kind], previous[kind]) };
});
summary.push({
planner: kind,
improved: changes.filter((row) => row.change < 0).length,
tied: changes.filter((row) => row.change === 0).length,
regressed: changes.filter((row) => row.change > 0).length,
'cuts before': sum(baseline, kind, 'cuts'),
'cuts after': sum(results, kind, 'cuts'),
'boards before': sum(baseline, kind, 'boards'),
'boards after': sum(results, kind, 'boards'),
errors: sum(results, kind, 'errors'),
});
const regressions = changes.filter((row) => row.change > 0).map((row) => row.name);
if (regressions.length) console.log(`${kind} regressions: ${regressions.join(', ')}`);
}
console.table(summary);
for (const kind of ['flooring', 'skirting']) {
const wastePercent = (rows) => {
const purchased = rows.reduce((total, row) => {
const { project } = cases.find((c) => c.name === row.name);
const size =
kind === 'flooring'
? project.flooring.length * project.flooring.width
: project.skirting.stockLength;
return total + row[kind].boards * size;
}, 0);
return ((sum(rows, kind, 'waste') / purchased) * 100).toFixed(2);
};
console.log(
`${kind} waste: ${wastePercent(baseline)}% → ${wastePercent(results)}% (weighted by purchased stock)`,
);
}
console.table(
['shapingCuts', 'ripCuts', 'patternPenalty', 'repeats', 'similarSteps', 'staircases'].map(
(metric) => ({
metric,
before: sum(baseline, 'flooring', metric),
after: sum(results, 'flooring', metric),
}),
),
);
}
for (const key of ['floorMs', 'skirtMs']) {
const times = results.map((row) => row[key]).sort((a, b) => a - b);
console.log(
`${key}: total ${times.reduce((a, b) => a + b, 0).toFixed(1)} ms; p95 ${times[Math.floor(times.length * 0.95)]} ms; max ${times.at(-1)} ms`,
);
}
const output = process.argv.find((arg) => arg.startsWith('--output='))?.slice('--output='.length);
if (output) await writeFile(output, JSON.stringify(results, null, 2) + '\n');
} finally {
await server.close();
}