Ir al contenido

Generación de documentos de alto volumen

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

Generar un PDF es llamar a una función. Generar cien mil de forma programada es un problema de sistemas: memoria que debe mantenerse acotada, trabajo que debe paralelizarse y cifras que deben significar algo. Esta página recorre el escenario de generación por lotes, desde el problema de rendimiento hasta un despliegue capaz de resistir la carga. Deja claro que la respuesta honesta es «medirlo con los propios documentos», no una cifra de titular.

La generación por lotes suele fallar de dos formas características. La primera es el crecimiento progresivo de la memoria. Un proceso de trabajo de larga duración acumula estado retenido, documento tras documento, hasta que agota la memoria a mitad del lote; la ejecución no queda ni completa ni fallida de forma limpia. La segunda es una cifra aparentemente segura, pero sin significado operativo: se usa una prueba de rendimiento de un documento trivial para dimensionar una flota que representa documentos complejos, y solo bajo carga de producción se descubre que era incorrecta.

Ambas se pueden evitar, pero solo si desde el principio se diseñan la forma de la memoria y el método de medición, en lugar de añadirlos tras el primer incidente.

  • La unidad de trabajo es un documento desechable, no uno compartido. Mantener los datos de duración del proceso (fuentes, caché de imágenes) en registros compartidos; crear y descartar el documento en cada representación.
  • La memoria tiene dos partes, y solo una importa para un proceso de trabajo de larga duración. El pico transitorio durante una representación es esperado; la memoria retenida que no vuelve es la fuga que acaba con un lote.
  • El rendimiento es paralelismo más un coste por representación acotado. La forma que resiste es una cola que alimenta procesos de trabajo sin estado, cada uno dedicado a representar y liberar.
  • Una cifra sin su método no es una cifra. NextPDF informa de las mediciones por representación como datos que se recopilan, y rechaza las afirmaciones de velocidad sin matices. La cifra más importante es la que se mide con las propias plantillas (ISO 24495-1 §5.x11: el mensaje importante debe quedar donde el lector lo encuentre).

La arquitectura se construye en torno a una única decisión: el estado que vive durante el proceso es compartido e inmutable; el estado que vive durante una representación es nuevo y se desecha. Las fuentes son datos estructurales que se analizan una sola vez y luego se bloquean, de modo que ninguna representación pueda mutarlas ni contaminar la siguiente. La caché de imágenes es un almacén acotado de tipo «el menos usado recientemente» que nunca se bloquea, de modo que la memoria se mantiene limitada sin fugarse entre solicitudes. La fábrica de documentos es un singleton sin estado; todo documento que crea es desechable.

Esa separación hace seguro ejecutar un proceso de trabajo durante horas bajo Octane, RoadRunner o Swoole. Elimina por construcción el modo de fallo en el que «la solicitud N corrompe la solicitud N+1», en lugar de confiar en que el documento se reinicie por sí mismo.

El escenario tiene cuatro etapas.

  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.
El escenario de alto volumen de extremo a extremo: el estado compartido inmutable se precalienta una vez; cada trabajo se representa sobre un documento desechable y lo libera; el rendimiento escala añadiendo procesos de trabajo, no agrandando uno.

Los puentes de framework hacen que esta forma sea la predeterminada, no algo que haya que ensamblar a mano. El proveedor de servicios de Laravel registra el registro de fuentes como un singleton precalentado y bloqueado, y enlaza el documento como una instancia nueva por cada resolución. Incluye un trabajo en cola con un número de intentos acotado, un tiempo de espera y un retroceso exponencial. Ese trabajo valida su ruta de salida en el lado del proceso de trabajo, porque una carga útil de cola serializada puede manipularse en tránsito. Las integraciones de Symfony y CodeIgniter siguen la misma disciplina de documento desechable y registro compartido.

El modelo de memoria está respaldado por código. Evidence: Code-backed El NextPdfServiceProvider de Laravel registra el FontRegistry como un singleton que se precalienta y luego se bloquea mediante lock(), el ImageRegistry como un singleton de LRU acotado que deliberadamente no se bloquea, y el Document como un enlace por resolución a través de una fábrica sin estado. El modelo de documento desechable está en el cableado, no en la prosa. El GeneratePdfJob define tries, timeout y backoff, y revalida su ruta de salida dentro de handle().

La superficie de medición está respaldada por pruebas de rendimiento. Evidence: Benchmark-backed El motor emite un RenderReport inmutable por cada generación que contiene el tiempo de representación en milisegundos, el pico de memoria en bytes, el número de páginas, los recuentos de advertencias y las apariciones de mecanismos de reserva: las entradas exactas necesarias para dimensionar una flota. Un analizador de fragmentación de memoria independiente distingue el pico (transitorio) de la memoria retenida. Esa distinción permite saber si un proceso de trabajo de larga duración está sano o tiene fugas lentas. El propio banco de pruebas está configurado para repetir ciclos con precalentamiento, porque una sola medición de tiempo es ruido.

