gestion_table_tournante_libre/scripts/banc/parallele.banc.js
Mathieu Benoit 3e4fd03a4c [IMP] generation: duration estimate counts the slowdown of busy cores
A core runs slower when its neighbours work too, so the estimate that
divided the run by the number of workers promised too short a wait. The
estimate now multiplies by rho(W), linear from 1 on one core to the
registry constant RALENTISSEMENT_PLEINE_CHARGE when every logical
processor works; the cores setting itself is unchanged. A bench file
measures the curve. An out-of-range setting is refused before
AUCUN_EVENEMENT.
Checked: red first; 3582 node and 659 browser tests green from the index.

--- FR ---

[IMP] génération : l'estimation compte le ralentissement des cœurs

Un cœur va moins vite quand ses voisins travaillent aussi : l'estimation
qui divisait le calcul par le nombre de travailleurs promettait une attente
trop courte. Elle multiplie désormais par rho(W), linéaire de 1 sur un cœur
à la constante RALENTISSEMENT_PLEINE_CHARGE du registre quand tous les
processeurs logiques travaillent ; le réglage des cœurs ne change pas. Un
fichier du banc mesure la courbe. Un réglage hors domaine est refusé avant
AUCUN_EVENEMENT.
Vérifié : rouge d'abord ; 3582 node et 659 navigateur verts depuis l'index.

Assisted-by: Claude Opus 5.5
2026-10-09 15:28:49 -04:00

