Teknik Makale

HotPDF RenderCacheFolder: Delphi'de disk page cache'i

HotPDF RenderCacheFolder, HotPDF Delphi bileşeninin bellek içi render edilmiş sayfa cache'ini kalıcı bir disk page cache'ine çevirir: render edilen sayfalar sizin seçtiğiniz bir klasör altında PNG dosyaları olarak yazılır ve aynı PDF kaynağı bir dahaki açılışında RenderLoadedPageToBitmapCached onları yeniden rasterize etmek yerine geri okur. Arama sırası bellek, sonra disk, sonra renderer'dır

Disk katmanı v2.416.0'dan beri API'de duruyordu ama v2.770.140'e dek sıradan bir LoadFromFile ya da LoadFromStream çağrısı için hiçbir zaman gerçekten bir sayfa sunmadı. Düzeltme, her kalıcı cache'in yanıtlamak zorunda olduğu bir soruyu dayattı: bugün açtığınız dosyanın dün render ettiğiniz belge olduğunu nereden biliyorsunuz ve değilse önbelleklenmiş sayfalara ne oluyor? Aşağıda HotPDF'in yerleştiği yanıtlar var; kasıtlı olarak cache'lemeyi reddettiği yerler dahil

HotPDF disk render cache'i nasıl çalışır?

HotPDF disk render cache'i, bellek içi raster cache'in ardındaki ikinci bir katmandır ve yalnızca RenderCacheFolder boş olmayan bir path iken işe karışır. RenderLoadedPageToBitmapCached(PageIndex, DPI) çağrısı önce bellek içi girdileri tarar; sayfa indeksi, DPI ve bir render-settings varyantıyla anahtarlanır. Iskalayınca disk katmanına sorar; disk isabeti PNG'yi decode eder, onu belleğe geri terfi ettirir ve çağıranın malı bir kopya döndürür. Yalnızca iki katman da ıskaladığında sayfa, yüklenen bir PDF sayfasını TBitmap'e render etmeyazısında anlatılan content-stream yorumlayıcısından geçer ve taze bitmap sonra diske de yazılır

RenderLoadedPageToBitmapCached için HotPDF render cache arama şeması: önce sayfa, DPI ve render varyantıyla anahtarlanan bellek içi katman denetlenir; sonra atomik replace ile RenderCacheFolder PNG disk katmanı; sonra content-stream yorumlayıcısı gelir ve her isabet çağıranın malı bir kopya döndürür
HotPDF önce belleğe, sonra diske bakar ve ancak ondan sonra rasterize eder; disk isabeti belleğe geri terfi ettirilir ve her yol size sahip olduğunuz ve free etmeniz gereken bir kopya verir

Diskte düzen kasıtlı olarak sıkıcıdır. Her belge, 16 onaltılık karakterlik bir belge anahtarı artı 16 onaltılık karakterlik bir render varyantından türetilen adla bir alt klasör alır; her sayfa <page>@<dpi>.png olarak saklanır ve kökteki bir index.txt, belgeleri bir schema etiketinin ardında en son kullanılan sırasında tutar. Schema uyuşmazlığı ilk kullanımda klasörü temizler. Yazımlar önce geçici bir dosyaya gider ve atomik replace ile yerine takılır; böylece yazım ortasında bir çökme ya eski sayfayı ya hiçbir şey bırakır, yarım bir PNG asla. Decode edilemeyen bir PNG silinir ve ıskalama sayılır

Üç sınır klasörü çepeçevre sarar:

  • RenderCacheMaxDocuments (varsayılan 20) belge alt klasörlerinin sayısını tavanlar; en az yakın kullanılan klasör önce tahliye edilir
  • RenderCacheMaxBytes (varsayılan 524288000, yani 500 MB) kök altındaki bütün PNG dosyalarının toplam boyutunu tavanlar
  • Her belge klasörü en çok 200 sayfa image'i tutar; o belge-başı tavan THotPDF tarafından sabitlenmiştir ve yayımlanmış bir property değildir

