Page de test et page de mesure du moteur WebAssembly
web/ : le moteur tourne dans un Web Worker (module ES) ; la page de test donne le meilleur coup d'une position et son temps de calcul ; la page de mesure chronomètre les positions de référence, mesure la taille téléchargée et la mémoire, compare aux seuils de l'étape 0b et produit un rapport à copier. make bench fait la même mesure sous Node, make deploy-web sert les pages sur le réseau local. Guide de test pour le Fairphone 4 dans docs/guide-test-etape-0.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b0030521d8
commit
120367e6d3
@@ -0,0 +1,138 @@
|
||||
// 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";
|
||||
}
|
||||
Reference in New Issue
Block a user