Pular para o conteúdo

Memória e streaming

Spec: ISO 32000-2, §7.5.4 Evidence: Mixed evidence

Um PDF grande não deveria exigir um heap grande. Esta página explica como o NextPDF mantém a memória do processo limitada conforme o documento cresce, quando faz streaming para o disco em vez de acumular dados na memória, e o que “performance budget” significa aqui: um contrato verificado, não um número chamativo.

O formato PDF não obriga um gerador a usar um heap grande. A tabela de referência cruzada registra um deslocamento em bytes para cada objeto indireto; portanto, um leitor precisa de acesso aleatório ao arquivo, não do arquivo inteiro na memória. Um gerador pode seguir esse modelo: emitir objetos à medida que são concluídos e lembrar apenas onde eles ficaram. Se, em vez disso, o documento inteiro permanecer no heap até a gravação final, a contagem de páginas faz a memória crescer linearmente, e um relatório que funciona bem com cem páginas faz o processo cair com cinquenta mil.

Para cargas de trabalho em lote e em workers, essa é a diferença entre um serviço estável e outro que falha de forma imprevisível sob carga. Memória limitada é uma propriedade de design que precisa ser concebida intencionalmente, não um número que se espera alcançar.

  • O streaming writer é construído para manter a memória limitada por documento. Cada página é gravada na saída assim que é finalizada. Em seguida, seu buffer é liberado.
  • O controle interno que, de outra forma, cresceria com a contagem de objetos — os deslocamentos da referência cruzada e as referências Kids da árvore de páginas — é gravado em streams temporários abertos com php://temp/maxmemory:0, que extravasam imediatamente para o disco, em vez de ocupar o heap do PHP.
  • O objetivo de design é heap O(1) por página: manter o documento não fica mais caro à medida que páginas são adicionadas. Essa é a meta de engenharia em torno da qual o writer é moldado.
  • Um performance budget é um conceito real e estruturado no sistema de documentação: um limite de tempo de relógio e um limite de pico de memória, expressos como um contrato verificado. Ele declara uma obrigação, não um resultado de benchmark.
  • Números concretos são tratados como um living signal, medidos segundo um método declarado, e não congelados na prosa, onde poderiam ficar silenciosamente desatualizados.

O streaming writer segue uma decisão simples: nunca retenha o que você pode emitir.

  1. Start page A single active cursor; no document-wide page graph in memory.
  2. Finalise page Page content + page object written straight to the output stream.
  3. Release buffer The finalised page buffer is dropped; the heap returns to baseline.
  4. Record offset to disk Xref and Kids entries go to php://temp/maxmemory:0 — immediate disk spill.
  5. Close Pages-tree root, Catalog, and trailer written once at the end.
O ciclo por página do streaming writer: cada página é emitida e liberada, e o controle interno crescente é enviado a streams temporários respaldados em disco, de modo que o heap não cresce com a contagem de páginas.

O detalhe do extravasamento para o disco é o que sustenta tudo. O php://temp do PHP mantém uma pequena quantidade de dados na memória e só extravasa quando excede um limite. O writer abre esses streams temporários com a opção maxmemory:0, que os força a extravasar imediatamente — o limite em memória é zero. O efeito prático é que o controle interno por objeto, que por definição cresce com o documento, nunca se acumula no heap. Ele se acumula no disco, onde o tamanho não é a restrição principal. Sem essa opção, a janela em memória padrão teria que encher antes de extravasar, o que anularia o objetivo de memória limitada justamente quando ele mais importa.

O performance budget é a outra metade da história. Ele é um contrato do sistema de documentação, não uma afirmação de marketing. O schema define um budget como dois inteiros limitados: um limite de tempo de relógio em milissegundos e um limite de pico de memória residente em mebibytes. Uma recipe que declara um budget assume uma obrigação verificável, da mesma forma que uma assinatura tipada declara uma obrigação que um compilador pode verificar. O valor de um budget está no fato de ele ser declarado e aplicado, não no fato de ser pequeno.

Esta página é Evidence: Mixed evidence , e essa combinação é intencional porque as evidências são, de fato, de três tipos.

  • Mecanismo respaldado por código. O streaming writer em src/Writer/Streaming/StreamingPdfWriter.php documenta e implementa o ciclo por página de emitir e depois liberar, e abre seus streams de xref e Kids com php://temp/maxmemory:0 para forçar o extravasamento imediato para o disco, de modo que “a memória do PHP permanece limitada independentemente da contagem de objetos.” O design de streaming, com cursor único e sem árvore retida, também é a decisão arquitetural registrada no ADR-001 (a pipeline de renderização mantém no máximo um estado O(depth), não O(n) nós).
  • Budget como princípio de design. O campo performance_budget é uma parte real e opcional do schema de documentação, definido como { wall_ms, peak_mb } com limites superiores explícitos. Ele é, por design, um contrato aplicável.
  • Benchmark como living signal. O ADR-001 afirma explicitamente que os valores controlados de pico de memória e tempo de relógio para documentos grandes são uma meta empírica a ser coletada e registrada sob um método declarado — não um número a ser afirmado na prosa. Por isso, esta página declara o mecanismo e o contrato, e direciona os valores concretos para o lugar que os mede.