RenderCacheCapacity (varsayılan 8) ayrı bir düğmedir: bellek içi katmanın kaç render edilmiş sayfa tutacağını set eder ve disk ayak iziyle hiçbir ilgisi yoktur

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // İlk önbellekli render'dan önce disk katmanını yapılandır:
    // klasör ve iki sınır, katman ilk kullanıldığında okunur
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // bellekteki sayfalar

    if Pdf.LoadFromFile(FileName) > 0 then
      for I := 0 to Pdf.LoadedPageCount - 1 do
      begin
        Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
        if Bmp <> nil then
        try
          // Kopyayı burada thumbnail şeridine devredin
        finally
          Bmp.Free; // önbellekli çağrı daima çağıranın malı bir kopya döndürür
        end;
      end;
  finally
    Pdf.Free; // v2.770.140'ten beri bu artık disk girdilerini silmiyor
  end;
end;

Aynı prosedürü iki kez koşturun; ikinci çalıştırma cache'e sığan hiçbir sayfayı rasterize etmez. Disk cache objecti, ilk önbellekli render'da tembelce kurulur ve THotPDF instance'ı free edilene dek yaşar; dolayısıyla RenderCacheFolder, RenderCacheMaxDocuments ya da RenderCacheMaxBytes'i o noktadan sonra değiştirmek, hâlihazırda açık bir cache'i taşımaz ya da yeniden boyutlamaz. Bellek içi kabul politikası için fazla büyük sayfalar (varsayılanla tek bir girdi 32 bitlik pixel cinsinden 64 MiB'yi aşamaz) kalıcılaştırılmaz ve disk katmanına yalnızca RenderFallbackPolicy varsayılan rfpIgnore'unu korurken başvurulur; çünkü fallback teşhisleri PNG'nin yanında saklanmaz

RenderCacheFolder v2.770.140'ten önce neden hiç çalışmadı?

RenderCacheFolder v2.770.140'ten önce etkisizdi, çünkü disk katmanı belgeleri sıradan yüklemelerin hiç saklamadığı bir kaynak-baytı hash'ine göre anahtarlıyordu. Belge anahtarı, ham PDF baytlarının dâhilî bir kopyası üzerinden SHA-256'dan geliyordu ama LoadFromFile ile LoadFromStream kaynağı yerinde ayrıştırır ve böyle bir kopyayı tutmaz; alan yalnızca şifreli-kurtarma yolunda geçici olarak dolduruluyor ve hemen ardından temizleniyordu. Bayt olmayınca anahtar hep boştu ve boş anahtar, disk katmanının bypass edildiği demektir. Ne hata ne uyarı; sadece boş kalan bir klasör

Anahtarı boş olmaktan çıkarmak, ilkinin arkasına saklanmış ikinci bir hatayı açığa çıkardı. Eski InvalidateRenderedPageCache, belgenin disk klasörünü siliyordu ve InvalidateRenderedPageCache her yükleme başında, her düzenlemede ve Free içinde koşar. Anahtar çalıştığı ana, her görüntüleyici oturumu çıkışta kendi cache'ini yok ederdi ve sonraki oturum yine de soğuk başlardı. Daha kötüsü, anahtar bir düzenlemeden sonra aynı kaynaktan yeniden hesaplanıyordu; böylece düzenlenmiş belgenin render'ları özgün dosyanın anahtarı altında saklanacak ve değişmemiş PDF'i açan sonraki oturuma sunulacaktı. v2.770.140 kimliği ile geçersiz kılmayı birlikte düzeltti; yalnızca birini düzeltmek ya ölü bir cache ya yalancı bir cache çıkaracaktı

HotPDF bütün dosyayı okumadan bir PDF'i nasıl tanımlar?

HotPDF, yerel dosyadan yüklenen bir PDF'i boyutunun, son yazma zamanının ve ilk ile son 64 KiB'sinin parmak iziyle tanımlar; stream ya da random-access kaynağını ise bütün içeriğinin SHA-256'sıyla. İkisi de bir yükleme başarılı olduğunda bir kez yakalanır ve SHA-256 digest'inin ilk 16 onaltılık karakteri (64 bit) belge anahtarı olur

