Lewati ke konten

Memori dan streaming

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

PDF berukuran besar tidak seharusnya membutuhkan heap yang besar. Halaman ini menjelaskan cara NextPDF menjaga memori proses tetap terbatas saat dokumen bertambah besar, bagian mana yang dialirkan ke disk alih-alih ditumpuk di memori, serta arti “performance budget” di sini: kontrak yang diperiksa, bukan angka sorotan.

Format PDF tidak memaksa generator menggunakan heap yang besar. Tabel cross-reference-nya mencatat byte offset untuk setiap indirect object, sehingga pembaca memerlukan akses acak di dalam berkas, bukan seluruh berkas di memori. Generator dapat mengikuti model itu: mengeluarkan object begitu selesai dan hanya mengingat di mana object tersebut ditempatkan. Sebaliknya, jika seluruh dokumen tetap berada di heap hingga penulisan akhir, jumlah halaman mendorong penggunaan memori secara linear, dan laporan yang berjalan baik pada seratus halaman akan membuat proses gagal pada lima puluh ribu halaman.

Untuk beban kerja batch dan worker, inilah perbedaan antara layanan yang stabil dan layanan yang gagal secara tak terduga di bawah beban. Memori yang terbatas adalah properti desain yang harus direkayasa, bukan angka yang sekadar diharapkan.

  • Streaming writer dirancang agar memori tetap terbatas per dokumen. Setiap halaman ditulis ke output begitu selesai. Kemudian buffer-nya dilepaskan.
  • Pembukuan yang memang bertambah seiring jumlah object — yaitu offset cross-reference dan referensi Kids pada page-tree — ditulis ke stream sementara yang dibuka dengan php://temp/maxmemory:0, yang langsung dialirkan ke disk alih-alih memenuhi heap PHP.
  • Tujuan desainnya adalah heap O(1) per halaman: mempertahankan dokumen tidak makin mahal seiring penambahan halaman. Inilah target rekayasa yang membentuk writer ini.
  • Performance budget adalah konsep nyata dan terstruktur dalam sistem dokumentasi: batas wall-clock dan batas memori puncak, yang dinyatakan sebagai kontrak yang diperiksa. Ia menyatakan kewajiban, bukan hasil benchmark.
  • Angka-angka konkret diperlakukan sebagai living signal, yang diukur dengan metode yang dinyatakan, bukan dibekukan dalam prosa yang bisa diam-diam menjadi usang.

Streaming writer mengikuti satu keputusan: jangan pernah menahan apa pun yang bisa Anda keluarkan.

  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.
Siklus per halaman dari streaming writer: setiap halaman dikeluarkan dan dilepaskan, dan pembukuan yang terus bertambah dikirim ke stream sementara yang didukung disk, sehingga heap tidak bertambah seiring jumlah halaman.

Detail disk-spill inilah yang menjadi penopang utama. php://temp milik PHP menyimpan sejumlah kecil data di memori dan baru mengalirkannya ke disk ketika melampaui ambang batas. Writer membuka stream sementara tersebut dengan opsi maxmemory:0, yang memaksanya untuk dialirkan ke disk seketika — ambang batas di memori adalah nol. Efek praktisnya adalah pembukuan per object, yang menurut definisinya bertambah seiring dokumen, tidak pernah menumpuk di heap. Ia menumpuk di disk, tempat ukuran bukanlah kendala utamanya. Tanpa opsi tersebut, jendela in-memory bawaan harus terisi penuh sebelum dialirkan ke disk, yang justru menggagalkan tujuan memori terbatas tepat saat hal itu paling dibutuhkan.

Performance budget adalah sisi lain dari cerita ini. Ia adalah kontrak sistem dokumentasi, bukan klaim pemasaran. Skema mendefinisikan budget sebagai dua bilangan bulat dengan batas: batas wall-clock dalam milidetik dan batas resident-memory puncak dalam mebibyte. Sebuah recipe yang mendeklarasikan budget berarti mendeklarasikan kewajiban yang dapat diperiksa, sebagaimana signature bertipe mendeklarasikan kewajiban yang dapat diperiksa oleh compiler. Nilai sebuah budget terletak pada fakta bahwa ia dinyatakan dan ditegakkan, bukan pada seberapa kecil angkanya.

Halaman ini bersifat Evidence: Mixed evidence , dan campuran ini disengaja karena bukti pendukungnya memang terdiri dari tiga jenis.

  • Mekanisme yang didukung kode. Streaming writer di src/Writer/Streaming/StreamingPdfWriter.php mendokumentasikan dan mengimplementasikan siklus emit-lalu-release per halaman serta membuka stream xref dan Kids-nya dengan php://temp/maxmemory:0 untuk memaksa disk spill seketika sehingga “memori PHP tetap terbatas terlepas dari jumlah object.” Desain yang streaming, single-cursor, dan tidak mempertahankan pohon ini juga merupakan keputusan arsitektur yang dicatat dalam ADR-001 (pipeline renderer paling banyak menahan state sebesar O(depth), bukan O(n) node).
  • Budget sebagai prinsip desain. Field performance_budget adalah bagian nyata dan opsional dari skema dokumentasi, yang didefinisikan sebagai { wall_ms, peak_mb } dengan batas atas eksplisit. Berdasarkan desainnya, ia adalah kontrak yang dapat ditegakkan.
  • Benchmark sebagai living signal. ADR-001 menyatakan secara eksplisit bahwa angka peak-memory dan wall-clock untuk dokumen besar yang terkontrol adalah target empiris yang harus dikumpulkan dan dicatat dengan metode yang dinyatakan — bukan angka yang diklaim dalam prosa. Karena itu, halaman ini menyatakan mekanisme dan kontraknya, serta mengarahkan angka-angka konkret ke tempat yang mengukurnya.

