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(); }