KaynakKimlikMaliyetNe zaman yakalanır
LoadFromFileBoyut + LastWriteTime + baştaki ve sondaki 64 KiB, SHA-256 ile hash'lenirEn çok 128 KiB okuma, dosya boyutundan bağımsızHer başarılı yüklemede, RenderCacheFolder sonra set edilse bile
LoadFromStreamBütün stream'in SHA-256'sıKaynak üzerinde tek tam geçişYalnızca RenderCacheFolder yüklemeden önce set edilmişse
LoadFromRandomAccessSourceBütün kaynağın SHA-256'sıKaynak üzerinde tek tam geçişYalnızca klasör önce set edilmişse ve bütün aralık kullanılabilirse
/Encrypt girdisi taşıyan her kaynakYokYokAsla; disk katmanı bypass edilir
Disk render cache'i için HotPDF kaynak kimlik haritası: LoadFromFile boyutu, LastWriteTime'ı ve ilk ile son 64 KiB'yi hash'ler; LoadFromStream ile LoadFromRandomAccessSource bütün içeriği yalnızca RenderCacheFolder önce set edildiğinde hash'ler ve /Encrypt trailer'ı taşıyan hiçbir kaynak kimlik yakalamaz
dosyalar uçlarından parmak izlenir, çünkü header, xref ve trailer orada durur; stream'ler tam hash'in bedelini yalnızca cache'i önce istediğinizde öder ve şifreli belgeler diske asla yazılmaz

Dosya parmak izi kasıtlı bir takastır. 400 MBlik taranmış bir arşivi her açılışta baştan sona hash'lemek, kullanıcının gerçekten baktığı iki sayfayı render etmekten pahalıya gelebilir. Örneklenen bölgeler rastgele değildir: header dosyanın başında, trailer ile son cross-reference bölümü ise sonunda durur (ISO 32000-1 §7.5). Bir incremental update yeni bir gövde, cross-reference bölümü ve trailer ekler (§7.5.6); böylece boyutu ve kuyruğu aynı anda değiştirir. Sıradan bir aracın yaptığı tam yeniden yazım son yazma zamanını değiştirir. 128 KiB'ye kadar dosyalarda iki örnek her baytı kapsar; küçük belgeler fiilen baştan sona hash'lenir

Artık risk, büyük bir dosyanın ortasında aynı boyutta yerinde yapılan ve ardından özgün zaman damgasını geri koyan bir değişikliktir. Bu, içeriği düzenlerken değişiklik zamanlarını kasıtlı koruyan bir araç gerektirir; nadirdir ama imkânsız değildir ve o durumda cache bayat sayfalar sunar. Öteki yüzü iyiliktedir: Windows'ta bir dosyayı kopyalamak normalde son yazma zamanını korur; dolayısıyla cache'te hâlihazırda duran bir belgenin kopyası aynı girdilere çarpar ve baytlar özdeş olduğu için bu da doğrudur

Stream'lerin değişiklik zamanı hiç yoktur; dolayısıyla dürüst tek kimlik içeriğin kendisidir. HotPDF o tam SHA-256 geçişinin bedelini yalnızca yüklemeden önce disk cache istemişseniz öder; LoadFromStream'in diğer her çağıranı fazladan hiçbir maliyet görmez. Bu da property atama sırasını yük taşıyan kılar:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Stream'ler için yanlış sıra: içerik hash'i yalnızca klasör hâlihazırda
  // setliyken hesaplanır; bu belge disk katmanını bypass ederdi
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

  Pdf.RenderCacheFolder := CacheRoot; // önce set edin
  Data.Position := 0;
  if Pdf.LoadFromStream(Data) <= 0 then
    raise Exception.Create('The stream is not a loadable PDF');
end;

Hâlâ inen bir random-access kaynak (bazı aralıklar henüz kullanılabilir değil) kısmi içeriğin hash'i yerine kimlik alamaz; ve kimliği hesaplamak herhangi bir nedenle başarısız olursa yükleme yine de başarılı olur, belge disk katmanı olmadan render eder

Bir HotPDF disk cache girdisini ne geçersiz kılar?

Bir HotPDF disk cache girdisi, düzenlemede silinerek asla geçersiz kılınmaz; onun yerine yüklenen belgeyi düzenlemek belge kimliğini düşürür, böylece disk katmanı o yüklemenin geri kalanı için bypass edilir ve saklanan sayfalar değişmemiş kaynak için geçerli kalır. Girdiler diskten yalnızca LRU ile bayt sınırlarından, bozuk bir PNG'den ya da schema değişiminden ayrılır

