HotPDF טוען PDF ממקור גישה אקראית כלשהו שמממשים, ו-THPDFCoalescingRandomAccessSource עוטף את המקור הזה כך שהקריאות הקטנות והמפוזרות של המנתח הופכות לסט מוגבל של טווחי בלוקים במטמון עם קריאה מקדימה אסינכרונית. במסמך המוגש דרך בקשות טווח HTTP, זה ההבדל בין כמה מאות מסעות הלוך-חזור לכמה עשרות
שום דבר במנתח לא משתנה. עדיין קוראים ל-LoadFromRandomAccessSource, אותו אובייקט מסמך חוזר, ואותו API לעמודים עובד. מה שמשתנה הוא התעבורה שמתחתיו
מדוע אותו PDF נטען מיידית באופן מקומי וזוחל ברשת?
מפני שמנתח PDF לא קורא קובץ, הוא מנווט בו. הוא קופץ לסוף עבור startxref, קופץ חזרה לטבלת ההפניות הצולבות, פותר את מילון ה-trailer, עוקב אחר הפניה ל-Catalog, ואז לשורש עץ העמודים, ואז לצומת עמוד, ואז למילון המשאבים שלו. כל אחד מהצעדים האלה קורא עשרות בייטים מאופסט אחר
בקובץ מקומי הדפוס הזה כמעט חינם: מערכת ההפעלה כבר החזיקה במטמון את דף ה-4 KiB הסובב, כך שהקריאה השנייה עולה memcpy. מעל תעבורת רשת אין מקומיות כזו. כל קריאה היא בקשה עם השהיה משלה, ו-300 בקשות רציפות ב-40 מ"ש כל אחת הן שתים-עשרה שניות של המתנה כמעט טהורה. התיקון אינו לקרוא פחות; המנתח צריך בדיוק את מה שהוא מבקש. התיקון הוא לגרום לכל קריאה פיזית לכסות יותר ממה שהקריאה הלוגית הבאה תרצה
מה איחוד הטווחים משנה
מקור האיחוד מעגל כל קריאה כלפי מעלה לבלוק ושומר את הבלוק במטמון. BlockSize ברירת מחדל 262,144 בייטים ו-MaxCacheBytes ברירת מחדל 2,097,152, כך ששמונה בלוקים נמצאים כברירת מחדל ומפונים לפי סדר שימוש-אחרון-לפני-שני כנגד תקציב בייטים קשיח. הקריאה של 40 בייטים של המנתח למפתח trailer מושכת פנימה את 256 ה-KiB סביבו, והתריסר קריאות הבאות באותה שכונה, ששם יושבים נתוני הפניות צולבות ו-Catalog, מוגשות מהזיכרון
המקור שלך נשאר פשוט. ממשו את 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 פשוט, מה שאומר שהתכונה היא opt-in לכל תעבורה: לקוחות 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. ובייטים שנקראו שעולים בהרבה על גודל הקובץ אומר שהמטמון סובל מ-thrashing, אז העלו את MaxCacheBytes לפני שנוגעים בכל דבר אחר
קבצים ליניאריים משנים את החשבון
אם שולטים במחולל, יצירת קובץ ליניארי משנה את הבעיה ולא רק מייעלת אותה. PDF ליניארי ממקם את האובייקטים של העמוד הראשון וטבלת רמזים בתחילת הקובץ, כך שצופה יכול לרנדר את עמוד אחד מהמגה-בייט הפותח מבלי לראות את שאר הקובץ. HotPDF חושף את הנתיב הזה ישירות דרך GetProgressiveLinearizedLoadInfo ו-ReadProgressiveLinearizedFirstPageSection, וצד הכתיבה מכוסה ביצירת PDF ליניארי עם טבלאות רמזים
שתי הטכניקות משתלבות. איחוד הופך כל מסמך לנסבל מעל קישור איטי; ליניאריות הופכת את הגעת העמוד הראשון למהירה במסמכים שאתם מייצרים בעצמכם. עבור קבצים שיושבים על דיסק מקומי אבל גדולים מדי מכדי להיכנס לזיכרון, נתיבי המיפוי והזרם העצל המתוארים בה-Direct File API הם בדרך כלל הכלי הטוב יותר, מפני שאין שם השהיית מסע הלוך-חזור לפרוס מלכתחילה
HotPDF הוא רכיב VCL מקורי ל-PDF ל-Delphi ול-C++Builder, ללא DLL חיצוני למנתח וקוד מקור מלא זמין. API מקור הגישה האקראית, עטיפת האיחוד ונקודות הכניסה לטעינה מתקדמת מתועדים בדף רכיב HotPDF ל-Delphi