يُحمّل HotPDF مستند PDF من أي مصدر وصول عشوائي تنفذه بنفسك، وتغلّف THPDFCoalescingRandomAccessSource ذلك المصدر بحيث تتحول القراءات الصغيرة المتناثرة للمحلل إلى مجموعة محدودة من نطاقات كتل مخزَّنة مؤقتاً مع جلب مسبق غير متزامن. في مستند يُقدَّم عبر طلبات نطاق HTTP، هذا هو الفرق بين بضع مئات من الرحلات ذهاباً وإياباً وبضع عشرات فقط
لا شيء يتغير في المحلل. ما زلت تستدعي LoadFromRandomAccessSource، ويعود كائن المستند نفسه، وتعمل واجهة برمجة الصفحات نفسها. ما يتغير هو حركة البيانات تحت السطح
لماذا يُحمَّل ملف PDF نفسه فوراً محلياً ويزحف عبر الشبكة؟
لأن محلل PDF لا يقرأ ملفاً، بل يتنقّل فيه. يبحث عن النهاية لأجل startxref، ثم يقفز إلى الخلف نحو جدول المراجع المتقاطعة، ويحلّ قاموس المؤخرة، ويتبع مرجعاً إلى الفهرس، ثم إلى جذر شجرة الصفحات، ثم إلى عقدة صفحة، ثم إلى قاموس مواردها. كل خطوة من هذه تقرأ عشرات البايتات من إزاحة مختلفة
في ملف محلي، هذا النمط شبه مجاني: نظام التشغيل يكون قد خزّن مؤقتاً بالفعل صفحة الـ4 كيبيبايت المحيطة، لذا تكلّف القراءة الثانية عملية memcpy فقط. عبر نقل شبكي لا يوجد مثل هذا الموقع المحلي. كل قراءة طلب له زمن استجابة خاص به، و300 طلب متسلسل بمعدل 40 مللي ثانية لكل واحد يعني اثنتي عشرة ثانية تُقضى تقريباً بالكامل في الانتظار. الحل ليس القراءة أقل؛ فالمحلل يحتاج بالضبط ما يطلبه. الحل هو جعل كل قراءة فعلية تغطي المزيد مما ستريده القراءة المنطقية التالية
ما الذي يغيره الدمج
يُقرِّب المصدر الدامج كل قراءة إلى كتلة ويخزّن تلك الكتلة مؤقتاً. القيمة الافتراضية لـ BlockSize هي 262,144 بايت ولـ MaxCacheBytes هي 2,097,152، بحيث تكون ثماني كتل مقيمة افتراضياً ويُخلى منها وفق ترتيب الأقل استخداماً مؤخراً مقابل ميزانية بايتات صارمة. قراءة الـ40 بايت لمفتاح مؤخرة من قِبل المحلل تجلب معها الـ256 كيبيبايت المحيطة بها، وتُخدَم القراءات الاثنتا عشرة التالية في ذلك الجوار، حيث تسكن بيانات المراجع المتقاطعة والفهرس، من الذاكرة
يبقى مصدرك الخاص بسيطاً. نفّذ GetSize وReadAt، وتجاوز ReadAtCancellable إذا كان نقلك يستطيع الإجهاض أثناء الرحلة، ودع الغلاف يتولى التخزين المؤقت والدمج والجلب المسبق
type
THttpRangeSource = class(THPDFRandomAccessSource)
private
FClient: TMyHttpClient;
FUrl: string;
FSize: Int64;
public
function GetSize: Int64; override;
function ReadAt(Offset: Int64; var Buffer; Count: Longint): Longint; override;
function ReadAtCancellable(Offset: Int64; var Buffer; Count: Longint;
CancellationToken: THPDFCancellationToken): Longint; override;
end;
var
Raw: THttpRangeSource;
Cached: THPDFCoalescingRandomAccessSource;
Pdf: THotPDF;
begin
Raw := THttpRangeSource.Create('https://files.example.com/contract.pdf');
// OwnsSource=True: يحرر الغلاف Raw مع نفسه
Cached := THPDFCoalescingRandomAccessSource.Create(Raw, True, 262144, 8388608);
Pdf := THotPDF.Create(nil);
try
Cached.AsyncPrefetchEnabled := True;
Cached.AdaptiveReadAheadEnabled := True;
Cached.MaxReadAheadBlocks := 8;
if Pdf.LoadFromRandomAccessSource(Cached, True) = 1 then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
إلى أي مدى ينبغي أن تقرأ مسبقاً؟
تجيب القراءة الاستباقية التكيّفية على هذا السؤال لكل مستند بدلاً من إجبارك على التخمين. عند ضبط AdaptiveReadAheadEnabled، تنمو النافذة عبر 1 و2 و4 و8 كتل مع تراكم قراءات أمامية مستمرة، ولا تتجاوز أبداً MaxReadAheadBlocks أو سعة التخزين المؤقت المُهيَّأة. في اللحظة التي تصل فيها قراءة لا تقع تقريباً حيث انتهت القراءة السابقة، تنهار النافذة ويُثبَّط الجلب المسبق
يحدد SequentialReadToleranceBytes، وقيمته الافتراضية 4,096، معنى «تقريباً». القراءات التي تقع ضمن تلك المسافة من نهاية القراءة السابقة تُحتسب مع ذلك كقراءات متسلسلة، وهذا مهم لأن محلل PDF الذي يمشي عبر تيار محتوى لا ينتج إزاحات متجاورة تماماً؛ فهو يتخطى حقل طول هنا، وقاموساً مضمَّناً هناك. اضبط التسامح منخفضاً جداً وسيُصنَّف مسح أمامي عادي كعشوائي، بحيث لا تنشط القراءة الاستباقية أبداً. اضبطه مرتفعاً جداً ويبدو الوصول العشوائي الحقيقي متسلسلاً، بحيث تجلب ميغابايتات لا يريدها أحد. القيمة الافتراضية معايَرة لاجتياز تيار المحتوى، وستخبرك الإحصاءات إذا كان نقلك يختلف
عدم التماثل هذا متعمَّد: النمو تدريجي، والانهيار فوري. الإفراط في الجلب في حِمل عمل ذي وصول عشوائي يكلّف عرض نطاق ترددي حقيقياً ومالاً حقيقياً على النقل المُقاس، لذا يُفضَّل الخطأ الرخيص على الخطأ المكلف
إلغاء يوقف النقل فعلياً
يعلن الصنف الأساسي ReadAtCancellable، ويحترمه المصدر الدامج من طرف إلى طرف. عندما تصل قراءة أمامية لنطاق لا يخدمه جلب مسبق قيد التنفيذ، يُلغى الجلب المسبق بدلاً من تركه لينتهي، بحيث لا يُصفّ طلب صفحة المستخدم خلف حركة بيانات تخمينية. التنفيذ الافتراضي في THPDFRandomAccessSource يتراجع إلى ReadAt بسيطة، مما يعني أن الميزة اختيارية لكل نقل: عملاء HTTP الذين يدعمون إجهاض الطلب يحصلون على إلغاء حقيقي، والمصادر الأبسط تستمر في العمل دون تغيير
اجمع ذلك مع رمز إلغاء ممرَّر عبر واجهة المستخدم لديك، وسيؤدي إغلاق المستخدم لمستند إلى إيقاف حركة بيانات الشبكة فعلياً بدلاً من انتظار تصريفها. نموذج الرمز نفسه يدعم آلية الطابور الموضحة في العرض في الخلفية بطابور طلبات، بحيث يستطيع رمز واحد تغطية المسار بأكمله من منفذ العرض إلى المقبس
قراءة إحصاءات ذاكرة تخزين النطاقات المؤقتة
تملأ GetStatistics سجل THPDFRangeCacheStatistics الذي يفصل ما فعله نقلك عمّا فعلته ذاكرة التخزين المؤقت. SourceReadCount وSourceBytesRead حركة بيانات فعلية. CacheHitCount وCacheMissCount حركة بيانات منطقية. يوضح SequentialReadCount وRandomReadCount كيف صُنِّف نمط الوصول، ويوضح CurrentReadAheadBlocks وPeakReadAheadBlocks إلى أي مدى انفتحت النافذة، ويوضح PrefetchRequestCount وPrefetchCompletedCount وPrefetchCancelledCount وSuppressedPrefetchCount ما إذا كان التخمين مجدياً
var
S: THPDFRangeCacheStatistics;
begin
Cached.GetStatistics(S);
Log(Format('physical %d reads / %d bytes, hits %d, misses %d',
[S.SourceReadCount, S.SourceBytesRead, S.CacheHitCount, S.CacheMissCount]));
Log(Format('pattern: %d sequential, %d random, peak window %d blocks',
[S.SequentialReadCount, S.RandomReadCount, S.PeakReadAheadBlocks]));
Log(Format('prefetch: %d issued, %d completed, %d cancelled, %d suppressed',
[S.PrefetchRequestCount, S.PrefetchCompletedCount,
S.PrefetchCancelledCount, S.SuppressedPrefetchCount]));
end;
ثلاث قراءات تخبرك بما تغيّره. كثرة عمليات الجلب المسبق الملغاة مع عدد كبير من القراءات العشوائية تعني أن المستند يُصَل إليه خارج الترتيب، لذا اخفض MaxReadAheadBlocks وتوقف عن دفع ثمن عرض نطاق ترددي تتخلص منه. كثرة الإخفاقات مع نافذة ذروة لا تزال عند 1 تعني أن التسامح يرفض نمطاً متسلسلاً فعلياً، لذا ارفع SequentialReadToleranceBytes. وتجاوز عدد البايتات المقروءة حجم الملف بكثير يعني أن ذاكرة التخزين المؤقت تتخبط، لذا ارفع MaxCacheBytes قبل لمس أي شيء آخر
الملفات الخطية تغيّر الحسابات
إذا كنت تتحكم في المولّد، فإن جعل المستند خطياً يغيّر المشكلة بدلاً من تحسينها. يضع ملف PDF الخطي كائنات الصفحة الأولى وجدول تلميحات في مقدمة الملف، بحيث يستطيع العارض رسم الصفحة الأولى من الميغابايت الافتتاحي دون رؤية الباقي. يعرض HotPDF ذلك المسار مباشرة عبر GetProgressiveLinearizedLoadInfo وReadProgressiveLinearizedFirstPageSection، والجانب الخاص بالكتابة موضح في توليد ملفات PDF الخطية بجداول تلميحات
تتكامل التقنيتان معاً. الدمج يجعل أي مستند محتملاً عبر رابط بطيء؛ والخطية تجعل الصفحة الأولى تصل بسرعة في المستندات التي تنتجها بنفسك. بالنسبة للملفات التي تعيش على قرص محلي لكنها كبيرة جداً بحيث لا تتسع في الذاكرة، فإن مساري الملف المعيَّن والتيار الكسول الموضحين في سير عمل واجهة الملف المباشرة عادة ما يكونان الأداة الأفضل، إذ لا يوجد أصلاً زمن استجابة ذهاباً وإياباً يستحق الاستهلاك
HotPDF هو مكون VCL PDF أصلي لـ Delphi وC++Builder، دون أي DLL خارجي للمحلل ومع توفر الشيفرة المصدرية الكاملة. واجهة برمجة مصدر الوصول العشوائي، والغلاف الدامج، ونقاط دخول التحميل التدريجي موثَّقة على صفحة مكون HotPDF لـ Delphi