Перейти к содержимому

Память и потоковая запись

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

Большой PDF не должен требовать большой кучи. На этой странице объясняется, как NextPDF удерживает потребление памяти процесса в заданных пределах по мере роста документа, где выполняет потоковую запись на диск вместо накопления в памяти и что здесь означает «бюджет производительности»: проверяемый контракт, а не громкая цифра.

Формат PDF не вынуждает генератор использовать большую кучу. Его таблица перекрёстных ссылок записывает байтовое смещение для каждого косвенного объекта, поэтому средству чтения нужен произвольный доступ к файлу, а не весь файл в памяти. Генератор может следовать этой модели: выдавать объекты по мере их завершения и запоминать только то, куда они были записаны. Если же весь документ остаётся в куче до финальной записи, расход памяти растёт линейно с количеством страниц, и отчёт, который нормально работает при ста страницах, может уронить процесс при пятидесяти тысячах.

Для пакетных и фоновых нагрузок это разница между стабильным сервисом и сервисом, который непредсказуемо отказывает под нагрузкой. Ограниченное потребление памяти — это проектное свойство: его нужно реализовать, а не надеяться на удачное число.

  • Потоковый писатель устроен так, чтобы потребление памяти оставалось ограниченным в расчёте на документ. Каждая страница записывается в вывод сразу после завершения. Затем её буфер освобождается.
  • Служебные данные, которые иначе росли бы вместе с количеством объектов — смещения перекрёстных ссылок и ссылки Kids дерева страниц — записываются во временные потоки, открытые с помощью php://temp/maxmemory:0. Эти потоки сразу сбрасываются на диск, а не заполняют кучу PHP.
  • Проектная цель — O(1) кучи на страницу: удержание документа не становится дороже по мере добавления страниц. Именно вокруг этой инженерной цели выстроен писатель.
  • «Бюджет производительности» — это реальная структурированная сущность в системе документации: предел по реальному времени и предел по пиковой памяти, выраженные как проверяемый контракт. Он формулирует обязательство. Это не результат теста производительности.
  • Конкретные числа рассматриваются как живой сигнал: их измеряют по заявленной методике, а не фиксируют в тексте, где они могли бы незаметно устареть.

Потоковый писатель следует простому правилу: не удерживать то, что можно выдать.

  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.
Постраничный цикл потокового писателя: каждая страница выдаётся и освобождается, а растущие служебные данные отправляются во временные потоки на диске, поэтому куча не растёт вместе с количеством страниц.

Сброс на диск здесь ключевой. php://temp в PHP держит небольшой объём данных в памяти и сбрасывает их на диск только при превышении порога. Писатель открывает эти временные потоки с параметром maxmemory:0, который заставляет их сбрасываться на диск немедленно: порог хранения в памяти равен нулю. Практический эффект таков: служебные данные на каждый объект, которые по определению растут вместе с документом, никогда не накапливаются в куче. Они накапливаются на диске, где размер обычно не является ограничивающим фактором. Без этого параметра окно хранения в памяти по умолчанию сначала заполнилось бы и только потом сбросилось на диск, что свело бы на нет цель ограниченного потребления памяти именно тогда, когда это важнее всего.

Сам бюджет производительности — вторая половина истории. Это контракт системы документации, а не маркетинговое утверждение. Схема определяет бюджет как два целочисленных ограничения: предел по реальному времени в миллисекундах и предел по пиковому резидентному потреблению памяти в мебибайтах. Рецепт, объявляющий бюджет, объявляет проверяемое обязательство — так же как типизированная сигнатура объявляет обязательство, которое может проверить компилятор. Ценность бюджета в том, что он заявлен и обеспечен соблюдением, а не в том, что он мал.

Эта страница относится к категории Evidence: Mixed evidence , и это сделано намеренно: свидетельства действительно бывают трёх видов.

  • Механизм, подтверждённый кодом. Потоковый писатель в src/Writer/Streaming/StreamingPdfWriter.php документирует и реализует постраничный цикл «выдать, затем освободить» и открывает свои потоки xref и Kids с php://temp/maxmemory:0, чтобы принудительно выполнить немедленный сброс на диск, так что “PHP memory stays bounded regardless of object count.” Потоковая архитектура с одним курсором и без удержания дерева в памяти — это также архитектурное решение, зафиксированное в ADR-001 (конвейер отрисовки удерживает не более чем O(depth) состояния, а не O(n) узлов).
  • Бюджет как принцип проектирования. Поле performance_budget — реальная, необязательная часть схемы документации, определённая как { wall_ms, peak_mb } с явными верхними границами. По сути это контракт, соблюдение которого можно обеспечить.
  • Тест производительности как живой сигнал. В ADR-001 прямо указано, что контролируемые показатели пиковой памяти и реального времени для больших документов — это эмпирический ориентир, который нужно собирать и фиксировать по заявленной методике, а не число, которое утверждается в тексте. Поэтому эта страница излагает механизм и контракт, а за конкретными цифрами отсылает туда, где их измеряют.

