Aller au contenu

Génération de documents à grande échelle

Spec: ISO 24495-1:2023, §5 Spec: ISO 9241-112:2025, §6.1.2.3 Evidence: Benchmark-backed

Générer un seul PDF relève d’un appel de fonction. En générer cent mille selon un planning relève d’un problème système : la mémoire doit rester bornée, le travail doit être parallélisé et les chiffres doivent avoir un sens. Cette page suit le scénario de génération par lots depuis la question du débit jusqu’à un déploiement capable de tenir la charge. Elle pose clairement que la réponse honnête est « mesure-le sur tes documents », et non un chiffre vitrine.

La génération par lots échoue typiquement de deux façons. La première est la dérive mémoire. Un worker à longue durée de vie accumule de l’état retenu document après document jusqu’à être tué en plein lot ; l’exécution n’est alors ni terminée ni proprement mise en échec. La seconde est le chiffre affirmé mais dépourvu de sens : un test de performance fondé sur un document trivial sert à dimensionner un parc qui rend des documents complexes, et son erreur n’apparaît qu’en charge de production.

Tu peux éviter les deux, à condition de concevoir dès le départ le modèle mémoire et la méthode de mesure, au lieu de les ajouter après le premier incident.

  • L’unité de travail est un document jetable, pas un document partagé. Conserve les données à durée de vie du processus (polices, cache d’images) dans des registres partagés ; crée puis jette le document à chaque rendu.
  • La mémoire a deux composantes, et une seule menace un worker à longue durée de vie. Le pic transitoire pendant un rendu est attendu ; la mémoire retenue qui ne revient pas est la fuite qui met fin à un lot.
  • Le débit, c’est du parallélisme et un coût borné par rendu. La forme qui tient la charge est une file d’attente alimentant des workers sans état, chacun effectuant un rendu puis libérant.
  • Un chiffre sans sa méthode n’est pas un chiffre. NextPDF rapporte les mesures par rendu comme des données à collecter, et refuse les allégations de vitesse non qualifiées. Le chiffre le plus important est celui que tu mesures sur tes propres gabarits (ISO 24495-1 §5.x11 — place le message qui compte là où le lecteur le trouve).

L’architecture repose sur une seule décision : l’état qui vit pour le processus est partagé et immuable ; l’état qui vit pour un rendu est neuf et jeté. Les polices sont des données structurelles analysées une fois puis verrouillées, de sorte qu’aucun rendu ne peut les muter et polluer le suivant. Le cache d’images est un magasin borné de type least-recently-used qui n’est jamais verrouillé, afin que la mémoire reste plafonnée sans fuir d’une requête à l’autre. La fabrique de documents est un singleton sans état ; chaque document qu’elle crée est jetable.

Cette séparation permet d’exécuter un worker pendant des heures sous Octane, RoadRunner ou Swoole. Elle élimine par construction le mode de défaillance où « la requête N corrompt la requête N+1 », plutôt que de compter sur un document qui se réinitialiserait de lui-même.

Le scénario se déroule en quatre étapes.

  1. Warm the shared state once On worker boot, parse and lock the font registry and size the image cache. This cost is paid once, not per document.
  2. Enqueue the work A queue holds the render jobs. The queue is the throughput dial — workers scale horizontally behind it.
  3. Render on a disposable document Each worker creates a fresh document from the factory, renders, emits the bytes, and lets the document go.
  4. Measure, then size Collect per-render time and peak memory. Size the fleet from measurements on your own templates, not a generic figure.
Le scénario à grand volume de bout en bout : l'état immuable partagé est préchauffé une fois ; chaque tâche effectue son rendu sur un document jetable puis libère ; le débit monte en charge en ajoutant des workers, pas en en agrandissant un.

Les ponts de framework font de cette forme le comportement par défaut, plutôt qu’un assemblage à refaire à la main. Le service provider Laravel enregistre le registre de polices comme un singleton préchauffé et verrouillé, et lie le document comme une instance neuve à chaque résolution. Il fournit une tâche mise en file d’attente avec un nombre d’essais borné, un délai d’expiration et un backoff exponentiel. Cette tâche valide son chemin de sortie côté worker, car une charge utile de file d’attente sérialisée peut être altérée en transit. Les intégrations Symfony et CodeIgniter suivent la même discipline de document jetable et de registre partagé.

Le modèle mémoire est étayé par le code. Evidence: Code-backed Le NextPdfServiceProvider Laravel enregistre le FontRegistry comme un singleton préchauffé puis verrouillé par lock(), le ImageRegistry comme un singleton doté d’une LRU bornée et délibérément non verrouillé, et le Document comme une liaison par résolution via une fabrique sans état. Le modèle de document jetable est dans le câblage, pas dans la prose. Le GeneratePdfJob déclare tries, timeout et backoff, puis revalide son chemin de sortie à l’intérieur de handle().

La surface de mesure est étayée par des tests de performance. Evidence: Benchmark-backed Le moteur émet un RenderReport immuable par génération, avec le temps de rendu en millisecondes, le pic de mémoire en octets, le nombre de pages, le nombre d’avertissements et les occurrences de repli — les données exactes dont tu as besoin pour dimensionner un parc. Un analyseur distinct de fragmentation de la mémoire distingue le pic (transitoire) de la mémoire retenue. Cette distinction te dit si un worker à longue durée de vie est sain ou s’il fuit lentement. Le banc d’essai lui-même est configuré pour des itérations répétées avec préchauffage, car une mesure unique n’est que du bruit.

