Bỏ qua để đến nội dung

Bộ nhớ và ghi luồng

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

Một tệp PDF lớn không nên đòi hỏi một heap lớn. Trang này giải thích cách NextPDF giữ bộ nhớ của tiến trình trong giới hạn khi tài liệu ngày càng lớn, khi nào hệ thống ghi luồng ra đĩa thay vì tích lũy trong bộ nhớ, và “ngân sách hiệu năng” ở đây có nghĩa là gì: một cam kết được kiểm tra, chứ không phải một con số giật tít.

Định dạng PDF không buộc bộ tạo tài liệu phải dùng một heap lớn. Bảng tham chiếu chéo ghi lại một byte offset cho mỗi đối tượng gián tiếp, nên trình đọc chỉ cần truy cập ngẫu nhiên vào tệp, chứ không cần giữ toàn bộ tệp trong bộ nhớ. Bộ tạo tài liệu có thể đi theo mô hình đó: phát ra các đối tượng ngay khi chúng hoàn tất và chỉ ghi nhớ vị trí của chúng. Ngược lại, nếu toàn bộ tài liệu nằm trong heap cho đến lần ghi cuối cùng, số trang sẽ làm bộ nhớ tăng tuyến tính; một báo cáo chạy tốt ở một trăm trang có thể làm tiến trình sụp đổ ở năm mươi nghìn trang.

Với các khối lượng công việc theo lô và theo worker, đây là điểm khác biệt giữa một dịch vụ ổn định và một dịch vụ hỏng khó lường khi chịu tải. Bộ nhớ có giới hạn là một thuộc tính thiết kế phải được tạo dựng bằng kỹ thuật, chứ không phải một con số để kỳ vọng.

  • Bộ ghi luồng được xây dựng để bộ nhớ luôn có giới hạn theo từng tài liệu. Mỗi trang được ghi ra đầu ra ngay khi hoàn tất. Sau đó vùng đệm của trang được giải phóng.
  • Phần ghi sổ vốn tăng theo số lượng đối tượng — các offset tham chiếu chéo và các tham chiếu Kids của cây trang — được ghi vào các luồng tạm thời mở bằng php://temp/maxmemory:0, vốn tràn ra đĩa ngay lập tức thay vì lấp đầy heap của PHP.
  • Mục tiêu thiết kế là O(1) heap mỗi trang: việc giữ tài liệu không tốn thêm heap khi có thêm trang được bổ sung. Đó là mục tiêu kỹ thuật định hình bộ ghi.
  • Một ngân sách hiệu năng là một khái niệm thực sự, có cấu trúc trong hệ thống tài liệu: một giới hạn thời gian thực và một giới hạn bộ nhớ đỉnh, được biểu đạt như một cam kết được kiểm tra. Nó nêu ra một nghĩa vụ. Nó không phải là một kết quả đo điểm chuẩn.
  • Các con số cụ thể được xem là một tín hiệu sống, được đo theo một phương pháp đã nêu rõ, chứ không bị đóng băng trong văn bản để rồi âm thầm lỗi thời.

Bộ ghi luồng tuân theo một quyết định duy nhất: không bao giờ giữ lại những gì có thể phát ra.

  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.
Chu trình theo từng trang của bộ ghi luồng: mỗi trang được phát ra rồi giải phóng, và phần ghi sổ ngày một lớn được gửi đến các luồng tạm thời dựa trên đĩa, nên heap không tăng theo số trang.

Chi tiết tràn ra đĩa là phần cốt yếu nhất. php://temp của PHP giữ một lượng nhỏ dữ liệu trong bộ nhớ và chỉ tràn ra đĩa khi vượt quá một ngưỡng. Bộ ghi mở các luồng tạm thời đó với tùy chọn maxmemory:0, buộc chúng tràn ra đĩa ngay lập tức — ngưỡng trong bộ nhớ bằng không. Hệ quả thực tế là phần ghi sổ theo từng đối tượng, vốn theo định nghĩa sẽ tăng theo tài liệu, không bao giờ tích lũy trong heap. Nó tích lũy trên đĩa, nơi kích thước không phải là ràng buộc. Nếu không có tùy chọn đó, cửa sổ mặc định trong bộ nhớ sẽ phải được lấp đầy trước khi tràn ra đĩa, phá hỏng mục tiêu giới hạn bộ nhớ đúng vào lúc mục tiêu đó quan trọng nhất.

