§ 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
548 lines
28 KiB
JavaScript
548 lines
28 KiB
JavaScript
// © 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 });
|
|
}
|
|
});
|
|
});
|