Ir al contenido

Memoria y streaming

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

Un PDF grande no debería exigir mucha memoria. Esta página explica cómo NextPDF mantiene acotado el montículo del proceso a medida que crece un documento, cuándo transmite al disco en lugar de acumular en memoria y qué significa aquí un «presupuesto de rendimiento»: un contrato verificable, no una cifra para titulares.

El formato PDF no obliga a un generador a consumir mucha memoria. Su tabla de referencias cruzadas registra un desplazamiento en bytes para cada objeto indirecto, de modo que un lector solo necesita acceso aleatorio al archivo, no el archivo completo en memoria. Un generador puede replicar ese modelo: puede emitir los objetos a medida que los termina y recordar únicamente dónde quedaron. Si, en cambio, el documento entero permanece en el montículo hasta la escritura final, el número de páginas empuja la memoria de forma lineal, y un informe que funciona bien con cien páginas hace fallar el proceso con cincuenta mil.

Para las cargas de trabajo por lotes y los procesos trabajadores, esta es la diferencia entre un servicio estable y uno que falla de forma impredecible bajo carga. La memoria acotada es una propiedad de diseño que hay que construir, no una cifra en la que confiar con optimismo.

  • El escritor de streaming está diseñado para que la memoria se mantenga acotada por documento. Cada página se escribe en la salida en cuanto se finaliza. Después se libera su búfer.
  • El registro contable que, de otro modo, crecería con el número de objetos —los desplazamientos de las referencias cruzadas y las referencias Kids del árbol de páginas— se escribe en flujos temporales abiertos con php://temp/maxmemory:0, que se vuelcan al disco de inmediato en lugar de llenar el montículo de PHP.
  • El objetivo de diseño es O(1) de montículo por página: mantener el documento no cuesta más a medida que se añaden páginas. Ese es el objetivo de ingeniería en torno al cual se diseña el escritor.
  • Un presupuesto de rendimiento es un concepto real y estructurado dentro del sistema de documentación: un límite de tiempo de reloj y un límite de memoria máxima, expresados como un contrato verificable. Establece una obligación. No es el resultado de una prueba de rendimiento.
  • Las cifras concretas se tratan como una señal viva, medida con un método declarado, no congeladas en la prosa, donde podrían quedar obsoletas sin que nadie lo note.

Toda la forma del escritor de streaming se deriva de una sola decisión: no retener nunca lo que se puede 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.
El ciclo por página del escritor de streaming: cada página se emite y se libera, y el registro contable creciente se envía a flujos temporales respaldados en disco, de modo que el montículo no crece con el número de páginas.

El detalle del volcado a disco es lo que sostiene todo. El php://temp de PHP mantiene una pequeña cantidad en memoria y solo se vuelca cuando supera un umbral. El escritor abre esos flujos temporales con la opción maxmemory:0, que los obliga a volcarse de inmediato: el umbral en memoria es cero. El efecto práctico es que el registro contable por objeto, que por definición crece con el documento, nunca se acumula en el montículo. Se acumula en el disco, donde el tamaño no es la restricción. Sin esa opción, la ventana en memoria predeterminada tendría que llenarse antes de volcarse, lo que frustraría el objetivo de memoria acotada precisamente cuando más importa.

El presupuesto de rendimiento es la otra mitad de la historia, y es un contrato del sistema de documentación, no una afirmación de marketing. El esquema define un presupuesto como dos enteros acotados: un límite de tiempo de reloj en milisegundos y un límite de memoria residente máxima en mebibytes. Al declarar un presupuesto, una receta declara una obligación verificable, del mismo modo que una firma tipada declara una obligación que un compilador puede verificar. El valor de un presupuesto está en que se declara y se hace cumplir, no en que sea pequeño.

Esta página es Evidence: Mixed evidence , y la mezcla es deliberada porque la evidencia pertenece realmente a tres clases.

  • Mecanismo respaldado por código. El escritor de streaming en src/Writer/Streaming/StreamingPdfWriter.php documenta e implementa el ciclo por página de emitir y luego liberar, y abre sus flujos de xref y Kids con php://temp/maxmemory:0 para forzar el volcado inmediato a disco, de modo que «la memoria de PHP se mantiene acotada con independencia del número de objetos». El diseño de streaming, con un único cursor y sin árbol retenido, es además la decisión arquitectónica registrada en ADR-001 (la canalización de renderizado mantiene como máximo un estado O(profundidad), no O(n) nodos).
  • Presupuesto como principio de diseño. El campo performance_budget es una parte real y opcional del esquema de documentación, definida como { wall_ms, peak_mb } con límites superiores explícitos. Es un contrato exigible por diseño.
  • Prueba de rendimiento como señal viva. ADR-001 afirma explícitamente que las cifras controladas de memoria máxima y tiempo de reloj para documentos grandes son un objetivo empírico que debe recopilarse y registrarse con un método declarado, no una cifra que se afirme en la prosa. Por eso esta página expone el mecanismo y el contrato, y remite las cifras concretas al lugar que las mide.