Format ini membuat tujuannya masuk akal alih-alih sekadar aspiratif. Karena tabel cross-reference adalah indeks offset per object menurut Spec: ISO 32000-2, §7.5.4 , sebuah generator mampu menulis object begitu menyelesaikannya dan hanya menyimpan offset-nya. Memori yang terbatas konsisten dengan format berkas, bukan bertentangan dengannya.

Memori yang terbatas adalah properti dari cara Anda membuat dokumen, bukan flag yang Anda atur. Loop batch yang memfinalkan dan melepaskan setiap dokumen menjaga heap tetap datar sepanjang proses berjalan:

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

Registry tersebut dibagikan karena menguraikan font dan gambar sekali saja adalah inti dari worker. Dokumen tidak dibagikan, dan dilepaskan pada setiap iterasi — sehingga memori batch tetap dibatasi oleh satu dokumen, bukan oleh keseluruhan batch.

Kesalahpahaman yang paling umum adalah memperlakukan “memori terbatas” sebagai klaim benchmark — mengharapkan adanya angka megabyte untuk dikutip. Hal itu membalikkan maksud sebenarnya. Jaminan di sini bersifat struktural: writer dirancang sehingga mempertahankan sebuah dokumen tidak makin mahal seiring penambahan halaman. Angka puncak yang spesifik bergantung pada konten halaman, font, dan gambar, serta hanya bermakna bila disertai metode pengukurannya. Karena itu, angka tersebut menjadi bagian dari benchmark, bukan dari kalimat ini.

Jebakan kedua: mengasumsikan php://temp sudah melindungi Anda. Ia memang melindungi — tetapi hanya setelah jendela in-memory bawaannya terisi penuh. Opsi maxmemory:0 inilah yang membuat spill terjadi seketika. Detail inilah mekanismenya. Tanpanya, properti tersebut tidak akan bertahan justru pada dokumen besar yang menjadi alasan keberadaannya.

Halaman ini menjelaskan mekanisme streaming dan makna sebuah performance budget. Halaman ini tidak menyatakan angka peak-memory atau throughput hasil pengukuran. Angka-angka tersebut dihasilkan oleh disiplin benchmarking dengan metode yang dideklarasikan, dan ADR-001 secara eksplisit menyerahkan angka empiris ke pengukuran itu. Terbatas “per dokumen” bukan berarti konstan terlepas dari konten satu dokumen: halaman dengan banyak gambar tersemat berukuran besar tetap memerlukan biaya memori sebesar gambar-gambar itu. Yang tidak bertambah adalah pembukuan per halaman dan graf halaman yang dipertahankan. Tidak setiap jalur generasi menggunakan streaming writer. Jalur mana yang melakukan streaming dan mana yang melakukan buffering ditentukan oleh kode dan bentuk pipeline, bukan oleh ikhtisar ini. Mekanisme yang dijelaskan akurat hingga tanggal peninjauan halaman ini. Sumber otoritatifnya adalah src/Writer/Streaming/ dan ADR-001 di repositori inti.

Desain streaming dan memori terbatas merupakan properti Core. Edisi tidak mengubahnya:

Bounded-memory streaming writer — edition availability
Edition Availability
Core Core menyediakan desain writer dengan streaming dan disk-spilling.
Pro Pro mewarisi writer dengan memori terbatas yang sama; ia menambahkan fitur, bukan model memori yang berbeda.
Enterprise Enterprise mewarisi writer dengan memori terbatas yang sama; ia menambahkan fitur, bukan model memori yang berbeda.
  • Memori terbatas — properti desain di mana mempertahankan dokumen tidak mengonsumsi lebih banyak heap seiring penambahan halaman (tujuan O(1)-per-halaman).
  • Streaming writer — writer yang mengeluarkan setiap halaman ke output dan melepaskan buffer-nya alih-alih mempertahankan seluruh dokumen.
  • php://temp/maxmemory:0 — stream sementara PHP yang dipaksa untuk langsung dialirkan ke disk seketika, digunakan untuk pembukuan per object yang terus bertambah.
  • Performance budget — kontrak dokumentasi yang terstruktur: batas wall-clock dan batas memori puncak, yang dinyatakan dan dapat diperiksa.
  • Living signal — nilai terukur yang dilaporkan beserta metodenya dalam kondisi yang dinyatakan, alih-alih angka tetap yang ditanamkan dalam prosa.