تخطَّ إلى المحتوى

الدفق والذاكرة: دليل عملي لقياس الأداء وعمال المعالجة على دفعات

يُجري ⁨NextPDF⁩ العرض في تمريرة واحدة ولا يحتفظ مطلقًا بنموذج كائنات المستند (⁨DOM⁩) على مستوى المستند؛ لذلك تكون ذاكرة جانب الإدخال محدودة بعمق التداخل، لا بعدد العناصر. تشرح هذه الصفحة نموذج الدفق، والقيود الواردة في سجل قرار المعمارية (⁨ADR⁩)-001، وكيف تشغّل المحرّك بأمان داخل عامل طابور طويل العمر.

Terminal window
composer require nextpdf/core:^3

لدى ⁨NextPDF⁩ مسارا كتابة يختلفان في نمط استهلاك الذاكرة.

يُنشئ كاتب الذاكرة الافتراضي المستند بأكمله، ثم يُسلسله. ويرتبط الحد الأقصى للذاكرة بإجمالي حجم المُخرَج. يعمل ذلك جيدًا مع المستندات المعتادة، لكنه قد يكون مكلفًا مع المستندات الكبيرة جدًا.

يُسلسل كاتب الدفق كل صفحة أثناء إنشائها، ثم يُفرغها قبل بدء الصفحة التالية. المحرّك المُسلَّم — StreamingPdfWriter وStreamingCursor وDevNullWriter، والتعداد WriterState في src/Writer/Streaming/ — حقيقي ونهائي ومُختبَر ومُسلَّم منذ الإصدار 3.1.0. وهو متاح عبر العقدين StreamingWriterInterface وCursorInterface من الفئة experimental. أصناف المحرّك داخلية، لذا اعتمد على العقود ودع ⁨Core⁩ يوفّر التنفيذ. (وصف تعليق سابق في .ai/contracts-map.md الدفق خطأً بأنه “عقد فقط / دون تنفيذ”؛ ويُتعقَّب عيب ذلك التعليق القديم في المسألة #610 وقد صُحِّح في وثائق عقد ⁨B1⁩ — فالمحرّك مُسلَّم منذ الإصدار 3.1.0.)

صُمِّم محرّك الدفق بحيث لا تنمو الذاكرة المقيمة مع عدد الصفحات. يُسلَّم المخزن المؤقت لكل صفحة مكتملة إلى الكاتب ثم يُحرَّر. يُكتَب جدول الإحالات المتقاطعة ومراجع شجرة الصفحات /Kids إلى أدفاق مؤقتة من نوع php://temp/maxmemory:0 تنسكب إلى القرص فورًا بدلًا من التراكم في كومة ⁨PHP.⁩ والنتيجة المُسلسَلة هي شجرة صفحات قياسية يكون مُدخَل Count فيها عددَ العُقد الورقية (كائنات الصفحات) المتفرّعة من عُقدة ما (⁨ISO 32000-2⁩ §7.7.3.3)، ويكون مُدخَل Kids فيها مصفوفةً من المراجع غير المباشرة إلى الأبناء المباشرين لتلك العُقدة (⁨ISO 32000-2⁩ §7.7.3.2). نمط الذاكرة الدقيق خاصية من الفئة experimental وقد يتغيّر بين الإصدارات الثانوية، لذا لا تُثبّت افتراضًا مستمدًا من قياس واحد في شِفرتك.

يحكم ⁨ADR-001⁩ نموذج الذاكرة لخط أنابيب عرض ⁨HTML.⁩ يُنتج المُجزِّئ قائمة رموز في تمريرة واحدة. يستهلكها المُحلِّل من اليسار إلى اليمين ويُصدِر معاملات دفق المحتوى في مخزن سلاسل مؤقت. لا تُبنى أي شجرة عناصر دائمة: يحتفظ المُحلِّل بحالة HtmlStyleState واحدة على الأكثر لكل مستوى تداخل، محدودةً بـ MAX_NESTING_DEPTH = 100، ويفرض حدًا أقصى صارمًا قدره MAX_ELEMENT_COUNT = 50_000. العمليتان اللتان تتطلّبان نظرًا مسبقًا — تحديد أحجام أعمدة الجداول وعائلة المُحدِّدات :has() / :last-child — تستخدمان مصفوفات مفهرسة ومحدودة للمسح المسبق فوق قائمة الرموز المسطّحة، لا شجرة ⁨DOM⁩ محتفَظًا بها. قاس معيار المرحلة 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⁩ من ذلك إلى دفق المحتوى المتراكم الثابت معماريًا، ويعزل ميزة على جانب الإدخال تتراوح بين 4–5 أضعاف لنموذج الدفق على ذلك المُعطى الاختباري. رُصِدت هذه الأرقام على ذلك العتاد والمُعطى الاختباري وحدهما، وليست مضمونة.

قِس الذاكرة قبل أن تضبط الأداء

قسم بعنوان «قِس الذاكرة قبل أن تضبط الأداء»

قِس قبل أن تُغيّر أي شيء. يخضع خط أنابيب ⁨HTML⁩ لبوابة tools/perf-benchmark.php (تُشغَّل عبر composer ai:perf-check)، التي تُبلِّغ عن peak_memory_delta_bytes — وهو الحد الأقصى التزايدي لكل هدف والمستخدَم محورًا للانحدار، لا الحد الأقصى المطلق للعملية. رصد أساس الدورة 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.⁩ عُزِيت الفوارق الأربعة غير الصفرية إلى تخصيصات ذاكرة مخبأ الخطوط ومخزن التتبّع المؤقت عند الاستخدام الأول، وهي تبقى ثابتة في عمليات العرض اللاحقة. اقرأ هذه القيم على أنها قيم مرصودة لذلك العتاد، لا ثوابت قابلة للنقل. لإجراء قياس عابر لمستندك الخاص، خذ عيّنة من memory_get_peak_usage(true) قبل العرض وبعده، وأعد ضبط الحد الأقصى بـ memory_reset_peak_usage() بين التكرارات، بالطريقة نفسها التي يعزل بها المعيار التكلفة لكل هدف.

تشغيل ⁨NextPDF⁩ داخل عامل معالجة دفعية

قسم بعنوان «تشغيل ⁨NextPDF⁩ داخل عامل معالجة دفعية»

عامل الطابور هو عملية ⁨PHP⁩ طويلة العمر: يُقلِع إطار العمل مرة واحدة، ويبقى مقيمًا، ويعالج المهام في حلقة. هذا ما يجعله سريعًا، وهو أيضًا ما يجعل نظافة الذاكرة مهمة. قد يتراكم تسرّب بطيء لا يظهر في طلب واحد عبر آلاف المهام. يذكر ⁨PERFORMANCE-BUDGETS⁩ §1 هذا النمط من الإخفاق صراحةً: قد يستنفد عامل يعرض ملفات ⁨PDF⁩ كثيرة على التوالي الذاكرةَ بعد ساعات، حتى عندما تبدو عمليات العرض الفردية سليمة.

يدعم ⁨NextPDF⁩ بيئات العمال. يتيح DocumentFactory للعامل إنشاء مستند جديد لكل مهمة مع مشاركة FontRegistry وImageRegistry طوال عمر العملية، فيحدث تحليل الخطوط والصور مرة واحدة بدلًا من مرة لكل مهمة. يُسجِّل ⁨ADR-001⁩ أن مُحلِّل ⁨HTML⁩ يُنشَأ لكل طلب دون حالة ثابتة قابلة للتغيير، وأن كائنات سياق التنسيق المستقبلية يجب أن تتّبع نطاق “لكل طلب” نفسه. هيّئ العامل بأمان عبر الخطوات التالية.

الخطوة 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⁩ (https://laravel.com/docs/12.x/queues)، يُشغِّل الأمر queue:work العاملَ كعملية طويلة العمر. الخيارات الموثَّقة هي --memory (الافتراضي 128 ⁨MB⁩؛ يخرج العامل عندما تتجاوز ذاكرته الحد)، و--max-jobs (الخروج بعد عدد من المهام)، و--max-time (الخروج بعد عدد من الثواني). يُشير الأمر queue:restart إلى العمال بالخروج بسلاسة بعد المهمة الحالية، فيستطيع نشر جديد أو مؤقّت دوري إعادة تدويرهم دون مقاطعة عملية عرض جارية. يُشرِف ⁨Laravel Horizon⁩ (https://laravel.com/docs/12.x/horizon) على عمال ⁨Redis⁩ باستراتيجية موازنة auto وأمر سلِس php artisan horizon:terminate، الذي يُنهي المهام الجارية قبل أن يُعيد مراقب العمليات تشغيل المُشرِف.

في ⁨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 — إعادة التشغيل عند النشر

قسم بعنوان «الخطوة 3 — إعادة التشغيل عند النشر»

عند كل نشر، أرسل إشارة بإعادة تشغيل سلسة كي يلتقط العمال الشِفرة الجديدة: php artisan queue:restart (أو php artisan horizon:terminate) لـ ⁨Laravel⁩، وphp bin/console messenger:stop-workers لـ ⁨Symfony.⁩ يبدأ مدير العمليات عندئذ — ⁨Supervisor⁩ أو ⁨systemd⁩ أو مُشرِف ⁨Horizon/Octane⁩ — عملية جديدة من قاعدة الشِفرة الجديدة. هذه ممارسة نشر عامة لعمال ⁨PHP⁩ طويلي العمر ومستقلة عن ⁨NextPDF.⁩

صُمِّم مسار الدفق ليحدّ من الحد الأقصى للذاكرة عبر إفراغ كل صفحة مكتملة وترحيل سجلات الإحالات المتقاطعة وشجرة الصفحات إلى أدفاق مؤقتة مدعومة بالقرص. ونتيجةً لذلك، يُقصد ألّا تنمو المجموعة المقيمة مع عدد الصفحات. يُرصَد هذا السلوك في محرّك الإصدار 3.1.0 المُسلَّم وتُثبِّته اختبارات قابلية إعادة الإنتاج ذات الأساس الذهبي، لكنه يُذكَر بوصفه سلوكًا تصميميًا لا رقمًا ثابتًا لأن النمط خاصية من الفئة experimental. ذاكرة جانب الإدخال لخط أنابيب ⁨HTML⁩ محدودة بـ MAX_NESTING_DEPTH = 100، لا بعدد العناصر (⁨ADR-001⁩). جميع الأرقام الملموسة في هذه الصفحة مرتبطة بأثر مؤرَّخ — معيار ⁨ADR-001⁩ بتاريخ 2026-04-06 وأساس الدورة 36 لـ ⁨PERFORMANCE-BUDGETS⁩ بتاريخ 2026-05-17 — ورُصِدت على العتاد الذي تذكره هاتان الوثيقتان؛ فعامِلها على أنها مشاهدات لا ضمانات قابلة للنقل. إن performance_budget البالغ 1500 ⁨ms⁩ / 64 ⁨MB⁩ هو غلاف اللوحة، لا حد أقصى تعاقدي.

يُلحِق writeContent() الخاص بمؤشّر الدفق البايتات بدفق محتوى الصفحة حرفيًا. وهو لا يتحقّق من صحة بناء جملة المعاملات. في عامل يعرض محتوى متأثّرًا بالمُستدعي، لا تمرّر مطلقًا مدخلات غير موثوقة إلى writeContent()؛ استخدم writeText()، الذي يُهرِّبه المؤشّر المُسلَّم وفق قواعد سلاسل ⁨PDF⁩ الحرفية. يملك المُستدعي دفق المُخرَج: يكتب فيه المحرّك لكنه لا يُغلِقه أو يُعيد فتحه مطلقًا، فلا يمكنه إعادة توجيه المُخرَج. يجب على العامل أن يُغلِق المِقبض بنفسه بعد عودة close() الخاص بالكاتب، وإلا سرّب واصف ملف عبر المهام. مشاركة السجلّات بين المهام تحسين للأداء لا حدّ ثقة: يُخبِّئ ImageRegistry المُشترَك الصورَ المُحلَّلة، لذا حدّد حجم maxCacheBytes الخاص به عمدًا ولا تفترض عزل المخبأ بين المستأجرين في عامل متعدّد المستأجرين.

الادّعاءالمعيارالبندالدليل
يُصدِر كاتب الدفق شجرة صفحات يكون مُدخَل Kids فيها مصفوفةً من المراجع غير المباشرة إلى الأبناء المباشرين للعُقدة.⁨ISO 32000-2⁩§7.7.3.2
يُصدِر كاتب الدفق مُدخَل Count مساويًا لعدد كائنات الصفحات الورقية المتفرّعة من عُقدة شجرة الصفحات.⁨ISO 32000-2⁩§7.7.3.3

البنود مُعاد صياغتها ومُثبَّتة بالمسرد؛ ولا يُعاد إنتاج أي نص معياري هنا.

  • العقود / الدفق — عقدا experimentalStreamingWriterInterface وCursorInterface، إضافةً إلى آلتي الحالة الخاصتين بهما.
  • ⁨HTML⁩ / قيود الدفق (⁨ADR-001⁩) — قرار التمريرة الواحدة دون الاحتفاظ بشجرة ⁨DOM⁩ وعتبات إعادة النظر.
  • الأداء — بوابة انحدار زمن الاستجابة والذاكرة لخط أنابيب ⁨HTML.⁩
  • التخطيط — محرّكات تأثيث الصفحة التي لا تحتفظ بأي حالة لكل صفحة.
  • ⁨PERFORMANCE-BUDGETS⁩ — نمط إخفاق العامل المُسرِّب للذاكرة وأساس بوابة الانحدار.