Aller au contenu
getnextpdf.com

Génération de documents à grande échelle

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

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 onceOn worker boot, parse and lock the font registry and size the image cache. This cost is paid once, not per document.
  2. Enqueue the workA queue holds the render jobs. The queue is the throughput dial — workers scale horizontally behind it.
  3. Render on a disposable documentEach worker creates a fresh document from the factory, renders, emits the bytes, and lets the document go.
  4. Measure, then sizeCollect 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 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
EditionAvailability
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.