213 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le gain parallèle de la génération (§ 5.10, Q14) : la durée t(W) d'une
// génération de W propositions sur W travailleurs, une seule vague, par
// l'exécuteur de production — creerExecuteurTravailleur et le travailleur
// inclus dans la page, celui que src/main.js construit —, dans Chromium. Le
// poste annonce L processeurs logiques (navigator.hardwareConcurrency, par
// coeursDe) ; W parcourt 1, 2, 4, … jusqu'à L, et L lui-même. Le
// ralentissement ρ(W) = t(W) / t(1) d'un cœur chargé en sort, et ρ(L) est la
// valeur proposée pour RALENTISSEMENT_PLEINE_CHARGE : le banc l'écrit dans
// sa sortie, jamais dans le registre (décision 6).
//
// Chaque génération part de la grande démonstration, graine 1, arret
// 200 000 : la configuration et le compte que l'opérateur emploie. Les
// répétitions alternent les W — une ronde passe chaque W une fois —, de
// sorte qu'une dérive de la machine pèse sur tous ; t(W) est la médiane de
// ses répétitions. Un ralentissement ne décroît pas quand W croît : des
// médianes qui décroissent disent une mesure bruitée, qui gagne une ronde
// de plus et se signale « bruite » dans la sortie. La durée d'une génération
// d'un travailleur neuf compte sa naissance : c'est celle que l'opérateur
// attend.
//
// La sortie, scripts/banc/sorties/parallele.json, a la forme d'un relevé
// (releve.js) que verifierReleve accepte : la configuration ne nomme pas
// la machine, seulement ses processeurs logiques. Elle porte la courbe
// t(W), ρ(W) et le ρ du modèle de l'estimation à la même pleine charge, le
// ρ proposé, et la durée de la génération par défaut sur min(5, L) cœurs,
// qui situe le « bien en deçà » du plafond d'usage (§ 5.10).
//
// Une horloge réelle se lit ici, performance.now, pour mesurer seulement :
// la sortie ne porte que des durées.
import { beforeAll, describe, expect, test } from 'vitest';
import { commands } from 'vitest/browser';
import { creerExecuteurTravailleur } from '../../src/application/executeur_travailleur.js';
import { coeursDe, ralentissement } from '../../src/application/generation.js';
import { texteLivre } from '../../src/demo/livrees.js';
import Travailleur from '../../src/interface/generation/travailleur.js?worker&inline';
import { GENERATION_PAR_DEFAUT, analyser, configurationDepuisCharge } from '../../src/stockage/document.js';
import { controlerCadre, ecrireSortie, ralentirCpu } from './navigateur.js';
import { verifierReleve } from './releve.js';
// Le compte d'arrêt et la graine de chaque génération mesurée.
const ARRET = 200_000;
const GRAINE = 1;
// Les rondes de mesure, et la ronde de plus d'une mesure bruitée.
const REPETITIONS = 3;
const CONFIGURATION = configurationDepuisCharge(analyser(texteLivre('grande')).charge);
// Les processeurs logiques du poste, tels que l'exécuteur de production les lit.
const L = coeursDe(navigator.hardwareConcurrency);
let cadre = null;
// Par W, les durées de ses répétitions, en ms, dans l'ordre des rondes.
const durees = new Map();
let bruite = false;
let parDefaut = null;
/**
* Les nombres de cœurs mesurés : les puissances de 2 sous L, puis L.
* @param {number} l
* @returns {number[]}
*/
function coeursMesures(l) {
const liste = [];
for (let w = 1; w < l; w *= 2) liste.push(w);
liste.push(l);
return liste;
}
const mediane = (valeurs) => {
const triees = [...valeurs].sort((a, b) => a - b);
const milieu = Math.floor(triees.length / 2);
return triees.length % 2 === 1 ? triees[milieu] : (triees[milieu - 1] + triees[milieu]) / 2;
};
// Vrai quand chaque valeur est au moins la précédente.
const nonDecroissante = (valeurs) => valeurs.every((valeur, rang) => rang === 0 || valeur >= valeurs[rang - 1]);
// Une durée au dixième de milliseconde, un rapport au millième.
const dixieme = (ms) => Math.round(ms * 10) / 10;
const millieme = (rapport) => Math.round(rapport * 1000) / 1000;
/**
* La durée murale d'une génération de reglages sur coeurs travailleurs
* neufs, en ms, de lancer à la dernière proposition rendue. Lève quand
* l'exécution n'emploie pas coeurs travailleurs ou ne rend pas une
* proposition par rang : la mesure ne serait pas celle de W.
*
* @param {number} coeurs
* @param {{graine: number, nombre: number, arret: number, historique: number}} reglages
* @returns {Promise<number>}
*/
async function dureeGeneration(coeurs, reglages) {
const executeur = creerExecuteurTravailleur(() => new Travailleur(), { coeursDisponibles: L });
const debut = performance.now();
const execution = executeur.lancer(CONFIGURATION, reglages, { coeurs });
const propositions = await execution.resultat;
const duree = performance.now() - debut;
if (execution.coeurs !== coeurs) throw new Error(`${execution.coeurs} travailleurs employés, ${coeurs} attendus`);
if (propositions.length !== reglages.nombre) {
throw new Error(`${propositions.length} propositions rendues, ${reglages.nombre} attendues`);
}
return duree;
}
const reglagesSur = (nombre) => ({ graine: GRAINE, nombre, arret: ARRET, historique: GENERATION_PAR_DEFAUT.historique });
// Une ronde : chaque W une fois, dans l'ordre croissant.
async function ronde() {
for (const w of coeursMesures(L)) durees.get(w).push(await dureeGeneration(w, reglagesSur(w)));
}
const medianes = () => coeursMesures(L).map((w) => mediane(durees.get(w)));
beforeAll(async () => {
cadre = await controlerCadre();
await ralentirCpu(1);
for (const w of coeursMesures(L)) durees.set(w, []);
// Une génération d'échauffement, hors mesure : le premier travailleur
// compile son module.
await dureeGeneration(1, reglagesSur(1));
});
describe('banc : le gain parallèle de la génération (§ 5.10, Q14)', () => {
test('les cœurs mesurés : les puissances de 2 sous L, puis L ; un seul sur un seul processeur', () => {
expect(coeursMesures(1)).toEqual([1]);
expect(coeursMesures(2)).toEqual([1, 2]);
expect(coeursMesures(6)).toEqual([1, 2, 4, 6]);
expect(coeursMesures(16)).toEqual([1, 2, 4, 8, 16]);
});
test('une mesure bruitée se reconnaît : des médianes qui décroissent', () => {
expect(nonDecroissante([300, 300, 310, 420])).toBe(true);
expect(nonDecroissante([300, 290, 310])).toBe(false);
expect(mediane([5, 1, 3])).toBe(3);
expect(mediane([4, 1, 3, 2])).toBe(2.5);
});
test(`t(W) pour W de 1 à L, ${REPETITIONS} répétitions, une de plus si bruité : ρ(1) = 1, ρ(W) = t(W) / t(1)`, async () => {
for (let rang = 0; rang < REPETITIONS; rang += 1) await ronde();
if (!nonDecroissante(medianes())) {
bruite = true;
await ronde();
}
const t = medianes();
expect(t.every((ms) => Number.isFinite(ms) && ms > 0)).toBe(true);
for (const w of coeursMesures(L)) expect(durees.get(w).length).toBe(bruite ? REPETITIONS + 1 : REPETITIONS);
});
test('la génération par défaut, sur min(nombre, L) cœurs : sa durée médiane', async () => {
const coeurs = Math.min(GENERATION_PAR_DEFAUT.nombre, L);
const reglages = { ...GENERATION_PAR_DEFAUT, graine: GRAINE };
const releves = [];
for (let rang = 0; rang < REPETITIONS; rang += 1) releves.push(await dureeGeneration(coeurs, reglages));
parDefaut = { coeurs, dureesMs: releves.map(dixieme), medianeMs: dixieme(mediane(releves)) };
expect(parDefaut.medianeMs).toBeGreaterThan(0);
});
test('parallele.json : un relevé que verifierReleve accepte, écrit dans scripts/banc/sorties et relu tel quel', async () => {
expect(parDefaut).not.toBeNull();
const t = medianes();
const rhoPleineCharge = t.at(-1) / t[0];
const courbe = coeursMesures(L).map((w, rang) => ({
coeurs: w,
dureesMs: durees.get(w).map(dixieme),
medianeMs: dixieme(t[rang]),
ralentissement: millieme(t[rang] / t[0]),
modele: millieme(ralentissement(w, L, rhoPleineCharge)),
}));
const sortie = {
format: 1,
configuration: {
cadre: { largeur: cadre.largeur, hauteur: cadre.hauteur },
facteurEchelle: cadre.facteurEchelle,
navigateur: null,
node: null,
processeursLogiques: L,
ralentissementCpu: 1,
police: null,
},
mesures: [
{
nom: 'RALENTISSEMENT_PLEINE_CHARGE',
unite: '×',
valeurProposee: Math.round(rhoPleineCharge * 100) / 100,
critere:
`t(L) / t(1), médianes de ${REPETITIONS} répétitions ou plus : une génération de W propositions ` +
`sur W travailleurs, grande démonstration, arret ${ARRET}, graine ${GRAINE}`,
courbe,
bruite,
},
{
nom: 'DUREE_GENERATION_PAR_DEFAUT',
unite: 'ms',
valeurProposee: parDefaut.medianeMs,
critere:
`médiane de ${REPETITIONS} générations au réglage par défaut, ${GENERATION_PAR_DEFAUT.nombre} × ` +
`${GENERATION_PAR_DEFAUT.arret} mouvements sur ${parDefaut.coeurs} travailleurs, grande démonstration`,
courbe: [parDefaut],
},
],
parallele: courbe,
};
expect(courbe[0]).toMatchObject({ coeurs: 1, ralentissement: 1, modele: 1 });
expect(courbe.at(-1)).toMatchObject({ coeurs: L, modele: millieme(rhoPleineCharge) });
expect(verifierReleve(sortie)).toEqual([]);
await ecrireSortie('parallele', sortie);
expect(JSON.parse(await commands.readFile('scripts/banc/sorties/parallele.json'))).toEqual(sortie);
});
});