// benchmark.mjs -- mesure du moteur WebAssembly, partagée par la page de // mesure (bench.html, dans le navigateur) et `make bench` (sous Node). // // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). // // Sur les positions de référence (tests/reference/positions.json) : temps du // meilleur coup à 0 et 2 plis pour les positions avec dés, temps de la // décision de videau à 2 plis pour les autres. Le rapport compare aux seuils // de l'étape 0b (docs/etape-0.md). // Seuils de l'étape 0b. Un temps respecte son seuil si son maximum, sur // toutes les positions, est en dessous (la médiane est donnée en plus). export const TIME_THRESHOLDS = [ { key: "move0", label: "Meilleur coup, 0 pli", limitMs: 300 }, { key: "move2", label: "Meilleur coup, 2 plis", limitMs: 3000 }, { key: "cube2", label: "Décision de videau, 2 plis", limitMs: 3000 }, ]; // Taille totale à télécharger, compressée : moins de 40 Mo. export const DOWNLOAD_LIMIT_BYTES = 40e6; // Fichiers téléchargés pour faire tourner le moteur dans la page, chemins // relatifs à la racine du site (web/ ; wasm/ est build/wasm/). export const DOWNLOADED_FILES = [ "index.html", "style.css", "page.mjs", "engine-client.mjs", "engine-worker.mjs", "engine-wasm.mjs", "wasm/engine.mjs", "wasm/engine.wasm", "wasm/engine.data", ]; // engine : objet avec bestMove(positionId, matchId, dice, ply) et // cubeDecision(positionId, matchId, ply), qui renvoient (ou promettent) // { result, ms }. // positions : le tableau « positions » de positions.json. // onProgress(done, total) : appelé après chaque calcul. // Renvoie, pour chaque clé de TIME_THRESHOLDS, la liste des // { id, ms } mesurés. export async function measureTimes(engine, positions, onProgress = () => {}) { const tasks = []; for (const p of positions) { if (p.type === "move") { tasks.push(["move0", p, () => engine.bestMove(p.position_id, p.match_id, p.dice, 0)]); tasks.push(["move2", p, () => engine.bestMove(p.position_id, p.match_id, p.dice, 2)]); } else { tasks.push(["cube2", p, () => engine.cubeDecision(p.position_id, p.match_id, 2)]); } } const times = Object.fromEntries(TIME_THRESHOLDS.map((t) => [t.key, []])); for (const [i, [key, position, compute]] of tasks.entries()) { const { ms } = await compute(); times[key].push({ id: position.id, ms }); onProgress(i + 1, tasks.length); } return times; } function median(values) { const sorted = [...values].sort((a, b) => a - b); const middle = Math.floor(sorted.length / 2); return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2; } // Nombres à la française : virgule décimale. function decimal(value, digits) { return value.toFixed(digits).replace(".", ","); } function megabytes(bytes) { return decimal(bytes / 1e6, 1) + " Mo"; } function verdict(ok) { return ok ? "OK" : "DÉPASSÉ"; } // Résumé d'une série de temps : { count, medianMs, maxMs, maxId, ok }. export function summarize(times) { return TIME_THRESHOLDS.map((threshold) => { const list = times[threshold.key]; const worst = list.reduce((a, b) => (b.ms > a.ms ? b : a)); return { ...threshold, count: list.length, medianMs: median(list.map((t) => t.ms)), maxMs: worst.ms, maxId: worst.id, ok: worst.ms < threshold.limitMs, }; }); } // Rapport texte, à coller dans la conversation. // environment : description de l'appareil (navigateur ou Node) ; // files : [{ name, bytes, gzipBytes }] ; loadMs : chargement du moteur ; // memoryBytes : mémoire WebAssembly après la mesure ; times : measureTimes. export function formatReport({ environment, files, loadMs, memoryBytes, times }) { const total = files.reduce((sum, f) => sum + f.bytes, 0); const totalGzip = files.reduce((sum, f) => sum + f.gzipBytes, 0); const sizeOk = totalGzip < DOWNLOAD_LIMIT_BYTES; const summary = summarize(times); const allOk = sizeOk && summary.every((s) => s.ok); const lines = [ "Ustagammon : mesure du moteur WebAssembly (étape 0b)", "Date : " + new Date().toISOString(), "Environnement : " + environment, "", "Chargement du moteur : " + Math.round(loadMs) + " ms", "Mémoire WebAssembly après la mesure : " + megabytes(memoryBytes), "Taille téléchargée : " + megabytes(total) + " non compressée, " + megabytes(totalGzip) + " compressée gzip ; seuil " + megabytes(DOWNLOAD_LIMIT_BYTES) + " compressée : " + verdict(sizeOk), "", ]; for (const s of summary) { lines.push(s.label + " (" + s.count + " positions) : médiane " + decimal(s.medianMs, 1) + " ms, maximum " + decimal(s.maxMs, 1) + " ms (" + s.maxId + ") ; seuil " + s.limitMs + " ms : " + verdict(s.ok)); } lines.push("", "Bilan : " + (allOk ? "tous les seuils sont respectés." : "au moins un seuil est dépassé."), "", "Détail des fichiers :"); for (const f of files) { lines.push(" " + f.name + " : " + f.bytes + " octets, " + f.gzipBytes + " compressé gzip"); } lines.push("", "Détail des temps (ms) :"); for (const s of summary) { lines.push(" " + s.label + " : " + times[s.key] .map((t) => t.id + " " + Math.round(t.ms)).join(", ")); } return lines.join("\n") + "\n"; }