Fusion de main dans moteur-analyse ; plan 1a clos
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// bench.mjs -- la mesure de la page bench.html, sous Node, sur le PC
|
||||
// bench.mjs -- mesure du moteur WebAssembly sous Node, sur le PC
|
||||
// (`make bench`). Premier ordre de grandeur seulement : le chiffre qui compte
|
||||
// est celui du téléphone.
|
||||
//
|
||||
@@ -10,7 +10,7 @@ import os from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import createEngineModule from "../build/wasm/engine.mjs";
|
||||
import { DOWNLOADED_FILES, formatReport, measureTimes } from "../web/moteur/benchmark.mjs";
|
||||
import { DOWNLOADED_FILES, formatReport, measureTimes } from "./benchmark.mjs";
|
||||
import { commands, loadEngine } from "../web/engine-wasm.mjs";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// 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";
|
||||
}
|
||||
Reference in New Issue
Block a user