Render Lock u PDFiumPasu kritični je odjeljak po dokumentu — EnterRenderLock i LeaveRenderLock, potpomognuti poljem TRTLCriticalSection u klasi TPdf — koji treba obuhvatiti svaki poziv PDFiumovu rasterizatoru kako se stranica ne bi oslobodila ili ponovno učitala usred iscrtavanja. Šest metoda, ravnomjerno podijeljenih između TPdf i TPdfView, izravno je pozivalo PDFiumove API-je za izdvajanje bitmapa i minijatura te potpuno preskakalo to zaključavanje, a PDFiumPas v2.26.0 zatvorio je tu prazninu obuhvativši svih šest istim parom zaključavanja koji su već koristile ostale ulazne točke za iscrtavanje
Praznina obrađena ovdje nije prolaz za učvršćivanje ABI-ja opisan drugdje na ovom blogu, koji je prošao kroz nepodudaranje konvencije pozivanja cdecl i skraćivanje širine pokazivača FPC Win64 u istom povezivanju s PDFiumom. Ono što slijedi uže je i mehaničkije: kontrolni popis pokrivenosti zaključavanjem za šest mjesta poziva koja sva ulaze u put iscrtavanja PDFiuma, zašto je svako od njih bilo lako previdjeti i zašto je utrku nastalu zbog propuštenog zaključavanja teško reproducirati po potrebi
Što Render Lock zapravo štiti
PDFiumPas serijalizira iscrtavanje jer učitana stranica PDFiuma nije sigurna za čitanje iz jedne niti dok je druga nit može osloboditi. TPdf posjeduje TRTLCriticalSection u FRenderLock, inicijaliziranom u konstruktoru i zaštićenom zastavicom FRenderLockReady tako da poziv koji stigne nakon rastavljanja postaje tiha operacija bez učinka umjesto da uđe u obrisani kritični odjeljak. EnterRenderLock i LeaveRenderLock jedini su odobreni načini ulaska u taj odjeljak i izlaska iz njega
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 slijedili tu disciplinu prije početka ove revizije, svaki uzimajući zaključavanje prije poziva u PDFium i oslobađajući ga u bloku finally kako se pozadinsko pred-iscrtavanje i prednji UnloadPage na istoj instanci TPdf ne bi preklopili. Praznina koju je pronašao PDFiumPas v2.26.0 nije bila u tim očitim ulaznim točkama — pojavila se u šest metoda koje izgledaju kao pristupnici, a ne kao iscrtavanje, iako svaka od njih traži od PDFiuma rasterizaciju piksela prije nego što može išta vratiti
Kojih je šest poziva preskočilo Render Lock?
TPdf.GetObjectBitmap, TPdf.GetBitmap i TPdf.GetThumbnail činili su polovicu popisa, a TPdfView.GetObjectBitmap, TPdfView.GetBitmap i TPdfView.GetThumbnail drugu polovicu — iste tri operacije udvostručene u dvjema klasama komponenti koje izlažu istu temeljnu stranicu. Svih šest naposljetku poziva ili FPDFImageObj_GetBitmap ili FPDFPage_GetThumbnailAsBitmap, a obje te PDFiumove ulazne točke rasteriziraju odmah umjesto da vrate referencu na nešto već iscrtano. Nijedan naziv od tih šest metoda ne kaže render, što razumno objašnjava zašto prvi put nisu pisane prema istom popisu 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 poziv zaključavanja štiti provjerom nil
TPdfView nema vlastiti kritični odjeljak — svaki od njegovih šest poziva zaključavanja prosljeđuje na FPdf.EnterRenderLock i FPdf.LeaveRenderLock, uz provjeru da pridružena referenca TPdf najprije nije nil. Ta zaštita postoji jer se TPdfView može nalaziti na obrascu u vrijeme dizajna ili nakratko između zatvaranja jednog dokumenta i otvaranja sljedećeg, dok FPdf još nema dodijeljen TPdf. Preskakanje zaštite zamijenilo bi jedan pad drugim jer poziv zaključavanja nad nil referencom nije ništa sigurniji od utrke koju zaključavanje treba spriječiti
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 provjeri?
TPdfView.RenderPage prema kontekstu uređaja nije jedan od šest poziva — pojavio se izdanje ranije, u PDFiumPasu v2.25.0, i pripada ovom popisu jer je to ista greška s drugačijim potpisom. To je preopterećenje izravno pozivalo FPDF_RenderPage bez EnterRenderLock i bez poziva SetArithmeticMask koji štiti od FPU iznimki u starijim prevoditeljima Delphi, dok je preopterećenje TBitmap nekoliko redaka niže u istoj klasi već imalo oba. Dvije revizije koje su u razmaku od jednog izdanja otkrile isti način kvara govore manje o pojedinoj metodi, a više o obliku greške: skriva se u onom preopterećenju koje nitko ponovno ne čita kada njegov srodni poziv 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 utrku gotovo nemoguće reproducirati?
Praznina u Render Locku PDFiumPasa ne otkazuje pri svakom pokretanju, pa čak ni pri većini pokretanja, jer se na istoj instanci TPdf istodobno moraju dogoditi dvije određene stvari: poziv rasterizacije koji je već u tijeku i istodobni UnloadPage ili ReloadPage koji stigne unutar tog prozora. Jednonitno testiranje uopće ne prolazi tim putem, a čak ga i stvarna višenitna opterećenja aktiviraju samo kada se pozadinsko iscrtavanje i događaj životnog ciklusa dokumenta slučajno preklapaju tijekom života jedne stranice. Najrealniji okidač je pozadinsko pred-iscrtavanje PDF-a temeljeno na budućnostima koje se mogu otkazati, pri kojem radna nit rasterizira sljedeću stranicu dok nit sučelja na korisnički unos ponovno učitava ili oslobađa trenutačnu stranicu
FPDFImageObj_GetBitmap i FPDFPage_GetThumbnailAsBitmap prolaze kroz strukture objekata stranice koje UnloadPage može osloboditi usred prolaska, pa utrka koja se doista aktivira ne mora uvijek proizvesti ni neposrednu povredu pristupa. Čitanje strukture trenutak prekasno jednako lako može vratiti besmislene piksele ili oštetiti metapodatke hrpe koji će se srušiti tek nekoliko nepovezanih alokacija kasnije, u funkciji koja nikad nije dotaknula PDF stranicu. To je iskren razlog zbog kojeg ova vrsta greške može preživjeti u bazi koda kroz nekoliko ciklusa izdanja: trag stoga na mjestu pada rijetko pokazuje i približno prema šest redaka kojima je zaključavanje stvarno nedostajalo
Što se mijenja za pozivatelje
GetBitmap, GetObjectBitmap, GetThumbnail i HDC preopterećenje metode RenderPage zadržavaju javne potpise točno onakvima kakvi su bili jer je popravak unutarnje zaključavanje dodano oko postojećih poziva, a ne migracija. Vrijedi zapamtiti da je Render Lock ograničen na svaku instancu TPdf, a nije globalan za proces, pa dvije niti koje iscrtavaju dva zasebno učitana dokumenta i dalje rade potpuno paralelno — zaključavanje serijalizira samo operacije nad jednim dokumentom koji obje niti slučajno dijele. Ako je vaše zaključavanje već ispravno, a iscrtavanje je i dalje sporo pri povećavanju ili pomicanju, to je drugo pitanje na koje odgovara članak o predmemoriji iscrtavanja PDFiuma i strategijama performansi povećanja — ispravnost i brzina ovdje su odvojene osi, a ovaj se popravak dotiče samo prve
Šest metoda i jedno srodno preopterećenje mali su dio površine PDFiuma koju PDFiumPas izlaže, ali upravo taj dio ponašao se pogrešno samo pod opterećenjem koje nitko nije slučajno pokretao u ispravljaču pogrešaka. Sam Render Lock i potpuni skup ulaznih točaka za iscrtavanje koje sada pokriva isporučuju se kao dio komponente PDFium Component za Delphi, C++Builder i Lazarus/FPC