Zámek vykreslování PDFiumPas je kritická sekce na dokument — EnterRenderLock a LeaveRenderLock, podložené polem TRTLCriticalSection na TPdf — určená k obalení každého volání do rasterizéru PDFium, aby stránka nemohla být uvolněna nebo znovu načtena zpod probíhajícího vykreslování. Šest metod, rovnoměrně rozdělených mezi TPdf a TPdfView, volalo API PDFium pro bitmapy a extrakci miniatur přímo a tento zámek úplně přeskakovalo, mezeru, kterou PDFiumPas v2.26.0 uzavřel obalením všech šesti do stejné dvojice zámků, jakou už používal každý jiný vstupní bod vykreslování
Mezera popsaná zde není průchod zpevňování ABI popsaný jinde na tomto blogu, který procházel nesoulad volací konvence cdecl a zkrácení šířky ukazatele na FPC Win64 ve stejném bindingu PDFium. To, co následuje, je užší a mechaničtější: kontrolní seznam pokrytí zámkem pro šest míst volání, která všechna sahají do vykreslovací cesty PDFium, proč bylo každé z nich snadné přehlédnout, a proč je závod, který plyne z chybějícího zámku, jeden z těžších defektů v této codebase reprodukovat na požádání
Co zámek vykreslování skutečně chrání
PDFiumPas serializuje vykreslování, protože načtená stránka PDFium není bezpečná ke čtení z jednoho vlákna, zatímco jiné vlákno má volnost ji uvolnit. TPdf vlastní TRTLCriticalSection v FRenderLock, inicializovanou v konstruktoru a chráněnou příznakem FRenderLockReady, takže volání přicházející po zániku se stane tichým no-opem místo vstupu do smazané kritické sekce. EnterRenderLock a LeaveRenderLock jsou jediná schválená cesta dovnitř a ven z této sekce
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 už tuto disciplínu dodržovaly ještě předtím, než tento konkrétní audit vůbec začal, každá bere zámek dřív, než zavolá do PDFium, a uvolní jej v bloku finally, takže se vykreslování na pozadí a UnloadPage na popředí na stejné instanci TPdf nemohou překrýt. Mezera, kterou PDFiumPas v2.26.0 našel, nebyla v těchto zjevných vstupních bodech — objevila se v šesti metodách, které čtou spíš jako přístupové metody než vykreslení, přestože každá z nich žádá PDFium rasterizovat pixely dřív, než může cokoli vrátit
Kterých šest volání přeskočilo zámek vykreslování?
TPdf.GetObjectBitmap, TPdf.GetBitmap a TPdf.GetThumbnail tvořily polovinu seznamu a TPdfView.GetObjectBitmap, TPdfView.GetBitmap a TPdfView.GetThumbnail tvořily druhou polovinu — stejné tři operace, zdvojené napříč oběma třídami komponent, které vystavují stejnou podkladovou stránku. Všech šest nakonec volá buď FPDFImageObj_GetBitmap, nebo FPDFPage_GetThumbnailAsBitmap, a oba tyto vstupní body PDFium rasterizují na místě místo toho, aby vrátily odkaz na něco už vykresleného. Nic ve jménu žádné z šesti metod neříká „render", což je rozumné vysvětlení, proč nebyly napsány podle stejného kontrolního seznamu jako RenderPage a RenderTile napoprvé
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;
Proč TPdfView chrání volání zámku kontrolou na nil?
TPdfView nevlastní vlastní kritickou sekci — každé z jeho šesti volání zámku přeposílá na FPdf.EnterRenderLock a FPdf.LeaveRenderLock, obalené kontrolou, že přidružená reference TPdf nejdřív není nil. Tato ochrana existuje proto, že TPdfView může sedět na formuláři v návrhovém čase, nebo krátce mezi zavřením jednoho dokumentu a otevřením dalšího, bez TPdf ještě přiřazeného do FPdf. Přeskočení ochrany by vyměnilo jeden pád za jiný, protože zamykací volání proti referenci nil selže neméně elegantně než závod, kterému má zámek zabránit
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;
Proč patří RenderPage(HDC) do stejného auditu?
TPdfView.RenderPage proti kontextu zařízení není jedno z šesti — objevilo se o vydání dřív, v PDFiumPas v2.25.0, a zaslouží si místo v tomto kontrolním seznamu, protože je to stejný defekt v jiné podobě. Tento přetížený tvar volal FPDF_RenderPage přímo bez EnterRenderLock i bez volání SetArithmeticMask, které chrání proti výjimkám FPU na starších kompilátorech Delphi, zatímco přetížení TBitmap sedící o pár řádků níž ve stejné třídě už neslo obojí. To, že dva auditní průchody odchytily stejný režim selhání o vydání od sebe, vypovídá méně o kterékoli jednotlivé metodě a víc o tvaru chyby: schovává se v tom přetíženém tvaru, který si nikdo znovu nepřečte, jakmile jeho sourozenec vypadá správně
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;
Proč je tento závod téměř nemožné reprodukovat?
Mezera zámku vykreslování v PDFiumPas neselže při každém běhu, ani při většině běhů, protože potřebuje, aby se na stejné instanci TPdf současně sešly dvě konkrétní věci: rasterizační volání už v běhu, a souběžný UnloadPage nebo ReloadPage přicházející uvnitř stejného okna. Jednovláknové testování tuto cestu vůbec neprocvičí, a i skutečně vícevláknové zátěže ji spustí jen tehdy, když se vykreslování na pozadí a událost životního cyklu dokumentu náhodou překryjí uvnitř životnosti jedné stránky. Nejrealističtější spouštěč je předvykreslování PDF na pozadí postavené na zrušitelných futures, kde pracovní vlákno rasterizuje další stránku, zatímco vlákno UI na základě vstupu uživatele znovu načte nebo uvolní tu aktuální
FPDFImageObj_GetBitmap a FPDFPage_GetThumbnailAsBitmap procházejí struktury objektů stránky, které UnloadPage smí uvolnit uprostřed průchodu, takže závod, který skutečně spustí, ne vždy vyprodukuje okamžitou access violation. Struktura přečtená o chvíli později může stejně snadno vrátit odpad z pixelů, nebo poškodit metadata haldy, která pak spadnou až o několik nesouvisejících alokací později, ve funkci, která se nikdy stránky PDF nedotkla. To je poctivý důvod, proč tato třída chyb dokáže přežít v codebase napříč několika cykly vydání: stopa zásobníku v bodě selhání málokdy ukazuje kamkoli blízko šesti řádků, kterým skutečně chyběl zámek
Co se mění pro volající
GetBitmap, GetObjectBitmap, GetThumbnail a přetížení RenderPage pro HDC si podržují své veřejné signatury přesně tak, jak byly, protože oprava je interní zamykání přidané kolem existujících volání, ne migrace. Stojí za připomenutí, že zámek vykreslování je vázaný na instanci TPdf, ne globální pro proces, takže dvě vlákna vykreslující dva samostatně načtené dokumenty pořád běží plně paralelně — zámek serializuje jen operace proti tomu jednomu dokumentu, který obě vlákna náhodou sdílejí. Pokud je vaše zamykání už v pořádku a vykreslování se přesto zdá pomalé při zoomu nebo scrollování, to je jiná otázka, zodpovězená v článku o cache vykreslování PDFium a taktikách výkonu při zoomu — správnost a rychlost jsou zde samostatné osy a tato oprava se dotýká jen té první
Šest metod a jedno sourozenecké přetížení jsou malý zlomek plochy PDFium, kterou PDFiumPas vystavuje, ale byl to ten zlomek, který se choval špatně jen pod zátěží, kterou náhodou nikdo nespouštěl v debuggeru. Samotný zámek vykreslování a plná sada vstupních bodů vykreslování, které teď pokrývá, se dodávají jako součást komponenty PDFium pro Delphi, C++Builder a Lazarus/FPC