Lewati ke konten

Streaming dan memori: panduan profiling dan batch worker

NextPDF melakukan render dalam sekali lintas dan tidak pernah menyimpan Document Object Model (DOM) tingkat dokumen, sehingga memori di sisi masukan dibatasi oleh kedalaman susunan bersarang, bukan oleh jumlah elemen. Halaman ini menjelaskan model streaming, batasan dalam Architecture Decision Record (ADR)-001, dan cara menjalankan mesin dengan aman pada queue worker yang berjalan lama.

Terminal window
composer require nextpdf/core:^3

NextPDF memiliki dua jalur penulisan dengan profil memori yang berbeda.

Writer in-memory standar menyusun seluruh dokumen, lalu menyerialkannya. Memori puncak mengikuti total ukuran keluaran. Cara ini bekerja baik untuk dokumen pada umumnya, tetapi dapat menjadi mahal untuk dokumen yang sangat besar.

Writer streaming menyerialkan setiap halaman saat halaman tersebut selesai disusun, lalu membilasnya sebelum halaman berikutnya dimulai. Mesin yang dirilis — StreamingPdfWriter, StreamingCursor, DevNullWriter, dan enum WriterState di src/Writer/Streaming/ — merupakan implementasi nyata, final, teruji, dan telah dirilis sejak 3.1.0. Mesin ini diekspos melalui kontrak StreamingWriterInterface dan CursorInterface pada tier experimental. Kelas-kelas mesinnya bersifat internal; karena itu, bergantunglah pada kontrak dan biarkan Core menyediakan implementasinya. (Anotasi .ai/contracts-map.md sebelumnya secara keliru menggambarkan streaming sebagai “hanya kontrak / tanpa implementasi”; cacat pada anotasi usang itu dilacak dalam isu #610 dan diperbaiki dalam dokumen kontrak B1 — mesin telah dirilis sejak 3.1.0.)

Mesin streaming dirancang agar memori residen tidak bertambah seiring jumlah halaman. Buffer setiap halaman yang telah difinalisasi diserahkan ke writer lalu dilepaskan. Tabel referensi-silang dan referensi pohon halaman /Kids ditulis ke stream sementara php://temp/maxmemory:0 yang langsung dilimpahkan ke disk alih-alih menumpuk di heap PHP. Hasil serialisasinya berupa pohon halaman standar dengan entri Count yang berisi jumlah simpul daun (objek halaman) keturunan dari suatu simpul (ISO 32000-2 §7.7.3.3) dan entri Kids yang berisi larik referensi tidak langsung ke anak-anak langsung dari simpul tersebut (ISO 32000-2 §7.7.3.2). Profil memori yang tepat merupakan properti pada tier experimental dan dapat berubah antar rilis minor, jadi jangan menanamkan asumsi permanen dari satu pengukuran.

ADR-001 mengatur model memori pipeline render HTML. Tokenizer menghasilkan daftar token dalam sekali lintas. Parser mengonsumsinya dari kiri ke kanan dan memancarkan operator content-stream ke dalam buffer string. Tidak ada pohon elemen persisten yang dibangun: parser menyimpan paling banyak satu HtmlStyleState per tingkat susunan bersarang, dibatasi oleh MAX_NESTING_DEPTH = 100, dan menerapkan batas keras MAX_ELEMENT_COUNT = 50_000. Dua operasi yang membutuhkan lookahead — penentuan ukuran kolom tabel dan keluarga selektor :has() / :last-child — menggunakan larik indeks pra-pindai dengan batas atas pada daftar token datar, bukan DOM yang dipertahankan. Benchmark Fase 0 (docs/architecture/adr-001-memory-benchmark.md, dijalankan 2026-04-06, PHP 8.5.3, memory_limit=1G) mengukur dokumen berisi 50,000 elemen pada puncak 50 MB untuk jalur stream dibandingkan dengan simulasi penyimpanan kerja-parsial sebesar 4 MB. Laporan tersebut mengaitkan sekitar 50 MB dari angka itu dengan content stream terakumulasi yang invarian terhadap arsitektur dan mengisolasi keunggulan sisi masukan sebesar 4–5x untuk model stream pada fixture itu. Angka-angka tersebut diamati pada satu rig dan fixture itu, bukan jaminan.

Ukur sebelum Anda mengubah apa pun. Pipeline HTML dijaga oleh tools/perf-benchmark.php (dijalankan melalui composer ai:perf-check), yang melaporkan peak_memory_delta_bytes — puncak inkremental per target yang digunakan sebagai metrik regresi, bukan puncak proses absolut. Baseline Cycle 36 (docs/architecture/PERFORMANCE-BUDGETS.md §6.3, direkam 2026-05-17 pada i9-13900K, 64 GB, PHP 8.5.3, opcache nonaktif) mengamati delta puncak 0-byte pada 12 dari 16 pasangan target/mode. Keempat delta bukan nol dikaitkan dengan alokasi cache-fon dan trace-buffer saat pertama kali disentuh yang tetap konstan pada render berikutnya. Bacalah angka-angka itu sebagai nilai yang diamati pada rig tersebut, bukan sebagai konstanta yang dapat dipindahkan. Untuk profiling ad-hoc terhadap dokumen Anda sendiri, ambil sampel memory_get_peak_usage(true) sebelum dan sesudah render, lalu setel ulang puncaknya dengan memory_reset_peak_usage() di antara iterasi, dengan cara yang sama seperti benchmark mengisolasi biaya per target.

Queue worker adalah proses PHP berumur panjang: proses ini menjalankan framework sekali, tetap residen, dan menangani job dalam loop. Itulah yang membuatnya cepat, dan itu pula yang membuat kebersihan memori penting. Kebocoran lambat yang tidak terlihat dalam satu permintaan dapat menumpuk selama ribuan job. PERFORMANCE-BUDGETS §1 menyebutkan mode kegagalan ini secara eksplisit: worker yang merender banyak PDF secara beruntun dapat menghabiskan memori setelah berjam-jam, meskipun render tunggal tampak baik-baik saja.

NextPDF mendukung lingkungan worker. DocumentFactory memungkinkan worker membuat dokumen baru untuk setiap job sambil berbagi FontRegistry dan ImageRegistry yang berumur sepanjang proses, sehingga penguraian fon dan gambar terjadi sekali, bukan sekali per job. ADR-001 mencatat bahwa parser HTML dibangun per permintaan tanpa keadaan mutabel statis, dan bahwa objek konteks-pemformatan di masa depan harus mengikuti pelingkupan per permintaan yang sama. Langkah-langkah berikut mengonfigurasi worker dengan aman.

Buat registry satu kali saat proses dijalankan dan gunakan kembali untuk setiap job, mengikuti 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 pada image registry membatasi cache bersama, sehingga cache tidak dapat tumbuh tanpa batas lintas job.

Ini adalah praktik pengendalian proses umum untuk worker PHP mana pun, bukan jaminan mesin NextPDF: mulai ulang worker secara berkala agar proses berumur panjang tidak dapat menumpuk memori atau terus menjalankan kode usang tanpa batas waktu. Kedua sistem antrean PHP utama menyediakan batas bawaan dan mekanisme mulai ulang yang mulus.

Untuk Laravel queues (https://laravel.com/docs/12.x/queues), perintah queue:work menjalankan worker sebagai proses berumur panjang. Opsi yang terdokumentasi adalah --memory (standar 128 MB; worker keluar ketika memorinya melebihi batas), --max-jobs (keluar setelah sejumlah job), dan --max-time (keluar setelah sejumlah detik). Perintah queue:restart memberi sinyal kepada worker agar keluar dengan mulus setelah job saat ini, sehingga deploy atau timer berkala dapat mendaur ulangnya tanpa mengganggu render yang sedang berjalan. Laravel Horizon (https://laravel.com/docs/12.x/horizon) menyupervisi worker Redis dengan strategi penyeimbangan auto dan php artisan horizon:terminate yang mulus, yang menyelesaikan job yang sedang berjalan sebelum monitor proses memulai ulang supervisor.

Untuk Symfony Messenger (https://symfony.com/doc/current/messenger.html), perintah messenger:consume berjalan selamanya secara default. Opsi batas yang terdokumentasi adalah --limit (tangani N pesan, lalu keluar), --memory-limit (misalnya 128M; keluar ketika memori mencapai batas), dan --time-limit (misalnya 3600; keluar setelah interval). Dokumentasi Symfony menyarankan menjalankan worker di bawah Supervisor atau systemd agar proses yang keluar dimulai ulang secara otomatis, dan messenger:stop-workers menyetel flag cache yang memberi tahu setiap worker untuk menyelesaikan pesan saat ini dan keluar dengan bersih.

Pada setiap deploy, kirim sinyal mulai ulang yang mulus agar worker mengambil kode baru: php artisan queue:restart (atau php artisan horizon:terminate) untuk Laravel, php bin/console messenger:stop-workers untuk Symfony. Pengelola proses — Supervisor, systemd, atau supervisor Horizon/Octane — kemudian memulai proses baru terhadap basis kode yang baru. Ini adalah praktik deployment umum untuk worker PHP berumur panjang dan tidak bergantung pada NextPDF.

Jalur streaming dirancang untuk membatasi memori puncak dengan membilas setiap halaman yang selesai dan melimpahkan pembukuan referensi-silang serta pohon halaman ke stream sementara yang didukung disk. Akibatnya, resident set dimaksudkan agar tidak bertambah seiring jumlah halaman. Perilaku tersebut diamati dalam mesin 3.1.0 yang dirilis dan dipatok oleh uji reproduksibilitas golden-baseline-nya, tetapi dinyatakan sebagai perilaku desain alih-alih angka tetap karena profilnya adalah properti pada tier experimental. Memori di sisi masukan pipeline HTML dibatasi oleh MAX_NESTING_DEPTH = 100 alih-alih jumlah elemen (ADR-001). Semua angka konkret pada halaman ini terikat pada artefak bertanggal — benchmark ADR-001 2026-04-06 dan baseline Cycle 36 PERFORMANCE-BUDGETS 2026-05-17 — dan diamati pada rig yang disebutkan dokumen-dokumen tersebut; perlakukan sebagai pengamatan, bukan jaminan yang dapat dipindahkan. performance_budget sebesar 1500 ms / 64 MB adalah envelope kanvas, bukan batas kontraktual.

writeContent() pada streaming cursor menambahkan byte ke content stream halaman apa adanya. Metode ini tidak memvalidasi sintaks operator. Pada worker yang merender konten yang dipengaruhi pemanggil, jangan pernah meneruskan masukan tidak tepercaya ke writeContent(); gunakan writeText(), yang di-escape oleh cursor yang dirilis untuk tata bahasa literal-string PDF. Pemanggil memiliki stream keluaran: mesin menulis ke stream tersebut tetapi tidak pernah menutup atau membukanya kembali, sehingga mesin tidak dapat mengalihkan keluaran. Worker harus menutup handle itu sendiri setelah close() pada writer kembali, atau ia akan membocorkan file descriptor lintas job. Berbagi registry antar job adalah optimasi performa, bukan batas kepercayaan: ImageRegistry bersama menyimpan cache gambar yang telah diurai, jadi tentukan ukuran maxCacheBytes-nya secara saksama dan jangan mengasumsikan adanya isolasi cache antar penyewa pada worker multipenyewa.

KlaimStandarKlausulBukti
Writer streaming memancarkan pohon halaman dengan entri Kids berupa larik referensi tidak langsung ke anak-anak langsung dari simpul tersebut.ISO 32000-2§7.7.3.2
Writer streaming memancarkan entri Count yang sama dengan jumlah objek halaman daun keturunan dari simpul pohon halaman.ISO 32000-2§7.7.3.3

Klausul diparafrasakan dan dipatok ke glosarium; tidak ada teks normatif yang direproduksi.