Zámok vykresľovania v PDFiumPas je kritická sekcia na jeden dokument — EnterRenderLock a LeaveRenderLock, podložené poľom TRTLCriticalSection na TPdf — určená na obalenie každého volania do rasterizéra PDFium, aby stránku nebolo možné uvoľniť alebo znova načítať spod prebiehajúceho vykreslenia. Šesť metód rozdelených rovnomerne medzi TPdf a TPdfView volalo API PDFium pre bitmapu a extrakciu miniatúry priamo a tento zámok úplne preskakovalo, medzeru, ktorú PDFiumPas v2.26.0 uzavrel obalením všetkých šiestich do tej istej dvojice zámkov, akú už používal každý iný vstupný bod vykresľovania
Medzera opísaná tu nie je prechod na spevnenie ABI opísaný inde na tomto blogu, ktorý prechádzal nesúlad konvencie volania cdecl a orezanie šírky ukazovateľa na FPC Win64 v tej istej väzbe PDFium. Nasledujúce je užšie a mechanickejšie: kontrolný zoznam pokrytia zámkom pre šesť miest volania, ktoré všetky siahajú do vykresľovacej cesty PDFium, prečo bolo každé z nich ľahké prehliadnuť, a prečo je pretek, ktorý z chýbajúceho zámku vyplýva, jednou z ťažšie reprodukovateľných chýb v tejto kódovej báze na požiadanie
Čo zámok vykresľovania v skutočnosti chráni
PDFiumPas serializuje vykresľovanie, pretože načítaná stránka PDFium nie je bezpečné čítať z jedného vlákna, zatiaľ čo iné vlákno ju môže voľne uvoľniť. TPdf vlastní TRTLCriticalSection v FRenderLock, inicializovanú v konštruktore a strážené príznakom FRenderLockReady, aby sa volanie prichádzajúce po zrušení stalo tichým no-op namiesto vstupu do zmazanej kritickej sekcie. EnterRenderLock a LeaveRenderLock sú jediný schválený spôsob, ako do tejto sekcie vstúpiť a z nej vystúpiť
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile, a RenderPageProgressive túto disciplínu dodržiavali ešte predtým, než tento konkrétny audit vôbec začal, každá z nich berie zámok pred volaním do PDFium a uvoľňuje ho v bloku finally, takže vykresľovanie na pozadí a UnloadPage na popredí nad tou istou inštanciou TPdf sa nemôžu prekrývať. Medzera, ktorú PDFiumPas v2.26.0 našiel, nebola v týchto zjavných vstupných bodoch — objavila sa v šiestich metódach, ktoré čítajú skôr ako prístupové metódy než ako vykreslenia, hoci každá z nich žiada PDFium o rasterizáciu pixelov ešte predtým, než dokáže čokoľvek vrátiť
Ktoré šesť volaní preskočilo zámok vykresľovania?
TPdf.GetObjectBitmap, TPdf.GetBitmap, a TPdf.GetThumbnail tvorili polovicu zoznamu, a TPdfView.GetObjectBitmap, TPdfView.GetBitmap, a TPdfView.GetThumbnail tvorili druhú polovicu — tie isté tri operácie, duplikované naprieč dvomi triedami komponentov, ktoré sprístupňujú tú istú podkladovú stránku. Všetkých šesť nakoniec volá buď FPDFImageObj_GetBitmap, alebo FPDFPage_GetThumbnailAsBitmap, a oba tieto vstupné body PDFium rasterizujú na mieste namiesto toho, aby vrátili odkaz na niečo už vykreslené. Nič v žiadnom z týchto šiestich názvov metód nehovorí vykresliť, čo je rozumné vysvetlenie, prečo neboli napísané voči rovnakému kontrolnému zoznamu ako RenderPage a RenderTile pri prvom prechode
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;
Prečo TPdfView stráži svoje volanie zámku kontrolou na nil
TPdfView nevlastní vlastnú kritickú sekciu — každé zo svojich šiestich volaní zámku presmerováva na FPdf.EnterRenderLock a FPdf.LeaveRenderLock, obalené kontrolou, že priradený odkaz TPdf nie je najprv nil. Táto stráž existuje preto, lebo TPdfView môže sedieť na formulári v čase návrhu, alebo krátko medzi zatvorením jedného dokumentu a otvorením ďalšieho, bez akéhokoľvek TPdf zatiaľ priradeného do FPdf. Preskočenie tejto stráže by vymenilo jeden pád za druhý, keďže zamykacie volanie voči nil odkazu zlyhá o nič elegantnejšie než pretek, ktorému má zámok zabrániť
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;
Prečo RenderPage(HDC) patrí do toho istého auditu?
TPdfView.RenderPage voči kontextu zariadenia nie je jedno zo šiestich — objavilo sa o vydanie skôr, v PDFiumPas v2.25.0, a zaslúži si miesto v tomto kontrolnom zozname, pretože je to tá istá chyba nosiaca odlišný podpis. Toto preťaženie volalo FPDF_RenderPage priamo bez EnterRenderLock aj bez volania SetArithmeticMask, ktoré chráni pred výnimkami FPU na starších kompilátoroch Delphi, zatiaľ čo preťaženie TBitmap sediace o pár riadkov nižšie v tej istej triede už nieslo oboje. Dva audítorské prechody zachytávajúce ten istý režim zlyhania o vydanie od seba hovoria menej o jednotlivej metóde a viac o tvare chyby: skrýva sa v akomkoľvek preťažení, ktoré si nikto znova neprečíta, hneď ako jeho súrodenec vyzerá správne
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;
Prečo je tento pretek takmer nemožné reprodukovať?
Medzera zámku vykresľovania v PDFiumPas nezlyhá pri každom behu, ani pri väčšine behov, pretože potrebuje, aby sa dve konkrétne veci naraz stretli na tej istej inštancii TPdf: rasterizačné volanie už v behu, a súbežné UnloadPage alebo ReloadPage prichádzajúce vnútri toho istého okna. Jednovláknové testovanie túto cestu vôbec necvičí, a aj naozaj viacvláknové záťaže ju zasiahnu iba vtedy, keď sa vykresľovanie na pozadí a udalosť životného cyklu dokumentu náhodou prekryjú vnútri životnosti jednej stránky. Najrealistickejším spúšťačom je predbežné vykresľovanie PDF na pozadí postavené na zrušiteľných futures, kde pracovné vlákno rasterizuje ďalšiu stránku, zatiaľ čo vlákno UI podľa vstupu používateľa znova načíta alebo uvoľní aktuálnu
FPDFImageObj_GetBitmap a FPDFPage_GetThumbnailAsBitmap prechádzajú štruktúrami objektov stránky, ktoré UnloadPage smie uvoľniť uprostred prechádzania, takže pretek, ktorý sa naozaj spustí, nemusí vždy vyprodukovať okamžité access violation. Štruktúra prečítaná o okamih neskoro môže rovnako ľahko vrátiť nezmyselné pixely, alebo poškodiť metadáta haldy, ktoré padnú až o niekoľko nesúvisiacich alokácií neskôr, vo funkcii, ktorá sa nikdy nedotkla stránky PDF. To je čestný dôvod, prečo táto trieda chýb dokáže prežiť v kódovej báze naprieč niekoľkými vydávacími cyklami: stack trace v bode zlyhania málokedy ukazuje niekde blízko tých šiestich riadkov, ktorým v skutočnosti chýbal zámok
Čo sa mení pre volajúcich
GetBitmap, GetObjectBitmap, GetThumbnail, a preťaženie HDC pre RenderPage si ponechávajú svoje verejné signatúry presne také, aké boli, keďže oprava je interné zamykanie pridané okolo existujúcich volaní, nie migrácia. Oplatí sa pamätať, že zámok vykresľovania je obmedzený na inštanciu TPdf, nie globálny pre proces, takže dve vlákna vykresľujúce dva samostatne načítané dokumenty stále bežia úplne paralelne — zámok serializuje iba operácie voči tomu jednému dokumentu, ktorý obe vlákna náhodou zdieľajú. Ak je vaše zamykanie už v poriadku a vykresľovania stále pôsobia pomaly pri priblížení alebo posúvaní, to je odlišná otázka, zodpovedaná v článku o taktikách vyrovnávacej pamäte vykresľovania PDFium a výkone pri priblížení — správnosť a rýchlosť sú tu oddelené osi, a táto oprava sa dotýka iba prvej z nich
Šesť metód a jedno súrodenecké preťaženie je malý zlomok plochy PDFium, ktorú PDFiumPas sprístupňuje, no boli to práve ten zlomok, ktorý sa nesprávne správal iba pod záťažou, akú náhodou nikto nespúšťal v debuggeri. Samotný zámok vykresľovania, a celá množina vstupných bodov vykresľovania, ktoré teraz pokrýva, sa dodávajú ako súčasť komponentu PDFium pre Delphi, C++Builder, a Lazarus/FPC