Streaming et mémoire : tutoriel de profilage et de workers par lots
En un coup d’œil
Section intitulée « En un coup d’œil »NextPDF effectue son rendu en une seule passe et ne conserve jamais de DOM au niveau du document : la mémoire côté entrée reste donc bornée par la profondeur d’imbrication plutôt que par le nombre d’éléments. Cette page explique le modèle de streaming, les contraintes imposées par ADR-001 et la façon d’exécuter le moteur en toute sécurité dans un worker de file d’attente à longue durée de vie.
Installation
Section intitulée « Installation »composer require nextpdf/core:^3Vue d’ensemble conceptuelle
Section intitulée « Vue d’ensemble conceptuelle »NextPDF propose deux chemins d’écriture aux profils mémoire différents.
L’écrivain en mémoire par défaut compose l’intégralité du document avant de le sérialiser. Le pic de mémoire suit la taille totale de la sortie : c’est acceptable pour des documents classiques, mais coûteux pour des documents très volumineux.
L’écrivain en streaming sérialise chaque page au fur et à mesure de sa composition et la libère avant le démarrage de la page suivante. Le moteur livré — StreamingPdfWriter, StreamingCursor, DevNullWriter, et l’énumération WriterState dans src/Writer/Streaming/ — est réel, finalisé, testé et livré depuis la 3.1.0. Il est exposé via les contrats de niveau experimental StreamingWriterInterface et CursorInterface. Les classes du moteur sont internes : tu t’appuies donc sur le contrat et tu laisses le cœur fournir l’implémentation. (Une ancienne annotation de .ai/contracts-map.md décrivait à tort le streaming comme « contrat seul / pas d’implémentation » ; il s’agit d’un défaut d’annotation périmée suivi dans le ticket #610 et corrigé dans la documentation des contrats B1 — le moteur est livré depuis la 3.1.0.)
L’objectif de conception du moteur de streaming est d’empêcher que la mémoire résidente croisse avec le nombre de pages. Le tampon de chaque page finalisée est remis à l’écrivain puis libéré, et la table de références croisées ainsi que les références /Kids de l’arbre de pages sont écrites dans des flux temporaires php://temp/maxmemory:0 qui se déversent immédiatement sur disque au lieu de s’accumuler dans le tas PHP. Le résultat sérialisé est un arbre de pages standard dont l’entrée Count correspond au nombre de nœuds feuilles (objets page) descendant d’un nœud (ISO 32000-2 §7.7.3.3) et dont l’entrée Kids est un tableau de références indirectes vers les enfants immédiats de ce nœud (ISO 32000-2 §7.7.3.2). Le profil mémoire exact relève du niveau experimental et peut changer d’une version mineure à l’autre — ne code pas en dur une hypothèse tirée d’une seule mesure.
ADR-001 régit le modèle mémoire du pipeline de rendu HTML. Le tokeniseur produit une liste de tokens en une seule passe ; le parseur la consomme de gauche à droite et émet des opérateurs de flux de contenu dans un tampon de chaînes. Aucun arbre d’éléments persistant n’est construit : le parseur conserve au plus un HtmlStyleState par niveau d’imbrication, borné par MAX_NESTING_DEPTH = 100, et applique un plafond strict MAX_ELEMENT_COUNT = 50_000. Les deux opérations qui nécessitent un lookahead — le dimensionnement des colonnes de tableau et la famille de sélecteurs :has() / :last-child — utilisent des tableaux d’index de pré-balayage bornés sur la liste plate de tokens, et non un DOM conservé. Le benchmark de la phase 0 (docs/architecture/adr-001-memory-benchmark.md, exécuté le 2026-04-06, PHP 8.5.3, memory_limit=1G) a mesuré un document de 50,000 éléments avec un pic de 50 Mo pour le chemin de streaming, contre une simulation de travail partiel conservé à 4 Mo. L’analyse du rapport attribue environ 50 Mo de ce total au flux de contenu accumulé, invariant de l’architecture, et isole un avantage côté entrée de 4 à 5 fois pour le modèle de streaming sur cette fixture. Ces chiffres ont été observés sur ce seul banc d’essai et cette seule fixture ; ce n’est pas une garantie.
Profile la mémoire avant d’ajuster quoi que ce soit
Section intitulée « Profile la mémoire avant d’ajuster quoi que ce soit »Mesure avant toute modification. Le pipeline HTML est contrôlé par tools/perf-benchmark.php (lancé via composer ai:perf-check), qui rapporte peak_memory_delta_bytes — le pic incrémental par cible qui constitue l’axe de régression, et non le pic absolu du processus. La référence du cycle 36 (docs/architecture/PERFORMANCE-BUDGETS.md §6.3, capturée le 2026-05-17 sur un i9-13900K, 64 Go, PHP 8.5.3, opcache désactivé) a observé un delta de pic de 0 octet sur 12 des 16 paires target/mode, les quatre deltas non nuls étant attribués aux allocations de premier accès du cache de polices et du tampon de trace, qui restent constantes lors des rendus suivants. Lis ces mesures comme des valeurs observées pour ce banc d’essai, pas comme des constantes portables. Pour un profilage ponctuel de ton propre document, échantillonne memory_get_peak_usage(true) avant et après le rendu, puis réinitialise le pic avec memory_reset_peak_usage() entre les itérations, de la même façon que le benchmark isole le coût par cible.
Exécuter NextPDF dans un worker par lots
Section intitulée « Exécuter NextPDF dans un worker par lots »Un worker de file d’attente est un processus PHP à longue durée de vie : il amorce le framework une seule fois et reste résident pour traiter les jobs en boucle. C’est ce qui le rend rapide, et c’est aussi ce qui rend l’hygiène mémoire importante. Une fuite lente, invisible sur une seule requête, s’accumule sur des milliers de jobs. PERFORMANCE-BUDGETS §1 nomme explicitement ce mode de défaillance : un worker qui rend de nombreux PDF coup sur coup peut épuiser la mémoire au bout de quelques heures, même quand les rendus isolés semblent corrects.
NextPDF prend en charge les environnements de worker. DocumentFactory permet à un worker de créer un document neuf par job tout en partageant un FontRegistry et un ImageRegistry pendant toute la durée de vie du processus, de sorte que l’analyse des polices et des images se fasse une fois plutôt qu’à chaque job. ADR-001 consigne que le parseur HTML est construit par requête, sans état mutable statique, et que les futurs objets de contexte de formatage doivent suivre le même cadrage par requête. Les étapes suivantes montrent comment configurer un worker en toute sécurité.
Étape 1 — Partage les registres entre les jobs
Section intitulée « Étape 1 — Partage les registres entre les jobs »Crée les registres une seule fois à l’amorçage du processus et réutilise-les pour chaque job, en suivant examples/14-worker-factory.php :
<?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;
// Created once at process boot — not per job.$fontRegistry = new FontRegistry();$imageRegistry = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$documentFactory = new DocumentFactory($fontRegistry, $imageRegistry);
$factory = PdfFactory::new() ->withCompress(true) ->withDocumentFactory($documentFactory);
// Per job: a fresh document, shared registries.$doc = $factory->create();$doc->addPage();$doc->setFont('helvetica', '', 11);$doc->cell(0, 8, 'Rendered inside a worker.', newLine: true);$doc->save('/path/to/output.pdf');Le maxCacheBytes du registre d’images borne le cache partagé afin qu’il ne puisse pas croître sans limite au fil des jobs.
Étape 2 — Borne la durée de vie du worker
Section intitulée « Étape 2 — Borne la durée de vie du worker »C’est une pratique générale de contrôle de processus pour tout worker PHP, pas une garantie du moteur NextPDF : redémarre les workers périodiquement pour qu’un processus à longue durée de vie ne puisse pas accumuler de mémoire ni exécuter indéfiniment du code périmé. Les deux principaux systèmes de file d’attente PHP fournissent des limites intégrées et des redémarrages en douceur.
Pour les files d’attente Laravel (https://laravel.com/docs/12.x/queues), la commande queue:work exécute le worker comme un processus à longue durée de vie. Les options documentées sont --memory (128 Mo par défaut ; le worker quitte quand sa mémoire dépasse la limite), --max-jobs (quitter après un certain nombre de jobs) et --max-time (quitter après un certain nombre de secondes). La commande queue:restart signale aux workers qu’ils doivent quitter proprement après le job en cours, afin qu’un déploiement ou un minuteur périodique puisse les recycler sans interrompre un rendu en cours. Laravel Horizon (https://laravel.com/docs/12.x/horizon) supervise les workers Redis avec une stratégie d’équilibrage auto et un php artisan horizon:terminate en douceur, qui termine les jobs en cours avant que le moniteur de processus ne redémarre le superviseur.
Pour Symfony Messenger (https://symfony.com/doc/current/messenger.html), la commande messenger:consume s’exécute indéfiniment par défaut. Les options de limite documentées sont --limit (traiter N messages puis quitter), --memory-limit (par exemple 128M ; quitter quand la mémoire atteint la limite) et --time-limit (par exemple 3600 ; quitter après l’intervalle). La documentation de Symfony recommande d’exécuter le worker sous Supervisor ou systemd afin qu’un processus terminé redémarre automatiquement, et messenger:stop-workers inscrit un drapeau dans le cache pour indiquer à chaque worker de terminer son message en cours et de quitter proprement.
Étape 3 — Redémarre au déploiement
Section intitulée « Étape 3 — Redémarre au déploiement »À chaque déploiement, signale un redémarrage en douceur pour que les workers prennent en compte le nouveau code : php artisan queue:restart (ou php artisan horizon:terminate) pour Laravel, php bin/console messenger:stop-workers pour Symfony. Le gestionnaire de processus — Supervisor, systemd, ou le superviseur Horizon/Octane — démarre alors un processus neuf avec le nouveau code. C’est une pratique de déploiement générale pour les workers PHP à longue durée de vie et c’est indépendant de NextPDF.
Performance
Section intitulée « Performance »La conception du chemin de streaming borne le pic de mémoire en libérant chaque page terminée et en déversant la comptabilité des références croisées et de l’arbre de pages vers des flux temporaires adossés au disque, de sorte que l’ensemble résident est censé ne pas croître avec le nombre de pages. Ce comportement a été observé dans le moteur 3.1.0 livré et figé par ses tests de reproductibilité à référence dorée, mais il est énoncé comme un comportement de conception plutôt que comme un chiffre fixe, car le profil est une propriété de niveau experimental. La mémoire côté entrée du pipeline HTML est bornée par MAX_NESTING_DEPTH = 100 plutôt que par le nombre d’éléments (ADR-001). Tous les chiffres concrets de cette page sont liés à un artefact daté — le benchmark ADR-001 du 2026-04-06 et la référence du cycle 36 de PERFORMANCE-BUDGETS du 2026-05-17 — et ont été observés sur les bancs d’essai que ces documents nomment ; traite-les comme des observations, pas comme des garanties portables. Le performance_budget de 1500 ms / 64 Mo est l’enveloppe du canvas, pas un plafond contractuel.
Notes de sécurité
Section intitulée « Notes de sécurité »Le writeContent() du curseur de streaming ajoute des octets au flux de contenu de la page tels quels — il ne valide pas la syntaxe des opérateurs. Dans un worker qui rend du contenu influencé par l’appelant, ne passe jamais d’entrée non fiable à writeContent() ; utilise writeText(), que le curseur livré échappe pour la grammaire des chaînes littérales PDF. L’appelant possède le flux de sortie : le moteur y écrit mais ne le ferme jamais et ne le rouvre jamais, il ne peut donc pas rediriger la sortie — un worker doit fermer le handle lui-même une fois que le close() de l’écrivain retourne, sinon il laisse fuir un descripteur de fichier d’un job à l’autre. Le partage des registres entre les jobs est une optimisation de performance, pas une frontière de confiance : un ImageRegistry partagé met en cache les images analysées, alors dimensionne son maxCacheBytes délibérément et ne suppose pas que le cache est isolé entre locataires dans un worker multi-locataire.
Conformité
Section intitulée « Conformité »| Affirmation | Norme | Article | Preuve |
|---|---|---|---|
L’écrivain en streaming émet un arbre de pages dont l’entrée Kids est un tableau de références indirectes vers les enfants immédiats du nœud. | ISO 32000-2 | §7.7.3.2 | |
L’écrivain en streaming émet une entrée Count égale au nombre d’objets de page feuilles descendant du nœud de l’arbre de pages. | ISO 32000-2 | §7.7.3.3 |
Les articles sont paraphrasés et rattachés au glossaire ; aucun texte normatif n’est reproduit.
Voir aussi
Section intitulée « Voir aussi »- Contracts / Streaming — les contrats
experimentalStreamingWriterInterfaceetCursorInterface, ainsi que leur machine à états. - HTML / Contraintes de streaming (ADR-001) — la décision en une seule passe, sans DOM conservé, avec ses seuils de réexamen.
- Performance — le portail de régression de latence et de mémoire du pipeline HTML.
- Layout — les moteurs de mise en page sans état conservé par page.
- PERFORMANCE-BUDGETS — le mode de défaillance du worker qui fuit et la référence du portail de régression.