diff --git a/scripts/banc/parallele.banc.js b/scripts/banc/parallele.banc.js new file mode 100644 index 0000000..f4fdc5a --- /dev/null +++ b/scripts/banc/parallele.banc.js @@ -0,0 +1,213 @@ +// © 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} + */ +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); + }); +}); diff --git a/src/application/generation.js b/src/application/generation.js index 1900fbf..43c69b5 100644 --- a/src/application/generation.js +++ b/src/application/generation.js @@ -19,7 +19,10 @@ // de processeurs logiques (coeursDisponibles, que src/main.js lit de la // plateforme) ; sinon le nombre saisi, de 1 à ces disponibles. Un lancement // en emploie min(nombre de propositions, cœurs réglés), et l'estimation -// compte en vagues de propositions calculées ensemble. +// compte en vagues de propositions calculées ensemble, chacune ralentie de +// ρ(W) : un cœur va moins vite quand ses voisins travaillent aussi. ρ croît +// de 1, sur un cœur seul, à RALENTISSEMENT_PLEINE_CHARGE du registre des +// constantes, quand les L processeurs logiques travaillent (ralentissement). // // Le contrôleur ne numérote rien et ne mesure aucun plan. Il refuse avant // tout lancement un réglage hors de son domaine (REGLAGE_HORS_DOMAINE), puis @@ -36,6 +39,7 @@ // génération (debuterCalcul), et repart à sa fin, quelle qu'elle soit // (finirCalcul, dans un finally). Passer en lecture, ou fermer l'événement, // annule la génération en cours (surAnnulation). +import { constante } from '../geometrie/constantes.js'; import { normaliser, nombrePlacesManquantes } from '../moteur/configuration.js'; import { ErreurAnnulee, ErreurConfiguration } from '../moteur/erreurs.js'; import { rechercher, verifierReglages } from '../moteur/recherche.js'; @@ -54,9 +58,15 @@ export const SEUIL_GENERATION_LONGUE_S = 60; export const PLAFOND_USAGE_S = 30 * 60; // Sans durée mesurée, une génération de plus de mouvements que ce compte — -// dix fois le réglage par défaut — demande confirmation. +// dix fois le réglage par défaut —, ralentissement compris, demande +// confirmation. const MOUVEMENTS_SANS_MESURE = 10 * GENERATION_PAR_DEFAUT.nombre * GENERATION_PAR_DEFAUT.arret; +// Le ralentissement d'un cœur quand tous les processeurs logiques +// travaillent, nommé dans le registre des constantes mesurées (§ 14.1) : un +// nom de constante, que la table des libellés ne connaît pas. +const RALENTISSEMENT = Object.freeze({ nom: 'RALENTISSEMENT_PLEINE_CHARGE' }); + // La plus grande graine, et le modulo de la suivante. const GRAINE_MAX = 2 ** 32 - 1; const GRAINES = 2 ** 32; @@ -124,6 +134,23 @@ export function coeursDe(valeur) { return Number.isInteger(valeur) && valeur >= 1 ? valeur : 1; } +/** + * Le ralentissement d'un cœur quand W cœurs travaillent ensemble sur un + * poste de L processeurs logiques : le rapport de la durée d'une même + * proposition sur l'un d'eux à sa durée sur un cœur seul. Il croît en ligne + * droite de 1, à W = 1, à rhoMax, à W = L : 1 + (rhoMax − 1)(W − 1)/(L − 1). + * Un poste d'un seul processeur logique n'a aucun ralentissement : 1. + * + * @param {number} W les cœurs employés, de 1 à L + * @param {number} L les processeurs logiques du poste, au moins 1 + * @param {number} rhoMax le ralentissement à pleine charge, W = L + * @returns {number} + */ +export function ralentissement(W, L, rhoMax) { + if (L === 1) return 1; + return 1 + ((rhoMax - 1) * (W - 1)) / (L - 1); +} + /** * L'erreur d'une recherche, la même dans les deux exécuteurs : ErreurAnnulee * telle quelle ; une ErreurConfiguration devient ErreurGeneration de son code @@ -226,11 +253,15 @@ function planRange(proposition, configuration, produitVersion) { * @param {import('./horloge.js').Horloge} parametres.horloge instant() mesure * la durée d'un calcul * @param {string} parametres.produitVersion + * @param {(nom: string) => number|null} [parametres.constante] la lecture + * du registre des constantes, constante de constantes.js par défaut ; + * le contrôleur y lit RALENTISSEMENT_PLEINE_CHARGE à chaque + * estimation, sans en garder la valeur * @returns {Object} le contrôleur : grainePropose, reglagesProposes, valider, * estimer, derniereGeneration, generer, annuler, coeurs, * reglerCoeurs, subscribe */ -export function creerControleurGeneration({ seance, executeur, horloge, produitVersion }) { +export function creerControleurGeneration({ seance, executeur, horloge, produitVersion, constante: lireConstante = constante }) { // graine : la graine proposée ; base : celle de l'événement ouvert, null // sans événement ; mesure : le dernier calcul abouti, null avant lui ; // execution : celle en cours, null hors génération ; enCours vaut vrai du @@ -284,14 +315,21 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV // ensemble. const mouvementsParCoeur = (nombre, arret, parallele) => Math.ceil(nombre / parallele) * arret; - // La durée d'une génération, proportionnelle aux mouvements qui se suivent - // sur un même cœur, ceux de la mesure ramenés aux cœurs qu'elle a employés. + // Le coût d'une génération sur parallele cœurs, en mouvements d'un cœur + // seul : ceux qui se suivent sur un même cœur, ralentis de ρ(parallele) + // sur les processeurs logiques du poste. ρ à pleine charge se lit au + // registre à chaque appel. + const cout = (nombre, arret, parallele) => + mouvementsParCoeur(nombre, arret, parallele) * ralentissement(parallele, disponibles, lireConstante(RALENTISSEMENT.nom)); + + // La durée d'une génération, proportionnelle à son coût, celui de la + // mesure ramené aux cœurs qu'elle a employés : à cœurs égaux, le + // ralentissement se simplifie. function estimer({ nombre, arret }) { if (mesure === null) return { inconnue: true }; const parallele = coeursPour(nombre); const secondes = - (mouvementsParCoeur(nombre, arret, parallele) * mesure.dureeMs) / - (mouvementsParCoeur(mesure.nombre, mesure.arret, mesure.coeurs) * 1000); + (cout(nombre, arret, parallele) * mesure.dureeMs) / (cout(mesure.nombre, mesure.arret, mesure.coeurs) * 1000); return { secondes, longue: secondes > SEUIL_GENERATION_LONGUE_S, @@ -301,11 +339,12 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV }; } - // Longue : au-delà du seuil estimé, ou, sans mesure, quand les mouvements - // qui se suivent sur un même cœur dépassent dix fois le réglage par défaut. + // Longue : au-delà du seuil estimé, ou, sans mesure, quand le coût — les + // mouvements qui se suivent sur un même cœur, ralentis de ρ — dépasse dix + // fois le réglage par défaut. function estLongue(reglages, estimation) { if (estimation.inconnue === true) { - return mouvementsParCoeur(reglages.nombre, reglages.arret, coeursPour(reglages.nombre)) > MOUVEMENTS_SANS_MESURE; + return cout(reglages.nombre, reglages.arret, coeursPour(reglages.nombre)) > MOUVEMENTS_SANS_MESURE; } return estimation.longue; } @@ -466,8 +505,9 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV }, /** * L'estimation des réglages, proportionnelle aux mouvements qui se - * suivent sur un même cœur — arret × ⌈nombre / cœurs⌉ —, ceux de la - * dernière mesure ramenés de même aux cœurs qu'elle a employés : + * suivent sur un même cœur — arret × ⌈nombre / cœurs⌉ — ralentis de + * ρ(cœurs) (ralentissement), ceux de la dernière mesure ramenés de même + * aux cœurs qu'elle a employés : * { inconnue: true } sans elle, sinon { secondes, longue, * auDelaDuPlafond, mesure, coeurs }, coeurs étant ceux que ces réglages * emploieraient. diff --git a/src/application/generation.test.js b/src/application/generation.test.js index 3f9de85..959f5b2 100644 --- a/src/application/generation.test.js +++ b/src/application/generation.test.js @@ -12,6 +12,7 @@ import assert from 'node:assert/strict'; import { creerFichiersSimules } from '../../test/fichiers_simules.js'; import { describe, test } from '../../test/lanceur.js'; import { texteLivre } from '../demo/livrees.js'; +import { constante } from '../geometrie/constantes.js'; import { normaliser } from '../moteur/configuration.js'; import { ErreurAnnulee as ErreurAnnuleeDuMoteur, ErreurConfiguration } from '../moteur/erreurs.js'; import { rechercher, regenerer } from '../moteur/recherche.js'; @@ -33,6 +34,7 @@ import { creerControleurGeneration, coeursDe, creerExecuteurDirect, + ralentissement, versErreurGeneration, } from './generation.js'; import { brancherTravailleur } from './generation_travailleur.js'; @@ -173,8 +175,17 @@ async function deposer(fs, charge) { } // Une séance sur les charges données, la première ouverte — en écriture -// sauf ecriture faux — et un contrôleur sur un exécuteur espionné. -async function monter({ charges, executeur = creerExecuteurDirect(), ecriture = true, fs = creerFichiersSimules(), disque = true } = {}) { +// sauf ecriture faux — et un contrôleur sur un exécuteur espionné ; +// lireConstante, quand il est donné, remplace la lecture du registre des +// constantes par le contrôleur. +async function monter({ + charges, + executeur = creerExecuteurDirect(), + ecriture = true, + fs = creerFichiersSimules(), + disque = true, + lireConstante, +} = {}) { fs.deposerDossier('documents'); if (disque) for (const charge of charges) await deposer(fs, charge); const horloge = creerHorlogePilotee(); @@ -192,7 +203,13 @@ async function monter({ charges, executeur = creerExecuteurDirect(), ecriture = annuler: horloge.annuler, }); const espion = espionner(executeur); - const controleur = creerControleurGeneration({ seance, executeur: espion, horloge, produitVersion: V1 }); + const controleur = creerControleurGeneration({ + seance, + executeur: espion, + horloge, + produitVersion: V1, + ...(lireConstante === undefined ? {} : { constante: lireConstante }), + }); const base = charges[0].evenement.nom; await seance.ouvrir(base); if (ecriture) await seance.passerEnEcriture(); @@ -237,6 +254,14 @@ const ids = (charge) => charge.propositions.map(({ id }) => id); const COURT = { graine: 1, nombre: 1, arret: 50, historique: 1000 }; // Le réglage des cœurs d'un contrôleur dont l'exécuteur n'en annonce pas. const UN_COEUR = Object.freeze({ automatique: true, manuel: 1, disponibles: 1, nombre: 1 }); +// Le ralentissement d'un cœur quand tous les processeurs logiques +// travaillent, tel que le registre le porte (§ 5.10, Q14). +const RHO = constante('RALENTISSEMENT_PLEINE_CHARGE'); +// Une lecture du registre où la constante du ralentissement vaut rho, les +// autres celles du registre ; un objet { rho } se lit à chaque appel, et +// l'épreuve le change en cours de route. +const lireAvec = (rho) => (nom) => + nom === 'RALENTISSEMENT_PLEINE_CHARGE' ? (typeof rho === 'number' ? rho : rho.rho) : constante(nom); // --- valider (§ 5.7) ------------------------------------------------------------ @@ -458,6 +483,10 @@ describe("génération : les refus d'avant lancement (§ 2.1, § 5.9, § 8.4, § // En lecture, le réglage d'abord. const lecture = await monter({ charges: [chargeDemo('petite')], ecriture: false }); await refuseSansRien(lecture, 'REGLAGE_HORS_DOMAINE', { reglage: 'nombre', remede: null }, { ...COURT, nombre: 0 }); + // Sans événement, le réglage encore d'abord : AUCUN_EVENEMENT vient après lui. + await lecture.seance.fermer(); + await refuseSansRien(lecture, 'REGLAGE_HORS_DOMAINE', { reglage: 'arret', remede: null }, { ...COURT, arret: 0 }); + await refuseSansRien(lecture, 'AUCUN_EVENEMENT', { remede: null }); }); test('une génération en cours sur un plan passé en lecture : MODE_LECTURE, la séance avant GENERATION_EN_COURS', async () => { @@ -1228,55 +1257,58 @@ describe('génération : les cœurs de calcul, un réglage du poste pour la séa assert.equal(autre.controleur.derniereGeneration().coeurs, 1); }); - test("l'estimation compte en vagues : coût d'une proposition × ⌈nombre / cœurs⌉, la mesure ramenée à ses propres vagues", async () => { + test("l'estimation compte en vagues, chacune au ralentissement de ses cœurs : coût d'une proposition × ⌈nombre / cœurs⌉ × ρ(cœurs), la mesure ramenée de même", async () => { const manuel = executeurManuel({ coeursDisponibles: 8 }); const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel }); const { controleur } = montage; + // ρ(W) sur 8 processeurs logiques, ρ(8) étant la valeur du registre. + const rho = (w) => 1 + ((RHO - 1) * (w - 1)) / 7; + const proche = (valeur, attendue) => assert.ok(Math.abs(valeur - attendue) < 1e-9, `${valeur} ≠ ${attendue}`); controleur.reglerCoeurs({ automatique: false }); controleur.reglerCoeurs({ saisie: '1' }); - // 5 × 200 000 mouvements en 2 s sur 1 cœur : 0,4 s par proposition. + // 5 × 200 000 mouvements en 2 s sur 1 cœur, ρ(1) = 1 : 0,4 s par proposition. await genererEn(montage, manuel, { graine: 1, nombre: 5, arret: 200000, historique: 1000 }, 2000); const sur1 = controleur.derniereGeneration(); assert.equal(sur1.coeurs, 1); controleur.reglerCoeurs({ saisie: '2' }); const deux = controleur.estimer({ graine: 1, nombre: 5, arret: 200000, historique: 1000 }); assert.equal(deux.coeurs, 2); - assert.ok(Math.abs(deux.secondes - 1.2) < 1e-9, String(deux.secondes)); + proche(deux.secondes, 0.4 * 3 * rho(2)); assert.deepEqual(deux.mesure, sur1); - assert.ok(Math.abs(controleur.estimer({ graine: 1, nombre: 4, arret: 200000 }).secondes - 0.8) < 1e-9); + proche(controleur.estimer({ graine: 1, nombre: 4, arret: 200000 }).secondes, 0.4 * 2 * rho(2)); controleur.reglerCoeurs({ automatique: true }); const automatique = controleur.estimer({ graine: 1, nombre: 5, arret: 200000 }); assert.equal(automatique.coeurs, 5); - assert.ok(Math.abs(automatique.secondes - 0.4) < 1e-9, String(automatique.secondes)); - // Longue au-delà de 60 s : 3 000 propositions, 1 200 s sur 1 cœur, 300 s - // sur 4, et 0,4 × ⌈3 000 / 8⌉ = 150 s sur 8. + proche(automatique.secondes, 0.4 * rho(5)); + // 3 000 propositions : 1 200 s sur 1 cœur ; 750 vagues sur 4, 375 sur 8. controleur.reglerCoeurs({ automatique: false }); controleur.reglerCoeurs({ saisie: '4' }); const quatre = controleur.estimer({ graine: 1, nombre: 3000, arret: 200000 }); - assert.ok(Math.abs(quatre.secondes - 300) < 1e-9, String(quatre.secondes)); + proche(quatre.secondes, 0.4 * 750 * rho(4)); assert.equal(quatre.longue, true); controleur.reglerCoeurs({ automatique: true }); - assert.ok(Math.abs(controleur.estimer({ graine: 1, nombre: 3000, arret: 200000 }).secondes - 150) < 1e-9); + proche(controleur.estimer({ graine: 1, nombre: 3000, arret: 200000 }).secondes, 0.4 * 375 * RHO); // Une mesure prise sur 5 cœurs, 5 propositions : une seule vague de - // 200 000 mouvements en 2 s ; sur 1 cœur, 5 vagues, 10 s. + // 200 000 mouvements à ρ(5) en 2 s ; sur 1 cœur, 5 vagues à ρ(1). await genererEn(montage, manuel, { graine: 1, nombre: 5, arret: 200000, historique: 1000 }, 2000); assert.equal(controleur.derniereGeneration().coeurs, 5); controleur.reglerCoeurs({ automatique: false }); controleur.reglerCoeurs({ saisie: '1' }); - assert.ok(Math.abs(controleur.estimer({ graine: 1, nombre: 5, arret: 200000 }).secondes - 10) < 1e-9); + proche(controleur.estimer({ graine: 1, nombre: 5, arret: 200000 }).secondes, (2 * 5) / rho(5)); }); - test('sans mesure, la question part au-delà de 10 000 000 mouvements par cœur : ⌈nombre / cœurs⌉ × arret', async () => { + test('sans mesure, la question part au-delà de 10 000 000 mouvements par cœur, ralentissement compris : ⌈nombre / cœurs⌉ × arret × ρ(cœurs)', async () => { + // 2 processeurs logiques, ρ(2) = ρ à pleine charge, que l'épreuve fixe à 2. const manuel = executeurManuel({ coeursDisponibles: 2 }); - const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel }); + const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel, lireConstante: lireAvec(2) }); const { controleur, espion } = montage; await refuseSansRien(montage, 'CONFIRMATION_REQUISE', { estimation: { inconnue: true }, remede: null }, { graine: 1, nombre: 10, - arret: 2000001, + arret: 1000001, historique: 1000, }); - const juste = controleur.generer({ graine: 1, nombre: 10, arret: 2000000, historique: 1000 }); + const juste = controleur.generer({ graine: 1, nombre: 10, arret: 1000000, historique: 1000 }); await Promise.resolve(); assert.equal(espion.lancements.length, 1); controleur.annuler(); @@ -1326,6 +1358,120 @@ describe('génération : les cœurs de calcul, un réglage du poste pour la séa }); }); +// --- Le ralentissement d'un cœur chargé (§ 5.10, Q14) ----------------------------------------- + +describe("génération : l'estimation compte le ralentissement d'un cœur chargé (§ 5.10, Q14)", () => { + const proche = (valeur, attendue, message) => + assert.ok(Math.abs(valeur - attendue) < 1e-9, `${message ?? ''} ${valeur} ≠ ${attendue}`); + const DEFAUT = { graine: 1, nombre: 5, arret: 200000, historique: 1000 }; + + test('ralentissement(W, L, ρmax) : 1 + (ρmax − 1)(W − 1)/(L − 1), 1 sur un seul processeur logique', () => { + const cas = [ + [1, 16, 2, 1], + [16, 16, 2, 2], + [2, 8, 2, 1 + 1 / 7], + [5, 5, 2, 2], + [3, 5, 1.5, 1.25], + [4, 7, 3, 2], + [1, 1, 3, 1], + [1, 1, 1, 1], + ]; + assert.equal(cas.length, 8); + for (const [w, l, rhoMax, attendu] of cas) proche(ralentissement(w, l, rhoMax), attendu, `ρ(${w}, ${l}, ${rhoMax})`); + }); + + test("même nombre de cœurs à la mesure et à l'estimation : la durée sans ralentissement, au flottant près — au défaut, 5 propositions sur 5 cœurs", async () => { + const manuel = executeurManuel({ coeursDisponibles: 5 }); + const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel, lireConstante: lireAvec(3) }); + const { controleur } = montage; + await genererEn(montage, manuel, DEFAUT, 2000); + assert.equal(controleur.derniereGeneration().coeurs, 5); + const memes = controleur.estimer(DEFAUT); + assert.equal(memes.coeurs, 5); + proche(memes.secondes, 2); + // Deux vagues sur les mêmes cinq cœurs : le double. + proche(controleur.estimer({ ...DEFAUT, nombre: 10 }).secondes, 4); + proche(controleur.estimer({ ...DEFAUT, nombre: 6, arret: 50000 }).secondes, 1); + }); + + test("plus de cœurs à l'estimation qu'à la mesure : la durée multipliée par ρ(W') / ρ(Wm) > 1 ; moins, divisée", async () => { + const manuel = executeurManuel({ coeursDisponibles: 8 }); + const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel, lireConstante: lireAvec(2) }); + const { controleur } = montage; + controleur.reglerCoeurs({ automatique: false }); + controleur.reglerCoeurs({ saisie: '1' }); + await genererEn(montage, manuel, { ...DEFAUT, nombre: 4 }, 1600); + // 0,4 s par proposition sur 1 cœur ; une vague de 4 sur 4 cœurs, ρ(4) = 1 + 3/7. + controleur.reglerCoeurs({ saisie: '4' }); + const quatre = controleur.estimer({ ...DEFAUT, nombre: 4 }); + assert.equal(quatre.coeurs, 4); + proche(quatre.secondes, 0.4 * (10 / 7)); + assert.ok(quatre.secondes > 0.4); + // Mesurée sur 8 cœurs, ρ(8) = 2 : estimée sur 1, chaque vague deux fois plus rapide. + controleur.reglerCoeurs({ saisie: '8' }); + await genererEn(montage, manuel, { ...DEFAUT, nombre: 8 }, 2000); + assert.equal(controleur.derniereGeneration().coeurs, 8); + controleur.reglerCoeurs({ saisie: '1' }); + proche(controleur.estimer({ ...DEFAUT, nombre: 8 }).secondes, (8 * 2) / 2); + }); + + test("un seul processeur logique : aucun facteur, quel que soit ρ à pleine charge", async () => { + const manuel = executeurManuel(); + const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel, lireConstante: lireAvec(5) }); + const { controleur } = montage; + await genererEn(montage, manuel, DEFAUT, 2000); + proche(controleur.estimer({ ...DEFAUT, nombre: 3000 }).secondes, 1200); + proche(controleur.estimer({ ...DEFAUT, nombre: 7, arret: 100000 }).secondes, 1.4); + // Sans mesure, le seuil est celui d'avant : 10 000 000 mouvements. + const neuf = await monter({ charges: [chargeDemo('petite-conflit')], executeur: executeurManuel(), lireConstante: lireAvec(5) }); + await refuseSansRien(neuf, 'CONFIRMATION_REQUISE', { estimation: { inconnue: true }, remede: null }, { ...DEFAUT, arret: 2000001 }); + const juste = neuf.controleur.generer({ ...DEFAUT, arret: 2000000 }); + await Promise.resolve(); + assert.equal(neuf.espion.lancements.length, 1); + neuf.controleur.annuler(); + await rejet(juste); + }); + + test('sans mesure, le seuil compte ρ : la question part plus tôt quand ρ à pleine charge grandit', async () => { + // 4 processeurs logiques, 4 propositions : une vague, ρ(4) = ρ à pleine charge. + const reglages = (arret) => ({ graine: 1, nombre: 4, arret, historique: 1000 }); + const sous = await monter({ charges: [chargeDemo('petite-conflit')], executeur: executeurManuel({ coeursDisponibles: 4 }), lireConstante: lireAvec(4) }); + await refuseSansRien(sous, 'CONFIRMATION_REQUISE', { estimation: { inconnue: true }, remede: null }, reglages(2500001)); + const juste = sous.controleur.generer(reglages(2500000)); + await Promise.resolve(); + assert.equal(sous.espion.lancements.length, 1); + sous.controleur.annuler(); + await rejet(juste); + }); + + test("ρ se lit au registre à chaque estimation, jamais recopié : la valeur changée par l'épreuve change l'estimation", async () => { + // Le contrôleur sans lecture injectée lit le registre lui-même. + const parDefaut = executeurManuel({ coeursDisponibles: 4 }); + const lu = await monter({ charges: [chargeDemo('petite-conflit')], executeur: parDefaut }); + lu.controleur.reglerCoeurs({ automatique: false }); + lu.controleur.reglerCoeurs({ saisie: '1' }); + await genererEn(lu, parDefaut, { ...DEFAUT, nombre: 4 }, 1600); + lu.controleur.reglerCoeurs({ automatique: true }); + proche(lu.controleur.estimer({ ...DEFAUT, nombre: 4 }).secondes, 0.4 * RHO); + + // Une lecture dont l'épreuve change la valeur entre deux estimations. + const valeur = { rho: 2 }; + const manuel = executeurManuel({ coeursDisponibles: 4 }); + const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel, lireConstante: lireAvec(valeur) }); + const { controleur } = montage; + controleur.reglerCoeurs({ automatique: false }); + controleur.reglerCoeurs({ saisie: '1' }); + await genererEn(montage, manuel, { ...DEFAUT, nombre: 4 }, 1600); + controleur.reglerCoeurs({ automatique: true }); + proche(controleur.estimer({ ...DEFAUT, nombre: 4 }).secondes, 0.8); + valeur.rho = 3.5; + proche(controleur.estimer({ ...DEFAUT, nombre: 4 }).secondes, 1.4); + assert.equal(controleur.estimer({ ...DEFAUT, nombre: 4 }).longue, false); + valeur.rho = 1; + proche(controleur.estimer({ ...DEFAUT, nombre: 4 }).secondes, 0.4); + }); +}); + // --- Une table, un tour (§ 15.4) --------------------------------------------------------- describe('génération : une table, un tour (§ 15.4)', () => { diff --git a/src/interface/generation/Generation.navigateur.test.js b/src/interface/generation/Generation.navigateur.test.js index bfe64b8..28398b5 100644 --- a/src/interface/generation/Generation.navigateur.test.js +++ b/src/interface/generation/Generation.navigateur.test.js @@ -29,6 +29,7 @@ import { creerExecuteurTravailleur } from '../../application/executeur_travaille import { ErreurAnnulee, creerExecuteurDirect } from '../../application/generation.js'; import { TABLES_APPLICATION } from '../../application/libelles.js'; import { creerTraduction } from '../../application/traduction.js'; +import { constante } from '../../geometrie/constantes.js'; import { rechercher } from '../../moteur/recherche.js'; import { configurationDepuisCharge } from '../../stockage/document.js'; import { lireJournal } from '../../stockage/journal.js'; @@ -777,7 +778,7 @@ describe('les cœurs de calcul, un réglage du poste (§ 5.7, § 5.10)', () => { await saisir(m, champCoeurs(m), '1'); const surUn = controleur(m).estimer(reglages); expect(surUn.coeurs).toBe(1); - expect(surUn.secondes).toBeCloseTo(5, 9); + expect(surUn.secondes).toBeCloseTo(5 / constante('RALENTISSEMENT_PLEINE_CHARGE'), 9); await attendre(() => expect(estimation().textContent).toBe(t('generation.duree.estimee', surUn))); expect(estimation().textContent).not.toContain('cœur'); expect(executions.length).toBe(1);