ข้ามไปยังเนื้อหา

หน่วยความจำและการสตรีม

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

PDF ขนาดใหญ่ไม่ควรต้องใช้ฮีปขนาดใหญ่ หน้านี้อธิบายวิธีที่ NextPDF จำกัดการใช้หน่วยความจำของกระบวนการให้อยู่ในขอบเขตเมื่อเอกสารมีขนาดใหญ่ขึ้น จุดที่ระบบสตรีมข้อมูลลงดิสก์แทนการสะสมไว้ในหน่วยความจำ และอธิบายว่า “performance budget” ในที่นี้หมายถึงข้อสัญญาที่ตรวจสอบได้ ไม่ใช่ตัวเลขสำหรับใช้เป็นพาดหัว

รูปแบบ PDF ไม่ได้บังคับให้ตัวสร้างต้องใช้ฮีปขนาดใหญ่ cross-reference table ของรูปแบบนี้บันทึก byte offset ของ indirect object ทุกตัว ดังนั้นตัวอ่านจึงต้องการการเข้าถึงแบบสุ่มภายในไฟล์ ไม่ใช่ทั้งไฟล์ในหน่วยความจำ ตัวสร้างจึงทำตามรูปแบบนั้นได้ โดยส่งออก object ทันทีที่เสร็จสิ้น และบันทึกไว้เพียงตำแหน่งที่ object เหล่านั้นถูกเขียน แต่หากยังคงทั้งเอกสารไว้ในฮีปจนถึงการเขียนขั้นสุดท้าย จำนวนหน้าจะทำให้การใช้หน่วยความจำเพิ่มขึ้นเชิงเส้น และรายงานที่ทำงานได้ดีเมื่อมีหลักร้อยหน้าอาจทำให้กระบวนการล้มเหลวเมื่อมีห้าหมื่นหน้า

สำหรับงานแบบ batch และ worker นี่คือความแตกต่างระหว่างบริการที่เสถียรกับบริการที่ล้มเหลวแบบคาดเดาไม่ได้เมื่อรับภาระงาน หน่วยความจำที่มีขอบเขตจำกัดเป็นคุณสมบัติเชิงออกแบบที่ต้องวิศวกรรมขึ้นมา ไม่ใช่ตัวเลขที่หวังว่าจะเกิดขึ้นเอง

  • streaming writer ถูกสร้างขึ้นเพื่อให้หน่วยความจำ มีขอบเขตจำกัดต่อเอกสาร แต่ละหน้าจะถูกเขียนลงเอาต์พุตทันทีที่สร้างเสร็จสมบูรณ์ จากนั้นบัฟเฟอร์ของหน้านั้นจะถูกปล่อยคืน
  • ข้อมูลบันทึกที่หากเก็บไว้ในฮีปจะเติบโตตามจำนวน object ได้แก่ cross-reference offset และการอ้างอิง Kids ของ page-tree จะถูกเขียนลงในสตรีมชั่วคราวที่เปิดด้วย php://temp/maxmemory:0 ซึ่งจะถ่ายลงดิสก์ทันทีแทนที่จะเพิ่มเข้าไปในฮีปของ PHP
  • เป้าหมายเชิงออกแบบคือ ฮีป O(1) ต่อหน้า การคงเอกสารไว้จะไม่ใช้ฮีปมากขึ้นเมื่อจำนวนหน้าเพิ่มขึ้น นั่นคือเป้าหมายเชิงวิศวกรรมที่ writer ถูกออกแบบให้รองรับ
  • ส่วน performance budget เป็นแนวคิดจริงที่มีโครงสร้างอยู่ในระบบเอกสาร คือเพดานเวลาตามนาฬิกาจริงและเพดานหน่วยความจำสูงสุด ซึ่งแสดงเป็นข้อสัญญาที่ตรวจสอบได้ performance budget ระบุข้อผูกพัน ไม่ใช่ผลลัพธ์จากการ benchmark
  • ตัวเลขที่เป็นรูปธรรมจะถูกถือเป็น สัญญาณที่มีชีวิต ซึ่งวัดภายใต้วิธีการที่ระบุไว้ ไม่ใช่ตรึงไว้ในเนื้อความจนตัวเลขอาจล้าสมัยไปโดยไม่มีสัญญาณ

