[ADD] storage: measured cost of a complete save on the big demo
§ 8.2 makes the cost of a full save a measured threshold, since every gesture rewrites the whole state file. The node-long test times a swap in the retained plan of the big demo with twenty propositions — journal line, .precedent, state, on the real file system — and the gesture that prunes the journal past 600 entries. It keeps sampling while the minimum stays above the threshold, within a bounded window, so a burst of load passes and a uniform regression fails. Each gesture's files and every revision are checked, so a save that skips a file cannot lower the measure. Checked: 3 node-long tests; red under a ×5 slowdown of every gesture. --- FR --- [ADD] stockage : coût mesuré d'un enregistrement complet, grande démo Le § 8.2 fait du coût d'un enregistrement complet un seuil mesuré, puisque chaque geste réécrit tout le fichier d'état. L'épreuve node-long chronomètre un échange dans le plan retenu de la grande démonstration à vingt propositions — ligne de journal, .precedent, état, sur le vrai système de fichiers — et le geste qui élague le journal au-delà de 600 entrées. Elle mesure encore tant que le minimum dépasse le seuil, dans une fenêtre bornée : une rafale de charge passe, une régression uniforme échoue. Les fichiers de chaque geste et chaque révision sont vérifiés. Vérifié : 3 épreuves node-long ; rouge sous un ralentissement ×5. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
This commit is contained in:
parent
fa2fc0eb2b
commit
220814a673
1 changed files with 548 additions and 0 deletions
548
src/stockage/depot.long.test.js
Normal file
548
src/stockage/depot.long.test.js
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Le coût d'un enregistrement complet (§ 8.2, § 8.9, § 14.4) : la durée d'un
|
||||
// geste qu'enregistrer (depot.js) écrit sur un vrai système de fichiers — le
|
||||
// journal, le .precedent, l'état — pour la grande démonstration livrée
|
||||
// portant vingt propositions, dont une retenue. Deux gestes se mesurent,
|
||||
// chacun sous son seuil : le geste ordinaire, dont la ligne s'ajoute en fin
|
||||
// de journal, et le geste dont l'entrée fait passer le journal au-delà de
|
||||
// PLAFOND_ENTREES, qui le réécrit en entier, élagué. Le système de fichiers
|
||||
// est celui de la plateforme electron : la page (fichiers_electron.js) devant
|
||||
// les primitives de la coquille (electron/fichiers.js), sous un dossier
|
||||
// temporaire neuf, retiré à la fin de l'épreuve ; seul l'IPC d'Electron y est
|
||||
// remplacé, par une copie des arguments et des réponses.
|
||||
//
|
||||
// La charge mesurée se construit une fois pour le fichier (chargeMesuree) :
|
||||
// le moteur produit ses vingt plans, et une épreuve affirme que l'analyse
|
||||
// l'admet et que la forme canonique la réécrit telle quelle. Chaque geste
|
||||
// mesuré retouche le placement retenu — deux personnes changent de table à
|
||||
// un tour — ; son entrée porte un correctif qui touche deux listes de table,
|
||||
// ou l'instantané de la charge à la cadence du journal. Après chaque geste,
|
||||
// hors de la mesure, l'état et le .precedent sur le disque sont, octet pour
|
||||
// octet, ceux que le geste écrit ; à la fin, le journal relu porte une
|
||||
// entrée par révision et restitue la charge de révisions choisies : la durée
|
||||
// retenue vient toujours d'un geste qui a écrit tout ce qu'écrit un
|
||||
// enregistrement. Un geste qui n'écrirait plus les
|
||||
// propositions, plus le .precedent ou plus le journal élagué mesurerait
|
||||
// autre chose, et passerait sous le seuil (§ 14.2).
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { PREFIXE_CANAL, PRIMITIVES, creerFichiers, decrireSysteme, servir } from '../../electron/fichiers.js';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { texteLivre } from '../demo/livrees.js';
|
||||
import { regenerer } from '../moteur/recherche.js';
|
||||
import { VERSION } from '../version.genere.js';
|
||||
import { canoniser, serialiser } from './canonique.js';
|
||||
import { creer, enregistrer, ouvrir } from './depot.js';
|
||||
import { analyser, configurationDepuisCharge } from './document.js';
|
||||
import { creerFichiersElectron } from './fichiers_electron.js';
|
||||
import {
|
||||
INTERVALLE_INSTANTANE,
|
||||
PLAFOND_ENTREES,
|
||||
PLANCHER_ENTREES,
|
||||
estInstantane,
|
||||
ligneEntree,
|
||||
lireJournal,
|
||||
reconstruire,
|
||||
} from './journal.js';
|
||||
import { SUFFIXES } from './noms.js';
|
||||
import { examiner, versFichier } from './placements.js';
|
||||
|
||||
const V = VERSION.affichee;
|
||||
const NOM_PRODUIT = 'Gestion table tournante Libre';
|
||||
|
||||
// Les propositions de la charge mesurée, la vingtaine du § 8.9 sur la grande
|
||||
// démonstration, aux graines 1 à PROPOSITIONS.
|
||||
const PROPOSITIONS = 20;
|
||||
|
||||
// Le compte d'arrêt de chaque proposition, réduit à côté de celui des
|
||||
// réglages : la taille d'un placement ne dépend que de sa forme, non de sa
|
||||
// qualité, et la construction de la charge reste brève.
|
||||
const ARRET_REDUIT = 2_000;
|
||||
|
||||
// L'échantillonnage, contre une charge passagère de la machine. Après un
|
||||
// geste de chauffe, chaque épreuve mesure son nombre de base de gestes, et
|
||||
// son seuil porte sur le plus court : le minimum écarte ce qu'une machine
|
||||
// occupée ajoute à un geste, pourvu qu'un geste au moins s'exécute quand la
|
||||
// charge se relâche. Tant que le plus court dépasse le seuil, l'épreuve
|
||||
// mesure d'autres gestes, jusqu'à son plafond de gestes ou jusqu'à ce que
|
||||
// FENETRE_MESURE_MS se soient écoulées depuis le premier geste mesuré, ce
|
||||
// qui vient d'abord. Une charge plus courte que la fenêtre laisse ainsi
|
||||
// passer un geste sous le seuil, et l'épreuve passe ; une régression, qui
|
||||
// allonge chaque geste, le laisse au-delà à chaque geste, et l'épreuve
|
||||
// échoue au plafond. Sous la charge que couvre MARGE, le nombre de base
|
||||
// suffit : l'épreuve ne s'allonge qu'au-delà.
|
||||
//
|
||||
// GESTES_MESURES, le nombre de base des gestes ordinaires : vingt tiennent
|
||||
// sous la charge que couvre MARGE, là où cinq n'y tiennent pas.
|
||||
// GESTES_AU_PLUS, cinq fois autant, borne les gestes ordinaires mesurés.
|
||||
const GESTES_MESURES = 20;
|
||||
const GESTES_AU_PLUS = 100;
|
||||
|
||||
// ESSAIS_ELAGAGE, le nombre de base des essais du geste qui élague : cinq
|
||||
// tiennent sous la charge que couvre MARGE. Chaque essai pose un journal
|
||||
// plein, d'environ 2 Mo, et relit celui qu'il écrit. ESSAIS_ELAGAGE_AU_PLUS,
|
||||
// cinq fois autant, borne les essais mesurés.
|
||||
const ESSAIS_ELAGAGE = 5;
|
||||
const ESSAIS_ELAGAGE_AU_PLUS = 25;
|
||||
|
||||
// La fenêtre de mesure, en millisecondes, au mur, vérifications hors mesure
|
||||
// comprises : la charge passagère que l'échantillonnage écarte dure moins
|
||||
// qu'elle. Elle borne aussi la durée d'une épreuve qui échoue : une mesure
|
||||
// s'arrête au premier geste qui finit au-delà, et la fenêtre vaut un sixième
|
||||
// du plafond de la série node-long (test/rapporteur.js).
|
||||
const FENETRE_MESURE_MS = 10_000;
|
||||
|
||||
// Les seuils du coût d'un enregistrement complet (§ 8.2, § 14.1), en
|
||||
// millisecondes, chacun au minimum de ses gestes mesurés, qui garde le coût
|
||||
// du geste. Le § 8.2 réécrit l'état entier à chaque geste : c'est ce coût
|
||||
// qui gouverne la forme du fichier (§ 8.9).
|
||||
//
|
||||
// SEUIL_MS borne le geste ordinaire : correctif depuis l'entrée qui précède,
|
||||
// état sérialisé puis relu, ligne du journal ajoutée et vidée sur le disque,
|
||||
// .precedent puis état réécrits par écriture atomique. L'entrée qui porte un
|
||||
// instantané, une sur INTERVALLE_INSTANTANE, coûte autant : canoniser la
|
||||
// charge y remplace le calcul du correctif, et sa ligne s'ajoute de même.
|
||||
// SEUIL_ELAGAGE_MS borne le geste dont l'entrée fait passer le journal
|
||||
// au-delà de PLAFOND_ENTREES : le journal se réécrit en entier — son texte
|
||||
// recomposé, lu par l'élagage, puis relu une fois élagué — par une écriture
|
||||
// atomique de la taille d'une dizaine d'instantanés de la charge, avant le
|
||||
// .precedent et l'état. Le plafond atteint, ce geste revient environ une
|
||||
// fois par centaine de gestes : l'élagage garde un peu plus de
|
||||
// PLANCHER_ENTREES entrées. Les autres réécritures entières du journal —
|
||||
// une entrée de reprise, des lignes écartées à la lecture, un journal qui
|
||||
// manque (depot.js) — réparent un événement sorti du cours ordinaire des
|
||||
// gestes, et ces épreuves ne les mesurent pas.
|
||||
//
|
||||
// REFERENCE_MS et REFERENCE_ELAGAGE_MS sont les minimums que relèvent ces
|
||||
// épreuves, le fichier lancé seul, médiane de plusieurs lancements arrondie
|
||||
// à la milliseconde supérieure, sur un processeur x86-64 de bureau de
|
||||
// génération Zen 3, seize cœurs virtuels sous KVM, le dossier temporaire en
|
||||
// mémoire (tmpfs), sous Linux et Node 26.
|
||||
// MARGE, le facteur qui tire chaque seuil de sa référence, couvre, sur la
|
||||
// machine des références, quatre séries node-long lancées ensemble —
|
||||
// autant que de conteneurs simultanés par défaut sous make
|
||||
// verifier_systemes (GTT_PARALLELES) —, chacune son dossier temporaire sur
|
||||
// disque, où vider sur le disque s'ajoute au calcul. Sous cette charge, le
|
||||
// plus court des GESTES_MESURES gestes ordinaires monte jusqu'à trois fois
|
||||
// REFERENCE_MS, et le plus court des ESSAIS_ELAGAGE essais jusqu'à trois
|
||||
// fois REFERENCE_ELAGAGE_MS : il reste un tiers de marge. MARGE ne couvre
|
||||
// pas ce qu'un autre conteneur de make verifier_systemes peut exécuter en
|
||||
// même temps — make test, npm ci, Chromium —, ni davantage de conteneurs à
|
||||
// la fois, ni une machine plus lente d'un tiers sous la même charge : contre
|
||||
// une telle charge, quand elle passe, l'échantillonnage mesure d'autres
|
||||
// gestes jusqu'à son plafond. Elle reste assez basse pour qu'un état dont
|
||||
// les placements pèsent dix fois plus, comme sous leur forme nommée (§ 8.9),
|
||||
// dépasse chacun des deux seuils.
|
||||
//
|
||||
// Propriétaire (§ 14.1) : la série node-long, où ces épreuves relèvent la
|
||||
// mesure à chaque exécution et la nomment quand elle dépasse son seuil. Le
|
||||
// relevé sur la plateforme livrée — l'exécutable Windows, sous un antivirus
|
||||
// qui inspecte chaque fichier temporaire — appartient au niveau manuel,
|
||||
// avant livraison : tant qu'il n'a pas eu lieu, ces seuils sont des défauts
|
||||
// de travail relevés sous Linux.
|
||||
const REFERENCE_MS = 20;
|
||||
const REFERENCE_ELAGAGE_MS = 87;
|
||||
const MARGE = 4;
|
||||
const SEUIL_MS = REFERENCE_MS * MARGE;
|
||||
const SEUIL_ELAGAGE_MS = REFERENCE_ELAGAGE_MS * MARGE;
|
||||
|
||||
// Durée affichée, une décimale, virgule décimale.
|
||||
const ms = (duree) => `${duree.toFixed(1).replace('.', ',')} ms`;
|
||||
|
||||
const croissant = (a, b) => a - b;
|
||||
|
||||
// Un nombre sur deux chiffres.
|
||||
const deux = (n) => String(n).padStart(2, '0');
|
||||
|
||||
// Horodatage d'affichage de l'instant n, une minute par instant depuis 8 h ;
|
||||
// il n'entre dans aucune mesure.
|
||||
const heure = (n) => `2031-11-08T${deux(8 + Math.floor(n / 60))}:${deux(n % 60)}:00-05:00`;
|
||||
|
||||
const CREATION = { libelle: "Création de l'événement", nomJalon: 'Création', horodatage: heure(0), produitVersion: V };
|
||||
const geste = (n) => ({ libelle: `Retouche ${n} du placement retenu`, horodatage: heure(n), produitVersion: V });
|
||||
|
||||
// Le retenu repris d'une proposition, dont l'identifiant devient l'origine ;
|
||||
// son placement est une copie, qu'une retouche modifie sans toucher la
|
||||
// proposition.
|
||||
function retenuDe({ id, siegesAttribues, tables, capacites, tours, participants, placement }) {
|
||||
return {
|
||||
proposition: id,
|
||||
siegesAttribues,
|
||||
tables: [...tables],
|
||||
capacites: [...capacites],
|
||||
tours,
|
||||
participants: [...participants],
|
||||
placement: structuredClone(placement),
|
||||
};
|
||||
}
|
||||
|
||||
// La grande démonstration livrée, portant PROPOSITIONS propositions que le
|
||||
// moteur produit aux graines 1 à PROPOSITIONS sous ARRET_REDUIT, numérotées
|
||||
// depuis le compteur de la charge, qui passe au-delà de la dernière ; la
|
||||
// première est retenue, et l'événement dans l'état retenu.
|
||||
function construire() {
|
||||
const charge = analyser(texteLivre('grande')).charge;
|
||||
const configuration = configurationDepuisCharge(charge);
|
||||
const { historique } = charge.reglages.generation;
|
||||
const decalage = charge.prochainsIds.proposition - 1;
|
||||
charge.propositions = Array.from({ length: PROPOSITIONS }, (_, rang) => {
|
||||
const graine = rang + 1;
|
||||
const plan = regenerer(configuration, { graine, arret: ARRET_REDUIT, historique });
|
||||
return versFichier({ id: graine, graine, arret: ARRET_REDUIT, historique, plan }, configuration, {
|
||||
produitVersion: V,
|
||||
attribuerSieges: charge.reglages.attribuerSieges,
|
||||
decalage,
|
||||
});
|
||||
});
|
||||
charge.prochainsIds.proposition = decalage + PROPOSITIONS + 1;
|
||||
charge.retenu = retenuDe(charge.propositions[0]);
|
||||
charge.evenement.etat = 'retenu';
|
||||
return charge;
|
||||
}
|
||||
|
||||
// La charge mesurée, construite au premier appel et partagée par les
|
||||
// épreuves du fichier, qui ne la modifient pas.
|
||||
let construite = null;
|
||||
const chargeMesuree = () => (construite ??= construire());
|
||||
|
||||
// La charge reçue, son placement retenu retouché au tour d'index tour : la
|
||||
// première personne sans réservation de la première table et celle de la
|
||||
// deuxième changent de table, comme une retouche à la main (§ 5.8). Une
|
||||
// personne réservée ne s'échange pas, ce que le geste refuserait (§ 5.9).
|
||||
// Les sièges n'étant pas attribués, chaque liste reste triée par
|
||||
// identifiant (§ 8.9).
|
||||
function echanger(charge, tour) {
|
||||
const apres = structuredClone(charge);
|
||||
const reservees = new Set(charge.reservations.map(({ participant }) => participant));
|
||||
const [premiere, deuxieme] = apres.retenu.placement[tour].sieges;
|
||||
const a = premiere.find((id) => !reservees.has(id));
|
||||
const b = deuxieme.find((id) => !reservees.has(id));
|
||||
assert.ok(a !== undefined && b !== undefined, `tour ${tour + 1} : aucune personne à échanger`);
|
||||
premiere.splice(premiere.indexOf(a), 1, b);
|
||||
deuxieme.splice(deuxieme.indexOf(b), 1, a);
|
||||
premiere.sort(croissant);
|
||||
deuxieme.sort(croissant);
|
||||
return apres;
|
||||
}
|
||||
|
||||
// Les textes d'un événement dont le journal est plein, à PLAFOND_ENTREES
|
||||
// entrées : le journal que creer a écrit pour la charge a — ouverture,
|
||||
// entrée 1 en instantané, jalon de la création —, suivi des entrées 2 à
|
||||
// PLAFOND_ENTREES, écrites ligne à ligne plutôt que par autant de gestes
|
||||
// enregistrés. Les charges alternent : a aux révisions impaires, b, son
|
||||
// retenu retouché au premier tour, aux paires. Chaque ligne a la forme que
|
||||
// ligneEntree donne à son passage — instantané à la cadence du journal
|
||||
// (estInstantane), correctif de a vers b ou de b vers a sinon —, calculée
|
||||
// une fois par sorte de passage, sous le libellé et l'horodatage de sa
|
||||
// révision : les clés remplacées gardent leur place, et chaque ligne est
|
||||
// celle qu'écrirait ligneEntree. Rend le journal, l'état à la dernière
|
||||
// révision, le .precedent à celle d'avant, et chargeDe, la charge d'une
|
||||
// révision.
|
||||
function evenementPlein(journalCree, a) {
|
||||
const b = echanger(a, 0);
|
||||
const chargeDe = (revision) => (revision % 2 === 1 ? a : b);
|
||||
const formes = new Map();
|
||||
const lignes = [];
|
||||
for (let revision = 2; revision <= PLAFOND_ENTREES; revision += 1) {
|
||||
const { libelle, horodatage } = geste(revision);
|
||||
const instantane = estInstantane(revision, 1);
|
||||
const sorte = `${instantane} ${revision % 2}`;
|
||||
if (!formes.has(sorte)) {
|
||||
const ligne = ligneEntree({
|
||||
revision,
|
||||
libelle,
|
||||
horodatage,
|
||||
avant: chargeDe(revision - 1),
|
||||
apres: chargeDe(revision),
|
||||
instantane,
|
||||
});
|
||||
formes.set(sorte, JSON.parse(ligne));
|
||||
}
|
||||
lignes.push(JSON.stringify({ ...formes.get(sorte), revision, libelle, horodatage }));
|
||||
}
|
||||
const etat = (revision) => serialiser(chargeDe(revision), { revision, produitVersion: V });
|
||||
return {
|
||||
journal: `${journalCree}${lignes.join('\n')}\n`,
|
||||
etat: etat(PLAFOND_ENTREES),
|
||||
precedent: etat(PLAFOND_ENTREES - 1),
|
||||
chargeDe,
|
||||
};
|
||||
}
|
||||
|
||||
// Les dialogues et l'explorateur ne servent pas à un enregistrement : un
|
||||
// appel lèverait TypeError.
|
||||
const SANS_DIALOGUE = Object.freeze({});
|
||||
|
||||
// Le système de fichiers de la plateforme electron sur dossier : la page
|
||||
// (creerFichiersElectron) devant les primitives de la coquille
|
||||
// (creerFichiers), que servir offre canal par canal, comme le processus
|
||||
// principal. L'IPC d'Electron y est remplacé par une copie structurée des
|
||||
// arguments et de la réponse, comme il les copie. Les Documents et l'AppData
|
||||
// du poste sont des dossiers de dossier ; rend le système et la racine
|
||||
// documents, créée.
|
||||
async function systemeSur(dossier) {
|
||||
const systeme = decrireSysteme({
|
||||
env: {},
|
||||
plateforme: process.platform,
|
||||
cheminSysteme: (nom) => (nom === 'temp' ? tmpdir() : join(dossier, nom)),
|
||||
nomProduit: NOM_PRODUIT,
|
||||
});
|
||||
const gestionnaires = new Map();
|
||||
servir(creerFichiers({ ...systeme, dialog: SANS_DIALOGUE, shell: SANS_DIALOGUE }), (canal, gestionnaire) =>
|
||||
gestionnaires.set(canal, gestionnaire),
|
||||
);
|
||||
const pont = Object.fromEntries(
|
||||
PRIMITIVES.map((primitive) => {
|
||||
const gestionnaire = gestionnaires.get(`${PREFIXE_CANAL}${primitive}`);
|
||||
const appeler = async (...parametres) =>
|
||||
structuredClone(await gestionnaire({ sender: null }, ...structuredClone(parametres)));
|
||||
return [primitive, appeler];
|
||||
}),
|
||||
);
|
||||
const fs = creerFichiersElectron(pont);
|
||||
const { documents } = await fs.racines();
|
||||
mkdirSync(documents.chemin, { recursive: true });
|
||||
return { fs, racine: documents };
|
||||
}
|
||||
|
||||
// Chemin, sur le disque, du fichier de suffixe de l'événement base sous
|
||||
// racine.
|
||||
const cheminDe = (racine, base, suffixe) => join(racine.chemin, `${base}${suffixe}`);
|
||||
|
||||
// Texte du fichier chemin, ou null quand il manque.
|
||||
const lireSiPresent = (chemin) => (existsSync(chemin) ? readFileSync(chemin, 'utf8') : null);
|
||||
|
||||
// Révision de l'en-tête d'un texte d'état, ou null sans texte.
|
||||
const revisionDe = (texte) => (texte === null ? null : analyser(texte).entete.revision);
|
||||
|
||||
// Affirme que le disque porte ce qu'écrit le geste de la révision revision :
|
||||
// l'état au texte etat, le .precedent au texte precedent, celui de l'état
|
||||
// d'avant le geste. Les révisions des en-têtes se comparent d'abord, null
|
||||
// pour un fichier qui manque : un fichier que le geste n'a pas écrit se
|
||||
// nomme par la révision qu'il garde, avant toute différence de deux textes.
|
||||
function affirmerEcrits(racine, base, revision, { etat, precedent }) {
|
||||
const ecrits = {
|
||||
etat: lireSiPresent(cheminDe(racine, base, SUFFIXES.etat)),
|
||||
precedent: lireSiPresent(cheminDe(racine, base, SUFFIXES.precedent)),
|
||||
};
|
||||
assert.deepEqual(
|
||||
{ etat: revisionDe(ecrits.etat), precedent: revisionDe(ecrits.precedent) },
|
||||
{ etat: revision, precedent: revision - 1 },
|
||||
`révisions de l'état et du .precedent sur le disque après le geste de la révision ${revision}`,
|
||||
);
|
||||
assert.ok(ecrits.etat === etat, `l'état sur le disque n'est pas celui qu'écrit le geste de la révision ${revision}`);
|
||||
assert.ok(
|
||||
ecrits.precedent === precedent,
|
||||
`le .precedent sur le disque n'est pas l'état d'avant le geste de la révision ${revision}`,
|
||||
);
|
||||
}
|
||||
|
||||
// La mesure échantillonnée d'un geste (GESTES_MESURES) : faire exécute un
|
||||
// geste et ses vérifications, et rend la durée du seul geste. Un premier
|
||||
// appel chauffe, sans compter ; suivent base gestes mesurés, puis d'autres
|
||||
// tant que le plus court dépasse seuil, jusqu'à auPlus gestes mesurés ou la
|
||||
// fin de FENETRE_MESURE_MS, comptée depuis le premier. Rend les durées, dans
|
||||
// l'ordre des gestes, et arret, ce qui a clos une mesure restée au-delà du
|
||||
// seuil.
|
||||
async function mesurer(faire, { base, auPlus, seuil }) {
|
||||
await faire();
|
||||
const durees = [];
|
||||
const debut = performance.now();
|
||||
const ouverte = () => performance.now() - debut < FENETRE_MESURE_MS;
|
||||
while (durees.length < base || (Math.min(...durees) > seuil && durees.length < auPlus && ouverte())) {
|
||||
durees.push(await faire());
|
||||
}
|
||||
const arret =
|
||||
durees.length >= auPlus
|
||||
? `plafond de ${auPlus} mesures atteint`
|
||||
: `fenêtre de ${FENETRE_MESURE_MS / 1000} s écoulée`;
|
||||
return { durees, arret };
|
||||
}
|
||||
|
||||
// Le message d'un seuil dépassé : le minimum, le nombre de mesures, leur
|
||||
// médiane et la plus longue, ce qui a clos la mesure, le seuil, et la
|
||||
// référence dont MARGE le tire.
|
||||
function depasse(quoi, { durees, arret }, seuil, reference) {
|
||||
const triees = [...durees].sort(croissant);
|
||||
return (
|
||||
`${quoi} s'enregistre en ${ms(triees[0])} au minimum de ${durees.length} mesures ` +
|
||||
`(médiane ${ms(triees[Math.floor((triees.length - 1) / 2)])}, la plus longue ${ms(triees.at(-1))} ; ` +
|
||||
`${arret}), au-delà du seuil de ${ms(seuil)} (§ 8.2) : ${reference}, MARGE ${MARGE}`
|
||||
);
|
||||
}
|
||||
|
||||
describe("dépôt : le coût d'un enregistrement complet, sur un vrai système de fichiers (§ 8.2)", () => {
|
||||
test("la charge mesurée est la grande démonstration portant vingt propositions du moteur, la première retenue : l'analyse l'admet, la forme canonique la réécrit octet pour octet, et aucune proposition n'est fautive ni périmée", () => {
|
||||
const texte = serialiser(chargeMesuree(), { revision: 1, produitVersion: V });
|
||||
const lu = analyser(texte);
|
||||
const { participants, propositions, retenu } = lu.entete.comptes;
|
||||
assert.deepEqual(
|
||||
{ participants, propositions, retenu },
|
||||
{ participants: 260, propositions: PROPOSITIONS, retenu: 1 },
|
||||
);
|
||||
assert.deepEqual(
|
||||
lu.charge.propositions.map(({ graine }) => graine),
|
||||
Array.from({ length: PROPOSITIONS }, (_, rang) => rang + 1),
|
||||
);
|
||||
assert.equal(serialiser(lu.charge, lu.entete), texte);
|
||||
const { fautives, derives, retenu: examenRetenu } = examiner(lu.charge);
|
||||
assert.deepEqual(
|
||||
{ fautives, derives, retenu: examenRetenu },
|
||||
{ fautives: [], derives: [], retenu: { fautes: [], raisons: [] } },
|
||||
);
|
||||
});
|
||||
|
||||
test(`un geste qui retouche le placement retenu s'enregistre — journal, .precedent, état — en au plus ${ms(SEUIL_MS)}, au minimum de ${GESTES_MESURES} à ${GESTES_AU_PLUS} mesures après un geste de chauffe ; chaque geste laisse sur le disque l'état et le .precedent qu'il écrit, et l'événement rouvert est celui du dernier geste, dont le journal porte chaque révision`, async () => {
|
||||
const dossier = mkdtempSync(join(tmpdir(), 'gtt-cout-'));
|
||||
try {
|
||||
const { fs, racine } = await systemeSur(dossier);
|
||||
let ouvert = await creer(fs, racine, chargeMesuree(), CREATION);
|
||||
// La charge de chaque révision, depuis la création, et le texte de
|
||||
// l'état à la dernière. Chaque geste retouche le tour qui suit celui du
|
||||
// geste d'avant.
|
||||
const charges = [chargeMesuree()];
|
||||
let texteEtat = serialiser(chargeMesuree(), { revision: 1, produitVersion: V });
|
||||
const faire = async () => {
|
||||
const revision = charges.length + 1;
|
||||
const avant = charges.at(-1);
|
||||
const apres = echanger(avant, (revision - 2) % avant.evenement.tours);
|
||||
const debut = performance.now();
|
||||
ouvert = await enregistrer(ouvert, apres, geste(revision));
|
||||
const duree = performance.now() - debut;
|
||||
const texteApres = serialiser(apres, { revision, produitVersion: V });
|
||||
assert.equal(ouvert.revision, revision);
|
||||
affirmerEcrits(racine, ouvert.base, revision, { etat: texteApres, precedent: texteEtat });
|
||||
charges.push(apres);
|
||||
texteEtat = texteApres;
|
||||
return duree;
|
||||
};
|
||||
const mesure = await mesurer(faire, { base: GESTES_MESURES, auPlus: GESTES_AU_PLUS, seuil: SEUIL_MS });
|
||||
|
||||
// L'événement se rouvre sans rien rattraper ni signaler, à la charge
|
||||
// du dernier geste ; l'état sur le disque porte les vingt propositions
|
||||
// et le retenu ; le journal porte une entrée par révision.
|
||||
const revision = charges.length;
|
||||
const rouvert = await ouvrir(fs, racine, ouvert.base);
|
||||
assert.deepEqual(rouvert.avertissements, []);
|
||||
assert.equal(rouvert.revision, revision);
|
||||
assert.deepEqual(rouvert.charge, canoniser(charges.at(-1)));
|
||||
const etat = analyser(readFileSync(cheminDe(racine, ouvert.base, SUFFIXES.etat), 'utf8'));
|
||||
const { propositions, retenu } = etat.entete.comptes;
|
||||
assert.deepEqual({ propositions, retenu }, { propositions: PROPOSITIONS, retenu: 1 });
|
||||
const journal = lireJournal(readFileSync(cheminDe(racine, ouvert.base, SUFFIXES.journal), 'utf8'));
|
||||
assert.deepEqual(
|
||||
journal.entrees.map((entree) => entree.revision),
|
||||
Array.from({ length: revision }, (_, rang) => rang + 1),
|
||||
);
|
||||
// Le journal restitue la charge de chaque révision jusqu'au dernier des
|
||||
// GESTES_MESURES gestes de base, puis celle de la dernière.
|
||||
// Reconstruire une révision rejoue chaque correctif depuis l'instantané
|
||||
// qui la précède : le coût de relire chaque révision croît comme le
|
||||
// carré du nombre de gestes entre deux instantanés. Les gestes de base,
|
||||
// en nombre fixe, se relisent donc tous, et les gestes de plus de
|
||||
// l'échantillonnage par le dernier. Un correctif faux se voit à sa
|
||||
// révision : un geste ultérieur qui récrit les mêmes listes de table
|
||||
// l'efface des suivantes.
|
||||
const relues = Array.from({ length: GESTES_MESURES + 2 }, (_, rang) => rang + 1);
|
||||
if (revision > relues.length) relues.push(revision);
|
||||
for (const relue of relues) {
|
||||
assert.deepEqual(reconstruire(journal, relue), canoniser(charges[relue - 1]), `révision ${relue}`);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
Math.min(...mesure.durees) <= SEUIL_MS,
|
||||
depasse('un geste sur la grande démonstration', mesure, SEUIL_MS, `REFERENCE_MS ${REFERENCE_MS} ms`),
|
||||
);
|
||||
} finally {
|
||||
rmSync(dossier, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test(`le geste dont l'entrée fait passer le journal au-delà de ${PLAFOND_ENTREES} entrées le réécrit élagué — journal, .precedent, état — en au plus ${ms(SEUIL_ELAGAGE_MS)}, au minimum de ${ESSAIS_ELAGAGE} à ${ESSAIS_ELAGAGE_AU_PLUS} essais après un essai de chauffe, chacun sur une copie neuve du journal plein ; le journal écrit part de l'instantané de la coupe et restitue le geste`, async () => {
|
||||
const dossier = mkdtempSync(join(tmpdir(), 'gtt-elagage-'));
|
||||
try {
|
||||
const { fs, racine } = await systemeSur(dossier);
|
||||
const { base } = await creer(fs, racine, chargeMesuree(), CREATION);
|
||||
const chemin = (suffixe) => cheminDe(racine, base, suffixe);
|
||||
const plein = evenementPlein(readFileSync(chemin(SUFFIXES.journal), 'utf8'), chargeMesuree());
|
||||
const poser = () => {
|
||||
writeFileSync(chemin(SUFFIXES.journal), plein.journal);
|
||||
writeFileSync(chemin(SUFFIXES.precedent), plein.precedent);
|
||||
writeFileSync(chemin(SUFFIXES.etat), plein.etat);
|
||||
};
|
||||
|
||||
// L'événement plein s'ouvre sans rien signaler, son journal à
|
||||
// PLAFOND_ENTREES entrées. enregistrer ne modifie pas l'Ouvert reçu :
|
||||
// chaque essai part du même, sur le journal plein reposé, la chauffe
|
||||
// comprise.
|
||||
poser();
|
||||
const ouvert = await ouvrir(fs, racine, base);
|
||||
assert.deepEqual(
|
||||
{ avertissements: ouvert.avertissements, revision: ouvert.revision, entrees: ouvert.journal.entrees.length },
|
||||
{ avertissements: [], revision: PLAFOND_ENTREES, entrees: PLAFOND_ENTREES },
|
||||
);
|
||||
|
||||
// La coupe de l'élagage : l'instantané le plus récent — révision 1,
|
||||
// puis toutes les INTERVALLE_INSTANTANE — d'où le journal garde au
|
||||
// moins PLANCHER_ENTREES entrées jusqu'au geste.
|
||||
const revision = PLAFOND_ENTREES + 1;
|
||||
const coupe = 1 + Math.floor((revision - PLANCHER_ENTREES) / INTERVALLE_INSTANTANE) * INTERVALLE_INSTANTANE;
|
||||
const apres = echanger(plein.chargeDe(PLAFOND_ENTREES), 1);
|
||||
const texteApres = serialiser(apres, { revision, produitVersion: V });
|
||||
let essai = 0;
|
||||
let journal = null;
|
||||
const faire = async () => {
|
||||
poser();
|
||||
const debut = performance.now();
|
||||
await enregistrer(ouvert, apres, geste(revision));
|
||||
const duree = performance.now() - debut;
|
||||
affirmerEcrits(racine, base, revision, { etat: texteApres, precedent: plein.etat });
|
||||
journal = lireJournal(readFileSync(chemin(SUFFIXES.journal), 'utf8'));
|
||||
assert.deepEqual(
|
||||
{
|
||||
premiere: journal.entrees[0].revision,
|
||||
entrees: journal.entrees.length,
|
||||
jalons: journal.jalons.length,
|
||||
ecartees: journal.ecartees,
|
||||
},
|
||||
{ premiere: coupe, entrees: revision - coupe + 1, jalons: 0, ecartees: 0 },
|
||||
`journal écrit par l'essai ${essai}`,
|
||||
);
|
||||
assert.deepEqual(reconstruire(journal, revision), canoniser(apres));
|
||||
essai += 1;
|
||||
return duree;
|
||||
};
|
||||
const mesure = await mesurer(faire, {
|
||||
base: ESSAIS_ELAGAGE,
|
||||
auPlus: ESSAIS_ELAGAGE_AU_PLUS,
|
||||
seuil: SEUIL_ELAGAGE_MS,
|
||||
});
|
||||
|
||||
// Le journal élagué restitue encore l'instant d'avant le geste, et
|
||||
// l'événement se rouvre sur le geste sans rien rattraper ni signaler.
|
||||
assert.deepEqual(reconstruire(journal, PLAFOND_ENTREES), canoniser(plein.chargeDe(PLAFOND_ENTREES)));
|
||||
const rouvert = await ouvrir(fs, racine, base);
|
||||
assert.deepEqual(
|
||||
{ avertissements: rouvert.avertissements, revision: rouvert.revision },
|
||||
{ avertissements: [], revision },
|
||||
);
|
||||
assert.deepEqual(rouvert.charge, canoniser(apres));
|
||||
|
||||
assert.ok(
|
||||
Math.min(...mesure.durees) <= SEUIL_ELAGAGE_MS,
|
||||
depasse(
|
||||
'le geste qui élague le journal plein',
|
||||
mesure,
|
||||
SEUIL_ELAGAGE_MS,
|
||||
`REFERENCE_ELAGAGE_MS ${REFERENCE_ELAGAGE_MS} ms`,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
rmSync(dossier, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue