Technický článek

HotPDF RenderCacheFolder: disková cache stránek v Delphi

HotPDF RenderCacheFolder mění in-memory cache vyrenderovaných stránek komponenty HotPDF pro Delphi v trvalou diskovou cache stránek: vyrenderované stránky se zapisují jako PNG soubory do složky, kterou si zvolíte, a až příště otevřete tentýž PDF zdroj, přečte je RenderLoadedPageToBitmapCached místo opětovné rasterizace. Pořadí hledání je paměť, pak disk, pak renderer

Disková vrstva je v API od v2.416.0, ale do v2.770.140 nikdy skutečně nepodala stránku pro běžné volání LoadFromFile nebo LoadFromStream. Oprava vynutila otázku, na kterou si musí odpovědět každá trvalá cache: odkud víte, že soubor, který dnes otevřete, je dokument, který jste včera renderovali, a co se stane s cachovanými stránkami, když není? Níže jsou odpovědi, na kterých HotPDF ustál, včetně míst, kde se cachovat záměrně odmítá

Jak funguje disková render cache HotPDF?

Disková render cache HotPDF je druhá vrstva za in-memory raster cache a účastní se jen tehdy, když RenderCacheFolder je neprázdná cesta. Volání RenderLoadedPageToBitmapCached(PageIndex, DPI) nejdřív prohledá in-memory položky, klíčované indexem stránky, DPI a variantou renderovacích nastavení. Při mimo jde se na diskovou vrstvu; zásah na disku dekóduje PNG, vrací ho zpátky do paměti a podá kopii vlastněnou volajícím. Jen když obě vrstvy minou, jde stránka přes interpret content streamů popsaný v renderování načtené PDF stránky do TBitmap a čerstvá bitmapa se pak zapisuje i na disk

Diagram HotPDF hledání v render cache pro RenderLoadedPageToBitmapCached: in-memory vrstva klíčovaná stránkou, DPI a render variantou se kontroluje první, pak disková vrstva RenderCacheFolder s PNG soubory a atomickou náhradou, pak interpret content streamů a každý zásah vrací kopii vlastněnou volajícím
HotPDF se dívá nejdřív do paměti, pak na disk a až potom rasterizuje; zásah na disku se vrací do paměti a každá cesta vám podá kopii, kterou vlastníte a musíte uvolnit

Na disku je rozložení záměrně nudné. Každý dokument dostává podsložku pojmenovanou z 16 znaků v hex dokumentového klíče plus 16 znaků v hex render varianty, každá stránka se ukládá jako <page>@<dpi>.png a index.txt v kořeni drží dokumenty v pořadí naposledy použitých za schema tagem. Neshoda schematu vymaže složku při prvním použití. Zápisy jdou nejdřív do dočasného souboru a na místo se vymění atomicky, takže pád uprostřed zápisu zanechá buď starou stránku, nebo nic, nikdy polovinu PNG. PNG, které se nedá dekódovat, se smaže a počítá jako míno

Složku ohraničují tři limity:

  • RenderCacheMaxDocuments (default 20) omezuje počet podsložek dokumentů; nejdřív se vystěhovává nejdéle nepoužitá složka
  • RenderCacheMaxBytes (default 524288000, tedy 500 MB) omezuje celkovou velikost všech PNG souborů pod kořenem
  • Každá složka dokumentu drží nejvýš 200 obrázků stránek; tenhle limit na dokument je daný THotPDF a není publikovanou vlastností

RenderCacheCapacity (default 8) je samostatný knob: nastavuje, kolik vyrenderovaných stránek drží in-memory vrstva, a s diskovou stopou nemá nic společného

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Nakonfigurujte diskovou vrstvu před prvním cachovaným renderem:
    // složka a oba limity se čtou, když se vrstva poprvé použije
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // stránky v paměti

    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
          // Kopii tady předejte liště miniatur
        finally
          Bmp.Free; // cachované volání vždy vrací kopii vlastněnou volajícím
        end;
      end;
  finally
    Pdf.Free; // od v2.770.140 to už nemaže diskové položky
  end;
end;

Spusťte tutéž proceduru dvakrát a druhý průchod už nerasterizuje stránku, která se do cache vešla. Objekt diskové cache vzniká lazy při prvním cachovaném renderu a žije, dokud se neuvolní instance THotPDF, takže změna RenderCacheFolder, RenderCacheMaxDocuments nebo RenderCacheMaxBytes po tom okamžiku už přesune ani nezmenší otevřenou cache. Stránky příliš velké pro in-memory přijímací politiku (default nesmí jedna položka přesáhnout 64 MiB 32bitových pixelů) se neukládají taky a disková vrstva se konzultuje jen, dokud RenderFallbackPolicy drží svůj default rfpIgnore, protože diagnostika fallbacku se vedle PNG neukládá

