Techninis straipsnis

Šeši PDFium iškvietimai, pamiršę renderavimo užraktą Delphi

PDFiumPas renderavimo užraktas yra vienam dokumentui skirta kritinė sekcija — EnterRenderLock ir LeaveRenderLock, remiama TRTLCriticalSection lauku ant TPdf — skirta apgaubti kiekvieną iškvietimą į PDFium rasterizatorių, kad puslapis negalėtų būti iškrautas arba iš naujo pakrautas iš po vykstančio renderavimo. Šeši metodai, pasidaliję lygiai tarp TPdf ir TPdfView, tiesiogiai iškvietė PDFium bitmap ir miniatiūrų ekstrahavimo API ir visiškai praleido tą užraktą, spragą, kurią PDFiumPas v2.26.0 uždarė suvyniodama visus šešis į tą patį užrakto porą, kurį kiekvienas kitas renderavimo įėjimo taškas jau naudojo

Spraga, aptarta čia, nėra ABI-sustiprinimo praėjimas, aptartas kitur šiame tinklaraštyje, kuris perėjo cdecl iškvietimo-konvencijos neatitikimą ir FPC Win64 rodyklės-ilgio trumpinimą tame pačiame PDFium susiejime. Kas seka yra siauresnis ir mechaninis: užrakto-padengties kontrolinis sąrašas šešiems iškvietimo taškams, kurie visi pasiekia PDFium renderavimo kelią, kodėl kiekvieną buvo lengva praleisti, ir kodėl lenktynės, kurios seka iš praleisto užrakto, yra viena iš sunkesnių defektų šiame kode reprodukuoti pagal pareikalavimą

Ką renderavimo užraktas iš tikrųjų saugo

PDFiumPas serializuoja renderavimą, nes PDFium pakrautas puslapis nėra saugu skaityti iš vienos gijos, kol kita gija laisva jį paleisti. TPdf turi TRTLCriticalSection FRenderLock, inicializuotą konstruktoriuje ir saugomą FRenderLockReady vėliavos, kad iškvietimas, atvykęs po ardymo, taptų tyliu no-op vietoj įėjimo į ištrintą kritinę sekciją. EnterRenderLock ir LeaveRenderLock yra vienintelis sankcionuotas kelias į ir iš tos sekcijos

TPdf atvaizdavimo užrakto PDFiumPas diagrama: fono atvaizdavimo gija laiko kritinę sekciją, kol UI gijos UnloadPage iškvietimas laukia
Dokumentui skirtasis atvaizdavimo užraktas serijizuoja kiekvieną rastrizavimo kvietimą prieš UnloadPage ir ReloadPage tame pačiame TPdf egzemplioriuje
procedure TPdf.EnterRenderLock;
begin
  if FRenderLockReady then
    EnterCriticalSection(FRenderLock);
end;

procedure TPdf.LeaveRenderLock;
begin
  if FRenderLockReady then
    LeaveCriticalSection(FRenderLock);
end;

TPdf.RenderPage, RenderTile ir RenderPageProgressive jau laikėsi tos disciplinos dar prieš šiam konkrečiam auditui prasidedant, kiekvienas imdamas užraktą prieš iškviesdamas PDFium ir paleisdamas jį finally bloke, kad foninis pre-render ir priekinis UnloadPage tame pačiame TPdf egzemplioriuje negalėtų persidengti. Spraga, kurią PDFiumPas v2.26.0 rado, nebuvo tuose akivaizdžiuose įėjimo taškuose — ji pasirodė šešiuose metoduose, kurie skaitomi kaip prieigos būdai, o ne renderiai, nors kiekvienas iš jų paprašo PDFium rasterizuoti pikselius, kol gali grąžinti ką nors

Kurie šeši iškvietimai praleido renderavimo užraktą?

TPdf.GetObjectBitmap, TPdf.GetBitmap ir TPdf.GetThumbnail sudarė pusę sąrašo, o TPdfView.GetObjectBitmap, TPdfView.GetBitmap ir TPdfView.GetThumbnail sudarė kitą pusę — tos pačios trys operacijos, dubliuotos per dvi komponentų klases, kurios atskleidžia tą patį pagrindinį puslapį. Visi šeši galiausiai iškviečia arba FPDFImageObj_GetBitmap, arba FPDFPage_GetThumbnailAsBitmap, ir abu tie PDFium įėjimo taškai rasterizuoja vietoje, užuot grąžinę nuorodą į ką nors jau renderuotą. Niekas jokiame iš šešių metodų pavadinimų nesako render, kas yra protingas paaiškinimas, kodėl jie nebuvo parašyti prieš tą patį kontrolinį sąrašą kaip RenderPage ir RenderTile pirmą kartą

PDFium Component šešių TPdf ir TPdfView taškinės grafikos bei miniatiūrų metodų, sueinančių į FPDFImageObj_GetBitmap ir FPDFPage_GetThumbnailAsBitmap, diagrama
Visi šeši prieigos funkcijų stiliaus metodai pasiekia PDFium rastrizatorių ir dabar dalijasi ta pačia EnterRenderLock ir LeaveRenderLock pora nuo v2.26.0
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;

Kodėl TPdfView saugo savo užrakto iškvietimą nil patikra