Anahtar, bellekteki object grafiğini değil diskteki bir kaynağı tanımlar. Bir sayfaya damga bastığınız ya da bir annotation'ı değiştirdiğiniz anda belge o kaynağa artık uymaz; dolayısıyla ne onun anahtarı altında okumak ne yazmak doğru olurdu. v2.770.140'tan beri belge düzeyi ile sayfa düzeyi geçersiz kılma, klasöre dokunmak yerine kimliği temizler ve InvalidateRenderedPageCache çağırmamış düzenlemeler için ikinci bir koruma vardır: disk katmanını kullanmadan önce THotPDF, yüklenmiş herhangi bir objectin kirli olup olmadığına bakar ve kirli belgeyi kimliksiz sayar

Render ayarları ters yönde işler. PageRenderBackend'i değiştirmek (ya da UseNativeGDIRenderBackend çağırmak) ve ConfigureRenderICCWorkflow ya da ClearRenderICCWorkflow çağırmak, bellek içi sayfaları boşaltır ama kimliği tutar; çünkü belge hâlâ kaynağına uymaktadır. O ayarlar, bellek içi varyantın parçası olmadan pixel'leri değiştirir; dolayısıyla disk anahtarı backend adını, black-point compensation bayrağını ve ICC proof ile output profillerinin SHA-256 digest'lerini içine katlar. Varyantın kendisi renk niyetini, çıktı dithering'ini, overprint önizlemesini, luminosity mask modunu, fallback politikasını ve her isteğe bağlı content grubunun görünürlüğünü zaten kapsar; böylece bir katmanı açıp kapatmak varsayılan görünümün üzerine yazmak yerine farklı bir klasöre render eder

RenderCacheFolder disk cache'i için HotPDF geçersiz kılma semantiği: yüklenen belgeyi ya da kirli bir objecti düzenlemek kaynak kimliğini düşürür ve katman bypass edilir; render backend ya da ICC workflow değişikliği kimliği yeni bir varyant anahtarı altında tutar; kaydetmek ve yeniden yüklemek ise belgeyi yeni anahtara taşır
bir düzenleme saklanan klasörü asla silmez, bir ayar değişikliği farklı bir anahtar altında render eder ve yalnızca kaydetme artı yeniden yükleme, düzenlenmiş belgeye taze bir kimlik kazandırır

Düzenlenmiş bir belgeyi disk katmanına geri getirmek için onu kaydedip sonucu yükleyerek yeni bir kaynak kimliği verin:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Yüklenen belgeyi düzenledikten sonra: bellek içi sayfaları tazele.
  // Kaynak kimliği çoktan gitmiştir; özgün belgenin disk klasöründen
  // hiçbir şey okunmaz ya da oraya hiçbir şey yazılmaz
  Pdf.InvalidateRenderedPageCache;

  // Kaydedilmiş dosyanın yeni bir boyutu ve son-yazma zamanı vardır, yani
  // yeni bir kimlik; bu yüklemeden sonraki render'lar yeni anahtar altında önbelleklenir
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Özgün belgenin klasörü rahat bırakılır ve diğer her girdi gibi RenderCacheMaxDocuments ile RenderCacheMaxBytes üzerinden eskir. Kullanıcı düzenlenmemiş özgünü yeniden açarsa sayfaları hâlâ oradadır

Güvenlik sınırları: şifreli kaynaklar ve bağlantılı klasörler

HotPDF disk render cache'i iki tür girdiyi kasıtlı olarak reddeder: şifreli bir PDF'in sayfalarını asla diske yazmaz ve junction ya da başka bir reparse point olan bir belge alt klasörünü asla izlemez. Her iki kural da veri sızdırmamak ya da yanlış dosyaları silmemek için cache isabetlerinden feragat eder

Şifreli PDF'ler diskte asla cache'lenmez

Render edilmiş bir sayfa, çözülmüş içeriktir. Onu sade bir PNG olarak bir cache klasörüne yazmak, parola korumalı bir belgenin okunabilir bir kopyasını diske, yazarın seçtiği korumanın dışına bırakır (ISO 32000-1 §7.6). Bu yüzden HotPDF, trailer'ında /Encrypt girdisi taşıyan hiçbir kaynak için kimlik yakalamaz; parolayla ya da boş kullanıcı parolasıyla açılmış dosyalar dahil. O belgeler bellek içi katmanı kullanmaya devam eder; o da süreçle birlikte ölür