Phần ngân sách hiệu năng là nửa còn lại của câu chuyện. Đó là một cam kết của hệ thống tài liệu, không phải một tuyên bố tiếp thị. Lược đồ định nghĩa một ngân sách bằng hai số nguyên có giới hạn: một giới hạn thời gian thực tính bằng mili giây và một giới hạn bộ nhớ thường trú đỉnh tính bằng mebibyte. Một công thức khai báo ngân sách tức là khai báo một nghĩa vụ có thể kiểm tra được, giống như cách một chữ ký có kiểu khai báo một nghĩa vụ mà trình biên dịch có thể kiểm tra. Giá trị của một ngân sách nằm ở chỗ nó được nêu rõ và thực thi, chứ không phải ở chỗ nó nhỏ.

Trang này là Evidence: Mixed evidence , và sự pha trộn này là có chủ ý vì bằng chứng thực sự thuộc ba loại khác nhau.

  • Cơ chế có mã hỗ trợ. Bộ ghi luồng trong src/Writer/Streaming/StreamingPdfWriter.php ghi lại và triển khai chu trình phát-rồi-giải-phóng theo từng trang, đồng thời mở các luồng xref và Kids của nó bằng php://temp/maxmemory:0 để buộc tràn ra đĩa ngay lập tức, nhờ đó “bộ nhớ PHP luôn có giới hạn bất kể số lượng đối tượng.” Thiết kế ghi luồng, một con trỏ duy nhất, không giữ lại cây cũng chính là quyết định kiến trúc được ghi lại trong ADR-001 (đường ống kết xuất giữ tối đa trạng thái O(depth), chứ không phải O(n) nút).
  • Ngân sách theo nguyên tắc thiết kế. Trường performance_budget là một phần thực sự nhưng tùy chọn của lược đồ tài liệu, được định nghĩa là { wall_ms, peak_mb } với các giới hạn trên rõ ràng. Theo thiết kế, nó là một cam kết có thể thực thi được.
  • Điểm chuẩn như một tín hiệu sống. ADR-001 nói rõ rằng các con số về bộ nhớ đỉnh và thời gian thực của tài liệu lớn trong điều kiện kiểm soát là một mục tiêu thực nghiệm cần được thu thập và ghi lại theo một phương pháp đã nêu rõ — chứ không phải một con số để khẳng định trong văn bản. Vì vậy, trang này nêu rõ cơ chế và cam kết, đồng thời trỏ các con số cụ thể về nơi chúng được đo lường.

Định dạng này khiến mục tiêu trở nên hợp lý, chứ không chỉ là khát vọng. Bởi vì bảng tham chiếu chéo là một chỉ mục offset theo từng đối tượng theo Spec: ISO 32000-2, §7.5.4 , một bộ tạo tài liệu có thể ghi các đối tượng ngay khi hoàn tất chúng và chỉ giữ lại offset của chúng. Bộ nhớ có giới hạn nhất quán với định dạng tệp, chứ không phải một cuộc chiến chống lại nó.

Bộ nhớ có giới hạn là một thuộc tính của cách bạn tạo tài liệu, chứ không phải một cờ để bật. Một vòng lặp theo lô hoàn tất rồi giải phóng từng tài liệu sẽ giữ cho heap ổn định trong suốt quá trình chạy:

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

Các registry được dùng chung vì phân tích phông chữ và hình ảnh một lần chính là mục đích của một worker. Tài liệu thì không được dùng chung, và nó được giải phóng sau mỗi lượt — điều này giữ cho bộ nhớ theo lô bị giới hạn bởi một tài liệu, chứ không phải bởi cả lô.

