หน่วยความจำและการสตรีม
Spec: ISO 32000-2, §7.5.4 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
- ตัวเลขที่เป็นรูปธรรมจะถูกถือเป็น สัญญาณที่มีชีวิต ซึ่งวัดภายใต้วิธีการที่ระบุไว้ ไม่ใช่ตรึงไว้ในเนื้อความจนตัวเลขอาจล้าสมัยไปโดยไม่มีสัญญาณ
แนวทางของ NextPDF ในเรื่องนี้
หัวข้อที่มีชื่อว่า “แนวทางของ NextPDF ในเรื่องนี้”streaming writer ตั้งอยู่บนหลักการเดียว คืออย่าคงสิ่งที่ส่งออกได้แล้วไว้
- 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://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 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 รุ่นต่าง ๆ ไม่เปลี่ยนแปลงคุณสมบัตินี้:
| Edition | Availability |
|---|---|
| Core | Core มีการออกแบบ writer ที่สตรีมและถ่ายข้อมูลลงดิสก์ |
| Pro | Pro สืบทอด writer แบบหน่วยความจำมีขอบเขตจำกัดเดียวกัน โดยเพิ่มคุณสมบัติ ไม่ใช่โมเดลหน่วยความจำที่แตกต่าง |
| Enterprise | Enterprise สืบทอด writer แบบหน่วยความจำมีขอบเขตจำกัดเดียวกัน โดยเพิ่มคุณสมบัติ ไม่ใช่โมเดลหน่วยความจำที่แตกต่าง |
เอกสารที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เอกสารที่เกี่ยวข้อง”- โมเดลไปป์ไลน์ — ตำแหน่งของขั้นตอน writer ในกระแสการทำงานของเอกสาร
- การทำ benchmark อย่างซื่อตรง — วิธีที่ NextPDF รายงานตัวเลขที่หน้านี้จงใจไม่ยืนยัน
- การสร้างเอกสารปริมาณมาก — สถานการณ์แบบ batch ที่กลไกนี้ถูกสร้างขึ้นมาเพื่อรองรับ
อภิธานศัพท์
หัวข้อที่มีชื่อว่า “อภิธานศัพท์”- Bounded memory — คุณสมบัติเชิงออกแบบที่การคงเอกสารไว้ไม่ใช้ฮีปมากขึ้นเมื่อจำนวนหน้าเพิ่มขึ้น (เป้าหมาย O(1) ต่อหน้า)
- Streaming writer — writer ที่ส่งออกแต่ละหน้าไปยังเอาต์พุตและปล่อยบัฟเฟอร์คืนแทนการคงทั้งเอกสารไว้
php://temp/maxmemory:0— สตรีมชั่วคราวของ PHP ที่ถูกบังคับให้ถ่ายลงดิสก์ทันที ใช้สำหรับข้อมูลบันทึกต่อ object ที่เพิ่มขึ้น- Performance budget — ข้อสัญญาเชิงโครงสร้างของเอกสาร คือเพดานเวลาตามนาฬิกาจริงและเพดานหน่วยความจำสูงสุด ซึ่งระบุไว้และตรวจสอบได้
- Living signal — ค่าที่วัดได้ซึ่งรายงานพร้อมวิธีการภายใต้เงื่อนไขที่ระบุไว้ แทนที่จะเป็นตัวเลขตายตัวที่ฝังไว้ในเนื้อความ