Proč RenderCacheFolder před v2.770.140 nikdy nefungovala?

RenderCacheFolder před v2.770.140 neměla žádný efekt, protože disková vrstva klíčovala dokumenty hashem zdrojových bajtů, který běžná načtení nikdy nedržela. Dokumentový klíč pocházel ze SHA-256 nad interní kopií syrových PDF bajtů, ale LoadFromFile a LoadFromStream parsují zdroj na místě a takovou kopii nenechávají; pole se plnilo jen dočasně na cestě šifrované záchrany a hned potom se zase vymazalo. Bez bajtů byl klíč vždycky prázdný a prázdný klíč znamená, že se disková vrstva obchází. Žádná chyba, žádné varování, jen složka, která zůstávala prázdná

Zprůchodnění klíče vynalo druhý bug, který se dřív kryl za tím prvním. Staré InvalidateRenderedPageCache mazalo diskovou složku dokumentu a InvalidateRenderedPageCache běží na začátku každého načtení, při každé editaci a uvnitř Free. V okamžiku, kdy by klíč fungoval, by každá relace prohlížeče zničila vlastní cache při ukončení a další relace by stejně startovala studená. Hůř: klíč se po editaci počítal znovu ze stejného zdroje, takže rendery editovaného dokumentu by se uložily pod klíčem původního souboru a podaly by se další relaci, která otevřela nezměněné PDF. v2.770.140 opravuje identitu i zneplatnění naráz; oprava jen jedné z těch věcí by vyvezla buď mrtvou cache, nebo lživou

Jak HotPDF identifikuje PDF, aniž by četl celý soubor

HotPDF identifikuje PDF načtené z lokálního souboru otiskem své velikosti, času posledního zápisu a svých prvních a posledních 64 KiB a stream nebo random-access zdroj identifikuje SHA-256 svého celého obsahu. Oboje se zachytí jednou, když načtení uspěje, a prvních 16 hex znaků digestu SHA-256 (64 bitů) se stává dokumentovým klíčem

ZdrojIdentitaCenaKdy se zachytí
LoadFromFileVelikost + LastWriteTime + úvodních a závěrečných 64 KiB, hashované SHA-256Nejvýš čtení 128 KiB, nezávislé na velikosti souboruKaždé úspěšné načtení, i když se RenderCacheFolder nastaví později
LoadFromStreamSHA-256 celého streamuJeden plný průchod zdrojemJen když byl RenderCacheFolder nastaven před načtením
LoadFromRandomAccessSourceSHA-256 celého zdrojeJeden plný průchod zdrojemJen když byla složka nastavená první a celý rozsah je dostupný
Jakýkoli zdroj s položkou /EncryptŽádnáŽádnáNikdy; disková vrstva se obchází
Mapa identity zdroje pro diskovou render cache HotPDF: LoadFromFile hashuje velikost, LastWriteTime a prvních a posledních 64 KiB, LoadFromStream a LoadFromRandomAccessSource hashují celý obsah jen tehdy, když byl RenderCacheFolder nastavený první, a jakýkoli trailer s /Encrypt nezachytí žádnou identitu
soubory se otiskují od svých konců, protože tam bydlí hlavička, xref a trailer, streamy platí za plný hash jen tehdy, když jste si cache vyžádali nejdřív, a šifrované dokumenty se na disk nikdy nezapisují

Otisk souboru je záměrný kompromis. Hashovat 400MB naskenovaný archiv celý při každém otevření může stát víc než vyrenderovat ty dvě stránky, které si uživatel skutečně prohlédne. Vzorkované regiony nejsou libovolné: hlavička sedí na začátku souboru a trailer a poslední cross-reference sekce sedí na konci (ISO 32000-1 §7.5). Inkrementální aktualizace připojuje nové tělo, cross-reference sekci a trailer (§7.5.6), takže změní velikost i ocas naráz. Úplný přepis jakýmkoli normálním nástrojem změní čas posledního zápisu. U souborů do 128 KiB pokrývají oba vzorky každý bajt, takže malé dokumenty se efektivně hashují celé

Reziduální riziko je změna stejné velikosti na místě uprostřed velkého souboru, jejíž zapisovač pak obnoví původní časovou značku. To chce nástroj, který záměrně zachovává časy modifikace při editaci obsahu — vzácné, ale ne nemožné — a v takovém případě podá cache zastaralé stránky. Druhá strana mince je neškodná: zkopírování souboru ve Windows normálně zachovává čas posledního zápisu, takže kopie dokumentu už v cache zasáhne tytéž položky, což je správně, protože bajty jsou identické