La disciplina es un principio de diseño: Evidence: Design principle NextPDF informa del rendimiento junto con su método y rechaza las afirmaciones de velocidad sin matices. Eso es coherente con la forma en que está escrita esta documentación: Spec: ISO 24495-1:2023, §5 coloca el mensaje que importa donde el lector lo encontrará. El mensaje que importa aquí es «medir la propia carga de trabajo».

El código siguiente muestra el bucle de documento desechable con medición e ilustra el modelo. El motor produce el RenderReport. La cola pertenece a la infraestructura de despliegue.

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

El unset() no es cosmético. El estado por representación está diseñado para liberarse en cada iteración, de modo que la memoria retenida vuelva al valor de referencia. Un proceso de trabajo cuyo valor de referencia aumenta a lo largo de las iteraciones es el fallo que este bucle está diseñado para evitar.

El concepto erróneo principal es «¿cuántos PDF por segundo puede generar NextPDF?», como si tuviera una sola respuesta. No la tiene; citar una es una forma de dimensionar mal las flotas. El coste de representación está dominado por el documento, así que la única cifra que vale la pena utilizar es la que se mide con las propias plantillas mediante el informe por representación del propio motor. Una cifra sin el documento, el hardware y el método que la respaldan es decoración, no datos.

El segundo concepto erróneo es que el pico de memoria es lo que se debe vigilar. El pico es transitorio y esperado: vuelve. La cifra que termina un lote es la memoria retenida que no vuelve. Precisamente por eso el motor separa ambas.

  • No existe una cifra de rendimiento universal, y esta página deliberadamente no indica ninguna. El coste de representación depende de los documentos; debe medirse con el informe por representación.
  • La memoria acotada depende de que se use el modelo de documento desechable. Mantener un documento a lo largo de muchas representaciones, o compartir estado mutable por representación, revierte la garantía. Los puentes de framework adoptan por defecto la forma segura. El cableado hecho a mano debe replicarla.
  • La caché de imágenes es acotada, no ilimitada. Bajo cargas de trabajo intensas con imágenes únicas, la LRU expulsa elementos. Ese es el diseño, no una regresión.
  • El dimensionamiento del grupo de procesos de trabajo, la elección de la cola y el autoescalado son decisiones de despliegue ajenas al motor. NextPDF proporciona las mediciones y la primitiva acotada. No ejecuta la cola.
  • RenderReport aporta datos, no un veredicto. Indica qué ocurrió en una representación. Convertir eso en un plan de capacidad es parte del análisis.
  • Esta página está respaldada por pruebas de rendimiento en cuanto a la superficie de medición y respaldada por código en cuanto al modelo de memoria. No afirma ninguna tasa concreta.
Queued high-volume generation primitives — edition availability
Edition Availability
Core

El modelo de documento desechable, los registros compartidos inmutables, el RenderReport por representación y el analizador de fragmentación de memoria son Core. La generación de PDF de alto volumen por sí sola no requiere ningún nivel comercial.

Pro

Las mismas primitivas; las funciones comerciales (firma, PDF/A) añaden un coste por representación que hay que medir, no suponer.

Enterprise

Las mismas primitivas; el trabajo de facturas estructuradas y validación añade un coste por representación adicional que escala con el tamaño de la carga útil y del conjunto de reglas.

  • Memoria y streaming: cómo el motor mantiene acotada la memoria en documentos grandes y en qué puntos transmite por streaming.
  • Pruebas de rendimiento honestas: qué valor tiene una cifra de prueba de rendimiento sin su método, y cómo NextPDF informa del rendimiento.
  • Operar NextPDF en producción: convertir los informes por representación en señales de salud cuando el lote se ejecuta realmente.
  • Documento desechable: una instancia de documento creada para una sola representación y descartada después, de modo que ningún estado se filtre a la siguiente representación.
  • Registro compartido: estado de duración del proceso, inmutable tras el precalentamiento (fuentes, caché de imágenes), reutilizado entre representaciones sin coste por representación.
  • Pico de memoria: la marca máxima transitoria durante una representación; es esperada y vuelve al valor de referencia.
  • Memoria retenida: memoria todavía retenida después de que termina una representación; un valor de referencia de memoria retenida que sube entre representaciones es una fuga.
  • Proceso de trabajo: un proceso de larga duración que extrae trabajos de representación de una cola; debe permanecer acotado en memoria para sobrevivir a un lote.
  • RenderReport: la instantánea inmutable de métricas por representación del motor (tiempo, pico de memoria, número de páginas, advertencias) que se usa para dimensionar la capacidad a partir de datos reales.