Aller au contenu
getnextpdf.com

Mémoire et diffusion en flux

Spec: ISO 32000-2, §7.5.4

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 pageA single active cursor ; no document-wide page graph in memory.
  2. Finalise pagePage content + page object written straight to the output stream.
  3. Release bufferThe finalised page buffer is dropped ; the heap returns to baseline.
  4. Record offset to diskXref and Kids entries go to php://temp/maxmemory:0 — immediate disk spill.
  5. ClosePages-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.

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
EditionAvailability
CoreCore fournit la conception du générateur en flux qui déverse sur disque.
ProPro 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.
EnterpriseEnterprise 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.