Формат делает эту цель обоснованной, а не просто желательной. Поскольку таблица перекрёстных ссылок — это индекс смещений на каждый объект согласно Spec: ISO 32000-2, §7.5.4 , генератор способен записывать объекты по мере их завершения и хранить только их смещения. Ограниченное потребление памяти согласуется с форматом файла, а не борется с ним.

Ограниченное потребление памяти — это свойство способа генерации, а не флаг, который вы устанавливаете. Пакетный цикл, который завершает и освобождает каждый документ, удерживает кучу на ровном уровне на протяжении всего выполнения:

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

Реестры используются совместно, потому что однократный разбор шрифтов и изображений — в этом и состоит смысл фонового обработчика. Документ не используется совместно и освобождается на каждом проходе. Благодаря этому потребление памяти при пакетной обработке ограничено одним документом, а не всей партией.

Самое распространённое заблуждение — воспринимать «ограниченное потребление памяти» как утверждение из теста производительности и ждать число в мегабайтах, которое можно процитировать. Это переворачивает смысл сказанного. Гарантия здесь структурная: писатель устроен так, чтобы удержание документа не обходилось дороже по мере добавления страниц. Конкретное пиковое значение зависит от содержимого страницы, шрифтов и изображений и имеет смысл только вместе с методикой измерения, поэтому ему место в тесте производительности, а не на этой странице.

Вторая ловушка — думать, что php://temp уже защищает вас. Защищает, но только после того, как заполнится его окно хранения в памяти по умолчанию. Именно параметр maxmemory:0 делает сброс на диск немедленным. Эта деталь и есть механизм. Без него это свойство не выполнялось бы как раз на тех больших документах, ради которых оно существует.

Эта страница объясняет механизм потоковой записи и значение бюджета производительности. Она не приводит измеренные показатели пиковой памяти или пропускной способности. Их даёт тестирование производительности по заявленной методике, и ADR-001 явно оставляет эмпирические числа за этим измерением. Ограниченность «в расчёте на документ» не означает постоянство независимо от содержимого отдельного документа: страница со множеством крупных встроенных изображений по-прежнему требует столько памяти, сколько стоят эти изображения. Не растут служебные данные на каждую страницу и удерживаемый граф страниц. Не каждый путь генерации использует потоковый писатель. Какие пути выполняют потоковую запись, а какие буферизуют, определяется кодом и формой конвейера, а не этим обзором. Описанный механизм точен на дату проверки этой страницы. Авторитетные источники — src/Writer/Streaming/ и ADR-001 в основном репозитории.

Архитектура с потоковой записью и ограниченным потреблением памяти — это свойство Core. Редакции этого не меняют:

Bounded-memory streaming writer — edition availability
Edition Availability
Core Core предоставляет архитектуру писателя с потоковой записью и сбросом на диск.
Pro Pro наследует тот же писатель с ограниченным потреблением памяти; он добавляет возможности, а не другую модель памяти.
Enterprise Enterprise наследует тот же писатель с ограниченным потреблением памяти; он добавляет возможности, а не другую модель памяти.
  • Ограниченное потребление памяти — проектное свойство, при котором удержание документа не требует больше кучи по мере добавления страниц (цель O(1) на страницу).
  • Потоковый писатель — писатель, который выдаёт каждую страницу в вывод и освобождает её буфер, а не удерживает весь документ.
  • php://temp/maxmemory:0 — временный поток PHP, принудительно сбрасываемый на диск немедленно, используемый для растущих служебных данных на каждый объект.
  • Бюджет производительности — структурированный контракт документации: предел по реальному времени и предел по пиковой памяти, заявленные и проверяемые.
  • Живой сигнал — измеренное значение, сообщаемое вместе со своей методикой в заявленных условиях, а не фиксированное число, встроенное в текст.