Aller au contenu

Mémoire et diffusion en flux

Spec: ISO 32000-2, §7.5.4 Evidence: Mixed evidence

Un PDF volumineux ne devrait pas exiger beaucoup de mémoire. Cette page explique comment NextPDF garde le tas du processus borné à mesure qu’un document grandit, quand il diffuse en flux vers le disque au lieu d’accumuler les données dans le tas, et ce que signifie ici un « budget de performance » : un contrat vérifié, pas un argument d’accroche.

Le format PDF n’oblige pas un générateur à consommer beaucoup de mémoire. Sa table de références croisées enregistre un décalage en octets pour chaque objet indirect, si bien qu’un lecteur n’a besoin que d’un accès aléatoire au fichier, pas du fichier entier en mémoire. Un générateur peut suivre le même principe : émettre les objets à mesure qu’ils sont terminés et ne retenir que leur position. Si, à l’inverse, le document entier reste dans le tas jusqu’à l’écriture finale, le nombre de pages fait croître la mémoire de façon linéaire, et un rapport qui fonctionne à cent pages fait échouer le processus à cinquante mille.

Pour les charges de travail par lots et les workers, c’est la différence entre un service stable et un service qui échoue de manière imprévisible sous la charge. La mémoire bornée est une propriété de conception qu’il faut obtenir par l’ingénierie, pas un chiffre qu’on espère.

  • Le générateur en flux est construit pour que la mémoire reste bornée par document. Chaque page est écrite dans la sortie dès qu’elle est finalisée, puis son tampon est libéré.
  • La comptabilité qui croîtrait sinon avec le nombre d’objets — les décalages de références croisées et les références Kids de l’arbre des pages — est écrite dans des flux temporaires ouverts avec php://temp/maxmemory:0, qui déversent immédiatement sur le disque au lieu de remplir le tas PHP.
  • L’objectif de conception est un tas en O(1) par page : conserver le document ne coûte pas davantage à mesure que des pages sont ajoutées. C’est la cible d’ingénierie autour de laquelle le générateur est façonné.
  • Un budget de performance est un concept réel et structuré dans le système de documentation : un plafond de temps écoulé et un plafond de mémoire de pointe, exprimés sous forme de contrat vérifié. Il énonce une obligation. Ce n’est pas un résultat de banc d’essai.
  • Les chiffres concrets sont traités comme un signal vivant : ils sont mesurés selon une méthode déclarée, et non figés dans la prose où ils pourraient devenir obsolètes sans qu’on s’en aperçoive.

Toute l’architecture du générateur en flux découle d’une seule décision : ne jamais retenir ce que tu peux émettre.

  1. Start page A single active cursor ; no document-wide page graph in memory.
  2. Finalise page Page content + page object written straight to the output stream.
  3. Release buffer The finalised page buffer is dropped ; the heap returns to baseline.
  4. Record offset to disk Xref and Kids entries go to php://temp/maxmemory:0 — immediate disk spill.
  5. Close Pages-tree root, Catalog, and trailer written once at the end.
Le cycle par page du générateur en flux : chaque page est émise puis libérée, et la comptabilité croissante est envoyée vers des flux temporaires adossés au disque, de sorte que le tas ne croît pas avec le nombre de pages.

Le détail qui fait tenir l’ensemble, c’est le déversement sur disque. Le flux php://temp de PHP conserve une petite quantité en mémoire et ne déverse que lorsqu’il dépasse un seuil. Le générateur ouvre ces flux temporaires avec l’option maxmemory:0, qui les force à déverser immédiatement : le seuil en mémoire est nul. Concrètement, la comptabilité par objet, qui par définition croît avec le document, ne s’accumule jamais dans le tas. Elle s’accumule sur le disque, où la taille n’est pas le facteur limitant. Sans cette option, la fenêtre en mémoire par défaut se remplirait avant le déversement, ce qui réduirait à néant l’objectif de mémoire bornée au moment précis où il compte le plus.

Le budget de performance est l’autre moitié du sujet, et c’est un contrat porté par le système de documentation plutôt qu’une promesse marketing. Le schéma définit un budget comme deux entiers bornés : un plafond de temps écoulé en millisecondes et un plafond de mémoire résidente de pointe en mébioctets. Une recette qui déclare un budget déclare une obligation qui peut être vérifiée, de la même manière qu’une signature typée déclare une obligation qu’un compilateur peut vérifier. La valeur d’un budget tient au fait qu’il est déclaré et appliqué, pas au fait qu’il soit petit.

Cette page est Evidence: Mixed evidence , et ce classement est délibéré parce que les preuves sont véritablement de trois natures.

  • Mécanisme étayé par le code. Le générateur en flux dans src/Writer/Streaming/StreamingPdfWriter.php documente et implémente le cycle page par page « émettre puis libérer » et ouvre ses flux xref et Kids avec php://temp/maxmemory:0 pour forcer le déversement immédiat sur disque, de sorte que « la mémoire PHP reste bornée quel que soit le nombre d’objets. » La conception en flux, à curseur unique et sans arbre retenu, est aussi la décision d’architecture consignée dans l’ADR-001 (le pipeline de rendu ne conserve au plus qu’un état en O(profondeur), et non O(n) nœuds).
  • Budget en tant que principe de conception. Le champ performance_budget est une partie réelle, optionnelle, du schéma de documentation, défini comme { wall_ms, peak_mb } avec des bornes supérieures explicites. C’est un contrat exécutoire par conception.
  • Le banc d’essai comme signal vivant. L’ADR-001 est explicite : les mesures contrôlées de mémoire de pointe et de temps écoulé pour un document volumineux sont une cible empirique à collecter et à consigner selon une méthode déclarée — et non un chiffre à affirmer dans la prose. Cette page énonce donc le mécanisme et le contrat, et renvoie les chiffres concrets à l’endroit qui les mesure.

