Merender PDF secara aman di worker yang berjalan lama
Sekilas
Bagian berjudul “Sekilas”Worker PHP (PHP: Hypertext Preprocessor) yang berjalan lama (RoadRunner, Swoole, Laravel Octane) menjaga satu proses tetap aktif untuk banyak permintaan. Jika Anda mengurai fon yang sama dan mendekode citra yang sama untuk setiap permintaan, Anda membuang waktu prosesor dan meningkatkan memori residen. NextPDF menghindari biaya ini dengan memisahkan dua masa hidup:
- Masa hidup proses, dibagikan:
FontRegistrydanImageRegistrymenyimpan tabel fon yang telah diurai dan cache citra yang telah didekode. Buat registry satu kali saat worker boot. - Masa hidup permintaan, sekali pakai:
Documentyang dikembalikan olehDocumentFactory::create(). Buat, tulis, lalu biarkan keluar dari cakupan. Garbage collector PHP kemudian dapat mengambil kembali seluruh graf objek.
Resep ini menunjukkan urutan boot worker, badan per permintaan, dan reset per siklus yang menjaga puncak memori tetap stabil.
Instalasi
Bagian berjudul “Instalasi”composer require nextpdf/core:^3Pola worker tidak memerlukan ekstensi tambahan, dan runtime worker (RoadRunner / Swoole / Octane) bersifat opsional. Anda dapat menjalankan pola factory yang sama dalam perulangan for di antarmuka baris perintah (CLI), seperti yang diuji oleh harness.
Ikhtisar konseptual
Bagian berjudul “Ikhtisar konseptual”Untuk kode worker, mulailah dengan DocumentFactory. Buat sekali dengan FontRegistry dan ImageRegistry yang dibagikan:
FontRegistry::warmup()mengurai berkas fon yang Anda sediakan dan menyimpan tabel hasil uraian ke cache.FontRegistry::lock()membekukan registry sehingga kode per permintaan tidak dapat memutasi set fon yang dibagikan.isLocked()melaporkan keadaan saat ini. Setelah dikunci, registry aman dibagikan di seluruh coroutine yang berjalan bersamaan.- Buat
ImageRegistrydengan anggaranmaxCacheBytes. Ketika anggaran terlampaui, registry mengeluarkan entri yang paling lama tidak digunakan. Citra yang lebih besar daripada anggaran melewati cache, alih-alih membebaninya. ImageRegistry::reset()mengeluarkan setiap citra yang di-cache sementara registry tetap siap digunakan. Permintaan berikutnya mengisinya kembali sesuai kebutuhan. Panggil metode ini secara berkala (setiap N permintaan, atau ketikamemoryUsage()melewati ambang batas) untuk mengembalikan batas tertinggi ke garis dasar.
Setiap dokumen yang dibuat oleh factory adalah berkas Portable Document Format (PDF) yang independen. ISO 32000-2 §7.5.5 mendefinisikan trailer untuk berkas yang tidak pernah diperbarui sebagai tidak memiliki entri Prev, dan setiap permintaan worker menghasilkan berkas generasi pertama seperti itu. Oleh karena itu, permintaan tidak berbagi keadaan dokumen, meskipun mereka berbagi cache fon dan citra. Tag BaseFont fon subset (ISO 32000-2 §9.6.4) tetap stabil di seluruh permintaan karena fon yang telah diurai berada di registry yang dibagikan.
Permukaan API
Bagian berjudul “Permukaan API”Resep ini menggunakan permukaan API yang dihasilkan dari PHPDoc untuk NextPDF\Core\DocumentFactory, NextPDF\Typography\FontRegistry, NextPDF\Graphics\ImageRegistry, dan NextPDF\Support\MemoryReport. Anggota utamanya adalah DocumentFactory::create(), FontRegistry::warmup() / lock() / isLocked() / memoryUsage(), ImageRegistry::reset() / memoryUsage(), dan MemoryReport::$currentBytes / $peakBytes / $entryCount / utilizationPercent().
Contoh kode — Mulai cepat
Bagian berjudul “Contoh kode — Mulai cepat”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Worker boot (run ONCE, before the request loop) ---------------------$fonts = new FontRegistry();$fonts->lock(); // freeze the shared font set$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$factory = new DocumentFactory($fonts, $images);
// --- Per request ---------------------------------------------------------$doc = $factory->create();$doc->setTitle('Worker output');$doc->addPage();$doc->setFont('helvetica', 'B', 16);$doc->cell(0, 12, 'Generated in a shared-registry worker', newLine: true);$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');// $doc leaves scope here → GC reclaims the whole document tree.Contoh kode — Produksi
Bagian berjudul “Contoh kode — Produksi”Contoh lengkap ini mengikuti kanal keluaran harness. Contoh ini menunjukkan urutan boot, perulangan permintaan yang terbatas, reset() per siklus, dan asersi batas tertinggi memori. Ini adalah skrip yang dijalankan dua kali oleh harness reproduktibilitas.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Worker boot: shared, process-lifetime registries --------------------$fonts = new FontRegistry();$fonts->lock(); // share-safe once locked$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$factory = new DocumentFactory($fonts, $images);
$resetEvery = 4; // reset cadence in requests$peakAfterReset = 0;
// --- Simulated request loop ---------------------------------------------for ($request = 1; $request <= 12; $request++) { $doc = $factory->create(); $doc->setTitle("Worker Request #{$request}"); $doc->addPage(); $doc->setFont('helvetica', 'B', 16); $doc->cell(0, 12, "Worker Request #{$request}", newLine: true); $doc->setFont('helvetica', '', 11); $doc->cell(0, 8, 'Shared FontRegistry / ImageRegistry across requests.', newLine: true);
// The harness captures the LAST request's PDF via the side channel. if ($request === 12) { $doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf'); } else { $doc->getPdfData(); // force render, then drop }
unset($doc); // explicit end-of-request
// Bound the cache high-water mark on a fixed cadence. if ($request % $resetEvery === 0) { $images->reset(); \gc_collect_cycles(); $report = $images->memoryUsage(); $peakAfterReset = \max($peakAfterReset, $report->currentBytes); }}
$final = $images->memoryUsage();
fwrite(STDERR, \sprintf( "fonts.locked=%s images.entries=%d images.current=%dB peak_after_reset=%dB\n", $fonts->isLocked() ? 'yes' : 'no', $final->entryCount, $final->currentBytes, $peakAfterReset,));STDOUT tetap tersedia untuk harness; teks kemajuan dikirim ke STDERR. PDF hanya ditulis ke NEXTPDF_COOKBOOK_OUTPUT; file itu tidak pernah digemakan.
Kasus tepi & jebakan
Bagian berjudul “Kasus tepi & jebakan”- Kunci sebelum Anda membagikan. Panggil
FontRegistry::lock()saat boot. Registry yang masih dapat dimutasi saat dua coroutine mengaksesnya merupakan kondisi balapan data. GunakanisLocked()sebagai asersi dalam pemeriksaan kesehatan. reset()bukanlahunset().ImageRegistry::reset()mengeluarkan data biner yang di-cache dan menjaga registry tetap dapat digunakan, sehingga metode ini merupakan pemanggilan berkala yang tepat. Jika Anda menghancurkan dan membangun ulang registry untuk setiap permintaan, Anda kehilangan manfaat cache yang dibagikan.- Pelewatan citra berukuran berlebih. Citra yang lebih besar daripada
maxCacheBytesdidekode per penggunaan dan tidak pernah di-cache, sehingga tidak dapat menyebabkan working set dikeluarkan dari cache. Ini disengaja. Tentukan ukuran anggaran untuk citra umum Anda, bukan untuk citra besar yang langka. - Dokumen harus keluar dari cakupan. Jika Anda menahan
Documentdi dalam properti statis, binding container yang berumur panjang, atau closure yang ditangkap oleh worker, seluruh graf objek tetap hidup dan pengumpulan per permintaan tidak dapat berfungsi. Pemanggilanunset()atau keluarnya dokumen dari cakupan bersifat wajib. - Penempatan
gc_collect_cycles(). Kolektor siklus PHP tidak mengetahui batas permintaan. Panggil metode ini setelah interval reset, bukan pada setiap permintaan. Ini membatasi batas tertinggi tanpa menambahkan biaya pengumpulan ke jalur panas. - Peringatan determinisme. Stempel waktu dokumen dan trailer
/IDdibuat ulang per penyimpanan (ISO 32000-2 §14.3). Oleh karena itu, PDF yang ditangkap dibandingkan dengan profil semantik (abstract syntax tree (AST) struktural ditambah metadata, tidak pernah bita yang volatil). Lihat “Kesesuaian”.
Kinerja
Bagian berjudul “Kinerja”- Registry yang dibagikan mengubah penguraian fon dan pendekodean citra yang berulang menjadi biaya boot satu kali. Pekerjaan per permintaan kemudian menjadi tata letak dan serialisasi.
- Puncak memori residen dibatasi oleh
maxCacheBytesditambah working set dari satu dokumen yang sedang diproses.reset()per siklus mengembalikan cache ke garis dasar, sehingga worker yang berumur panjang tidak menunjukkan pola gigi gergaji yang cenderung naik. - Front-matter
performance_budget(wall_ms: 4000,peak_mb: 192) membatasi proses harness untuk perulangan 12 permintaan. Harness menegakkan anggaran ini; anggaran ini bukan jaminan untuk dokumen sembarang. - Resep ini menyediakan cakupan “memory/GC” dari daftar celah §4.3 untuk #31.
examples/14-worker-factory.phppendukungnya tersedia, dantests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.phpmenambahkan asersi memory/GC yang hilang (puncak tidak bertambah di seluruh siklus setelah reset).
Catatan keamanan
Bagian berjudul “Catatan keamanan”- Pola worker memproses satu dokumen per permintaan dan hanya berbagi cache fon yang telah diurai serta citra yang telah didekode. Konten dokumen tidak melintasi batas permintaan. Satu permintaan tidak dapat membaca data dokumen permintaan lain melalui registry yang dibagikan.
- Masukan tidak tepercaya tetap mengalir melalui batas masukan NextPDF yang normal, dan pola worker tidak melonggarkan validasi. Perlakukan masukan HyperText Markup Language (HTML) dan aset setiap permintaan sebagai tidak tepercaya, seperti yang Anda lakukan dalam proses per permintaan.
Kesesuaian
Bagian berjudul “Kesesuaian”| Pernyataan | Spesifikasi | Klausa | reference_id |
|---|---|---|---|
| Tanggal modifikasi dokumen dibuat ulang pada setiap penyimpanan, sehingga keluaran per permintaan tidak stabil secara bita. | ISO 32000-2 | §14.3 | |
Setiap dokumen worker adalah berkas yang tidak pernah diperbarui (tanpa Prev di dalam trailer); permintaan tidak berbagi keadaan dokumen. | ISO 32000-2 | §7.5.5 | |
| Awalan tag fon subset stabil di seluruh permintaan karena fon yang telah diurai berada di registry yang dibagikan. | ISO 32000-2 | §9.6.4 |
Karena trailer /ID dan tanggal modifikasi dibuat ulang per penyimpanan, resep ini diverifikasi dengan profil reproduktibilitas semantik (kesetaraan abstract syntax tree (AST) struktural ditambah perbandingan metadata saja). Klaim bitwise atau struktural akan tidak akurat untuk keluaran worker.