Zaključavanje renderovanja u PDFiumPas-u je kritična sekcija vezana za pojedinačni dokument — EnterRenderLock i LeaveRenderLock, iza kojih stoji polje tipa TRTLCriticalSection u klasi TPdf — i treba da obuhvati svaki poziv PDFium rasterizatoru kako stranica ne bi bila uklonjena ili ponovo učitana dok renderovanje još traje. Šest metoda, ravnomerno podeljenih između TPdf i TPdfView, direktno je pozivalo PDFium API-je za izdvajanje bitmapa i sličica i potpuno preskakalo to zaključavanje. PDFiumPas v2.26.0 zatvorio je taj propust tako što je svih šest metoda obavio istim parom zaključavanja koji su već koristile ostale ulazne tačke za renderovanje
Ovaj propust nije isti kao ranije opisana faza učvršćivanja ABI-ja, u kojoj su obrađeni neslaganje pozivne konvencije cdecl i skraćivanje širine pokazivača u FPC Win64 u istom PDFium povezivanju. Ono što sledi je uže i mehaničkije: kontrolna lista pokrivenosti zaključavanjem za šest mesta poziva koja ulaze u PDFium putanju renderovanja, razlog zbog kog je svako od njih bilo lako prevideti i objašnjenje zašto je trku izazvanu nedostatkom zaključavanja među težim greškama u ovoj bazi koda za reprodukciju na zahtev
Šta zaključavanje renderovanja zaista štiti
PDFiumPas serijalizuje renderovanje jer učitana stranica PDFium-a nije bezbedna za čitanje iz jedne niti dok druga nit može da je oslobodi. TPdf poseduje TRTLCriticalSection u FRenderLock, inicijalizovanu u konstruktoru i zaštićenu zastavicom FRenderLockReady, tako da poziv koji stigne posle uklanjanja objekta postaje tiha prazna operacija umesto ulaska u obrisanu kritičnu sekciju. EnterRenderLock i LeaveRenderLock jedini su predviđeni način ulaska u tu sekciju i izlaska iz nje
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile i RenderPageProgressive već su sledili tu disciplinu pre početka ove provere: svaki je uzimao zaključavanje pre poziva PDFium-u i oslobađao ga u bloku finally, pa pozadinsko prethodno renderovanje i poziv UnloadPage iz korisničkog interfejsa nad istom instancom TPdf nisu mogli da se preklapaju. Propust koji je PDFiumPas v2.26.0 pronašao nije bio u tim očiglednim ulaznim tačkama — pojavio se u šest metoda koje liče na pristupnike, iako svaka od njih traži od PDFium-a da rasterizuje piksele pre nego što može da vrati rezultat
Kojih šest poziva je preskočilo zaključavanje renderovanja
Polovinu spiska činili su TPdf.GetObjectBitmap, TPdf.GetBitmap i TPdf.GetThumbnail, a drugu polovinu TPdfView.GetObjectBitmap, TPdfView.GetBitmap i TPdfView.GetThumbnail — iste tri operacije ponovljene u dve klase komponenti koje izlažu istu osnovnu stranicu. Svih šest na kraju pozivaju FPDFImageObj_GetBitmap ili FPDFPage_GetThumbnailAsBitmap, a oba PDFium ulaza odmah rasterizuju podatke umesto da vrate referencu na nešto što je već izrenderovano. Nijedno od šest imena metoda ne kaže render, pa je razumljivo zašto prvi put nisu proverena istom kontrolnom listom kao RenderPage i 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;
Zašto TPdfView proverava da li je poziv zaključavanja nil
TPdfView nema sopstvenu kritičnu sekciju — svaki od njegovih poziva zaključavanja prosleđuje se metodama FPdf.EnterRenderLock i FPdf.LeaveRenderLock, uz prethodnu proveru da pridružena referenca TPdf nije nil. Ta zaštita je potrebna jer TPdfView može da bude postavljen na formu tokom projektovanja ili nakratko između zatvaranja jednog dokumenta i otvaranja sledećeg, dok FPdf još nema dodeljen objekat TPdf. Preskakanje provere samo bi zamenilo jednu grešku drugom, jer poziv zaključavanja preko nil reference nije ništa bezbedniji od trke koju zaključavanje treba da spreči
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;
Zašto RenderPage(HDC) pripada istoj proveri
TPdfView.RenderPage koji radi nad kontekstom uređaja nije jedan od šest poziva — pojavio se izdanje ranije, u PDFiumPas-u v2.25.0, i pripada ovoj kontrolnoj listi jer je isti nedostatak dobio drugačiji potpis. Ta preopterećena metoda direktno je pozivala FPDF_RenderPage bez EnterRenderLock i bez poziva SetArithmeticMask, koji na starijim Delphi kompajlerima štiti od FPU izuzetaka, dok je preopterećenje sa TBitmap, nekoliko redova niže u istoj klasi, već sadržalo oba poziva. Dve provere koje u razmaku od jednog izdanja hvataju isti način otkaza govore manje o bilo kojoj pojedinačnoj metodi, a više o obliku greške: ona se krije u onom preopterećenju koje niko ponovo ne pročita kada njegov srodni oblik izgleda ispravno
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;
Zašto je ovu trku gotovo nemoguće reprodukovati
Propust u zaključavanju renderovanja PDFiumPas-a ne javlja se pri svakom pokretanju, pa čak ni pri većini pokretanja, jer zahteva da se na istoj instanci TPdf istovremeno dogode dve konkretne stvari: poziv rasterizacije koji je već u toku i konkurentni poziv UnloadPage ili ReloadPage koji stigne u tom istom vremenskom prozoru. Jednonitno testiranje uopšte ne prolazi ovom putanjom, a i stvarno višenitna opterećenja je aktiviraju samo kada se pozadinsko renderovanje i događaj životnog ciklusa dokumenta slučajno preklopi tokom života jedne stranice. Najrealniji okidač je pozadinsko prethodno renderovanje PDF-a zasnovano na budućim zadacima koji mogu da se otkažu, pri kom radna nit rasterizuje sledeću stranicu dok nit korisničkog interfejsa ponovo učitava ili uklanja trenutnu na osnovu korisničkog unosa
FPDFImageObj_GetBitmap i FPDFPage_GetThumbnailAsBitmap prolaze kroz strukture objekata stranice koje UnloadPage može da oslobodi usred prolaza, pa stvarno aktivirana trka ne mora odmah da izazove ni kršenje pristupa. Čitanje strukture sa zakašnjenjem od samo jednog trenutka može jednako lako da vrati pogrešne piksele ili ošteti metapodatke hipa, što će se srušiti tek posle nekoliko nepovezanih alokacija, u funkciji koja nikada nije dodirnula PDF stranicu. To je pravi razlog zbog kog ova klasa grešaka može da preživi više ciklusa izdanja: trag steka na mestu pada retko pokazuje blizu šest redova kojima je zaključavanje zapravo nedostajalo
Šta se menja za pozivaoca
GetBitmap, GetObjectBitmap, GetThumbnail i HDC preopterećenje metode RenderPage zadržavaju javne potpise tačno onakve kakvi su bili, jer je ispravka unutrašnje zaključavanje oko postojećih poziva, a ne migracija. Važno je zapamtiti da je zaključavanje renderovanja vezano za instancu TPdf, a ne globalno za proces, pa dve niti koje renderuju dva zasebno učitana dokumenta i dalje rade potpuno paralelno — zaključavanje samo serijalizuje operacije nad onim jednim dokumentom koji obe niti dele. Ako je vaše zaključavanje već ispravno, a renderovanje je i dalje sporo pri uveličavanju ili pomeranju, to je drugo pitanje, obrađeno u članku o PDFium kešu renderovanja i tehnikama za bolje performanse pri uveličavanju — ispravnost i brzina su ovde odvojene ose, a ova ispravka dotiče samo prvu
Šest metoda i jedno srodno preopterećenje čine mali deo površine PDFium-a koju PDFiumPas izlaže, ali upravo taj deo se pogrešno ponašao pod opterećenjem koje niko nije slučajno pokrenuo u programu za otklanjanje grešaka. Samo zaključavanje renderovanja i kompletan skup ulaznih tačaka za renderovanje koje ono sada pokriva isporučuju se u okviru PDFium komponente za Delphi, C++Builder i Lazarus/FPC