La discipline est un principe de conception : Evidence: Design principle NextPDF présente les performances avec leur méthode et refuse les allégations de vitesse sans qualification. C’est cohérent avec la façon dont cette documentation est rédigée — Spec: ISO 24495-1:2023, §5 place le message qui compte là où le lecteur le trouvera. Le message central ici est « mesure ta propre charge de travail ».

Le code ci-dessous illustre le modèle : une boucle de document jetable avec mesure. Le moteur produit le RenderReport. La file d’attente relève de ton infrastructure.

<?php
declare(strict_types=1);
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Observability\RenderReport;
use Psr\Log\LoggerInterface;
/**
* One batch worker iteration: render, emit, release, measure.
*
* The factory and its registries are process-lifetime singletons; the
* document is disposable. Retained memory must return to baseline between
* iterations or the worker is leaking.
*
* @param iterable<int, callable(\NextPDF\Core\Document): \NextPDF\Core\Document> $jobs
*/
function runBatch(
DocumentFactoryInterface $factory,
LoggerInterface $logger,
iterable $jobs,
): void {
foreach ($jobs as $jobId => $build) {
$startedAt = hrtime(true);
// Fresh, disposable document — shares the warmed registries.
$doc = $factory->create();
$doc = $build($doc);
$bytes = $doc->getPdfData();
// Hand the bytes off to your sink (object store, response, etc.).
unset($doc, $bytes); // let the per-render state go
$elapsedMs = (hrtime(true) - $startedAt) / 1_000_000;
$logger->info('pdf.render.complete', [
'job_id' => $jobId,
'render_time_ms' => round($elapsedMs, 2),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1_048_576, 2),
]);
}
}

Le unset() n’est pas cosmétique. L’état propre à chaque rendu doit être libéré à chaque itération afin que la mémoire retenue revienne à son niveau de référence. Un worker dont le niveau de référence grimpe au fil des itérations est précisément la défaillance que cette boucle est conçue pour éviter.

L’idée fausse la plus courante est « combien de PDF par seconde NextPDF peut-il produire ? », comme s’il y avait une seule réponse. Il n’y en a pas, et en citer une est précisément la façon dont les parcs sont mal dimensionnés. Le coût de rendu est dominé par le document ; le seul chiffre exploitable est donc celui mesuré sur tes propres gabarits avec le rapport par rendu du moteur lui-même. Un chiffre sans le document, le matériel et la méthode qui le sous-tendent est de la décoration, pas une donnée.

La seconde idée fausse est de croire que le pic de mémoire est la métrique à surveiller. Le pic est transitoire et attendu — il redescend. Le chiffre qui met fin à un lot est la mémoire retenue qui ne revient pas. C’est exactement pourquoi le moteur sépare les deux.

  • Il n’existe aucun chiffre de débit universel, et cette page n’en énonce délibérément aucun. Le coût de rendu dépend de tes documents ; mesure-le avec le rapport par rendu.
  • La mémoire bornée dépend de l’utilisation effective du modèle de document jetable. Conserver un document à travers plusieurs rendus, ou partager un état mutable par rendu, annule la garantie. Les ponts de framework adoptent par défaut la forme sûre. Un câblage manuel doit la reproduire.
  • Le cache d’images est borné, pas illimité. Sous de fortes charges d’images uniques, le LRU évince. C’est la conception, pas une régression.
  • Le dimensionnement du pool de workers, le choix de la file d’attente et l’autoscaling sont des décisions de déploiement hors du moteur. NextPDF fournit les mesures et la primitive bornée. Il n’exécute pas ta file d’attente.
  • RenderReport est une donnée, pas un verdict. Il te dit ce qui s’est passé lors d’un rendu. Transformer cela en plan de capacité relève de ton analyse.
  • Cette page est étayée par des tests de performance pour la surface de mesure et par le code pour le modèle mémoire. Elle n’affirme aucun taux spécifique.
Queued high-volume generation primitives — edition availability
Edition Availability
Core

Le modèle de document jetable, les registres immuables partagés, le RenderReport par rendu et l’analyseur de fragmentation de la mémoire relèvent du Core. La génération simple de PDF à grande échelle n’a besoin d’aucun palier commercial.

Pro

Mêmes primitives ; les fonctionnalités commerciales (signature, PDF/A) ajoutent un coût par rendu que tu devrais mesurer, pas supposer.

Enterprise

Mêmes primitives ; les traitements de facture structurée et de validation ajoutent un coût par rendu supplémentaire qui croît avec la charge utile et la taille du jeu de règles.

  • Mémoire et flux — comment le moteur maintient la mémoire bornée sur les grands documents et où il traite en flux.
  • Tests de performance honnêtes — ce que vaut un chiffre de test de performance sans sa méthode, et comment NextPDF rapporte les performances.
  • Exploiter NextPDF en production — transformer les rapports par rendu en signaux de santé une fois le lot réellement exécuté.
  • Document jetable — une instance de document créée pour un seul rendu puis jetée, de sorte qu’aucun état ne fuit vers le rendu suivant.
  • Registre partagé — état à durée de vie du processus, immuable après préchauffage (polices, cache d’images), réutilisé d’un rendu à l’autre sans coût par rendu.
  • Pic de mémoire — le maximum transitoire atteint pendant un rendu ; il est attendu et revient au niveau de référence.
  • Mémoire retenue — mémoire encore conservée après l’achèvement d’un rendu ; un niveau de référence retenu qui monte d’un rendu à l’autre est une fuite.
  • Worker — un processus à longue durée de vie qui tire des tâches de rendu depuis une file d’attente ; il doit rester borné en mémoire pour survivre à un lot.
  • RenderReport — l’instantané immuable de métriques par rendu du moteur (temps, pic de mémoire, nombre de pages, avertissements) utilisé pour dimensionner la capacité à partir de données réelles.