Hiểu lầm phổ biến nhất là xem “bộ nhớ có giới hạn” như một tuyên bố điểm chuẩn — mong đợi một con số megabyte để trích dẫn. Điều đó đảo ngược ý đang được nói đến. Bảo đảm ở đây mang tính cấu trúc: bộ ghi được xây dựng sao cho việc giữ một tài liệu không tốn thêm heap khi có thêm trang được bổ sung. Một con số đỉnh cụ thể phụ thuộc vào nội dung trang, phông chữ và hình ảnh, và chỉ có ý nghĩa khi đi kèm phương pháp đo lường của nó; đó là lý do nó thuộc về một điểm chuẩn, chứ không thuộc về câu này.

Một cái bẫy thứ hai: cho rằng php://temp đã bảo vệ bạn rồi. Đúng là có — nhưng chỉ sau khi cửa sổ mặc định trong bộ nhớ của nó được lấp đầy. Chính tùy chọn maxmemory:0 mới làm cho việc tràn ra đĩa diễn ra ngay lập tức. Chi tiết đó chính là cơ chế. Nếu không có nó, thuộc tính này sẽ không đứng vững với chính những tài liệu lớn mà nó tồn tại để xử lý.

Trang này giải thích cơ chế ghi luồng và ý nghĩa của một ngân sách hiệu năng. Nó không nêu các con số bộ nhớ đỉnh hay thông lượng đã đo. Những con số đó được tạo ra bởi quy trình đo điểm chuẩn theo một phương pháp đã khai báo, và ADR-001 dứt khoát giao các con số thực nghiệm cho phép đo đó. “Có giới hạn theo từng tài liệu” không có nghĩa là không đổi bất kể nội dung của một tài liệu cụ thể: một trang có nhiều hình ảnh nhúng lớn vẫn tốn đúng bằng những gì các hình ảnh đó tốn. Cái không tăng là phần ghi sổ theo từng trang và đồ thị trang được giữ lại. Không phải mọi đường tạo tài liệu đều là bộ ghi luồng. Đường nào ghi luồng và đường nào dùng vùng đệm là do mã và hình dạng đường ống quyết định, chứ không phải do bản tổng quan này. Cơ chế được mô tả là chính xác tính đến ngày xét duyệt của trang này. Nguồn có thẩm quyền là src/Writer/Streaming/ và ADR-001 trong kho lưu trữ core.

Thiết kế ghi luồng và bộ nhớ có giới hạn là một thuộc tính của Core. Các phiên bản không thay đổi điều đó:

Bounded-memory streaming writer — edition availability
Edition Availability
Core Core cung cấp thiết kế bộ ghi ghi luồng, tràn ra đĩa.
Pro Pro kế thừa cùng một bộ ghi bộ nhớ có giới hạn; nó bổ sung tính năng, chứ không phải một mô hình bộ nhớ khác.
Enterprise Enterprise kế thừa cùng một bộ ghi bộ nhớ có giới hạn; nó bổ sung tính năng, chứ không phải một mô hình bộ nhớ khác.
  • Bộ nhớ có giới hạn — một thuộc tính thiết kế trong đó việc giữ tài liệu không tiêu tốn thêm heap khi có thêm trang được bổ sung (mục tiêu O(1) mỗi trang).
  • Bộ ghi luồng — bộ ghi phát ra từng trang đến đầu ra và giải phóng vùng đệm của nó thay vì giữ lại toàn bộ tài liệu.
  • php://temp/maxmemory:0 — một luồng tạm thời của PHP bị buộc phải tràn ra đĩa ngay lập tức, dùng cho phần ghi sổ theo từng đối tượng ngày một lớn.
  • Ngân sách hiệu năng — một cam kết tài liệu có cấu trúc: một giới hạn thời gian thực và một giới hạn bộ nhớ đỉnh, được nêu rõ và có thể kiểm tra được.
  • Tín hiệu sống — một giá trị đo lường được báo cáo kèm phương pháp của nó dưới các điều kiện đã nêu rõ, chứ không phải một con số cố định được nhúng trong văn bản.