214 lines
9.4 KiB
JavaScript
214 lines
9.4 KiB
JavaScript
|
|
// © 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);
|
|||
|
|
});
|
|||
|
|
});
|