streaming writer ตั้งอยู่บนหลักการเดียว คืออย่าคงสิ่งที่ส่งออกได้แล้วไว้

  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.
วงจรต่อหน้าของ streaming writer แต่ละหน้าจะถูกส่งออกและปล่อยคืน ส่วนข้อมูลบันทึกที่เพิ่มขึ้นจะถูกส่งไปยังสตรีมชั่วคราวที่รองรับด้วยดิสก์ ดังนั้นฮีปจึงไม่เติบโตตามจำนวนหน้า

รายละเอียดเรื่องการถ่ายข้อมูลลงดิสก์คือหัวใจสำคัญ php://temp ของ PHP จะเก็บข้อมูลปริมาณเล็กน้อยไว้ในหน่วยความจำ และจะถ่ายลงดิสก์ต่อเมื่อข้อมูลเกินค่าขีดเริ่มต้นเท่านั้น writer จะเปิดสตรีมชั่วคราวเหล่านั้นด้วยตัวเลือก maxmemory:0 ซึ่งบังคับให้ถ่ายลงดิสก์ ทันที เพราะค่าขีดเริ่มต้นในหน่วยความจำเป็นศูนย์ ผลในทางปฏิบัติคือ ข้อมูลบันทึกต่อ object ซึ่งโดยนิยามแล้วเติบโตไปพร้อมกับเอกสาร จะไม่สะสมอยู่ในฮีป แต่จะสะสมบนดิสก์แทน ซึ่งไม่ถูกจำกัดด้วยขนาดฮีป หากไม่มีตัวเลือกนี้ หน้าต่างในหน่วยความจำตามค่าเริ่มต้นจะต้องเต็มเสียก่อนจึงจะถ่ายลงดิสก์ ซึ่งจะทำลายเป้าหมายเรื่องหน่วยความจำที่มีขอบเขตจำกัดตรงจังหวะที่สำคัญที่สุด

ส่วน performance budget เป็นอีกส่วนสำคัญของเรื่องนี้ เป็นข้อสัญญาของระบบเอกสาร ไม่ใช่ข้ออ้างเพื่อการตลาด schema กำหนด budget เป็นจำนวนเต็มที่จำกัดขอบเขตไว้สองค่า ได้แก่ เพดานเวลาตามนาฬิกาจริงเป็นมิลลิวินาที และเพดานหน่วยความจำ resident สูงสุดเป็นเมบิไบต์ recipe ที่ประกาศ budget คือการประกาศข้อผูกพันที่ตรวจสอบได้ ในทำนองเดียวกับที่ลายเซ็นแบบมีชนิดประกาศข้อผูกพันที่คอมไพเลอร์ตรวจสอบได้ คุณค่าของ budget อยู่ที่การ ระบุไว้และบังคับใช้ ไม่ใช่ที่ตัวเลขจะเล็กเพียงใด

