Pular para o conteúdo

Geração de documentos em alto volume

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

Gerar um PDF é uma chamada de função. Gerar cem mil de forma agendada é um problema de sistemas: memória que precisa permanecer limitada, trabalho que precisa ser paralelo e números que precisam significar algo. Esta página percorre o cenário de geração em lote, do problema de throughput até uma implantação que se sustenta. Ela afirma claramente que a resposta honesta é “meça nos seus documentos”, e não um número chamativo.

A geração em lote falha de duas formas típicas. A primeira é o crescimento gradual da memória. Um worker de vida longa acumula estado retido, documento após documento, até ser encerrado no meio do lote, e a execução não termina nem falha de forma limpa. A segunda é um número confiante, porém sem sentido: um benchmark de um documento trivial é usado para dimensionar uma frota que renderiza documentos complexos, e o erro só fica evidente sob carga de produção.

Você pode evitar os dois problemas, mas apenas se projetar o modelo de memória e o método de medição desde o início, em vez de adicioná-los depois do primeiro incidente.

  • A unidade de trabalho é um documento descartável, não um documento compartilhado. Mantenha os dados de duração do processo (fontes, cache de imagens) em registries compartilhados; crie e descarte o documento a cada renderização.
  • A memória tem duas partes, e apenas uma importa para um worker de vida longa. O pico transitório durante uma renderização é esperado; a memória retida que não volta é o vazamento que encerra um lote.
  • O throughput é paralelismo somado a um custo limitado por renderização. O formato que se sustenta é uma fila alimentando workers stateless, cada um renderizando e liberando.
  • Um número sem o seu método não é um número. O NextPDF reporta medições por renderização como dados que você coleta, e recusa alegações de velocidade não qualificadas. O número mais importante é aquele que você mede nos seus próprios templates (ISO 24495-1 §5.x11 — coloque a mensagem que importa onde o leitor vai encontrá-la).

A arquitetura é construída em torno de uma única decisão: o estado que vive durante o processo é compartilhado e imutável; o estado que vive durante uma renderização é novo e descartado. As fontes são dados estruturais analisados uma vez e então bloqueados, de modo que nenhuma renderização possa alterá-los e poluir a próxima. O cache de imagens é um armazenamento limitado, do tipo least-recently-used, que nunca é bloqueado, de modo que a memória permanece limitada sem vazar entre requisições. A factory de documentos é um singleton stateless; todo documento que ela cria é descartável.

Essa separação é o que torna seguro executar um worker por horas sob Octane, RoadRunner ou Swoole. Ela elimina por construção o modo de falha em que “a requisição N corrompe a requisição N+1”, em vez de torcer para que o documento se reinicialize sozinho.

O cenário tem quatro estágios.

  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.
O cenário de alto volume de ponta a ponta: o estado imutável compartilhado é aquecido uma vez; cada job renderiza em um documento descartável e libera; o throughput escala adicionando workers, não aumentando um deles.

As bridges de framework tornam esse formato o padrão, em vez de algo que você precisa montar. O service provider do Laravel registra o registry de fontes como um singleton aquecido e bloqueado, e vincula o documento como uma nova instância a cada resolução. Ele inclui um job enfileirado com tentativas limitadas, um timeout e backoff exponencial. Esse job valida o caminho de saída no lado do worker, porque um payload de fila serializado pode ser adulterado em trânsito. As integrações com Symfony e CodeIgniter seguem a mesma disciplina de documento descartável e registry compartilhado.

O modelo de memória é respaldado por código. Evidence: Code-backed O NextPdfServiceProvider do Laravel registra o FontRegistry como um singleton que é aquecido e então bloqueado com lock(), o ImageRegistry como um singleton bounded-LRU que deliberadamente não é bloqueado, e o Document como um binding por resolução via uma factory stateless. O modelo de documento descartável está no wiring, não na prosa. O GeneratePdfJob carrega tries, timeout e backoff e revalida o caminho de saída dentro de handle().

A superfície de medição é respaldada por benchmark. Evidence: Benchmark-backed O motor emite um RenderReport imutável por geração, carregando o tempo de renderização em milissegundos, pico de memória em bytes, contagem de páginas, contagens de avisos e ocorrências de fallback — as dados exatos de que você precisa para dimensionar uma frota. Um analisador separado de fragmentação de memória distingue a memória de pico (transitória) da memória retida. Essa distinção indica se um worker de vida longa está saudável ou vazando lentamente. O próprio harness de benchmark é configurado para iterações repetidas com aquecimento, porque uma única medição de tempo é só ruído.

