Lewati ke konten

Merender PDF secara aman di worker yang berjalan lama

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: FontRegistry dan ImageRegistry menyimpan tabel fon yang telah diurai dan cache citra yang telah didekode. Buat registry satu kali saat worker boot.
  • Masa hidup permintaan, sekali pakai: Document yang dikembalikan oleh DocumentFactory::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.

Terminal window
composer require nextpdf/core:^3

Pola 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.

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 ImageRegistry dengan anggaran maxCacheBytes. 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 ketika memoryUsage() 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.

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().

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

  • Kunci sebelum Anda membagikan. Panggil FontRegistry::lock() saat boot. Registry yang masih dapat dimutasi saat dua coroutine mengaksesnya merupakan kondisi balapan data. Gunakan isLocked() sebagai asersi dalam pemeriksaan kesehatan.
  • reset() bukanlah unset(). 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 maxCacheBytes didekode 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 Document di 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. Pemanggilan unset() 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 /ID dibuat 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”.
  • 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 maxCacheBytes ditambah 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.php pendukungnya tersedia, dan tests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.php menambahkan asersi memory/GC yang hilang (puncak tidak bertambah di seluruh siklus setelah reset).
  • 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.
PernyataanSpesifikasiKlausareference_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.