内存与流式处理
Spec: ISO 32000-2, §7.5.4 ISO 32000-2 §7.5.4 Evidence: Mixed evidence
大型 PDF 不应该需要大量内存。本页说明 NextPDF 如何在文档持续增长时让进程堆保持有界、何时流式写入磁盘而非持续累积,以及「性能预算」在此处所代表的意义——一份经过校验的契约,而非一个营销数字。
为何这很重要
标题为“为何这很重要”的章节PDF 格式并未强迫生成器使用大量内存。其交叉引用表为每个间接对象记录一个字节偏移量,因此读取器只需要对文件进行随机访问,而不需要将整个文件载入内存。生成器可以仿效这一点:它可以在对象完成时即输出,并只记住它们的写入位置。相反,如果整份文档在最终写出之前都留在堆中,那么页数就会线性推高内存用量,一份在一百页时运作正常的报表,到了五万页就会导致进程失败。
对于批处理和工作进程负载来说,这正是服务稳定运行和在负载下不可预测地失败之间的差别。有界内存是一项需要通过工程设计实现的特性,而不是一个只能寄望的数字。
简短版本
标题为“简短版本”的章节- 流式写入器的设计目标是让内存保持 每份文档有界。每一页一旦定稿,就立即写入输出,随后释放其缓冲区。
- 那些原本会随对象数量增长的记账数据——交叉引用偏移量以及页面树的
Kids引用——会写入以php://temp/maxmemory:0打开的临时流,这些流会立即溢写至磁盘,而不会填满 PHP 堆。 - 设计目标是 每页 O(1) 堆:持有文档并不会因为页面增加而付出更多成本。这正是写入器围绕实现的工程目标。
- 性能预算 是文档系统中一个真实且结构化的概念:一个挂钟时间上限与一个峰值内存上限,以一份经过校验的契约形式表达。它陈述的是一项义务,而不是一项基准测试结果。
- 具体数字被视为一种 动态信号,在一套明确的方法下测量得出,而不是固定在文字中悄然过时。
NextPDF 如何处理这件事
标题为“NextPDF 如何处理这件事”的章节流式写入器的整体设计源自一个决策:能够输出的东西,就绝不持有。
- Start page A single active cursor; no document-wide page graph in memory.
- Finalise page Page content + page object written straight to the output stream.
- Release buffer The finalised page buffer is dropped; the heap returns to baseline.
- Record offset to disk Xref and Kids entries go to php://temp/maxmemory:0 — immediate disk spill.
- Close Pages-tree root, Catalog, and trailer written once at the end.
磁盘溢写这个细节才是关键。PHP 的 php://temp 会先在内存中保留少量数据,只有超过某个阈值时才溢写。写入器以 maxmemory:0 选项打开这些临时流,强制它们 立即 溢写——内存阈值为零。实际效果是:那些按定义会随文档增长的逐对象记账数据,永远不会在堆中累积;它们累积在磁盘上,而磁盘容量并不是这里的限制因素。若没有该选项,默认的内存窗口就必须先填满才会溢写,这恰恰会在最关键的时刻破坏有界内存的目标。
性能预算 是故事的另一半:它是一份文档系统契约,而非一项营销主张。schema 将预算定义为两个有界整数:以毫秒为单位的挂钟时间上限,以及以 mebibyte 为单位的峰值常驻内存上限。一份声明了预算的配方,也就声明了一项可被校验的义务;这就像带有类型的签名声明了一项编译器可以校验的义务。预算的价值在于它是 被明确声明且被强制执行的,而不在于它很小。
证据怎么说
标题为“证据怎么说”的章节本页属于 Evidence: Mixed evidence ,这种混合是刻意为之,因为证据确实有三种类型。
- 由代码佐证的机制。 位于
src/Writer/Streaming/StreamingPdfWriter.php的流式写入器记录并实现了逐页先输出后释放的循环,并以php://temp/maxmemory:0打开其 xref 与 Kids 流,以强制立即溢写至磁盘,使得「无论对象数量多少,PHP 内存都维持有界」。这种流式、单一游标、不保留树状结构的设计,也是 ADR-001 中所记录的架构决策(渲染管线最多只持有 O(depth) 的状态,而非 O(n) 个节点)。 - 设计原则层面的预算。
performance_budget字段是文档 schema 中一个真实且可选的部分,定义为{ wall_ms, peak_mb },并带有明确的上限。它在设计上就是一份可强制执行的契约。 - 作为动态信号的基准测试。 ADR-001 明确指出,受控大型文档的峰值内存与挂钟时间数字是 一项在明确方法下收集并记录的实证目标——而非一个在文字中断言的数字。因此,本页陈述机制与契约,并将具体数字指向实际测量它们的地方。
这种文件格式使该目标合情合理,而非仅是空想。由于交叉引用表是一个逐对象的偏移量索引,依照 Spec: ISO 32000-2, §7.5.4 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 的一项特性。各版本并不会改变它:
| Edition | Availability |
|---|---|
| Core | Core 提供流式、磁盘溢写的写入器设计。 |
| Pro | Pro 沿用相同的有界内存写入器;它增添功能,而非采用不同的内存模型。 |
| Enterprise | Enterprise 沿用相同的有界内存写入器;它增添功能,而非采用不同的内存模型。 |
相关文档
标题为“相关文档”的章节术语表
标题为“术语表”的章节- 有界内存——一种设计特性,即持有文档不会因为页面增加而耗用更多堆(也就是每页 O(1) 的目标)。
- 流式写入器——将每一页输出并释放其缓冲区,而不保留整份文档的写入器。
php://temp/maxmemory:0——一个被强制立即溢写至磁盘的 PHP 临时流,用于那些不断增长的逐对象记账数据。- 性能预算——一份结构化的文档契约:一个挂钟时间上限与一个峰值内存上限,经过明确声明且可校验。
- 动态信号——一个在明确条件下连同其方法一并报告的测量值,而非一个内嵌于文字中的固定数字。