Teknik Makale

Delphi'de Render Kilidini Unutan Altı PDFium Çağrısı

PDFiumPas'ın render kilidi, belge başına bir kritik bölümdür -TPdf üzerindeki bir TRTLCriticalSection alanıyla desteklenen EnterRenderLock ve LeaveRenderLock- ve PDFium'un rasterleştiricisine yapılan her çağrıyı sarmalayarak, uçuştaki bir render işleminin altından bir sayfanın yüklenmemesi veya yeniden yüklenmemesi için tasarlanmıştır. TPdf ve TPdfView arasında eşit şekilde bölünmüş altı metot, PDFium'un bitmap ve küçük resim çıkarma API'lerini doğrudan çağırdı ve bu kilidi tamamen atladı; PDFiumPas v2.26.0, diğer her render giriş noktasının zaten kullandığı aynı kilit çiftine tümünü sarmalayarak bu boşluğu kapattı

Burada ele alınan boşluk, bu blogda başka bir yerde ele alınan, aynı PDFium bağlamasında bir cdecl çağrı kuralı uyumsuzluğunu ve bir FPC Win64 işaretçi genişliği kesmesini ele alan ABI sertleştirme geçişi değildir. Aşağıda daha dar ve daha mekanik bir şey var: hepsi PDFium'un render yoluna uzanan altı çağrı noktası için bir kilit-kapsama kontrol listesi, her birinin neden gözden kaçırılmasının kolay olduğu ve kilidin eksikliğinden kaynaklanan yarışın neden bu kod tabanındaki talep üzerine yeniden üretilmesi en zor kusurlardan biri olduğu

Render kilidi gerçekte neyi korur?

PDFiumPas render'ı serileştirir, çünkü PDFium'un yüklü sayfası, bir iş parçacığı ondan okurken başka bir iş parçacığının onu serbest bırakmakta özgür olduğu durumda güvenli değildir. TPdf, yapıcıda başlatılan ve teardown sonrası gelen bir çağrının silinmiş bir kritik bölüme girmek yerine sessiz bir hiçbir şey yapmama haline gelmesi için bir FRenderLockReady bayrağıyla korunan bir TRTLCriticalSectionFRenderLock içinde sahiplenir. EnterRenderLock ve LeaveRenderLock, o bölüme girip çıkmanın onaylı tek yoludur

procedure TPdf.EnterRenderLock;
begin
  if FRenderLockReady then
    EnterCriticalSection(FRenderLock);
end;

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

TPdf.RenderPage, RenderTile ve RenderPageProgressive, bu belirli denetim hiç başlamadan önce zaten bu disiplini izliyordu; her biri PDFium'a çağrı yapmadan önce kilidi alıyor ve bir finally bloğunda serbest bırakıyordu; böylece aynı TPdf örneğinde bir arka plan ön-render'ı ile ön plan bir UnloadPage çakışamıyordu. PDFiumPas v2.26.0'ın bulduğu boşluk bu bariz giriş noktalarında değildi -bunlar, her biri bir şey döndürmeden önce PDFium'dan piksel rasterleştirmesini istese de, render'lardan çok erişimci gibi okunan altı metotta ortaya çıktı

Render kilidini hangi altı çağrı atladı?

TPdf.GetObjectBitmap, TPdf.GetBitmap ve TPdf.GetThumbnail listenin yarısını oluşturdu ve TPdfView.GetObjectBitmap, TPdfView.GetBitmap ve TPdfView.GetThumbnail diğer yarısını -aynı üç işlem, aynı altta yatan sayfayı sunan iki bileşen sınıfı genelinde çoğaltılmış olarak. Altısı da sonunda ya FPDFImageObj_GetBitmap ya da FPDFPage_GetThumbnailAsBitmap'i çağırır ve bu PDFium giriş noktalarının ikisi de zaten render edilmiş bir şeye bir referans geri vermek yerine yerinde rasterleştirir. Altı metot adının hiçbirinde "render" kelimesi geçmez ki bu, ilk seferinde neden RenderPage ve RenderTile ile aynı kontrol listesine karşı yazılmadıklarının makul bir açıklamasıdır

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;

TPdfView kilit çağrısını neden bir nil kontrolüyle koruyor?

TPdfView kendi kritik bölümünü sahiplenmez -altı kilit çağrısının her biri, önce ilişkili TPdf referansının nil olmadığı kontrolüyle sarmalanmış olarak FPdf.EnterRenderLock ve FPdf.LeaveRenderLock'a iletir. Bu koruma vardır çünkü bir TPdfView, tasarım zamanında bir form üzerinde veya bir belge kapanışı ile bir sonrakinin açılışı arasında kısaca, FPdf'e henüz atanmış bir TPdf olmadan oturabilir. Korumayı atlamak bir çökmeyi başka bir çökmeyle takas ederdi, çünkü nil bir referansa karşı bir kilitleme çağrısı, kilidin önlemek için var olduğu yarıştan daha zarif bir şekilde başarısız olmaz

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;