หน้านี้ใช้ Evidence: Mixed evidence และสถานะผสมนี้เป็นไปโดยตั้งใจ เพราะหลักฐานมีอยู่จริงสามประเภท

  • กลไกที่มีโค้ดรองรับ streaming writer ใน src/Writer/Streaming/StreamingPdfWriter.php มีเอกสารกำกับไว้ และทำให้วงจรต่อหน้าที่ emit แล้วปล่อยคืนเกิดขึ้นจริง และเปิดสตรีม xref และ Kids ด้วย php://temp/maxmemory:0 เพื่อบังคับให้ถ่ายลงดิสก์ทันที เพื่อให้ “PHP memory stays bounded regardless of object count.” การออกแบบที่สตรีม ใช้เคอร์เซอร์เดียว และไม่คงทรีไว้ในหน่วยความจำ ยังเป็นการตัดสินใจเชิงสถาปัตยกรรมที่บันทึกไว้ใน ADR-001 (ไปป์ไลน์การเรนเดอร์ถือสถานะมากที่สุดเพียง O(depth) ไม่ใช่ O(n) โหนด)
  • budget ตามหลักการออกแบบ ฟิลด์ performance_budget เป็นฟิลด์ทางเลือกที่มีอยู่จริงใน schema เอกสาร นิยามไว้เป็น { wall_ms, peak_mb } พร้อมขอบเขตบนที่ระบุชัดเจน เป็นข้อสัญญาที่บังคับใช้ได้โดยการออกแบบ
  • benchmark ในฐานะสัญญาณที่มีชีวิต ADR-001 ระบุชัดเจนว่าตัวเลขหน่วยความจำสูงสุดและเวลาตามนาฬิกาจริงจากเอกสารขนาดใหญ่ภายใต้สภาวะควบคุมเป็น เป้าหมายเชิงประจักษ์ที่ต้องเก็บรวบรวมและบันทึกภายใต้วิธีการที่ระบุไว้ ไม่ใช่ตัวเลขที่ยืนยันตายตัวในเนื้อความ ดังนั้นหน้านี้จึงระบุกลไกและข้อสัญญา และชี้ให้ไปดูตัวเลขที่เป็นรูปธรรมจากแหล่งที่วัดตัวเลขเหล่านั้น

รูปแบบไฟล์ทำให้เป้าหมายนี้สมเหตุสมผล ไม่ใช่เพียงความคาดหวัง เนื่องจาก cross-reference table เป็นดัชนี offset ต่อ object ตาม Spec: ISO 32000-2, §7.5.4 ตัวสร้างจึง สามารถ เขียน object ทันทีที่ทำเสร็จและเก็บไว้เพียง offset ของ object เหล่านั้น หน่วยความจำที่มีขอบเขตจำกัด สอดคล้องกับรูปแบบไฟล์ ไม่ใช่การฝืนรูปแบบไฟล์

หน่วยความจำที่มีขอบเขตจำกัดเป็นคุณสมบัติของวิธีการสร้าง ไม่ใช่แฟล็กที่ตั้งค่าได้ ลูป batch ที่สร้างเอกสารแต่ละชิ้นให้เสร็จและปล่อยคืนจะช่วยให้ฮีปคงอยู่ในระดับคงที่ตลอดการทำงาน:

<?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 ถูกใช้ร่วมกันเพราะ worker ควรแจงฟอนต์และรูปภาพเพียงครั้งเดียว เอกสาร ไม่ ถูกใช้ร่วมกัน และจะถูกปล่อยคืนในทุกรอบ ซึ่งทำให้หน่วยความจำของ batch ถูกจำกัดด้วยเอกสารหนึ่งชิ้น ไม่ใช่ด้วยทั้ง batch

ความเข้าใจผิดที่พบบ่อยที่สุดคือการมอง “bounded memory” เป็นข้ออ้างจาก benchmark โดยคาดหวังตัวเลขหน่วยเมกะไบต์ไว้ใช้อ้างอิง นั่นเป็นการตีความกลับด้านจากสิ่งที่กำลังอธิบาย การรับประกันในที่นี้เป็นเชิง โครงสร้าง writer ถูกสร้างขึ้นเพื่อให้การคงเอกสารไว้ไม่ใช้ฮีปมากขึ้นเมื่อจำนวนหน้าเพิ่มขึ้น ค่าสูงสุดที่เจาะจงขึ้นอยู่กับเนื้อหาในหน้า ฟอนต์ และรูปภาพ และจะมีความหมายก็ต่อเมื่อมีวิธีการวัดแนบมาด้วยเท่านั้น ด้วยเหตุนี้ตัวเลขดังกล่าวจึงเป็นของ benchmark ไม่ใช่ของประโยคนี้

กับดักข้อที่สองคือการคิดว่า php://temp ป้องกันปัญหานี้ให้อยู่แล้ว php://temp ช่วยป้องกันได้จริง แต่จะป้องกันก็ต่อเมื่อหน้าต่างในหน่วยความจำตามค่าเริ่มต้นเต็มเสียก่อนเท่านั้น ตัวเลือก maxmemory:0 คือสิ่งที่ทำให้การถ่ายลงดิสก์เกิดขึ้นทันที รายละเอียดนี้เป็นกลไกสำคัญ หากปราศจากตัวเลือกนี้ คุณสมบัติดังกล่าวจะไม่เป็นจริงในกรณีเอกสารขนาดใหญ่ ซึ่งเป็นกรณีที่กลไกนี้มีไว้รองรับโดยเฉพาะ