Streamy nemají čas modifikace vůbec, takže jediná upřímná identita je obsah. HotPDF platí za ten plný průchod SHA-256 jen tehdy, když jste si diskovou cache vyžádali před načtením; každý jiný volající LoadFromStream žádnou přirážku nevidí. Tím se pořadí přiřazování vlastností stává nosným:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Pro streamy špatné pořadí: hash obsahu se počítá jen tehdy, když je
  // složka už nastavená, takže tenhle dokument by diskovou vrstvu obešel
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

  Pdf.RenderCacheFolder := CacheRoot; // nastaví se první
  Data.Position := 0;
  if Pdf.LoadFromStream(Data) <= 0 then
    raise Exception.Create('The stream is not a loadable PDF');
end;

Random-access zdroj, který se teprve stahuje (některé rozsahy ještě nejsou dostupné), nedostane žádnou identitu místo hashe částečného obsahu a pokud výpočet identity z jakéhokoli důvodu selže, načtení stejně uspěje; dokument se prostě vyrenderuje bez diskové vrstvy

Co ruší položku diskové cache HotPDF?

Položka diskové cache HotPDF se nikdy nezneplatní smazáním při editaci; místo toho editace načteného dokumentu upustí identitu dokumentu, takže disková vrstva se pro zbytek toho načtení obchází a uložené stránky zůstávají validní pro nezměněný zdroj. Z disku odcházejí položky jen přes LRU a bajtové limity, poškozené PNG nebo změnu schematu

Klíč popisuje zdroj na disku, ne graf objektů v paměti. Jakmile na stránku něco zapíšete nebo změníte anotaci, dokument už tomuto zdroji neodpovídá, takže ani čtení, ani zápis pod jeho klíčem by nebyl správný. Od v2.770.140 zneplatnění na úrovni dokumentu i stránky maže identitu místo sahání na složku a existuje druhá pojistka pro editace, které InvalidateRenderedPageCache nevolaly: před použitím diskové vrstvy THotPDF zkontroluje, zda není některý načtený objekt dirty, a dirty dokument bere jako bez identity

Renderovací nastavení fungují obráceně. Přepnutí PageRenderBackend (nebo volání UseNativeGDIRenderBackend) a volání ConfigureRenderICCWorkflow nebo ClearRenderICCWorkflow spláchnou in-memory stránky, ale identitu ponechají, protože dokument stále odpovídá svému zdroji. Ta nastavení mění pixely, aniž by byla součástí in-memory varianty, takže diskový klíč do sebe skládá název backendu, příznak black-point kompenzace a SHA-256 digesty ICC proof a output profilů. Samotná varianta už pokrývá barevný záměr, výstupní dithering, náhled overprintu, režim luminosity masky, fallback politiku a viditelnost každé skupiny optional content, takže přepnutí vrstvy renderuje do jiné složky místo přepsání výchozího pohledu

Sémantika zneplatnění diskové cache RenderCacheFolder v HotPDF: editace načteného dokumentu nebo jakéhokoli dirty objektu upustí identitu zdroje, takže se vrstva obchází, změna render backendu nebo ICC workflow ponechá identitu pod novým klíčem varianty a uložení plus opětovné načtení dokumentu převezme na nový klíč
editace nikdy nemaže uloženou složku, změna nastavení renderuje pod jiným klíčem a čerstvou identitu si editovaný dokument vyslouží jen uložením a opětovným načtením

Aby se editovaný dokument vrátil na diskovou vrstvu, dejte mu novou identitu zdroje uložením a načtením výsledku:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Po editaci načteného dokumentu: obnovte in-memory stránky.
  // Identita zdroje už je pryč, takže se nic nečte z diskové složky
  // původního dokumentu ani do ní nezapisuje
  Pdf.InvalidateRenderedPageCache;

  // Uložený soubor má novou velikost a čas posledního zápisu, a tudíž novou
  // identitu; rendery po tomhle načtení se cachují pod novým klíčem
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Složka původního dokumentu se nechá být a odevzdává se přes RenderCacheMaxDocuments a RenderCacheMaxBytes jako jakákoli jiná položka. Pokud uživatel znovu otevře needitovaný originál, jeho stránky tam pořád jsou

Bezpečnostní hranice: šifrované zdroje a odkazované složky

