Salta ai contenuti

Generazione di documenti ad alto volume

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

Generare un PDF è una chiamata di funzione. Generarne centomila in base a una pianificazione è un problema di sistema: la memoria deve restare entro limiti controllati, il lavoro va parallelizzato e i numeri devono avere un significato. Questa pagina segue lo scenario di generazione in batch, dal problema del throughput fino a una distribuzione che regge il carico. Afferma chiaramente che la risposta onesta è «misuralo sui tuoi documenti», non una cifra a effetto.

La generazione in batch fallisce in due modi tipici. Il primo è l’aumento progressivo della memoria. Un worker a lunga durata accumula stato trattenuto, documento dopo documento, finché non viene terminato a metà del batch, e l’esecuzione non risulta né completa né fallita in modo pulito. Il secondo è un numero presentato con sicurezza ma privo di significato: un benchmark ricavato da un documento banale viene usato per dimensionare una flotta che esegue il rendering di documenti complessi, e si rivela errato solo sotto il carico di produzione.

Si possono evitare entrambi, ma solo progettando fin dall’inizio il modello di memoria e il metodo di misurazione, anziché aggiungerli dopo il primo incidente.

  • L’unità di lavoro è un documento usa e getta, non condiviso. Mantenere i dati con durata pari a quella del processo (font, cache delle immagini) nei registri condivisi; creare e scartare il documento per ogni rendering.
  • La memoria ha due parti, e solo una conta per un worker a lunga durata. Il picco transitorio durante un rendering è previsto; la memoria trattenuta che non viene rilasciata è la fuga che pone fine a un batch.
  • Il throughput è parallelismo più un costo limitato per rendering. La forma che regge è una coda che alimenta worker stateless: ciascuno esegue il rendering e rilascia.
  • Un numero senza il suo metodo non è un numero. NextPDF riporta le misurazioni per rendering come dati da raccogliere, e rifiuta affermazioni di velocità non qualificate. La cifra più importante è quella che si misura sui propri modelli (ISO 24495-1 §5.x11 — collocare il messaggio che conta dove il lettore lo trova).

L’architettura è costruita attorno a una sola decisione: lo stato che vive per il processo è condiviso e immutabile; lo stato che vive per un rendering è nuovo e viene scartato. I font sono dati strutturali analizzati una sola volta e poi bloccati, così nessun rendering può modificarli e contaminare quello successivo. La cache delle immagini è un archivio limitato di tipo least-recently-used che non viene mai bloccato, così la memoria resta limitata senza fughe tra le richieste. La factory dei documenti è un singleton stateless; ogni documento creato è usa e getta.

È questa separazione a rendere sicura l’esecuzione di un worker per ore sotto Octane, RoadRunner o Swoole. Elimina per costruzione la modalità di errore in cui «la richiesta N corrompe la richiesta N+1», anziché sperare che il documento si reinizializzi da sé.

Lo scenario ha quattro fasi.

  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.
Lo scenario ad alto volume dall'inizio alla fine: lo stato condiviso immutabile viene preriscaldato una sola volta; ogni job esegue il rendering su un documento usa e getta e rilascia; il throughput scala aggiungendo worker, non ingrandendone uno.

I bridge dei framework rendono questa forma predefinita, anziché lasciarla come qualcosa da assemblare. Il service provider di Laravel registra il registro dei font come singleton preriscaldato e bloccato e registra il documento come istanza nuova a ogni resolve. Include un job in coda con un numero limitato di tentativi, un timeout e un backoff esponenziale. Il job convalida il proprio percorso di output sul lato worker, perché un payload di coda serializzato può essere manomesso durante il transito. Le integrazioni Symfony e CodeIgniter seguono la stessa disciplina: documento usa e getta e registro condiviso.

Il modello di memoria è supportato dal codice. Evidence: Code-backed Il NextPdfServiceProvider di Laravel registra il FontRegistry come singleton preriscaldato e poi sottoposto a lock(), l’ImageRegistry come singleton limitato tramite LRU e deliberatamente non bloccato, e il Document come binding per-resolve tramite una factory stateless. Il modello di documento usa e getta è nel cablaggio, non nella prosa. Il GeneratePdfJob espone tries, timeout e backoff e riconvalida il proprio percorso di output all’interno di handle().

La superficie di misurazione è supportata da benchmark. Evidence: Benchmark-backed Per ogni generazione, il motore emette un RenderReport immutabile che riporta il tempo di rendering in millisecondi, il picco di memoria in byte, il numero di pagine, i conteggi degli avvisi e le occorrenze di fallback — gli input precisi necessari per dimensionare una flotta. Un analizzatore separato della frammentazione della memoria distingue la memoria di picco (transitoria) da quella trattenuta. Questa distinzione indica se un worker a lunga durata è sano o sta lentamente perdendo memoria. Lo stesso harness di benchmark è configurato per esecuzioni ripetute con riscaldamento, perché una singola misurazione del tempo è rumore.