หน้านี้อธิบายกลไกการสตรีมและความหมายของ performance budget หน้านี้ ไม่ ระบุตัวเลขหน่วยความจำสูงสุดหรือ throughput ที่วัดได้ ตัวเลขเหล่านั้นเกิดจากระเบียบวิธี benchmarking ภายใต้วิธีการที่ประกาศไว้ และ ADR-001 ระบุชัดเจนว่าให้การวัดนั้นเป็นผู้กำหนดตัวเลขเชิงประจักษ์ คำว่า “ต่อเอกสาร” ไม่ได้หมายความว่าคงที่โดยไม่ขึ้นกับเนื้อหาของเอกสารแต่ละชิ้น หน้าที่มีรูปภาพฝังขนาดใหญ่จำนวนมากยังคงสิ้นเปลืองตามที่รูปภาพเหล่านั้นต้องใช้ สิ่งที่ไม่เติบโตคือ ข้อมูลบันทึกต่อหน้า และกราฟของหน้าที่ถูกคงไว้ ไม่ใช่ทุกเส้นทางการสร้างที่ใช้ streaming writer เส้นทางใดสตรีมและเส้นทางใดบัฟเฟอร์นั้นกำหนดโดยโค้ดและรูปแบบของไปป์ไลน์ ไม่ใช่โดยภาพรวมนี้ กลไกที่อธิบายไว้ถูกต้อง ณ วันที่ทบทวนหน้านี้ แหล่งข้อมูลที่เชื่อถือได้คือ src/Writer/Streaming/ และ ADR-001 ใน core repository

การออกแบบที่สตรีมและใช้หน่วยความจำแบบมีขอบเขตจำกัดเป็นคุณสมบัติของ Core รุ่นต่าง ๆ ไม่เปลี่ยนแปลงคุณสมบัตินี้:

streaming writer แบบหน่วยความจำมีขอบเขตจำกัด — edition availability
Edition Availability
Core Core มีการออกแบบ writer ที่สตรีมและถ่ายข้อมูลลงดิสก์
Pro Pro สืบทอด writer แบบหน่วยความจำมีขอบเขตจำกัดเดียวกัน โดยเพิ่มคุณสมบัติ ไม่ใช่โมเดลหน่วยความจำที่แตกต่าง
Enterprise Enterprise สืบทอด writer แบบหน่วยความจำมีขอบเขตจำกัดเดียวกัน โดยเพิ่มคุณสมบัติ ไม่ใช่โมเดลหน่วยความจำที่แตกต่าง
  • Bounded memory — คุณสมบัติเชิงออกแบบที่การคงเอกสารไว้ไม่ใช้ฮีปมากขึ้นเมื่อจำนวนหน้าเพิ่มขึ้น (เป้าหมาย O(1) ต่อหน้า)
  • Streaming writer — writer ที่ส่งออกแต่ละหน้าไปยังเอาต์พุตและปล่อยบัฟเฟอร์คืนแทนการคงทั้งเอกสารไว้
  • php://temp/maxmemory:0 — สตรีมชั่วคราวของ PHP ที่ถูกบังคับให้ถ่ายลงดิสก์ทันที ใช้สำหรับข้อมูลบันทึกต่อ object ที่เพิ่มขึ้น
  • Performance budget — ข้อสัญญาเชิงโครงสร้างของเอกสาร คือเพดานเวลาตามนาฬิกาจริงและเพดานหน่วยความจำสูงสุด ซึ่งระบุไว้และตรวจสอบได้
  • Living signal — ค่าที่วัดได้ซึ่งรายงานพร้อมวิธีการภายใต้เงื่อนไขที่ระบุไว้ แทนที่จะเป็นตัวเลขตายตัวที่ฝังไว้ในเนื้อความ