跳到內容

記憶體與串流

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

大型 PDF 不應該需要大量記憶體。本頁說明 NextPDF 如何在文件持續增長時維持行程堆積有界、何時串流至磁碟而非不斷累積,以及「效能預算」在此處所代表的意義:一份可檢核的契約,而不是一個招牌數字。

PDF 格式並未強迫產生器使用大量記憶體。其交互參照表為每個間接物件記錄一個位元組偏移量,因此讀取器只需要隨機存取檔案,而不需要將整個檔案載入記憶體。產生器可以仿效這一點:它可以在物件完成時就輸出,並只記住它們寫入的位置。反之,如果整份文件在最終寫出之前都留在堆積中,那麼頁數就會以線性方式推高記憶體用量,一份在一百頁時運作正常的報表,到了五萬頁就會導致行程失敗。

對批次與工作者負載而言,這正是服務穩定與在負載下不可預期失敗之間的差別。有界記憶體是一項需要透過工程設計落實的特性,而不是一個只能寄望的數字。

  • 串流寫入器的設計讓記憶體維持在 每份文件有界 的狀態。每一頁一旦定稿便立即寫入輸出,接著釋放其緩衝區。
  • 那些原本會隨物件數量增長的記帳資料——交互參照偏移量以及頁面樹的 Kids 參照——會寫入以 php://temp/maxmemory:0 開啟的暫存串流,這些串流會立即溢寫至磁碟,而不會填滿 PHP 堆積。
  • 設計目標是 每頁 O(1) 堆積:持有文件並不會因為頁面增加而付出更多成本。寫入器正是圍繞這個工程目標而設計。
  • 效能預算 是文件系統中一個真實且結構化的概念:一個實際時間上限與一個尖峰記憶體上限,以可檢核的契約形式表達。它陳述了一項義務。它並非一項基準測試結果。
  • 具體數字被視為一種 動態訊號,在一套明定的方法下量測得出,而不是被凍結在文字中、逐漸過時。

串流寫入器的整體樣貌源自一個決策:凡是能輸出的東西,就絕不持有。

  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.
串流寫入器的逐頁循環:每一頁都被輸出並釋放,而不斷增長的記帳資料則被送往以磁碟為後盾的暫存串流,因此堆積不會隨頁數而增長。

真正支撐這項設計的是磁碟溢寫這個細節。PHP 的 php://temp 會在記憶體中保留少量資料,僅在超過某個門檻時才溢寫。寫入器以 maxmemory:0 選項開啟這些暫存串流,強制它們 立即 溢寫:記憶體內門檻為零。實際效果是:那些依定義會隨文件增長的逐物件記帳資料,永遠不會在堆積中累積。它累積在磁碟上,而磁碟大小並非限制所在。若沒有該選項,預設的記憶體內視窗就必須先填滿才會溢寫,這正會在最關鍵的時刻破壞有界記憶體的目標。

效能預算 是另一個重點:它是一份文件系統契約,而非一項行銷主張。綱要將預算定義為兩個有界整數:以毫秒為單位的實際時間上限,以及以 mebibyte 為單位的尖峰常駐記憶體上限。一份宣告了預算的配方,就等於宣告了一項可檢核的義務;就如同一個帶有型別的簽名,宣告了一項編譯器可以檢核的義務。預算的價值在於它是 被明定且被強制執行的,而不在於數字很小。

本頁屬於 Evidence: Mixed evidence ,這種混合是刻意為之,因為證據確實有三種類型。

  • 程式碼佐證的機制。 位於 src/Writer/Streaming/StreamingPdfWriter.php 的串流寫入器記錄並實作了逐頁先輸出後釋放的循環,並以 php://temp/maxmemory:0 開啟其 xref 與 Kids 串流,以強制立即溢寫至磁碟,從而讓「無論物件數量多少,PHP 記憶體都維持有界」。這種串流式、單一游標、不保留樹狀結構的設計,也是 ADR-001 中所記錄的架構決策(算繪管線最多只持有 O(depth) 的狀態,而非 O(n) 個節點)。
  • 設計原則層級的預算。 performance_budget 欄位是文件綱要中一個真實且選用的部分,定義為 { wall_ms, peak_mb },並帶有明確的上限。它在設計上就是一份可強制執行的契約。
  • 作為動態訊號的基準測試。 ADR-001 明確指出,受控大型文件的尖峰記憶體與實際時間數字是 一項在明定方法下蒐集並記錄的實證目標,而不是一個在文字中逕自斷言的數字。因此,本頁陳述機制與契約,並將具體數字指向實際量測它們的地方。