TPdfView neturi savo kritinės sekcijos — kiekvienas iš jo šešių užrakto iškvietimų persiunčia į FPdf.EnterRenderLock ir FPdf.LeaveRenderLock, suvyniotus į patikrą, kad susietas TPdf rodyklė nėra nil pirmiausia. To sargybūs egzistuoja todėl, kad TPdfView gali sėdėti formoje projektavimo metu, arba trumpai tarp vieno dokumento uždarymo ir kito atvėrimo, dar nepriskyrus TPdf FPdf. Sargybūs praleidimas mainytų vieną lūžį į kitą, nes užraktimo iškvietimas prieš nil rodyklę nepavyksta ne daugiau grakščiau nei lenktynės, kurias užraktas egzistuoja užkirsti kelią

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;

Kodėl RenderPage(HDC) priklauso tam pačiam auditui?

TPdfView.RenderPage prieš įrenginio kontekstą nėra vienas iš šešių — jis pasirodė reliazu anksčiau, PDFiumPas v2.25.0, ir pelno vietą šiame kontroliniame sąraše, nes tai yra tas pats defektas su kitu parašu. Ta perkrova iškvietė FPDF_RenderPage tiesiogiai be EnterRenderLock ir be SetArithmeticMask iškvietimo, kuris saugo nuo FPU išimčių ant senesnių Delphi kompiliatorių, o TBitmap perkrova, sėdinti keliomis eilutėmis žemiau toje pačioje klasėje, jau turėjo abu. Du audito praėjimai, pagaunantys tą patį nesėkmės režimą per reliazą, sako mažiau apie bet kurį vieną metodą ir daugiau apie klaidos formą: ji slepiasi toje perkrovoje, kurią niekas nebeskaito kartą, kai jos brolis atrodo teisingas

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;

Kodėl šios lenktynės beveik neįmanoma reprodukuoti?

PDFiumPas renderavimo-užrakto spraga nepavyksta kiekviename paleidime, ar net daugumoje paleidimų, nes jai reikia dviejų konkrečių dalykų, nusileidžiančių ant to paties TPdf egzemplioriaus vienu metu: rasterizavimo iškvietimo jau vykstančio, ir lygiagrečio UnloadPage arba ReloadPage, atvykstančio per tą patį langą. Vienagijis testavimas iš viso neeksploatuoja kelio, ir netgi genuine daugiagijės apkrovos jį suklupo tik tada, kai foninis renderavimas ir dokumento gyvavimo ciklo įvykis atsitinka persidengti per vieno puslapio gyvavimą. Realistiškiausias trigeris yra foninis PDF pre-renderavimas, sukurtas ant atšaukiamų future, kur darbo gija rasterizuoja kitą puslapį, kol UI gija iš naujo pakrauna arba iškrauna dabartinį pagal vartotojo įvestį

PDFium Component laiko juosta, rodanti UnloadPage iškvietimą, nusileidžiantį vykstančio FPDFImageObj_GetBitmap atvaizdavimo lange, ir jo tris galimus nesėkmės išėjimus
Lenktynės suveikia tik tada, kai gyvavimo ciklo įvykis nusileidžia vykstančiosios rastrizacijos viduje tame pačiame TPdf, o avarijos paviršius retai rodo atgal į neužrakintąjį kvietimą

FPDFImageObj_GetBitmap ir FPDFPage_GetThumbnailAsBitmap eina per puslapio-objekto struktūras, kurias UnloadPage laisvas paleisti viduryje-traversavimo, todėl lenktynės, kurios tikrai šaudo, ne visada gamina greitą prieigos pažeidimą. Struktūra, perskaityta akimirka per vėlai, gali lygiai taip pat lengvai grąžinti šiukšlių pikselius, arba sugadinti krūvos metaduomenis, kurie lūžta tik po kelių nesusijusių alokacijų vėliau, funkcijoje, kuri niekada nepalietė PDF puslapio. Tai yra sąžininga priežastis, kodėl šios klasės klaida gali išgyventi kode per kelias reliazų ciklus: dėklo sekimas nesėkmės taške retai rodo bet kur arti tų šešių eilučių, kurios iš tikrųjų trūko užrakto

Kas pasikeičia kviesčiams

GetBitmap, GetObjectBitmap, GetThumbnail ir HDC RenderPage perkrova išlaiko savo viešus parašus tiksliai tokius, kokie buvo, nes pataisa yra vidinis užraktimas, pridėtas aplink esamus iškvietimus, o ne migracija. Verta prisiminti, kad renderavimo užraktas yra apribotas vienam TPdf egzemplioriui, o ne globalus procesui, todėl dvi gijos, renderuojančios du atskirai pakrautus dokumentus, vis tiek veikia visiškai lygiagrečiai — užraktas serializuoja tik operacijas prieš tą vieną dokumentą, kurį abi gijos atsitiktinai dalijasi. Jei jūsų užraktimas jau tvirtas ir renderiai vis tiek jaučiasi lėti po zoom arba slinktimi, tai yra kitas klausimas, atsakytas PDFium render cachinimo ir zoom našumo taktikos straipsnyje — teisingumas ir greitis čia yra atskiros ašys, ir ši pataisa paliečia tik pirmąją

Šeši metodai ir vienas brolio perkrova yra maža PDFium paviršiaus, kurį PDFiumPas atskleidžia, dalis, bet tai buvo dalis, kuri elgėsi netinkamai tik po apkrova, kurią niekas atsitiktinai nepaleido derintuvėje. Pats renderavimo užraktas ir pilnas renderavimo įėjimo taškų rinkinys, kurį jis dabar padengia, atsiranda kaip PDFium Component Delphi, C++Builder ir Lazarus/FPC dalis