Disková render cache HotPDF záměrně odmítá dva druhy vstupu: nikdy nezapisuje stránky šifrovaného PDF na disk a nikdy nenásleduje podsložku dokumentu, která je junction nebo jiný reparse point. Obě pravidla směňují zásahy cache za to, že netečou data a nemažou špatné soubory

Šifrované PDF se na disk nikdy necachují

Vyrenderovaná stránka je dešifrovaný obsah. Zapsat ji jako obyčejné PNG do cache složky by zanechalo čitelnou kopii dokumentu chráněného heslem na disku, mimo ochranu, kterou si autor vybral (ISO 32000-1 §7.6). HotPDF proto nezachytává identitu pro žádný zdroj, jehož trailer nese položku /Encrypt, včetně souborů otevřených s heslem nebo s prázdným user heslem. Ty dokumenty pořád používají in-memory vrstvu, která umírá s procesem

Junction podsložky se od v2.770.173 odmítají

Kořen cache je vaše volba a nasměrovat ho na junction je povoleno. Podsložky dokumentů pod ním jsou jiná káva: cache je sama vytváří, čte, dotýká se jich a maže, během startupové záchrany (která odstraňuje zbylé dočasné soubory), hledání (které aktualizuje časové značky), ukládání, zneplatnění a tří vystěhovávacích limitů. Kdyby někdo s právem zápisu do kořene cache vyměnil složku dokumentu za junction na jiný adresář, sledovala by ji každá z těch cest a vystěhování by mazalo soubory někde, co cache nikdy nevlastnila. Od v2.770.173 každý z těch vstupních bodů kontroluje atribut reparse point a propojenou složku dokumentu přeskočí: hledání počítá míno, ukládání počítá selhání zápisu a vystěhování ji nechá být

Unicode cesty a sdílené kořeny

Dvě související opravy mají význam, když nasazujete do uživatelských profilů. Před v2.770.135 bylo RenderCacheFolder AnsiString, takže složka mimo systémovou kódovou stránku (čínské uživatelské jméno na anglické instalaci Windows) se konvertovala se ztrátou, než ji cache uviděla; vlastnost je teď Unicode string a atomická náhrada používá wide Windows API. Od v2.770.52 sdílí několik instancí THotPDF v jednom procesu ukazujících na tentýž kořen (po rozvinutí cesty, porovnávaném case-insensitive) jediný index počítaný referencemi a jeden zámek. Dřív si každá instance přepisovala index.txt vlastní kopií a vynucovala limity proti svému částečnému pohledu, takže složka mohla vyrůst několikanásobně za svůj rozpočet

Tohle sdílení končí na hranici procesu. Dva oddělené procesy na stejném kořeni pořád drží oddělené in-memory indexy, takže dejte každé souběžně běžící aplikaci vlastní kořen cache. Prohlížeče renderující na worker threadech jsou v pořádku v jednom procesu: PrefetchLoadedPages i fronta popsaná v renderování na pozadí s frontou požadavků jdou přes tutéž cachovanou cestu a tentýž zámek

Rychlá reference: kontrolní seznam RenderCacheFolder

  • Nastavte RenderCacheFolder, RenderCacheMaxDocuments a RenderCacheMaxBytes před prvním voláním RenderLoadedPageToBitmapCached; u načtení ze streamu a random-access zdroje nastavte složku před načtením
  • Přejděte na v2.770.140 a novější, pokud na diskové vrstvě závisíte; starší verze vlastnost přijmou, ale běžným načtením nikdy nepodají stránku z disku
  • Počítejte s tím, že se necachuje na disku u šifrovaných PDF, u dokumentů editovaných po načtení a dokud RenderFallbackPolicy není rfpIgnore
  • Uvolňujte instanci THotPDF normálně; od v2.770.140 nemaže diskové položky ani Free, ani InvalidateRenderedPageCache
  • Změna PageRenderBackend nebo ICC workflow ponechá dokument na diskové vrstvě pod jiným klíčem
  • Používejte jeden kořen cache na běžící aplikaci; instance v jednom procesu sdílejí index od v2.770.52
  • Držte kořen cache v lokaci na uživatele; podsložky dokumentů, které jsou junctions, se od v2.770.173 přeskakují

Trvalá cache stránek se vyplácí nejvíc v prohlížeči, který celý den znovu otevírá tytéž dokumenty — přesně ten tvar má architektura vlastního PDF prohlížeče v Delphi popsaná jinde na tomhle blogu. RenderCacheFolder, in-memory raster cache i renderer stránek lodí se s komponentou HotPDF Delphi PDF pro Delphi a C++Builder