這套格式讓該目標合理可行,而非只是空想。由於交互參照表是一個逐物件的偏移量索引,依照 Spec: ISO 32000-2, §7.5.4 ,產生器便 能夠 在完成各個物件時就寫出它們,並只保留它們的偏移量。有界記憶體與檔案格式相一致,而非與之對抗。

有界記憶體是你「如何產生」的一種特性,而不是你所設定的某個旗標。一個將每份文件定稿並釋放的批次迴圈,能讓堆積在整個執行過程中保持平穩:

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

這些登錄之所以共用,是因為將字型與影像只解析一次正是工作者的意義所在。而文件則 共用,並在每一輪都被釋放:這正是讓批次記憶體受限於單一文件、而非受限於整個批次的關鍵。

最常見的誤解,是把「有界記憶體」當成一項基準測試主張:期待有一個 megabyte 數字可以引用。這正好顛倒了我們所要表達的意思。這裡的保證是 結構性 的:寫入器的設計使得持有一份文件不會因為頁面增加而付出更多成本。一個具體的尖峰數字取決於頁面內容、字型與影像,且唯有附上其量測方法時才有意義,這正是為何它屬於基準測試,而非屬於這段說明。

第二個陷阱:以為 php://temp 已經保護了你。它確實會,但只在其預設的記憶體內視窗填滿之後。maxmemory:0 選項才是讓溢寫立即發生的關鍵。這個細節正是機制本身。少了它,這項特性正會在它原本要因應的大型文件下無法成立。

本頁說明串流機制以及效能預算的意義。它 陳述實際量測到的尖峰記憶體或吞吐量數字。那些數字由基準測試規範在一套明定的方法下產生,而 ADR-001 明確地將實證數字交由該量測來決定。「每份文件」有界並不代表無論單一文件內容為何都維持固定:一個內嵌許多大型影像的頁面,仍須付出那些影像所需的成本。不會增長的是 逐頁記帳資料 與被保留的頁面圖。並非每一條產生路徑都使用串流寫入器。哪些路徑採串流、哪些採緩衝,是由程式碼與管線的樣貌決定,而非由本概覽決定。所描述的機制截至本頁審閱日期為止皆屬正確。權威來源是核心儲存庫中的 src/Writer/Streaming/ 與 ADR-001。

串流與有界記憶體的設計是 Core 的一項特性。各版本並不會改變它:

Bounded-memory streaming writer — edition availability
Edition Availability
Core Core 提供串流式、磁碟溢寫的寫入器設計。
Pro Pro 沿用相同的有界記憶體寫入器;它增添功能,而非採用不同的記憶體模型。
Enterprise Enterprise 沿用相同的有界記憶體寫入器;它增添功能,而非採用不同的記憶體模型。
  • 有界記憶體——一種設計特性,意指持有文件不會因為頁面增加而耗用更多堆積(即每頁 O(1) 的目標)。
  • 串流寫入器——將每一頁輸出並釋放其緩衝區、而不保留整份文件的寫入器。
  • php://temp/maxmemory:0——一個被強制立即溢寫至磁碟的 PHP 暫存串流,用於那些不斷增長的逐物件記帳資料。
  • 效能預算——一份結構化的文件契約:一個實際時間上限與一個尖峰記憶體上限,經過明定且可檢核。
  • 動態訊號——一個在明定條件下連同其方法一併回報的量測值,而非一個內嵌於文字中的固定數字。