El formato hace que el objetivo sea razonable en lugar de aspiracional. Como la tabla de referencias cruzadas es un índice de desplazamientos por objeto según Spec: ISO 32000-2, §7.5.4 , un generador es capaz de escribir los objetos a medida que los termina y conservar solo sus desplazamientos. La memoria acotada es coherente con el formato de archivo, no una lucha contra él.

La memoria acotada es una propiedad de la forma de generar, no un indicador que se active. Un bucle por lotes que finaliza y libera cada documento mantiene el montículo plano a lo largo de toda la ejecución:

<?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
}

Los registros se comparten porque analizar las fuentes y las imágenes una sola vez es la razón de ser de un proceso trabajador. El documento no se comparte, y se libera en cada iteración, que es lo que mantiene la memoria del lote acotada por un documento, no por el lote.

El error más común es tratar la «memoria acotada» como una afirmación de prueba de rendimiento y esperar una cifra de megabytes para citar. Eso invierte lo que se está afirmando. La garantía aquí es estructural: el escritor está diseñado de modo que mantener un documento no cuesta más a medida que se añaden páginas. Una cifra de pico concreta depende del contenido de la página, las fuentes y las imágenes, y solo tiene sentido acompañada de su método de medición, razón por la cual pertenece a una prueba de rendimiento y no a esta frase.

Una segunda trampa: suponer que php://temp ya te protege. Lo hace, pero solo después de que se llene su ventana en memoria predeterminada. La opción maxmemory:0 es lo que hace que el volcado sea inmediato. El detalle es el mecanismo. Sin ella, la propiedad no se sostendría precisamente con los documentos grandes para los que existe.

Esta página explica el mecanismo de streaming y el significado de un presupuesto de rendimiento. No indica cifras medidas de memoria máxima ni de rendimiento (throughput). Esas cifras las produce la disciplina de pruebas de rendimiento con un método declarado, y ADR-001 remite explícitamente las cifras empíricas a esa medición. Acotada «por documento» no significa constante con independencia del contenido de un único documento: una página con muchas imágenes incrustadas de gran tamaño sigue costando lo que cuestan esas imágenes. Lo que no crece es el registro contable por página y el grafo de páginas retenido. No toda vía de generación es la del escritor de streaming. Qué vías transmiten y cuáles almacenan en búfer lo determinan el código y la forma de la canalización, no esta descripción general. El mecanismo descrito es exacto en la fecha de revisión de esta página. Las fuentes autorizadas son src/Writer/Streaming/ y ADR-001 en el repositorio del núcleo.

El diseño de streaming y memoria acotada es una propiedad del núcleo (Core). Las ediciones no lo modifican:

Bounded-memory streaming writer — edition availability
Edition Availability
Core Core aporta el diseño del escritor de streaming con volcado a disco.
Pro Pro hereda el mismo escritor de memoria acotada; añade funciones, no un modelo de memoria diferente.
Enterprise Enterprise hereda el mismo escritor de memoria acotada; añade funciones, no un modelo de memoria diferente.
  • Memoria acotada: una propiedad de diseño en la que mantener el documento no consume más montículo a medida que se añaden páginas (el objetivo de O(1) por página).
  • Escritor de streaming: el escritor que emite cada página a la salida y libera su búfer en lugar de retener el documento completo.
  • php://temp/maxmemory:0: un flujo temporal de PHP forzado a volcarse al disco de inmediato, usado para el registro contable por objeto que va creciendo.
  • Presupuesto de rendimiento: un contrato estructurado de documentación: un límite de tiempo de reloj y un límite de memoria máxima, declarados y verificables.
  • Señal viva: un valor medido que se reporta junto con su método bajo condiciones declaradas, en lugar de una cifra fija incrustada en la prosa.