A disciplina é um princípio de design: Evidence: Design principle o NextPDF reporta desempenho junto com o seu método e recusa alegações de velocidade não qualificadas. Isso é consistente com a forma como esta documentação é escrita — Spec: ISO 24495-1:2023, §5 coloca a mensagem que importa em um ponto onde o leitor a encontrará. A mensagem que importa aqui é “meça a sua própria carga de trabalho”.

O código abaixo é o loop com documento descartável e medição. O motor produz o RenderReport; a fila fica por conta da sua infraestrutura.

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

O unset() não é cosmético. O estado por renderização deve ser liberado a cada iteração para que a memória retida retorne ao baseline. Um worker cujo baseline sobe ao longo das iterações apresenta exatamente a falha que este loop foi projetado para evitar.

O equívoco principal é “quantos PDFs por segundo o NextPDF consegue fazer?” como se houvesse uma única resposta. Não há, e citar uma resposta é como as frotas acabam mal dimensionadas. O custo de renderização é dominado pelo documento, então o único número que vale a pena usar como base é aquele medido nos seus próprios templates usando o relatório por renderização do próprio motor. Um número sem o documento, o hardware e o método por trás dele é decoração, não dado.

O segundo equívoco é que a memória de pico é o que se deve observar. O pico é transitório e esperado — ele volta. O número que encerra um lote é a memória retida que não volta. É exatamente por isso que o motor separa os dois.

  • Não existe um número universal de throughput, e esta página deliberadamente não declara nenhum. O custo de renderização depende dos seus documentos; meça usando o relatório por renderização.
  • A memória limitada depende do uso do modelo de documento descartável. Manter um documento ao longo de muitas renderizações, ou compartilhar estado mutável por renderização, anula a garantia. As bridges de framework adotam por padrão o formato seguro. O wiring manual precisa replicá-lo.
  • O cache de imagens é limitado, não ilimitado. Sob cargas pesadas de imagens únicas, o LRU faz despejo. Isso faz parte do design, não é uma regressão.
  • O dimensionamento do pool de workers, a escolha da fila e o autoscaling são decisões de implantação fora do motor. O NextPDF fornece as medições e a primitiva limitada. Ele não executa sua fila.
  • O RenderReport é dado, não um veredito. Ele informa o que aconteceu em uma renderização. Transformar isso em um plano de capacidade é a sua análise.
  • Esta página é respaldada por benchmark para a superfície de medição e respaldada por código para o modelo de memória. Ela não afirma nenhuma taxa específica.
Queued high-volume generation primitives — edition availability
Edition Availability
Core

O modelo de documento descartável, os registries imutáveis compartilhados, o RenderReport por renderização e o analisador de fragmentação de memória são Core. A geração simples de PDF em alto volume não precisa de nenhum tier comercial.

Pro

As mesmas primitivas; recursos comerciais (assinatura, PDF/A) adicionam custo por renderização que você deve medir, não presumir.

Enterprise

As mesmas primitivas; o trabalho com notas fiscais estruturadas e validação adiciona ainda mais custo por renderização que escala com o payload e o tamanho do conjunto de regras.

  • Memória e streaming — como o motor mantém a memória limitada em documentos grandes e onde ele faz streaming.
  • Benchmarking honesto — o valor de um número de benchmark sem o seu método, e como o NextPDF reporta desempenho.
  • Operando o NextPDF em produção — transformando relatórios por renderização em sinais de saúde quando o lote roda de verdade.
  • Documento descartável — uma instância de documento criada para uma única renderização e descartada depois, de modo que nenhum estado vaze para a próxima renderização.
  • Registry compartilhado — estado de duração do processo, imutável após o aquecimento (fontes, cache de imagens), reutilizado entre renderizações sem custo por renderização.
  • Memória de pico — a marca máxima transitória durante uma renderização; é esperada e retorna ao baseline.
  • Memória retida — memória ainda mantida depois que uma renderização termina; um baseline de memória retida crescente ao longo das renderizações é um vazamento.
  • Worker — um processo de vida longa que puxa jobs de renderização de uma fila; precisa permanecer com memória limitada para sobreviver a um lote.
  • RenderReport — o snapshot imutável de métricas por renderização do motor (tempo, memória de pico, contagem de páginas, avisos) usado para dimensionar capacidade a partir de dados reais.