Junction alt klasörleri v2.770.173'ten beri reddedilir

Cache kökü sizin seçiminizdir ve onu bir junction'a işaretlemek serbesttir. Altındaki belge alt klasörleri ise başka bir meseledir: cache onları kendi başına kurar, okur, dokunur ve siler — başlangıç kurtarması sırasında (artık geçici dosyaları kaldıran), arama sırasında (zaman damgalarını güncelleyen), saklama, geçersiz kılma ve üç tahliye sınırı boyunca. Cache köküne yazma erişimi olan biri bir belge klasörünü başka bir dizine giden bir junction'la değiştirse o yolların her biri onu izler ve tahliye, cache'in hiç sahibi olmadığı bir yerdeki dosyaları silerdi. v2.770.173'ten beri o giriş noktalarının her biri reparse-point attribute'ını denetler ve bağlantılı bir belge klasörünü atlar: bir arama ıskalama sayar, bir saklama yazım başarısızlığı sayar ve tahliye onu rahat bırakır

Unicode yollar ve paylaşılan kökler

Kullanıcı profillerine dağıtıyorsanız iki ilişkili düzeltme önemlidir. v2.770.135 öncesinde RenderCacheFolder bir AnsiString'ti; dolayısıyla sistem kod sayfası dışındaki bir klasör (İngilizce bir Windows kurulumunda Çince bir kullanıcı adı gibi) cache onu görmeden kayıplı çevriliyordu; property artık Unicode bir string ve atomik replace geniş Windows API'sini kullanıyor. v2.770.52'den beri aynı kökü işaret eden tek süreçteki birkaç THotPDF instance'ı (path genişlemesinden sonra, büyük-küçük harf duyarsız karşılaştırmayla) tek bir referans sayan indeks ve kilit paylaşır. Öncesinde her instance index.txt'i kendi kopyasıyla üzerine yazıyor ve sınırları kendi kısmi görünümüne göre uyguluyordu; klasör bütçesini birkaç kez aşabiliyordu

O paylaşım süreç sınırında durur. Aynı kökteki iki ayrı süreç hâlâ ayrı bellek içi indeksler tutar; dolayısıyla eş zamanlı koşan her uygulamaya kendi cache kökünü verin. İşçi thread'lerinde render eden görüntüleyiciler tek süreç içinde sorun yaşamaz: PrefetchLoadedPages ile istek kuyruğuyla arka planda renderyazısındaki kuyruk ikisi de aynı önbellekli yoldan ve aynı kilitten geçer

Hızlı başvuru: RenderCacheFolder kontrol listesi

  • RenderCacheFolder, RenderCacheMaxDocuments ve RenderCacheMaxBytes'i ilk RenderLoadedPageToBitmapCached çağrısından önce set edin; stream ve random-access yüklemelerinde klasörü yüklemeden önce set edin
  • Disk katmanına dayanıyorsanız v2.770.140 ve sonrasına yükseltin; önceki sürümler property'yi kabul eder ama sıradan yüklemelerde diske hiç sayfa sunmaz
  • Şifreli PDF'ler için, yüklemeden sonra düzenlenmiş belgeler için ya da RenderFallbackPolicy rfpIgnore değilken disk cache beklemez
  • THotPDF instance'ını normal biçimde free edin; v2.770.140'tan beri ne Free ne InvalidateRenderedPageCache disk girdilerini siler
  • PageRenderBackend'i ya da ICC workflow'unu değiştirmek, belgeyi farklı bir anahtar altında disk katmanında tutar
  • Koşan uygulama başına tek cache kökü kullanın; tek süreçteki instance'lar v2.770.52'den beri indeksi paylaşır
  • Cache kökünü kullanıcı-başı bir konumda tutun; junction olan belge alt klasörleri v2.770.173'ten beri atlanır

Kalıcı bir page cache, aynı belgeleri bütün gün yeniden açan bir görüntüleyicide en çok karşılığını verir; bu blogda başka bir yerde anlatılan Delphi'de özel PDF görüntüleyici mimarisi tam olarak o biçimdedir. RenderCacheFolder, bellek içi raster cache ve page renderer, Delphi ve C++Builder için HotPDF Delphi PDF component ile gelir