153 lines
6.4 KiB
JavaScript
153 lines
6.4 KiB
JavaScript
// benchmark.mjs -- mesure du moteur WebAssembly sous Node, pour `make bench`
|
|
// (engine-wasm/bench.mjs). Elle servait aussi à la page de mesure du site,
|
|
// retirée avec les pages de test du moteur (ADR 0009, amendement du
|
|
// 2026-11-03).
|
|
//
|
|
// 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 (ADR 0009).
|
|
|
|
// 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 },
|
|
// Étape 1a : l'analyse du coup joué, pour le conseil du maître, calculée
|
|
// en arrière-plan ; même repère que le meilleur coup (ADR 0009).
|
|
{ key: "analysis2", label: "Analyse du coup joué, 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 l'application
|
|
// (hors code de l'application elle-même, quelques dizaines de Ko), chemins
|
|
// relatifs à la racine du site (web/ ; wasm/ est build/wasm/).
|
|
export const DOWNLOADED_FILES = [
|
|
"index.html",
|
|
"app.css",
|
|
"engine-worker.mjs",
|
|
"engine-wasm.mjs",
|
|
"wasm/engine.mjs",
|
|
"wasm/engine.wasm",
|
|
"wasm/engine.data",
|
|
];
|
|
|
|
// engine : objet avec bestMove(positionId, matchId, dice, ply),
|
|
// cubeDecision(positionId, matchId, ply) et analyseMove(positionId, matchId,
|
|
// dice, playedPositionId, ply), qui renvoient (ou promettent) { result, ms }.
|
|
// Le coup analysé est le meilleur coup à 0 pli : celui qu'un joueur aurait
|
|
// pu jouer, analysé à 2 plis comme pour le conseil du maître.
|
|
// 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 = [];
|
|
const played = new Map();
|
|
for (const p of positions) {
|
|
if (p.type === "move") {
|
|
tasks.push(["move0", p, async () => {
|
|
const answer = await engine.bestMove(p.position_id, p.match_id, p.dice, 0);
|
|
// Sans coup possible, n'importe quelle position fait l'affaire.
|
|
played.set(p.id, answer.result.best_move?.position_id ?? p.position_id);
|
|
return answer;
|
|
}]);
|
|
tasks.push(["move2", p, () => engine.bestMove(p.position_id, p.match_id, p.dice, 2)]);
|
|
tasks.push(["analysis2", p, () => engine.analyseMove(p.position_id, p.match_id, p.dice,
|
|
played.get(p.id), 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";
|
|
}
|