O formato torna o objetivo razoável, não apenas aspiracional. Como a tabela de referência cruzada é um índice de deslocamentos por objeto conforme a Spec: ISO 32000-2, §7.5.4 , um gerador é capaz de gravar objetos à medida que os conclui e manter apenas seus deslocamentos. A memória limitada é coerente com o formato de arquivo; não é uma luta contra ele.

Memória limitada é uma propriedade de como você gera o documento, não uma flag que você define. Um loop em lote que finaliza e libera cada documento mantém o heap estável ao longo da execução:

<?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;
// Process-lifetime, shared once.
$factory = PdfFactory::new()
->withCompress(true)
->withDocumentFactory(new DocumentFactory(
new FontRegistry(),
new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024),
));
// Per-document, created and released each iteration.
foreach ($invoiceBatch as $invoice) {
$doc = $factory->create();
$doc->addPage();
$doc->writeHtml($invoice->toHtml());
$doc->save($invoice->outputPath());
unset($doc); // the document model is not carried into the next iteration
}

Os registries são compartilhados porque analisar fontes e imagens uma única vez é justamente o propósito de um worker. O documento não é compartilhado; ele é liberado a cada passagem — é isso que mantém a memória do lote limitada por um documento, não pelo lote.

O equívoco mais comum é tratar “memória limitada” como uma afirmação de benchmark — esperar um número em megabytes para citar. Isso inverte o que está sendo dito. A garantia aqui é estrutural: o writer é construído de modo que manter um documento não fica mais caro à medida que páginas são adicionadas. Um valor de pico específico depende do conteúdo da página, das fontes e das imagens, e só faz sentido com o método de medição associado. Por isso, ele pertence a um benchmark, não a esta frase.

Uma segunda armadilha é supor que o php://temp já protege você. Ele protege — mas só depois que sua janela em memória padrão se enche. A opção maxmemory:0 é o que torna o extravasamento imediato. O detalhe é o mecanismo. Sem ela, a propriedade não se sustentaria justamente nos documentos grandes para os quais ela existe.

Esta página explica o mecanismo de streaming e o significado de um performance budget. Ela não declara valores medidos de pico de memória ou de throughput. Esses valores são produzidos pela disciplina de benchmarking, sob um método declarado, e o ADR-001 transfere explicitamente os números empíricos para essa medição. Limitado “por documento” não significa constante independentemente do conteúdo de um único documento: uma página com muitas imagens grandes incorporadas ainda custa o que essas imagens custam. O que não cresce é o controle interno por página e o grafo de páginas retido. Nem todo caminho de geração usa o streaming writer. Quais caminhos fazem streaming e quais fazem buffer é determinado pelo código e pelo formato da pipeline, não por esta visão geral. O mecanismo descrito está correto na data de revisão desta página. As fontes oficiais são src/Writer/Streaming/ e o ADR-001 no repositório core.

O design de streaming e memória limitada é uma propriedade do Core. As edições não o alteram:

Bounded-memory streaming writer — edition availability
Edition Availability
Core O Core fornece o design de writer com streaming e extravasamento para disco.
Pro O Pro herda o mesmo writer de memória limitada; ele adiciona recursos, não um modelo de memória diferente.
Enterprise O Enterprise herda o mesmo writer de memória limitada; ele adiciona recursos, não um modelo de memória diferente.
  • Memória limitada — uma propriedade de design em que manter o documento não consome mais heap à medida que páginas são adicionadas (o objetivo de O(1) por página).
  • Streaming writer — o writer que emite cada página na saída e libera o buffer dela em vez de reter o documento inteiro.
  • php://temp/maxmemory:0 — um stream temporário do PHP forçado a extravasar para o disco imediatamente, usado para o controle interno por objeto que cresce ao longo do documento.
  • Performance budget — um contrato de documentação estruturado: um limite de tempo de relógio e um limite de pico de memória, declarados e verificáveis.
  • Living signal — um valor medido reportado com seu método sob condições declaradas, em vez de um número fixo embutido na prosa.