Strumieniowanie i pamięć: samouczek dotyczący profilowania i procesów wsadowych
W skrócie
Dział zatytułowany „W skrócie”NextPDF renderuje w jednym przebiegu i nigdy nie utrzymuje modelu obiektowego dokumentu (DOM) dla całego dokumentu, dlatego zużycie pamięci po stronie wejścia ogranicza głębokość zagnieżdżenia, a nie liczba elementów. Ta strona wyjaśnia model strumieniowania, ograniczenia wynikające z dokumentu decyzji architektonicznej (ADR)-001 oraz sposób bezpiecznego uruchamiania silnika w długo działającym procesie kolejki.
Instalacja
Dział zatytułowany „Instalacja”composer require nextpdf/core:^3Przegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”NextPDF ma dwie ścieżki zapisu o różnych profilach pamięci.
Domyślny moduł zapisujący w pamięci komponuje cały dokument, a następnie go serializuje. Szczytowe zużycie pamięci odpowiada całkowitemu rozmiarowi wyniku. Sprawdza się to dobrze w przypadku typowych dokumentów, ale przy bardzo dużych może być kosztowne.
Strumieniowy moduł zapisujący serializuje każdą stronę w trakcie jej komponowania, a następnie opróżnia ją przed rozpoczęciem kolejnej strony. Dostarczany silnik — StreamingPdfWriter, StreamingCursor, DevNullWriter oraz wyliczenie WriterState w src/Writer/Streaming/ — istnieje jako finalna, przetestowana implementacja i jest dostarczany od wersji 3.1.0. Udostępniają go kontrakty poziomu experimental: StreamingWriterInterface i CursorInterface. Klasy silnika są wewnętrzne, więc opieraj się na kontraktach i pozwól, aby Core dostarczył implementację. (Wcześniejsza adnotacja .ai/contracts-map.md błędnie opisywała strumieniowanie jako „wyłącznie kontrakt / brak implementacji”; ten błąd w nieaktualnej adnotacji jest śledzony w zgłoszeniu #610 i poprawiony w dokumentacji kontraktów B1 — silnik jest dostarczany od wersji 3.1.0.)
Strumieniowy silnik zaprojektowano tak, aby pamięć rezydentna nie rosła wraz z liczbą stron. Bufor każdej sfinalizowanej strony jest przekazywany do modułu zapisującego i zwalniany. Tablica odsyłaczy (cross-reference) oraz odniesienia drzewa stron /Kids są zapisywane do strumieni tymczasowych php://temp/maxmemory:0, które od razu zrzucają dane na dysk, zamiast gromadzić je na stercie PHP. Zserializowany wynik to standardowe drzewo stron, którego wpis Count jest liczbą węzłów liści (obiektów stron) będących potomkami danego węzła (ISO 32000-2 §7.7.3.3), a wpis Kids jest tablicą odniesień pośrednich do bezpośrednich elementów potomnych tego węzła (ISO 32000-2 §7.7.3.2). Dokładny profil pamięci jest właściwością poziomu experimental i może się zmieniać między wydaniami pomocniczymi, więc nie zapisuj na stałe założeń opartych na jednym pomiarze.
ADR-001 reguluje model pamięci potoku renderowania HTML. Tokenizator wytwarza listę tokenów w jednym przebiegu. Parser przetwarza ją od lewej do prawej i emituje operatory strumienia treści do bufora tekstowego. Nie jest budowane trwałe drzewo elementów: parser utrzymuje najwyżej po jednym HtmlStyleState na poziom zagnieżdżenia, ograniczony przez MAX_NESTING_DEPTH = 100, i egzekwuje sztywny limit MAX_ELEMENT_COUNT = 50_000. Dwie operacje wymagające odczytu z wyprzedzeniem — ustalanie szerokości kolumn tabeli oraz rodzina selektorów :has() / :last-child — korzystają z ograniczonych tablic indeksów ze skanowania wstępnego nad płaską listą tokenów, a nie z utrzymywanego DOM. Test wydajności fazy 0 (docs/architecture/adr-001-memory-benchmark.md, wykonany 2026-04-06, PHP 8.5.3, memory_limit=1G) zmierzył dokument z 50,000 elementami, ze szczytem na poziomie 50 MB dla ścieżki strumieniowej, w porównaniu z 4 MB dla symulacji częściowego zachowywania pracy. Raport przypisuje około 50 MB z tego do niezmiennej architektonicznie, skumulowanej treści strumienia i wyodrębnia przewagę po stronie wejścia rzędu 4–5x dla modelu strumieniowego na tej próbce testowej. Te wartości zaobserwowano na tym jednym stanowisku i tej jednej próbce testowej; nie są gwarantowane.
Profiluj pamięć, zanim zaczniesz dostrajać
Dział zatytułowany „Profiluj pamięć, zanim zaczniesz dostrajać”Zmierz, zanim cokolwiek zmienisz. Potok HTML jest kontrolowany przez tools/perf-benchmark.php (uruchamiany przez composer ai:perf-check), który raportuje peak_memory_delta_bytes — szczytowy przyrost dla celu, używany jako oś regresji, a nie bezwzględny szczyt procesu. Punkt odniesienia z cyklu 36 (docs/architecture/PERFORMANCE-BUDGETS.md §6.3, zarejestrowany 2026-05-17 na i9-13900K, 64 GB, PHP 8.5.3, opcache wyłączony) wykazał zerowy przyrost szczytu pamięci dla 12 z 16 par target/mode. Cztery niezerowe przyrosty przypisano alokacjom pamięci podręcznej czcionek przy pierwszym użyciu oraz bufora śledzenia, które pozostają stałe przy późniejszych renderowaniach. Traktuj je jako wartości zaobserwowane na tym stanowisku, a nie jako przenośne stałe. Aby doraźnie sprofilować własny dokument, próbkuj memory_get_peak_usage(true) przed renderowaniem i po nim oraz resetuj szczyt za pomocą memory_reset_peak_usage() między iteracjami, tak samo jak test wydajności izoluje koszt dla celu.
Uruchamianie NextPDF w procesie wsadowym
Dział zatytułowany „Uruchamianie NextPDF w procesie wsadowym”Proces kolejki to długo działający proces PHP: uruchamia framework raz, pozostaje rezydentny i obsługuje zadania w pętli. To właśnie czyni go szybkim, ale też sprawia, że higiena pamięci ma znaczenie. Powolny wyciek, niewidoczny w pojedynczym żądaniu, może się kumulować przez tysiące zadań. PERFORMANCE-BUDGETS §1 wprost nazywa ten tryb awarii: proces renderujący wiele plików PDF jeden po drugim może wyczerpać pamięć po kilku godzinach, nawet gdy pojedyncze renderowania wyglądają poprawnie.
NextPDF obsługuje środowiska z procesami roboczymi. DocumentFactory pozwala procesowi roboczemu tworzyć świeży dokument dla każdego zadania, współdzieląc jednocześnie FontRegistry i ImageRegistry o czasie życia procesu, dzięki czemu parsowanie czcionek i obrazów odbywa się raz, a nie raz na zadanie. ADR-001 odnotowuje, że parser HTML jest tworzony na każde żądanie, bez statycznego stanu mutowalnego, oraz że przyszłe obiekty kontekstu formatowania muszą stosować ten sam zakres życia na żądanie. Poniższe kroki konfigurują proces roboczy w bezpieczny sposób.
Krok 1 — Współdziel rejestry między zadaniami
Dział zatytułowany „Krok 1 — Współdziel rejestry między zadaniami”Utwórz rejestry raz przy starcie procesu i używaj ich ponownie dla każdego zadania, zgodnie z 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');Parametr maxCacheBytes rejestru obrazów ogranicza współdzieloną pamięć podręczną, więc nie może ona rosnąć bez ograniczeń między zadaniami.
Krok 2 — Ogranicz czas życia procesu roboczego
Dział zatytułowany „Krok 2 — Ogranicz czas życia procesu roboczego”To ogólna praktyka zarządzania procesami dla każdego procesu roboczego PHP, a nie gwarancja silnika NextPDF: okresowo restartuj procesy robocze, aby długo działający proces nie mógł w nieskończoność gromadzić pamięci ani uruchamiać nieaktualnego kodu. Oba główne systemy kolejek PHP zapewniają wbudowane limity i łagodne restarty.
W przypadku kolejek Laravel (https://laravel.com/docs/12.x/queues) polecenie queue:work uruchamia proces roboczy jako długo działający proces. Udokumentowane opcje to --memory (domyślnie 128 MB; proces roboczy kończy działanie, gdy jego pamięć przekroczy limit), --max-jobs (zakończenie po określonej liczbie zadań) oraz --max-time (zakończenie po określonej liczbie sekund). Polecenie queue:restart sygnalizuje procesom roboczym łagodne zakończenie po bieżącym zadaniu, dzięki czemu wdrożenie lub cykliczny zegar może je odświeżyć bez przerywania trwającego renderowania. Laravel Horizon (https://laravel.com/docs/12.x/horizon) nadzoruje procesy robocze Redis ze strategią równoważenia auto oraz łagodnym php artisan horizon:terminate, które kończy trwające zadania, zanim monitor procesów zrestartuje nadzorcę.
W przypadku Symfony Messenger (https://symfony.com/doc/current/messenger.html) polecenie messenger:consume domyślnie działa w nieskończoność. Udokumentowane opcje limitów to --limit (obsłuż N wiadomości, a następnie zakończ), --memory-limit (na przykład 128M; zakończenie, gdy pamięć osiągnie limit) oraz --time-limit (na przykład 3600; zakończenie po upływie interwału). Dokumentacja Symfony zaleca uruchamianie procesu roboczego pod Supervisor lub systemd, aby zakończony proces był automatycznie uruchamiany ponownie, a messenger:stop-workers ustawia flagę w pamięci podręcznej, która informuje każdy proces roboczy, aby dokończył bieżącą wiadomość i zakończył działanie w czysty sposób.
Krok 3 — Restartuj przy wdrożeniu
Dział zatytułowany „Krok 3 — Restartuj przy wdrożeniu”Przy każdym wdrożeniu sygnalizuj łagodny restart, aby procesy robocze pobrały nowy kod: php artisan queue:restart (lub php artisan horizon:terminate) w przypadku Laravel, php bin/console messenger:stop-workers w przypadku Symfony. Menedżer procesów — Supervisor, systemd lub nadzorca Horizon/Octane — uruchamia następnie świeży proces na nowej bazie kodu. To ogólna praktyka wdrożeniowa dla długo działających procesów roboczych PHP i jest niezależna od NextPDF.
Wydajność
Dział zatytułowany „Wydajność”Ścieżka strumieniowa jest zaprojektowana tak, aby ograniczać szczytowe zużycie pamięci przez opróżnianie każdej ukończonej strony oraz zrzucanie ewidencji odsyłaczy i drzewa stron do strumieni tymczasowych opartych na dysku. W rezultacie zestaw rezydentny nie powinien rosnąć wraz z liczbą stron. To zachowanie obserwuje się w dostarczanym silniku 3.1.0 i potwierdzają je jego testy odtwarzalności względem wzorca odniesienia (golden baseline), ale jest ono podawane jako zachowanie projektowe, a nie jako stała liczba, ponieważ ten profil jest właściwością poziomu experimental. Zużycie pamięci po stronie wejścia potoku HTML jest ograniczone przez MAX_NESTING_DEPTH = 100, a nie przez liczbę elementów (ADR-001). Wszystkie konkretne liczby na tej stronie są powiązane z datowanym artefaktem — testem wydajności ADR-001 z 2026-04-06 oraz punktem odniesienia cyklu 36 PERFORMANCE-BUDGETS z 2026-05-17 — i zostały zaobserwowane na stanowiskach wskazanych w tych dokumentach; traktuj je jako obserwacje, a nie jako przenośne gwarancje. Wartość performance_budget wynosząca 1500 ms / 64 MB jest obwiednią płótna, a nie umownym limitem.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”Metoda writeContent() strumieniowego kursora dosłownie dopisuje bajty do strumienia treści strony. Nie weryfikuje składni operatorów. W procesie roboczym renderującym treść zależną od wywołującego nigdy nie przekazuj niezaufanych danych wejściowych do writeContent(); użyj writeText(), które w dostarczanym kursorze zabezpiecza dane zgodnie z gramatyką literałów łańcuchowych PDF. Wywołujący jest właścicielem strumienia wyjściowego: silnik do niego zapisuje, ale nigdy go nie zamyka ani nie otwiera ponownie, więc nie może przekierować wyjścia. Proces roboczy musi sam zamknąć uchwyt po zwróceniu close() przez moduł zapisujący, w przeciwnym razie między zadaniami będzie przeciekać deskryptor pliku. Współdzielenie rejestrów między zadaniami jest optymalizacją wydajności, a nie granicą zaufania: współdzielony ImageRegistry buforuje sparsowane obrazy, więc świadomie dobierz jego maxCacheBytes i nie zakładaj izolacji pamięci podręcznej między dzierżawcami w wielodzierżawnym procesie roboczym.
Zgodność
Dział zatytułowany „Zgodność”| Twierdzenie | Standard | Klauzula | Dowód |
|---|---|---|---|
Strumieniowy moduł zapisujący emituje drzewo stron, którego wpis Kids jest tablicą odniesień pośrednich do bezpośrednich elementów potomnych węzła. | ISO 32000-2 | §7.7.3.2 | |
Strumieniowy moduł zapisujący emituje wpis Count równy liczbie liściowych obiektów stron będących potomkami węzła drzewa stron. | ISO 32000-2 | §7.7.3.3 |
Klauzule są parafrazowane i przypięte do glosariusza; nie odtwarzają żadnego tekstu normatywnego.
Zobacz także
Dział zatytułowany „Zobacz także”- Kontrakty / Strumieniowanie —
experimentalStreamingWriterInterfaceiCursorInterfaceoraz ich maszyna stanów. - HTML / Ograniczenia strumieniowania (ADR-001) — decyzja o jednym przebiegu, bez utrzymywanego DOM, oraz progi ponownej oceny.
- Wydajność — bramka regresji opóźnień i pamięci potoku HTML.
- Układ — silniki elementów stałych strony, które nie utrzymują żadnego stanu na stronę.
- PERFORMANCE-BUDGETS — tryb awarii cieknącego procesu roboczego i punkt odniesienia bramki regresji.