L'application est à la racine du site : plateau en SVG, saisie au toucher (pion puis case d'arrivée, proposées à partir du moteur), boutons du jet, du videau et de nouvelle partie, coup de l'ordinateur affiché avec son temps, fin de partie, reprise depuis localStorage, aide lue dans les copies de docs/. Le moteur tourne dans le Web Worker existant ; main.ts est le seul assembleur (nouvelles règles dependency-cruiser). Le service worker met tout le site en cache, avec une empreinte du contenu comme version. Les pages de test du moteur passent sous /moteur/. Politique de sécurité du contenu dans le Caddyfile ; make test-browser joue une partie dans Chromium et Firefox sans écran (Playwright, hors de make test). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
5.5 KiB
JavaScript
139 lines
5.5 KiB
JavaScript
// 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 de test,
|
|
// chemins relatifs à la racine du site (web/ ; wasm/ est build/wasm/).
|
|
export const DOWNLOADED_FILES = [
|
|
"moteur/index.html",
|
|
"moteur/style.css",
|
|
"moteur/page.mjs",
|
|
"moteur/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";
|
|
}
|