מנעול-העיבוד של PDFiumPas הוא section קריטי לכל-מסמך — EnterRenderLock ו-LeaveRenderLock, נתמכים על ידי שדה TRTLCriticalSection על TPdf — נועד לעטוף כל קריאה לתוך מנוע הרסטריזציה של PDFium כך שעמוד לא יוכל להשתחרר או להיטען מחדש מתחת לעיבוד שבטיסה. שש פונקציות, מחולקות שווה בשווה בין TPdf ו-TPdfView, קראו ל-API-ים של ביטמאפ וחילוץ-תמונה-ממוזערת של PDFium ישירות ודילגו על המנעול הזה לגמרי, פער ש-PDFiumPas v2.26.0 סגר על ידי עטיפת כל שש הפונקציות באותו זוג-מנעול שכל נקודת כניסת עיבוד אחרת כבר השתמשה בו
הפער המכוסה כאן אינו מעבר חיזוק ה-ABI המכוסה במקום אחר בבלוג הזה, שעבר על אי-התאמת מוסכמת-קריאה מסוג cdecl וקיצוץ רוחב-מצביע ב-FPC Win64 באותו קישור בינארי של PDFium. מה שבא אחר כך צר יותר ומכני יותר: רשימת-בדיקה של כיסוי-מנעול עבור שש נקודות קריאה שכולן נוגעות בנתיב העיבוד של PDFium, למה כל אחת הייתה קלה לפספוס, ולמה התחרות (race) שנובעת מהיעדר המנעול היא אחד הפגמים הקשים יותר בבסיס הקוד הזה לשחזור לפי דרישה
מה מנעול-העיבוד בפועל מגן עליו
PDFiumPas מסדרת עיבוד (serializes) משום שהעמוד הטעון של PDFium לא בטוח לקרוא ממנו מתהליכון אחד בעוד תהליכון אחר חופשי לשחרר אותו. TPdf מחזיקה TRTLCriticalSection ב-FRenderLock, מאותחל בבנאי ומוגן על ידי דגל FRenderLockReady כך שקריאה שמגיעה אחרי פירוק (teardown) הופכת ל-no-op שקט במקום להיכנס ל-section קריטי שנמחק. EnterRenderLock ו-LeaveRenderLock הן הדרך היחידה המאושרת פנימה והחוצה מה-section ההוא
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile, ו-RenderPageProgressive כבר עקבו אחרי המשמעת ההיא לפני שהביקורת הספציפית הזו אי-פעם התחילה, כל אחת לוקחת את המנעול לפני קריאה לתוך PDFium ומשחררת אותו בבלוק finally כך שעיבוד-מוקדם ברקע ו-UnloadPage בחזית על אותו מופע TPdf לא יכולים לחפוף. הפער ש-PDFiumPas v2.26.0 מצאה לא היה בנקודות הכניסה הברורות ההן — הוא הופיע בשש פונקציות שנקראות כמו accessors ולא כמו רינדורים, אף על פי שכל אחת מהן מבקשת מ-PDFium לבצע רסטריזציה לפיקסלים לפני שהיא יכולה להחזיר משהו
אילו שש קריאות דילגו על מנעול-העיבוד?
TPdf.GetObjectBitmap, TPdf.GetBitmap, ו-TPdf.GetThumbnail היוו חצי מהרשימה, ו-TPdfView.GetObjectBitmap, TPdfView.GetBitmap, ו-TPdfView.GetThumbnail היוו את החצי השני — אותן שלוש פעולות, משוכפלות על פני שתי מחלקות הרכיב שחושפות את אותו עמוד בסיסי. כל שש בסופו של דבר קוראות ל-FPDFImageObj_GetBitmap או ל-FPDFPage_GetThumbnailAsBitmap, ושתי נקודות הכניסה האלה של PDFium מבצעות רסטריזציה במקום במקום להחזיר הפניה למשהו שכבר עובד. שום דבר בשמות שש הפונקציות לא אומר render, שהיא הסבר סביר לכך שהן לא נכתבו מול אותה רשימת-בדיקה כמו RenderPage ו-RenderTile בפעם הראשונה
function TPdf.GetObjectBitmap(Index: Integer): TBitmap;
var
Bitmap: FPDF_BITMAP;
begin
Result:= nil;
EnterRenderLock;
try
Bitmap:= FPDFImageObj_GetBitmap(GetObjectHandle(Index));
finally
LeaveRenderLock;
end;
if Bitmap<> nil then
try
Result:= ToBitmap(Bitmap);
finally
FPDFBitmap_Destroy(Bitmap);
end;
end;
למה TPdfView מגנה על קריאת המנעול שלה עם בדיקת nil
TPdfView לא מחזיקה section קריטי משלה — כל אחת משש קריאות המנעול שלה מעבירה הלאה אל FPdf.EnterRenderLock ו-FPdf.LeaveRenderLock, עטופה בבדיקה שהפניית ה-TPdf המשויכת אינה nil קודם. השמירה הזו קיימת משום ש-TPdfView יכולה לשבת על טופס בזמן-עיצוב, או בקצרה בין סגירת מסמך אחד לפתיחת הבא, ללא TPdf שהוקצה ל-FPdf עדיין. דילוג על השמירה היה מחליף קריסה אחת באחרת, שכן קריאת-נעילה מול הפניית nil נכשלת לא בחן יותר מהתחרות שהמנעול קיים כדי למנוע
function TPdfView.GetThumbnail: TBitmap;
var
PdfBitmap: FPDF_BITMAP;
begin
CheckActive;
Result:= nil;
if FPdf<> nil then
FPdf.EnterRenderLock;
try
PdfBitmap:= FPDFPage_GetThumbnailAsBitmap(Page);
finally
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
if PdfBitmap<> nil then
try
Result:= ToBitmap(PdfBitmap);
finally
FPDFBitmap_Destroy(PdfBitmap);
end;
end;
למה RenderPage(HDC) שייכת לאותה ביקורת?
TPdfView.RenderPage מול הקשר-התקן (device context) אינה אחת מהשש — היא הופיעה בגרסה מוקדמת יותר, ב-PDFiumPas v2.25.0, והיא זוכה למקום ברשימת-הבדיקה הזו משום שהיא אותו הפגם לובש חתימה שונה. הפונקציה המועמסת (overload) ההיא קראה ל-FPDF_RenderPage ישר בלי EnterRenderLock ובלי קריאת ה-SetArithmeticMask שמגנה מפני חריגות FPU במהדרי Delphi ישנים יותר, בעוד ההעמסה של TBitmap שיושבת כמה שורות מתחתיה באותה מחלקה כבר נשאה את שניהם. שני מעברי ביקורת שתופסים את אותו מצב-כשל מרחק גרסה זה מזה אומרים פחות על פונקציה בודדת ויותר על צורת הבאג: הוא מתחבא בכל העמסה שאף אחד לא קורא-מחדש ברגע שהאחות שלה נראית נכונה
procedure TPdfView.RenderPage(DeviceContext: HDC; Left, Top, Width,
Height: Integer; Rotation: TRotation; Options: TRenderOptions);
var
ArithmeticMask: TArithmeticMask;
begin
CheckActive;
if FPdf<> nil then
FPdf.EnterRenderLock;
ArithmeticMask:= SetArithmeticMask;
try
FPDF_RenderPage(DeviceContext, FPage, Left, Top, Width, Height,
Ord(Rotation), EncodeRenderOptions(Options));
finally
RestoreArithmeticMask(ArithmeticMask);
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
end;
למה התחרות (race) הזו כמעט בלתי-אפשרית לשחזור?
פער מנעול-העיבוד של PDFiumPas לא נכשל בכל ריצה, או אפילו ברוב הריצות, משום שהוא זקוק לשני דברים ספציפיים כדי לנחות על אותו מופע TPdf בו-זמנית: קריאת רסטריזציה שכבר בטיסה, ו-UnloadPage או ReloadPage בו-זמני שמגיע בתוך אותו חלון. בדיקה חד-תהליכונית אף פעם לא מפעילה את הנתיב בכלל, ואפילו עומסי-עבודה רב-תהליכוניים אמיתיים רק פוגעים בו כאשר עיבוד-רקע ואירוע מחזור-חיים של מסמך במקרה חופפים בתוך מחזור-חיים של עמוד אחד. הטריגר הריאלי ביותר הוא עיבוד-מוקדם ברקע של PDF בנוי על futures ניתנים-לביטול, שבו תהליכון-עבודה מבצע רסטריזציה לעמוד הבא בעוד תהליכון ה-UI טוען-מחדש או משחרר את הנוכחי בקלט משתמש
FPDFImageObj_GetBitmap ו-FPDFPage_GetThumbnailAsBitmap עוברות על מבני אובייקט-עמוד ש-UnloadPage חופשי לשחרר באמצע-מעבר, כך שתחרות שבפועל יורה גם לא תמיד מייצרת הפרת-גישה מיידית. קריאת מבנה רגע מאוחר מדי יכולה באותה קלות למסור בחזרה פיקסלים זבל, או להשחית מטא-נתוני ערימה שמקריסה רק כמה הקצאות בלתי-קשורות מאוחר יותר, בפונקציה שאף פעם לא נגעה בעמוד PDF. זו הסיבה הכנה שהמחלקה הזו של באג יכולה לשרוד בבסיס-קוד על פני כמה מחזורי-שחרור: ה-stack trace בנקודת הכישלון לעיתים רחוקות מצביע לאיזשהו מקום ליד שש השורות שבפועל חסרו מנעול
מה משתנה עבור קוד קורא
GetBitmap, GetObjectBitmap, GetThumbnail, וההעמסה HDC של RenderPage שומרים על החתימות הציבוריות שלהם בדיוק כפי שהיו, שכן התיקון הוא נעילה פנימית שנוספה סביב קריאות קיימות ולא הגירה. שווה לזכור שמנעול-העיבוד מוגדר-היקף לפי מופע TPdf, לא גלובלי לתהליך, כך ששני תהליכונים שמעבדים שני מסמכים טעונים בנפרד עדיין רצים במקביל מלא — המנעול רק מסדר פעולות מול המסמך הבודד ששני התהליכונים במקרה חולקים. אם הנעילה שלך כבר איתנה ועיבודים עדיין מרגישים איטיים תחת זום או גלילה, זו שאלה שונה, נענית במאמר מטמון העיבוד וטקטיקות ביצועי הזום של PDFium — נכונות ומהירות הם צירים נפרדים כאן, והתיקון הזה נוגע רק בראשון
שש פונקציות ואחות-העמסה אחת הן חלק קטן ממשטח ה-PDFium ש-PDFiumPas חושפת, אבל הן היו החלק שרק התנהג רע תחת עומס שאף אחד במקרה לא הריץ בדיבאגר. מנעול-העיבוד עצמו, וקבוצת נקודות-הכניסה המלאה לעיבוד שהוא עכשיו מכסה, נשלחים כחלק מרכיב PDFium עבור Delphi, C++Builder, ו-Lazarus/FPC