Tehnički članak

Šest PDFium poziva koji su u Delphiju zaboravili Render Lock

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