Потоковая обработка и память: профилирование и пакетные обработчики
Краткий обзор
Заголовок раздела «Краткий обзор»NextPDF выполняет отрисовку за один проход и никогда не хранит DOM всего документа, поэтому потребление памяти на стороне ввода ограничено глубиной вложенности, а не количеством элементов. На этой странице описаны модель потоковой обработки, ограничения, зафиксированные в Architecture Decision Record (ADR)-001, и безопасный способ запускать движок в длительно работающем обработчике очереди.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3Концептуальный обзор
Заголовок раздела «Концептуальный обзор»В NextPDF есть два пути записи с разным профилем памяти.
Устройство записи в память, используемое по умолчанию, формирует весь документ, а затем сериализует его. Пиковое потребление памяти пропорционально общему размеру выходных данных. Для типичных документов это подходит хорошо, но для очень больших может быть затратно.
Потоковое устройство записи сериализует каждую страницу по мере формирования, а затем сбрасывает её перед началом следующей страницы. Поставляемый движок — StreamingPdfWriter, StreamingCursor, DevNullWriter и перечисление WriterState в src/Writer/Streaming/ — полноценный, завершённый, протестированный и поставляется начиная с версии 3.1.0. Он доступен через контракты уровня experimental — StreamingWriterInterface и CursorInterface. Классы движка являются внутренними, поэтому опирайтесь на контракты и дайте Core предоставить реализацию. (Более ранняя аннотация в .ai/contracts-map.md ошибочно описывала потоковую обработку как “только контракт / без реализации”; этот дефект устаревшей аннотации отслеживается в задаче #610 и исправлен в контрактной документации B1 — движок поставляется начиная с версии 3.1.0.)
Потоковый движок спроектирован так, чтобы резидентная память не росла вместе с количеством страниц. Буфер каждой завершённой страницы передаётся устройству записи и освобождается. Таблица перекрёстных ссылок и ссылки дерева страниц /Kids записываются во временные потоки php://temp/maxmemory:0, которые сразу сбрасываются на диск, а не накапливаются в куче PHP. Сериализованный результат — это стандартное дерево страниц, где запись Count содержит число листовых узлов (объектов страниц), являющихся потомками узла (ISO 32000-2 §7.7.3.3), а запись Kids — массив косвенных ссылок на его непосредственные дочерние узлы (ISO 32000-2 §7.7.3.2). Точный профиль памяти относится к уровню experimental и может меняться между минорными выпусками, поэтому не закладывайте в код предположения, основанные на одном измерении.
ADR-001 определяет модель памяти конвейера отрисовки HTML. Токенизатор формирует список токенов за один проход. Парсер обрабатывает его слева направо и выдаёт операторы потока содержимого в строковый буфер. Постоянное дерево элементов не строится: парсер хранит не более одного HtmlStyleState на уровень вложенности, ограниченный значением MAX_NESTING_DEPTH = 100, и применяет жёсткий предел MAX_ELEMENT_COUNT = 50_000. Две операции, требующие упреждающего просмотра, — определение размеров столбцов таблицы и семейство селекторов :has() / :last-child — используют ограниченные массивы индексов предварительного сканирования по плоскому списку токенов, а не сохранённый DOM. Тест Phase 0 (docs/architecture/adr-001-memory-benchmark.md, выполнен 2026-04-06, PHP 8.5.3, memory_limit=1G) показал для документа из 50,000 элементов пик 50 MB для потокового пути против 4 MB для симуляции с сохранением частичной работы. Согласно отчёту, около 50 MB приходится на инвариантный для архитектуры накопленный поток содержимого; на этом наборе данных потоковая модель даёт преимущество на стороне ввода в 4–5 раз. Эти показатели получены на одной конкретной конфигурации и одном наборе данных, а не гарантированы.
Профилируйте память до настройки
Заголовок раздела «Профилируйте память до настройки»Измеряйте до того, как что-либо менять. Конвейер HTML контролируется через tools/perf-benchmark.php (запускается через composer ai:perf-check), который сообщает peak_memory_delta_bytes — приращение пика по каждой цели. Оно используется как ось регрессии, а не как абсолютный пик процесса. Базовая линия Cycle 36 (docs/architecture/PERFORMANCE-BUDGETS.md §6.3, зафиксирована 2026-05-17 на i9-13900K, 64 GB, PHP 8.5.3, opcache выключен) показала нулевое приращение пика для 12 из 16 пар target/mode. Четыре ненулевых приращения были отнесены к выделениям кэша шрифтов и буфера трассировки при первом обращении; при последующих отрисовках они остаются постоянными. Воспринимайте их как значения, наблюдавшиеся на той конфигурации, а не как переносимые константы. Для разового профилирования собственного документа снимайте значение memory_get_peak_usage(true) до и после отрисовки и сбрасывайте пик с помощью memory_reset_peak_usage() между итерациями — так же, как тест изолирует затраты по каждой цели.
Запуск NextPDF в пакетном обработчике
Заголовок раздела «Запуск NextPDF в пакетном обработчике»Обработчик очереди — это длительно работающий процесс PHP: он один раз загружает фреймворк, остаётся в памяти и обрабатывает задания в цикле. Именно поэтому он быстрый, и по этой же причине с памятью нужно обращаться аккуратно. Медленная утечка, незаметная в пределах одного запроса, может накапливаться на протяжении тысяч заданий. В PERFORMANCE-BUDGETS §1 этот режим отказа назван явно: обработчик, который отрисовывает множество PDF один за другим, может исчерпать память через несколько часов, даже если отдельные отрисовки выглядят нормально.
NextPDF поддерживает работу в среде обработчиков. DocumentFactory позволяет обработчику создавать новый документ для каждого задания и при этом совместно использовать FontRegistry и ImageRegistry, живущие столько же, сколько процесс. Поэтому шрифты и изображения разбираются один раз, а не для каждого задания. ADR-001 фиксирует, что парсер HTML создаётся для каждого запроса без статического изменяемого состояния, а будущие объекты контекста форматирования должны следовать той же области видимости в пределах запроса. Следующие шаги помогут настроить обработчик безопасно.
Шаг 1 — общий доступ к реестрам между заданиями
Заголовок раздела «Шаг 1 — общий доступ к реестрам между заданиями»Создайте реестры один раз при запуске процесса и используйте их повторно для каждого задания, следуя примеру examples/14-worker-factory.php:
<?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;
// Created once at process boot — not per job.$fontRegistry = new FontRegistry();$imageRegistry = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$documentFactory = new DocumentFactory($fontRegistry, $imageRegistry);
$factory = PdfFactory::new() ->withCompress(true) ->withDocumentFactory($documentFactory);
// Per job: a fresh document, shared registries.$doc = $factory->create();$doc->addPage();$doc->setFont('helvetica', '', 11);$doc->cell(0, 8, 'Rendered inside a worker.', newLine: true);$doc->save('/path/to/output.pdf');Параметр maxCacheBytes реестра изображений ограничивает общий кэш, поэтому между заданиями он не может расти без предела.
Шаг 2 — ограничьте время жизни обработчика
Заголовок раздела «Шаг 2 — ограничьте время жизни обработчика»Это общая практика управления процессами для любого обработчика PHP, а не гарантия движка NextPDF: периодически перезапускайте обработчики, чтобы длительно работающий процесс не накапливал память и не выполнял устаревший код без ограничения по времени. Обе основные системы очередей PHP предоставляют встроенные лимиты и корректные перезапуски.
Для очередей Laravel (https://laravel.com/docs/12.x/queues) команда queue:work запускает обработчик как длительно работающий процесс. Документированные параметры: --memory (по умолчанию 128 MB; обработчик завершается, когда его потребление памяти превышает предел), --max-jobs (завершение после заданного числа заданий) и --max-time (завершение после заданного числа секунд). Команда queue:restart сигнализирует обработчикам корректно завершиться после текущего задания, поэтому развёртывание или периодический таймер может перезапустить их, не прерывая текущую отрисовку. Laravel Horizon (https://laravel.com/docs/12.x/horizon) управляет обработчиками Redis со стратегией балансировки auto и корректной командой php artisan horizon:terminate, которая завершает выполняемые задания до того, как монитор процессов перезапустит супервизор.
Для Symfony Messenger (https://symfony.com/doc/current/messenger.html) команда messenger:consume по умолчанию работает бесконечно. Документированные параметры ограничения: --limit (обработать N сообщений, затем завершиться), --memory-limit (например 128M; завершение, когда память достигает предела) и --time-limit (например 3600; завершение по истечении интервала). Документация Symfony рекомендует запускать обработчик под управлением Supervisor или systemd, чтобы завершившийся процесс перезапускался автоматически. Команда messenger:stop-workers устанавливает флаг в кэше, который сообщает каждому обработчику: завершить обработку текущего сообщения и корректно остановиться.
Шаг 3 — перезапуск при развёртывании
Заголовок раздела «Шаг 3 — перезапуск при развёртывании»При каждом развёртывании подавайте сигнал на корректный перезапуск, чтобы обработчики подхватывали новый код: php artisan queue:restart (или php artisan horizon:terminate) для Laravel, php bin/console messenger:stop-workers для Symfony. Затем менеджер процессов — Supervisor, systemd или супервизор Horizon/Octane — запускает новый процесс с обновлённой кодовой базой. Это общая практика развёртывания для длительно работающих обработчиков PHP, не зависящая от NextPDF.
Производительность
Заголовок раздела «Производительность»Потоковый путь спроектирован так, чтобы ограничивать пиковое потребление памяти за счёт сброса каждой завершённой страницы и записи служебных данных перекрёстных ссылок и дерева страниц во временные потоки, размещаемые на диске. В результате резидентный набор памяти по замыслу не должен расти вместе с количеством страниц. Это поведение наблюдается в поставляемом движке 3.1.0 и зафиксировано его тестами воспроизводимости на эталонной базовой линии, но описано как проектное поведение, а не как фиксированное значение, поскольку профиль относится к уровню experimental. Потребление памяти конвейера HTML на стороне ввода ограничено значением MAX_NESTING_DEPTH = 100, а не количеством элементов (ADR-001). Все конкретные числа на этой странице привязаны к датированным артефактам — тесту ADR-001 от 2026-04-06 и базовой линии PERFORMANCE-BUDGETS Cycle 36 от 2026-05-17 — и получены на конфигурациях, указанных в этих документах; воспринимайте их как наблюдения, а не как переносимые гарантии. Значение performance_budget в 1500 мс / 64 MB — это ориентир для canvas, а не договорной предел.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»Метод writeContent() потокового курсора добавляет байты в поток содержимого страницы дословно. Он не проверяет синтаксис операторов. В обработчике, который отрисовывает содержимое, зависящее от вызывающей стороны, никогда не передавайте недоверенные данные в writeContent(); используйте writeText(), который в поставляемом курсоре экранирует данные для грамматики литеральных строк PDF. Выходной поток принадлежит вызывающей стороне: движок записывает в него, но никогда не закрывает и не открывает его повторно, поэтому он не может перенаправить вывод. Обработчик должен сам закрыть дескриптор после возврата метода close() устройства записи, иначе между заданиями произойдёт утечка файлового дескриптора. Совместное использование реестров между заданиями — это оптимизация производительности, а не граница доверия: общий ImageRegistry кэширует разобранные изображения, поэтому осознанно подбирайте значение maxCacheBytes и не рассчитывайте на изоляцию кэша между арендаторами в многоарендном обработчике.
Соответствие
Заголовок раздела «Соответствие»| Утверждение | Стандарт | Пункт | Свидетельство |
|---|---|---|---|
Потоковое устройство записи формирует дерево страниц, у которого запись Kids представляет собой массив косвенных ссылок на непосредственные дочерние узлы узла. | ISO 32000-2 | §7.7.3.2 | |
Потоковое устройство записи формирует запись Count, равную числу листовых объектов страниц, являющихся потомками узла дерева страниц. | ISO 32000-2 | §7.7.3.3 |
Пункты изложены своими словами и привязаны к глоссарию; нормативный текст не воспроизводится.
См. также
Заголовок раздела «См. также»- Contracts / Streaming —
experimentalStreamingWriterInterfaceиCursorInterface, а также их конечный автомат. - HTML / Streaming constraints (ADR-001) — решение об однопроходной обработке без сохранения DOM и пороги пересмотра.
- Performance — контроль регрессии задержки и памяти конвейера HTML.
- Layout — движки оформления страниц, которые не хранят состояние для каждой страницы.
- PERFORMANCE-BUDGETS — режим отказа с утечкой в обработчике и базовая линия контроля регрессии.