La disciplina è un principio di progettazione: Evidence: Design principle NextPDF riporta le prestazioni insieme al metodo usato e rifiuta affermazioni di velocità non qualificate. Questo è coerente con il modo in cui è scritta questa documentazione — Spec: ISO 24495-1:2023, §5 colloca il messaggio che conta dove il lettore lo troverà. Il messaggio che conta qui è «misura il tuo carico di lavoro».

Il codice qui sotto è il ciclo con documento usa e getta e misurazione inclusa che illustra il modello. Il motore produce il RenderReport. La coda appartiene all’infrastruttura applicativa.

<?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),
]);
}
}

L’unset() non è un dettaglio cosmetico. Lo stato specifico del rendering è pensato per essere rilasciato a ogni iterazione, così che la memoria trattenuta torni al valore di base. Un worker il cui valore di base cresce tra le iterazioni è l’errore che questo ciclo è progettato per evitare.

L’errore principale è «quanti PDF al secondo può fare NextPDF?» come se avesse una sola risposta. Non esiste una risposta unica, e citarne una è il modo in cui le flotte vengono dimensionate male. Il costo del rendering è dominato dal documento, quindi l’unico numero su cui valga la pena agire è quello misurato sui propri modelli con il report per rendering prodotto dal motore. Una cifra priva del documento, dell’hardware e del metodo che la sostengono è decorazione, non dato.

Il secondo errore è ritenere che la memoria di picco sia l’aspetto da monitorare. Il picco è transitorio e previsto — viene rilasciato. Il numero che pone fine a un batch è la memoria trattenuta che non viene rilasciata. È esattamente per questo che il motore separa le due.

  • Non esiste una cifra di throughput universale, e questa pagina deliberatamente non ne indica nessuna. Il costo del rendering dipende dai propri documenti; misurare con il report per rendering.
  • La memoria limitata dipende dall’uso del modello di documento usa e getta. Trattenere un documento attraverso molti rendering, o condividere stato mutabile per rendering, annulla la garanzia. I bridge dei framework adottano per impostazione predefinita la forma sicura. Il cablaggio fatto a mano deve replicarla.
  • La cache delle immagini è limitata, non illimitata. Sotto carichi di lavoro intensi con immagini uniche, la LRU effettua espulsioni. È il comportamento previsto dal progetto, non una regressione.
  • Il dimensionamento del pool di worker, la scelta della coda e l’autoscaling sono decisioni di distribuzione esterne al motore. NextPDF fornisce le misurazioni e la primitiva a memoria limitata. Non gestisce la coda.
  • RenderReport è un dato, non un verdetto. Indica cosa è accaduto in un rendering. Trasformarlo in un piano di capacità richiede un’analisi dedicata.
  • Questa pagina è supportata da benchmark per la superficie di misurazione e dal codice per il modello di memoria. Non afferma alcun valore specifico.
Queued high-volume generation primitives — edition availability
Edition Availability
Core

Il modello di documento usa e getta, i registri condivisi immutabili, il RenderReport per rendering e l’analizzatore della frammentazione della memoria sono parte di Core. La semplice generazione di PDF ad alto volume non richiede alcun livello commerciale.

Pro

Stesse primitive; le funzionalità commerciali (firma, PDF/A) aggiungono un costo per rendering da misurare, non da presumere.

Enterprise

Stesse primitive; il lavoro di fattura strutturata e convalida aggiunge un ulteriore costo per rendering che scala con la dimensione del payload e del set di regole.

  • Memoria e streaming — come il motore mantiene la memoria limitata su documenti di grandi dimensioni e dove ricorre allo streaming.
  • Benchmarking onesto — che valore ha un numero di benchmark senza il suo metodo, e come NextPDF riporta le prestazioni.
  • Gestione di NextPDF in produzione — trasformare i report per rendering in segnali di salute una volta che il batch è in esecuzione reale.
  • Documento usa e getta — un’istanza di documento creata per un singolo rendering e poi scartata, così che nessuno stato si propaghi al rendering successivo.
  • Registro condiviso — stato con durata pari a quella del processo, immutabile dopo il preriscaldamento (font, cache delle immagini), riutilizzato tra i rendering senza costo per rendering.
  • Memoria di picco — il livello massimo transitorio raggiunto durante un rendering; previsto e destinato a tornare al valore di base.
  • Memoria trattenuta — memoria ancora occupata dopo il completamento di un rendering; un valore di base trattenuto che aumenta tra i rendering è una fuga.
  • Worker — un processo a lunga durata che preleva job di rendering da una coda; deve mantenere la memoria entro limiti controllati per sopravvivere a un batch.
  • RenderReport — l’istantanea immutabile delle metriche per rendering del motore (tempo, memoria di picco, numero di pagine, avvisi) usata per dimensionare la capacità a partire da dati reali.