Le format rend cet objectif raisonnable plutôt qu’idéaliste. Parce que la table de références croisées est un index de décalages par objet conforme à Spec: ISO 32000-2, §7.5.4 , un générateur est capable d’écrire les objets à mesure qu’il les termine et de ne conserver que leurs décalages. La mémoire bornée est cohérente avec le format de fichier, et non une lutte contre lui.

La mémoire bornée est une propriété de ta façon de générer, pas un simple drapeau à activer. Une boucle de traitement par lots qui finalise et libère chaque document maintient le tas à plat tout au long de l’exécution :

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Core\PdfFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// Process-lifetime, shared once.
$factory = PdfFactory::new()
->withCompress(true)
->withDocumentFactory(new DocumentFactory(
new FontRegistry(),
new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024),
));
// Per-document, created and released each iteration.
foreach ($invoiceBatch as $invoice) {
$doc = $factory->create();
$doc->addPage();
$doc->writeHtml($invoice->toHtml());
$doc->save($invoice->outputPath());
unset($doc); // the document model is not carried into the next iteration
}

Les registres sont partagés parce qu’analyser les polices et les images une seule fois est tout l’intérêt d’un worker. Le document, lui, n’est pas partagé et il est libéré à chaque itération — c’est ce qui maintient la mémoire du traitement par lots bornée par un seul document, et non par le lot.

L’idée fausse la plus répandue consiste à traiter la « mémoire bornée » comme un résultat de banc d’essai, et donc à attendre un chiffre en mégaoctets à citer. Cela inverse le propos. La garantie est ici structurelle : le générateur est construit de sorte que conserver un document ne coûte pas davantage à mesure que des pages sont ajoutées. Un chiffre de pointe précis dépend du contenu des pages, des polices et des images, et n’a de sens qu’accompagné de sa méthode de mesure, raison pour laquelle il relève du banc d’essai, et non de cette phrase.

Un second piège : supposer que php://temp te protège déjà. C’est vrai — mais seulement une fois que sa fenêtre en mémoire par défaut est pleine. C’est l’option maxmemory:0 qui rend le déversement immédiat. Ce détail est le mécanisme. Sans elle, la propriété ne tiendrait pas précisément pour les documents volumineux pour lesquels elle existe.

Cette page explique le mécanisme de diffusion en flux et la signification d’un budget de performance. Elle n’énonce pas de chiffres mesurés de mémoire de pointe ou de débit. Ceux-ci sont produits par la démarche de banc d’essai selon une méthode déclarée, et l’ADR-001 renvoie explicitement les chiffres empiriques à cette mesure. « Bornée par document » ne signifie pas constante indépendamment du contenu d’un document donné : une page comportant de nombreuses images intégrées volumineuses coûte toujours ce que coûtent ces images. Ce qui ne croît pas, c’est la comptabilité par page et le graphe de pages retenu. Tous les chemins de génération ne passent pas par le générateur en flux. Les chemins qui diffusent en flux et ceux qui mettent en tampon sont déterminés par le code et la forme du pipeline, et non par ce tour d’horizon. Le mécanisme décrit est exact à la date de revue de cette page. Les sources faisant autorité sont src/Writer/Streaming/ et l’ADR-001 dans le dépôt core.

La conception en flux à mémoire bornée est une propriété de Core. Les éditions ne la modifient pas :

Bounded-memory streaming writer — edition availability
Edition Availability
Core Core fournit la conception du générateur en flux qui déverse sur disque.
Pro Pro hérite du même générateur à mémoire bornée ; il ajoute des fonctionnalités, pas un modèle de mémoire différent.
Enterprise Enterprise hérite du même générateur à mémoire bornée ; il ajoute des fonctionnalités, pas un modèle de mémoire différent.
  • Mémoire bornée — une propriété de conception dans laquelle conserver le document ne consomme pas davantage de tas à mesure que des pages sont ajoutées (l’objectif d’O(1) par page).
  • Générateur en flux — le générateur qui émet chaque page vers la sortie et libère son tampon, au lieu de conserver le document entier.
  • php://temp/maxmemory:0 — un flux temporaire PHP forcé à déverser immédiatement sur disque, utilisé pour la comptabilité par objet croissante.
  • Budget de performance — un contrat de documentation structuré : un plafond de temps écoulé et un plafond de mémoire de pointe, déclarés et vérifiables.
  • Signal vivant — une valeur mesurée, rapportée avec sa méthode dans des conditions déclarées, plutôt qu’un chiffre figé dans la prose.