RenderPage(HDC) neden aynı denetime aittir?

Bir cihaz bağlamına karşı çalışan TPdfView.RenderPage altılıdan biri değildir -bir sürüm önce, PDFiumPas v2.25.0'da ortaya çıktı ve bu kontrol listesinde bir yer kazanır çünkü aynı kusurun farklı bir imza taşıyan halidir. O aşırı yükleme, ne EnterRenderLock ne de eski Delphi derleyicilerinde FPU istisnalarına karşı koruyan SetArithmeticMask çağrısı olmadan FPDF_RenderPage'i doğrudan çağırdı; aynı sınıfta birkaç satır aşağıda oturan TBitmap aşırı yüklemesi ise ikisini de zaten taşıyordu. Bir sürüm arayla aynı başarısızlık modunu yakalayan iki denetim geçişi, herhangi bir tek metot hakkında daha az, hatanın şekli hakkında daha çok şey söyler: kardeşi doğru göründüğünde kimsenin yeniden okumadığı aşırı yüklemede saklanır

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;

Bu yarış neden yeniden üretmek neredeyse imkansız?

PDFiumPas'ın render-kilidi boşluğu her çalıştırmada, hatta çoğu çalıştırmada bile başarısız olmaz, çünkü aynı anda aynı TPdf örneğine inmesi gereken iki belirli şeye ihtiyaç duyar: zaten uçuşta olan bir rasterleştirme çağrısı ve aynı pencerenin içinde gelen eşzamanlı bir UnloadPage veya ReloadPage. Tek iş parçacıklı test bu yolu hiç çalıştırmaz ve gerçekten çok iş parçacıklı iş yükleri bile ancak bir arka plan render'ı ile bir belge yaşam döngüsü olayı bir sayfanın yaşam süresi içinde çakıştığında bunu tetikler. En gerçekçi tetikleyici, iptal edilebilir future'lar üzerine kurulu arka plan PDF ön-render'ıdır; burada bir işçi iş parçacığı bir sonraki sayfayı rasterleştirirken UI iş parçacığı kullanıcı girdisi üzerine mevcut olanı yeniden yükler veya kaldırır

FPDFImageObj_GetBitmap ve FPDFPage_GetThumbnailAsBitmap, UnloadPage'in dolaşım ortasında serbest bırakmakta özgür olduğu sayfa nesnesi yapılarını dolaşır; bu yüzden gerçekten tetiklenen bir yarış her zaman anında bir erişim ihlali üretmez de. Bir an geç okunan bir yapı, aynı kolaylıkla çöp piksel geri verebilir veya yalnızca daha sonra, hiçbir zaman bir PDF sayfasına dokunmamış bir fonksiyonda ilgisiz birkaç tahsisi çökerten yığın meta verisini bozabilir. Bu tür bir hatanın bir kod tabanında birkaç sürüm döngüsü boyunca hayatta kalabilmesinin dürüst nedeni budur: başarısızlık noktasındaki yığın izi nadiren gerçekten bir kilidin eksik olduğu altı satırın yakınına işaret eder

Çağıranlar için ne değişiyor

GetBitmap, GetObjectBitmap, GetThumbnail ve RenderPage'in HDC aşırı yüklemesi, düzeltme bir geçiş değil mevcut çağrıların etrafına eklenmiş dahili kilitleme olduğundan, genel imzalarını tam olarak eskisi gibi korur. Render kilidinin sürece değil TPdf örneği başına kapsamlandığını hatırlamakta fayda var; bu yüzden ayrı ayrı yüklenmiş iki belgeyi render eden iki iş parçacığı yine de tamamen paralel çalışır -kilit yalnızca iki iş parçacığının paylaştığı o tek belgeye karşı işlemleri serileştirir. Kilitlemeniz zaten sağlamsa ve render'lar yakınlaştırma veya kaydırma altında yine de yavaş hissettiriyorsa, bu farklı bir sorudur ve PDFium render önbelleği ve yakınlaştırma performansı taktikleri makalesinde yanıtlanmıştır -doğruluk ve hız burada ayrı eksenlerdir ve bu düzeltme yalnızca ilkine dokunur

Altı metot ve bir kardeş aşırı yükleme, PDFiumPas'ın sunduğu PDFium yüzeyinin küçük bir kesridir, ama bunlar yalnızca kimsenin bir hata ayıklayıcıda çalıştırmadığı bir yük altında yanlış davranan kesirdi. Render kilidinin kendisi ve şimdi kapsadığı tam render giriş noktası kümesi, Delphi, C++Builder ve Lazarus/FPC için PDFium Bileşeni'nin bir parçası olarak gönderilir