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

การสตรีมและหน่วยความจำ: คู่มือการทำโปรไฟล์และเวิร์กเกอร์แบบแบตช์

NextPDF เรนเดอร์แบบรอบเดียวและไม่เก็บ Document Object Model (DOM) ทั้งเอกสารไว้เลย ดังนั้นหน่วยความจำฝั่งอินพุตจึงถูกจำกัดด้วยความลึกของโครงสร้างซ้อน ไม่ใช่จำนวนองค์ประกอบ หน้านี้อธิบายโมเดลการสตรีม ข้อจำกัดใน Architecture Decision Record (ADR)-001 และวิธีรันเอนจินอย่างปลอดภัยในเวิร์กเกอร์คิวที่ทำงานเป็นเวลานาน

Terminal window
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() ระหว่างการวนซ้ำ เช่นเดียวกับที่การวัดประสิทธิภาพแยกต้นทุนต่อเป้าหมาย

เวิร์กเกอร์คิวคือกระบวนการ PHP ที่ทำงานเป็นเวลานาน: บูตเฟรมเวิร์กหนึ่งครั้ง คงอยู่ในหน่วยความจำ และจัดการงานแบบวนซ้ำ รูปแบบนี้ทำให้ทำงานได้เร็ว และยังเป็นเหตุผลที่การดูแลหน่วยความจำให้สะอาดมีความสำคัญ การรั่วไหลแบบค่อยเป็นค่อยไปที่มองไม่เห็นในคำขอเดียวสามารถสะสมข้ามงานหลายพันงานได้ PERFORMANCE-BUDGETS §1 ระบุรูปแบบความล้มเหลวนี้ไว้อย่างชัดเจน: เวิร์กเกอร์ที่เรนเดอร์ PDF จำนวนมากต่อเนื่องกันอาจใช้หน่วยความจำจนหมดหลังจากผ่านไปหลายชั่วโมง แม้ว่าการเรนเดอร์ครั้งเดียวจะดูปกติก็ตาม

NextPDF รองรับสภาพแวดล้อมแบบเวิร์กเกอร์ DocumentFactory ช่วยให้เวิร์กเกอร์สร้างเอกสารใหม่สำหรับแต่ละงาน ขณะใช้ FontRegistry และ ImageRegistry ร่วมกันตลอดอายุของกระบวนการ ดังนั้นการแจงส่วนฟอนต์และรูปภาพจึงเกิดขึ้นเพียงครั้งเดียว ไม่ใช่หนึ่งครั้งต่อหนึ่งงาน ADR-001 บันทึกไว้ว่าตัวแจงส่วน HTML ถูกสร้างขึ้นต่อคำขอโดยไม่มีสถานะที่เปลี่ยนแปลงได้แบบ static และอ็อบเจ็กต์ formatting-context ในอนาคตต้องเป็นไปตามการกำหนดขอบเขตต่อคำขอแบบเดียวกัน ขั้นตอนต่อไปนี้กำหนดค่าเวิร์กเกอร์อย่างปลอดภัย

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

นี่เป็นแนวปฏิบัติทั่วไปในการควบคุมกระบวนการสำหรับเวิร์กเกอร์ 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 ตั้งค่าแฟล็กในแคชที่บอกให้เวิร์กเกอร์แต่ละตัวทำข้อความปัจจุบันให้เสร็จและออกอย่างเรียบร้อย

ทุกครั้งที่ทำ 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 / StreamingexperimentalStreamingWriterInterface และ CursorInterface พร้อมสเตตแมชชีนของสัญญาเหล่านั้น
  • HTML / Streaming constraints (ADR-001) — การตัดสินใจแบบรอบเดียว การไม่เก็บ DOM ค้างไว้ และเกณฑ์การทบทวน
  • Performance — เกตวัดการถดถอยด้านความหน่วงและหน่วยความจำของไปป์ไลน์ HTML
  • Layout — เอนจินจัดวางหน้าที่ไม่เก็บสถานะต่อหน้า
  • PERFORMANCE-BUDGETS — รูปแบบความล้มเหลวจากเวิร์กเกอร์ที่รั่วและค่าฐานของเกตวัดการถดถอย