การสตรีมและหน่วยความจำ: คู่มือการทำโปรไฟล์และเวิร์กเกอร์แบบแบตช์
ภาพรวมโดยสรุป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสรุป”NextPDF เรนเดอร์แบบรอบเดียวและไม่เก็บ Document Object Model (DOM) ทั้งเอกสารไว้เลย ดังนั้นหน่วยความจำฝั่งอินพุตจึงถูกจำกัดด้วยความลึกของโครงสร้างซ้อน ไม่ใช่จำนวนองค์ประกอบ หน้านี้อธิบายโมเดลการสตรีม ข้อจำกัดใน Architecture Decision Record (ADR)-001 และวิธีรันเอนจินอย่างปลอดภัยในเวิร์กเกอร์คิวที่ทำงานเป็นเวลานาน
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”composer require nextpdf/core:^3ภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”NextPDF มีเส้นทางการเขียนสองแบบซึ่งมีโปรไฟล์หน่วยความจำต่างกัน
ตัวเขียนค่าเริ่มต้นแบบอยู่ในหน่วยความจำจะประกอบเอกสารทั้งฉบับก่อน แล้วจึงทำซีเรียลไลซ์ หน่วยความจำสูงสุดจะแปรผันตามขนาดเอาต์พุตรวม วิธีนี้ทำงานได้ดีกับเอกสารทั่วไป แต่อาจมีต้นทุนสูงสำหรับเอกสารที่ใหญ่มาก
ตัวเขียนแบบสตรีมจะทำซีเรียลไลซ์แต่ละหน้าขณะที่ประกอบหน้านั้น แล้วฟลัชหน้าออกก่อนเริ่มหน้าถัดไป เอนจินที่จัดส่งแล้ว — StreamingPdfWriter StreamingCursor DevNullWriter และ enum WriterState ใน src/Writer/Streaming/ — เป็นเอนจินจริงที่สมบูรณ์ ผ่านการทดสอบ และจัดส่งมาตั้งแต่ 3.1.0 เอนจินนี้เปิดให้ใช้งานผ่านสัญญาระดับ experimental ได้แก่ StreamingWriterInterface และ CursorInterface คลาสของเอนจินเป็นแบบภายใน จึงควรอ้างอิงผ่านสัญญาและปล่อยให้ Core เป็นผู้จัดหาการนำไปใช้งาน (คำอธิบายประกอบใน .ai/contracts-map.md ฉบับก่อนหน้าได้อธิบายการสตรีมไว้อย่างไม่ถูกต้องว่าเป็น “contract-only / no implementation” ข้อบกพร่องจากคำอธิบายประกอบที่ล้าสมัยนี้ถูกติดตามไว้ในประเด็น #610 และได้รับการแก้ไขแล้วในเอกสารสัญญา B1 — เอนจินได้จัดส่งมาตั้งแต่ 3.1.0)
เอนจินสตรีมได้รับการออกแบบให้หน่วยความจำที่ใช้งานอยู่ไม่เติบโตตามจำนวนหน้า บัฟเฟอร์ของแต่ละหน้าที่เสร็จสมบูรณ์แล้วจะถูกส่งให้ตัวเขียนและปล่อยคืน ตารางการอ้างอิงไขว้และการอ้างอิงทรีของหน้า /Kids จะถูกเขียนลงในสตรีมชั่วคราว php://temp/maxmemory:0 ซึ่งล้นลงดิสก์ทันทีแทนที่จะสะสมอยู่ใน PHP heap ผลลัพธ์ที่ทำซีเรียลไลซ์แล้วคือทรีของหน้าแบบมาตรฐาน โดยรายการ Count คือจำนวนโหนดใบ (อ็อบเจ็กต์หน้า) ที่เป็นลูกหลานของโหนดหนึ่ง (ISO 32000-2 §7.7.3.3) และรายการ Kids คืออาร์เรย์ของการอ้างอิงทางอ้อมไปยังลูกโดยตรงของโหนดนั้น (ISO 32000-2 §7.7.3.2) โปรไฟล์หน่วยความจำที่แน่นอนเป็นคุณสมบัติระดับ experimental และอาจเปลี่ยนแปลงระหว่างรุ่นย่อย ดังนั้นอย่าฮาร์ดโค้ดข้อสมมติจากการวัดเพียงครั้งเดียว
ADR-001 ควบคุมโมเดลหน่วยความจำของไปป์ไลน์การเรนเดอร์ HTML ตัวแยกโทเค็นสร้างรายการโทเค็นในรอบเดียว ตัวแจงส่วนอ่านรายการนี้จากซ้ายไปขวาและส่งตัวดำเนินการของ content stream ออกไปยังบัฟเฟอร์สตริง ไม่มีการสร้างทรีองค์ประกอบแบบถาวร: ตัวแจงส่วนเก็บ HtmlStyleState ไว้มากที่สุดหนึ่งตัวต่อระดับการซ้อน จำกัดด้วย MAX_NESTING_DEPTH = 100 และบังคับใช้เพดานคงที่ที่ MAX_ELEMENT_COUNT = 50_000 การดำเนินการสองอย่างที่ต้องมองล่วงหน้า — การกำหนดขนาดคอลัมน์ของตารางและกลุ่มตัวเลือก :has() / :last-child — ใช้อาร์เรย์ดัชนีพรีสแกนแบบมีขอบเขตบนรายการโทเค็นแบบแบน ไม่ใช่ DOM ที่เก็บค้างไว้ การวัดประสิทธิภาพ Phase 0 (docs/architecture/adr-001-memory-benchmark.md รันเมื่อ 2026-04-06 PHP 8.5.3 memory_limit=1G) วัดเอกสารที่มี 50,000 องค์ประกอบได้ค่าสูงสุด 50 MB สำหรับเส้นทางสตรีม เทียบกับ 4 MB ของการจำลองที่เก็บงานบางส่วนไว้ รายงานระบุว่าราว 50 MB ในนั้นมาจาก content stream ที่สะสมไว้ซึ่งคงที่ตามสถาปัตยกรรม และแยกให้เห็นข้อได้เปรียบฝั่งอินพุตที่ 4–5 เท่าสำหรับโมเดลสตรีมบนฟิกซ์เจอร์นั้น ตัวเลขเหล่านั้นเป็นค่าที่สังเกตได้บนเครื่องและฟิกซ์เจอร์นั้นเพียงชุดเดียว ไม่ใช่การรับประกัน
ทำโปรไฟล์หน่วยความจำก่อนปรับแต่ง
หัวข้อที่มีชื่อว่า “ทำโปรไฟล์หน่วยความจำก่อนปรับแต่ง”วัดผลก่อนเปลี่ยนแปลงสิ่งใด ไปป์ไลน์ HTML ควบคุมด้วย tools/perf-benchmark.php (รันผ่าน composer ai:perf-check) ซึ่งรายงาน peak_memory_delta_bytes — ค่าสูงสุดส่วนเพิ่มต่อเป้าหมายที่ใช้เป็นแกนวัดการถดถอย ไม่ใช่ค่าสูงสุดสัมบูรณ์ของกระบวนการ ค่าฐาน Cycle 36 (docs/architecture/PERFORMANCE-BUDGETS.md §6.3 บันทึกเมื่อ 2026-05-17 บน i9-13900K, 64 GB, PHP 8.5.3 ปิด opcache) พบค่าสูงสุดส่วนต่างเป็น 0 ไบต์ใน 12 จาก 16 คู่ target/mode ค่าส่วนต่างที่ไม่เป็นศูนย์สี่ค่าเกิดจากการจัดสรร font-cache และ trace-buffer ในการเข้าถึงครั้งแรก ซึ่งคงที่ในการเรนเดอร์ครั้งถัดไป ให้ตีความค่าเหล่านั้นเป็นค่าที่สังเกตได้สำหรับเครื่องนั้น ไม่ใช่ค่าคงที่ที่นำไปใช้ได้ทั่วไป สำหรับการทำโปรไฟล์เฉพาะกิจของเอกสารของคุณเอง ให้เก็บตัวอย่าง memory_get_peak_usage(true) ก่อนและหลังการเรนเดอร์ และรีเซ็ตค่าสูงสุดด้วย memory_reset_peak_usage() ระหว่างการวนซ้ำ เช่นเดียวกับที่การวัดประสิทธิภาพแยกต้นทุนต่อเป้าหมาย
รัน NextPDF ในเวิร์กเกอร์แบบแบตช์
หัวข้อที่มีชื่อว่า “รัน NextPDF ในเวิร์กเกอร์แบบแบตช์”เวิร์กเกอร์คิวคือกระบวนการ PHP ที่ทำงานเป็นเวลานาน: บูตเฟรมเวิร์กหนึ่งครั้ง คงอยู่ในหน่วยความจำ และจัดการงานแบบวนซ้ำ รูปแบบนี้ทำให้ทำงานได้เร็ว และยังเป็นเหตุผลที่การดูแลหน่วยความจำให้สะอาดมีความสำคัญ การรั่วไหลแบบค่อยเป็นค่อยไปที่มองไม่เห็นในคำขอเดียวสามารถสะสมข้ามงานหลายพันงานได้ PERFORMANCE-BUDGETS §1 ระบุรูปแบบความล้มเหลวนี้ไว้อย่างชัดเจน: เวิร์กเกอร์ที่เรนเดอร์ PDF จำนวนมากต่อเนื่องกันอาจใช้หน่วยความจำจนหมดหลังจากผ่านไปหลายชั่วโมง แม้ว่าการเรนเดอร์ครั้งเดียวจะดูปกติก็ตาม
NextPDF รองรับสภาพแวดล้อมแบบเวิร์กเกอร์ DocumentFactory ช่วยให้เวิร์กเกอร์สร้างเอกสารใหม่สำหรับแต่ละงาน ขณะใช้ FontRegistry และ ImageRegistry ร่วมกันตลอดอายุของกระบวนการ ดังนั้นการแจงส่วนฟอนต์และรูปภาพจึงเกิดขึ้นเพียงครั้งเดียว ไม่ใช่หนึ่งครั้งต่อหนึ่งงาน ADR-001 บันทึกไว้ว่าตัวแจงส่วน HTML ถูกสร้างขึ้นต่อคำขอโดยไม่มีสถานะที่เปลี่ยนแปลงได้แบบ static และอ็อบเจ็กต์ formatting-context ในอนาคตต้องเป็นไปตามการกำหนดขอบเขตต่อคำขอแบบเดียวกัน ขั้นตอนต่อไปนี้กำหนดค่าเวิร์กเกอร์อย่างปลอดภัย
ขั้นที่ 1 — ใช้รีจิสทรีร่วมกันข้ามงาน
หัวข้อที่มีชื่อว่า “ขั้นที่ 1 — ใช้รีจิสทรีร่วมกันข้ามงาน”สร้างรีจิสทรีหนึ่งครั้งเมื่อบูตกระบวนการ แล้วนำกลับมาใช้ซ้ำสำหรับทุกงาน ตาม examples/14-worker-factory.php:
<?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;
// Created once at process boot — not per job.$fontRegistry = new FontRegistry();$imageRegistry = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$documentFactory = new DocumentFactory($fontRegistry, $imageRegistry);
$factory = PdfFactory::new() ->withCompress(true) ->withDocumentFactory($documentFactory);
// Per job: a fresh document, shared registries.$doc = $factory->create();$doc->addPage();$doc->setFont('helvetica', '', 11);$doc->cell(0, 8, 'Rendered inside a worker.', newLine: true);$doc->save('/path/to/output.pdf');ค่า maxCacheBytes ของรีจิสทรีรูปภาพจำกัดขอบเขตของแคชที่ใช้ร่วมกัน จึงทำให้แคชไม่เติบโตอย่างไม่จำกัดข้ามงาน
ขั้นที่ 2 — จำกัดอายุของเวิร์กเกอร์
หัวข้อที่มีชื่อว่า “ขั้นที่ 2 — จำกัดอายุของเวิร์กเกอร์”นี่เป็นแนวปฏิบัติทั่วไปในการควบคุมกระบวนการสำหรับเวิร์กเกอร์ PHP ใดๆ ไม่ใช่การรับประกันของเอนจิน NextPDF: ให้รีสตาร์ตเวิร์กเกอร์เป็นระยะ เพื่อไม่ให้กระบวนการที่ทำงานเป็นเวลานานสะสมหน่วยความจำหรือรันโค้ดที่ล้าสมัยต่อไปอย่างไม่มีกำหนด ระบบคิว PHP หลักทั้งสองระบบมีขีดจำกัดในตัวและกลไกรีสตาร์ตแบบนุ่มนวล
สำหรับ Laravel queues (https://laravel.com/docs/12.x/queues) คำสั่ง queue:work รันเวิร์กเกอร์เป็นกระบวนการที่ทำงานเป็นเวลานาน ตัวเลือกที่เอกสารระบุไว้ได้แก่ --memory (ค่าเริ่มต้น 128 MB; เวิร์กเกอร์จะออกเมื่อหน่วยความจำเกินขีดจำกัด) --max-jobs (ออกหลังจากทำงานครบจำนวนงาน) และ --max-time (ออกหลังจากครบจำนวนวินาที) คำสั่ง queue:restart ส่งสัญญาณให้เวิร์กเกอร์ออกอย่างนุ่มนวลหลังจากงานปัจจุบัน ดังนั้นการ deploy หรือตัวจับเวลาตามรอบจึงสามารถรีไซเคิลเวิร์กเกอร์ได้โดยไม่ขัดจังหวะการเรนเดอร์ที่กำลังดำเนินอยู่ Laravel Horizon (https://laravel.com/docs/12.x/horizon) ดูแลเวิร์กเกอร์ Redis ด้วยกลยุทธ์การจัดสมดุลแบบ auto และ php artisan horizon:terminate แบบนุ่มนวล ซึ่งจะทำงานที่กำลังดำเนินอยู่ให้เสร็จก่อนที่ตัวตรวจสอบกระบวนการจะรีสตาร์ต supervisor
สำหรับ Symfony Messenger (https://symfony.com/doc/current/messenger.html) คำสั่ง messenger:consume จะรันไปเรื่อยๆ ตามค่าเริ่มต้น ตัวเลือกขีดจำกัดที่เอกสารระบุไว้ได้แก่ --limit (จัดการข้อความ N ข้อความ แล้วออก) --memory-limit (เช่น 128M; ออกเมื่อหน่วยความจำถึงขีดจำกัด) และ --time-limit (เช่น 3600; ออกหลังจากครบช่วงเวลา) เอกสารของ Symfony แนะนำให้รันเวิร์กเกอร์ภายใต้ Supervisor หรือ systemd เพื่อให้กระบวนการที่ออกไปแล้วรีสตาร์ตโดยอัตโนมัติ และ messenger:stop-workers ตั้งค่าแฟล็กในแคชที่บอกให้เวิร์กเกอร์แต่ละตัวทำข้อความปัจจุบันให้เสร็จและออกอย่างเรียบร้อย
ขั้นที่ 3 — รีสตาร์ตเมื่อ deploy
หัวข้อที่มีชื่อว่า “ขั้นที่ 3 — รีสตาร์ตเมื่อ deploy”ทุกครั้งที่ทำ deploy ให้ส่งสัญญาณรีสตาร์ตอย่างนุ่มนวลเพื่อให้เวิร์กเกอร์รับโค้ดใหม่: php artisan queue:restart (หรือ php artisan horizon:terminate) สำหรับ Laravel php bin/console messenger:stop-workers สำหรับ Symfony ตัวจัดการกระบวนการ — Supervisor systemd หรือ supervisor ของ Horizon/Octane — จะเริ่มกระบวนการใหม่กับโค้ดเบสใหม่ นี่เป็นแนวปฏิบัติทั่วไปในการ deploy สำหรับเวิร์กเกอร์ PHP ที่ทำงานเป็นเวลานานและเป็นอิสระจาก NextPDF
ประสิทธิภาพ
หัวข้อที่มีชื่อว่า “ประสิทธิภาพ”เส้นทางสตรีมได้รับการออกแบบให้จำกัดหน่วยความจำสูงสุดด้วยการฟลัชแต่ละหน้าที่เสร็จสมบูรณ์ และเทข้อมูลการอ้างอิงไขว้กับการบันทึกทรีของหน้าลงในสตรีมชั่วคราวที่อยู่บนดิสก์ ผลที่ได้คือ resident set ถูกออกแบบให้ไม่เพิ่มขึ้นตามจำนวนหน้า พฤติกรรมดังกล่าวเป็นค่าที่สังเกตได้ในเอนจิน 3.1.0 ที่จัดส่งแล้ว และถูกตรึงไว้ด้วยการทดสอบความสามารถในการทำซ้ำแบบ golden-baseline แต่ระบุไว้เป็นพฤติกรรมเชิงออกแบบมากกว่าตัวเลขตายตัว เนื่องจากโปรไฟล์นี้เป็นคุณสมบัติระดับ experimental หน่วยความจำฝั่งอินพุตของไปป์ไลน์ HTML ถูกจำกัดด้วย MAX_NESTING_DEPTH = 100 มากกว่าจำนวนองค์ประกอบ (ADR-001) ตัวเลขที่เป็นรูปธรรมทั้งหมดในหน้านี้ผูกกับสิ่งประดิษฐ์ที่มีวันที่ — การวัดประสิทธิภาพ ADR-001 เมื่อ 2026-04-06 และค่าฐาน PERFORMANCE-BUDGETS Cycle 36 เมื่อ 2026-05-17 — และเป็นค่าที่สังเกตได้บนเครื่องที่เอกสารเหล่านั้นระบุ ให้ถือว่าเป็นข้อสังเกต ไม่ใช่การรับประกันที่นำไปใช้ได้ทั่วไป performance_budget ที่ 1500 ms / 64 MB เป็นกรอบของ canvas ไม่ใช่เพดานตามสัญญา
หมายเหตุด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “หมายเหตุด้านความปลอดภัย”เมธอด writeContent() ของเคอร์เซอร์สตรีมจะต่อท้ายไบต์ลงในสตรีมเนื้อหาของหน้าแบบตรงตามไบต์ที่ได้รับ เมธอดนี้ไม่ตรวจสอบไวยากรณ์ของตัวดำเนินการ ในเวิร์กเกอร์ที่เรนเดอร์เนื้อหาซึ่งได้รับอิทธิพลจากผู้เรียก อย่าส่งอินพุตที่ไม่น่าเชื่อถือไปยัง writeContent() เด็ดขาด ให้ใช้ writeText() ซึ่งเคอร์เซอร์ที่จัดส่งแล้วจะ escape ให้ตามไวยากรณ์ literal-string ของ PDF ผู้เรียกเป็นเจ้าของสตรีมเอาต์พุต: เอนจินเขียนลงในสตรีมนั้นแต่ไม่ปิดหรือเปิดสตรีมขึ้นใหม่ จึงไม่สามารถเปลี่ยนเส้นทางเอาต์พุตได้ เวิร์กเกอร์ต้องปิด handle ด้วยตนเองหลังจากที่ close() ของตัวเขียนคืนค่า ไม่เช่นนั้นจะเกิดการรั่วไหลของ file descriptor ข้ามงาน การใช้รีจิสทรีร่วมกันข้ามงานเป็นการปรับให้เหมาะสมด้านประสิทธิภาพ ไม่ใช่ขอบเขตความน่าเชื่อถือ: ImageRegistry ที่ใช้ร่วมกันจะแคชรูปภาพที่แจงส่วนแล้ว ดังนั้นจงกำหนดขนาด maxCacheBytes อย่างรอบคอบ และอย่าสันนิษฐานว่ามีการแยกแคชระหว่างผู้เช่าในเวิร์กเกอร์แบบหลายผู้เช่า
ความสอดคล้องตามมาตรฐาน
หัวข้อที่มีชื่อว่า “ความสอดคล้องตามมาตรฐาน”| ข้อกล่าวอ้าง | มาตรฐาน | ข้อกำหนด | หลักฐาน |
|---|---|---|---|
ตัวเขียนแบบสตรีมสร้างทรีของหน้าที่รายการ Kids เป็นอาร์เรย์ของการอ้างอิงทางอ้อมไปยังลูกโดยตรงของโหนด | ISO 32000-2 | §7.7.3.2 | |
ตัวเขียนแบบสตรีมสร้างรายการ Count ที่เท่ากับจำนวนอ็อบเจ็กต์หน้าแบบโหนดใบที่เป็นลูกหลานของโหนดทรีของหน้า | ISO 32000-2 | §7.7.3.3 |
ข้อกำหนดถูกเรียบเรียงใหม่และตรึงด้วยอภิธานศัพท์ ไม่มีการนำข้อความเชิงบรรทัดฐานมาผลิตซ้ำ
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- Contracts / Streaming —
experimentalStreamingWriterInterfaceและCursorInterfaceพร้อมสเตตแมชชีนของสัญญาเหล่านั้น - HTML / Streaming constraints (ADR-001) — การตัดสินใจแบบรอบเดียว การไม่เก็บ DOM ค้างไว้ และเกณฑ์การทบทวน
- Performance — เกตวัดการถดถอยด้านความหน่วงและหน่วยความจำของไปป์ไลน์ HTML
- Layout — เอนจินจัดวางหน้าที่ไม่เก็บสถานะต่อหน้า
- PERFORMANCE-BUDGETS — รูปแบบความล้มเหลวจากเวิร์กเกอร์ที่รั่วและค่าฐานของเกตวัดการถดถอย