[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
This commit is contained in:
parent
0b2b2dc303
commit
3e4fd03a4c
4 changed files with 431 additions and 31 deletions
213
scripts/banc/parallele.banc.js
Normal file
213
scripts/banc/parallele.banc.js
Normal file
|
|
@ -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<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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue