Műszaki cikk

HotPDF RenderCacheFolder: lemezes oldal gyorsítótár

A HotPDF RenderCacheFolder a HotPDF Delphi component memóriabeli, renderelt oldalakat tároló gyorsítótárát perzisztens lemezes oldal-gyorsítótárrá alakítja: a renderelt oldalak PNG fájlokként íródnak egy általad választott mappába, és amikor legközelebb ugyanaz a PDF forrás nyílik meg, a RenderLoadedPageToBitmapCached visszaolvassa őket ahelyett, hogy újra raszterezne. A keresési sorrend: memória, aztán lemez, aztán a renderer

A lemezes szint v2.416.0 óta ott van az API-ban, de v2.770.140-ig egy normál LoadFromFile vagy LoadFromStream hívás számára sosem szolgált ki ténylegesen oldalt. A javítás felvetette a kérdést, amire minden perzisztens gyorsítótárnak válaszolnia kell: honnan tudod, hogy a ma megnyitott fájl az a dokumentum, amit tegnap rendereltél, és mi történik a kapcsolt oldalakkal, ha nem az? Az alábbiakban azok a válaszok állnak, amiken a HotPDF megállapodott, beleszámítva, hol utasítja el szándékosan a kapcsolást

Hogyan működik a HotPDF lemezes render gyorsítótár?

A HotPDF lemezes render gyorsítótára a memóriabeli rasztergyorsítótár mögötti második szint, és csak akkor vesz részt a játékban, ha a RenderCacheFolder nem üres elérési út. Egy RenderLoadedPageToBitmapCached(PageIndex, DPI) hívás előbb a memóriabeli bejegyzéseket nézi át, amik oldalindex, DPI és egy render-beállításvariáns szerint kulcsoltak. Találatlan esetén a lemezes szintet kérdezi; egy lemezes találat dekódolja a PNG-t, visszateszi a memóriába és hívó tulajdonú másolatot ad. Csak ha mindkét szint találatlan, megy át az oldal a betöltött PDF oldal TBitmapra renderelését taglaló cikk tartalomfolyam-értelmezőjén, és a friss bitmap szintén lemezre íródik

HotPDF ábra a RenderLoadedPageToBitmapCached render gyorsítótár-kereséséről: előbb a memóriabeli szint jön, oldal, DPI és render variáns szerint kulcsolva, aztán a RenderCacheFolder lemezes szintje atomi cserél PNG fájljaival, aztán a tartalomfolyam-értelmező, és minden találat hívó tulajdonú másolatot ad
A HotPDF előbb a memóriában, aztán a lemezen keres, és csak utána raszterez; egy lemezes találat visszakerül a memóriába, és minden út olyan másolatot ad a kezedbe, ami a tiéd, és amit neked kell felszabadítanod

Lemezen az elrendezés szándékosan unalmas. Minden dokumentum kap egy alszönyvtárt egy 16 hexakarakteres dokumentumkulcsból és egy 16 hexakarakteres render variánsból, minden oldal <page>@<dpi>.png néven tárolódik, és a gyökérben egy index.txt tartja a dokumentumokat legutóbb-használt sorrendben, egy schema tag mögött. Sémaeltérés első használatkor törli a mappát. Az írások előbb egy ideiglenes fájlba mennek, és atomi cserével kerülnek a helyükre, így egy írás közbeni összeomlás vagy a régi oldalt hagyja, vagy semmit, sosem fél PNG-t. A dekódolni nem tudó PNG törlődik, és találatlannak számolódik

Három limit határolja be a mappát:

  • A RenderCacheMaxDocuments (alapértéken 20) korlátozza a dokumentumalszönyvtárak számát; a legrégebben használt mappa távozik először
  • A RenderCacheMaxBytes (alapértéken 524288000, ami 500 MB) korlátozza a gyökér alatti összes PNG fájl összméretét
  • Minden dokumentummappa legfeljebb 200 oldalképet tart; ezt az oldalankénti felső határt a THotPDF rögzíti, és nem publikált property

A RenderCacheCapacity (alapértéken 8) külön gomb: azt állítja, hány renderelt oldalt tart a memóriabeli szint, és a lemezes lábnyommal semmi köze nincs

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Állítsd be a lemezes szintet az első kapcsolt render előtt:
    // a mappát és mindkét limitet a szint első használatakor olvassa
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // oldalak a memóriában

    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
          // Itt add át a másolatot a thumbnail sávnak
        finally
          Bmp.Free; // a kapcsolt hívás mindig hívó tulajdonú másolatot ad
        end;
      end;
  finally
    Pdf.Free; // v2.770.140 óta ez már nem törli a lemezes bejegyzéseket
  end;
end;

Futtasd le ugyanazt az eljárást kétszer, és a második futás egyetlen, a gyorsítótárba férő oldalt sem raszterez újra. A lemezes gyorsítótár-objektum lustán születik az első kapcsolt rendernél, és a THotPDF példány felszabadításáig él, így a RenderCacheFolder, RenderCacheMaxDocuments vagy RenderCacheMaxBytes megváltoztatása ezután már nem mozgatja és nem méretezi át a nyitva lévő gyorsítótárat. Azok az oldalak, amik túl nagyok a memóriabeli felvételi politikához (alapértelmezés szerint egyetlen bejegyzés nem haladhatja meg a 32 bites pixelek 64 MiB-jét), szintén nem perzisztálódnak, és a lemezes szint csak addig kérdezett, amíg a RenderFallbackPolicy az alapértelmezett rfpIgnore-t tartja, mert a fallback diagnosztikák nem tárolódnak a PNG mellett

Miért nem működött sosem a RenderCacheFolder v2.770.140 előtt?

A RenderCacheFolder azért nem volt hatással v2.770.140 előtt, mert a lemezes szint a dokumentumokat a forrásbájtok hashéből kulcsolta, amit a hétköznapi betöltések sosem tartottak meg. A dokumentumkulcs a nyers PDF bájtok egy belső másolata felett számolt SHA-256-ból jött, de a LoadFromFile és a LoadFromStream a forrást helyben parzolja, és nem tart meg ilyen másolatot; a mező csak átmenetileg töltődött fel egy titkosított-helyreállítási úton, és rögtön utána törlődött is. Bájtok híján a kulcs mindig üres volt, az üres kulcs pedig azt jelenti, hogy a lemezes szint kikerül. Nincs hiba, nincs figyelmeztetés, csak egy üresen maradt mappa

A kulcs kitöltése előhozott egy második hibát, ami eddig az első mögött rejtőzött. A régi InvalidateRenderedPageCache törölte a dokumentum lemezes mappáját, az InvalidateRenderedPageCache pedig minden betöltés elején, minden szerkesztéskor és a Free-n belül is fut. Szóval abban a pillanatban, hogy a kulcs működött, minden nézegető munkamenet kilépéskor megsemmisítette volna a saját gyorsítótárát, és a következő munkamenet úgyis hidegen indult volna. Rosszabb: a kulcs egy szerkesztés után ugyanabból a forrásból számolódott újra, így a szerkesztett dokumentum renderjei az eredeti fájl kulcsa alatt tárolódtak volna, és a következő munkamenet, ami a módosítatlan PDF-et nyitja meg, ezeket kapta volna. A v2.770.140 az azonosságot és az érvénytelenítést együtt javítja; ha csak az egyiket javítják, vagy halott, vagy hazug gyorsítótár születik

Hogyan azonosítja a HotPDF a PDF-et a teljes fájl olvasása nélkül

A HotPDF egy helyi fájlból betöltött PDF-et a mérete, utolsó-írás ideje és első, utolsó 64 KiB-ja ujjlenyomatával azonosít, egy streamet vagy random-access forrást pedig a teljes tartalma SHA-256-ával. Mindkettő egyszer kapódik el, amikor egy betöltés sikerül, és a SHA-256 digest első 16 hexakaraktere (64 bit) lesz a dokumentumkulcs

ForrásAzonosságKöltségMikor kapódik el
LoadFromFileMéret + LastWriteTime + első és utolsó 64 KiB, SHA-256-gyal hashelveLegfeljebb 128 KiB olvasás, a fájlmérettől függetlenülMinden sikeres betöltés, akkor is, ha a RenderCacheFolder csak később állítódik
LoadFromStreamA teljes stream SHA-256-aEgy teljes átfutás a forrásonCsak ha a RenderCacheFolder a betöltés előtt állt be
LoadFromRandomAccessSourceA teljes forrás SHA-256-aEgy teljes átfutás a forrásonCsak ha a mappa előbb állt be, és a teljes tartomány elérhető
Bármely /Encrypt bejegyzéses forrásNincsNincsSoha; a lemezes szint kikerül
HotPDF forrásazonosság-térkép a lemezes render gyorsítótárhoz: a LoadFromFile a méretet, a LastWriteTime-ot és az első, utolsó 64 KiB-t hasheli, a LoadFromStream és a LoadFromRandomAccessSource csak akkor hasheli a teljes tartalmat, ha a RenderCacheFolder előbb állt be, és bármely /Encrypt trailer egyáltalán nem kap azonosságot
A fájlok a végeikről ujjlenyomatozódnak, mert a fej, az xref és a trailer ott lakik; a streamek csak akkor fizetnek teljes hashért, ha előbb kérted a gyorsítótárat, a titkosított dokumentumok pedig sosem íródnak lemezre

A fájlujjlenyomat tudatos kompromisszum. Egy 400 MB-os szkenelt archívum teljes hashelése minden megnyitáskor többe kerülhet, mint annak a két oldalnak a renderelése, amit a felhasználó tényleg megnéz. A mintavett régiók nem véletlenszerűek: a fej a fájl elején ül, a trailer és az utolsó kereszthivatkozási szakasz pedig a végén (ISO 32000-1 §7.5). Egy inkrementális frissítés új testet, kereszthivatkozási szakaszt és trailert fűz hozzá (§7.5.6), így egyszerre változtatja meg a méretet és a farkat. Egy rendes eszköz teljes újraírása megváltoztatja az utolsó-írás időt. 128 KiB-ig terjedő fájloknál a két minta minden bájtot lefedi, így a kis dokumentumok gyakorlatilag teljes egészében hashelődnek

A maradék kockázat egy azonos méretű, helyben végzett módosítás egy nagy fájl közepén, aminek az írója aztán visszaállítja az eredeti időbélyeget. Ehhez olyan eszköz kell, ami szándékosan őrzi a módosítási időket, miközben a tartalmat szerkeszti, ami ritka, de nem lehetetlen, és ekkor a gyorsítótár elavult oldalakat szolgál ki. A másik oldal ártalmatlan: egy fájl lemásolása Windowson normál esetben megőrzi az utolsó-írás időt, így egy már gyorsítótárban lévő dokumentum másolata ugyanazokat a bejegyzéseket találja el, ami helyes, mert a bájtok azonosak

A streameknek egyáltalán nincs módosítási idejük, így az egyetlen becsületes azonosság a tartalom. A HotPDF csak akkor fizet a teljes SHA-256 átfutásért, ha betöltés előtt kértél lemezes gyorsítótárat; a LoadFromStream minden más hívója nem lát pluszköltséget. Ezért hordozó teher a property-hozzárendelés sorrendje:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Rossz sorrend streameknél: a tartalomhash csak akkor számolódik, ha a
  // mappa már be van állítva, így ez a dokumentum átugraná a lemezes szintet
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

  Pdf.RenderCacheFolder := CacheRoot; // előbb állítsd be
  Data.Position := 0;
  if Pdf.LoadFromStream(Data) <= 0 then
    raise Exception.Create('The stream is not a loadable PDF');
end;

Egy még letöltés alatt álló random-access forrás (néhány tartomány még nem elérhető) nem kap azonosságot, még részleges tartalom hashét sem, és ha az azonosság kiszámolása bármely okból elbukik, a betöltés akkor is sikerül; a dokumentum egyszerűen a lemezes szint nélkül renderel

Mi érvénytelenít egy HotPDF lemezes gyorsítótár-bejegyzést?

Egy HotPDF lemezes gyorsítótár-bejegyzést szerkesztéskor sosem törléssel érvénytelenítenek; ehelyett a betöltött dokumentum szerkesztése elveszi a dokumentumazonosságot, így a lemezes szint az adott betöltés további részére kikerül, a tárolt oldalak pedig érvényesek maradnak a módosítatlan forráshoz. A bejegyzések csak az LRU-n és a bájtlimiten át, egy sérült PNG-n vagy sémaváltáson át hagyják el a lemezt

A kulcs egy lemezen lévő forrást ír le, nem a memóriabeli objektumgráfot. Amint pecsételsz egy oldalt vagy megváltoztatsz egy annotációt, a dokumentum már nem egyezik azzal a forrással, így sem a kulcsa alatt olvasni, sem írni nem lenne helyes. v2.770.140 óta mind a dokumentumszintű, mind az oldalszintű érvénytelenítés az azonosságot törli a mappa érintése helyett, és van egy második őr is azokra a szerkesztésekre, amik nem hívták meg az InvalidateRenderedPageCache-t: mielőtt a lemezes szintet használná, a THotPDF megnézi, van-e dirty betöltött objektum, és egy dirty dokumentumot azonosság nélkülinek kezel

A render-beállítások fordítva működnek. A PageRenderBackend átkapcsolása (vagy a UseNativeGDIRenderBackend hívása), meg a ConfigureRenderICCWorkflow vagy ClearRenderICCWorkflow hívása kiüríti a memóriabeli oldalakat, de megtartja az azonosságot, mert a dokumentum továbbra is egyezik a forrásával. Ezek a beállítások a pixeleket változtatják meg anélkül, hogy a memóriabeli variáns részéi volnának, ezért a lemezes kulcs összefűzi a backend nevét, a black-point compensation flaget és az ICC proof meg kimeneti profilok SHA-256 digestjeit. Maga a variáns már lefedi a színszándékot, a kimeneti ditheringet, az overprint előnézetet, a luminosity maszk módot, a fallback politikát és minden opcionális tartalomcsoport láthatóságát, így egy réteg átkapcsolása másik mappába renderel az alapértelmezett nézet felülírása helyett

HotPDF érvénytelenítési szemantika a RenderCacheFolder lemezes gyorsítótárhoz: a betöltött dokumentum vagy bármely dirty objektum szerkesztése elveszi a forrásazonosságot, így a szint kikerül, a render backend vagy ICC munkafolyamat váltása új variánskulcs alatt megtartja az azonosságot, a mentés meg újratöltéssel újra kulcsolja a dokumentumot
egy szerkesztés sosem törli a tárolt mappát, egy beállításváltás másik kulcs alatt renderel, és csak a mentés meg újratöltés szerzi meg a szerkesztett dokumentumnak a friss azonosságot

Hogy egy szerkesztett dokumentum visszakerüljön a lemezes szintre, adj neki új forrásazonosságot azzal, hogy elmented és visszatöltöd az eredményt:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // A betöltött dokumentum szerkesztése után: frissítsd a memóriabeli oldalakat.
  // A forrásazonosság már elveszett, így az eredeti dokumentum lemezes
  // mappájából semmi nem olvasódik és semmi nem íródik
  Pdf.InvalidateRenderedPageCache;

  // A mentett fájlnak új mérete és last-write ideje van, tehát új
  // azonossága; a betöltés utáni renderek az új kulcs alá kerülnek
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Az eredeti dokumentum mappája békén marad, és a RenderCacheMaxDocuments és RenderCacheMaxBytes révén öregszik ki, mint bármely más bejegyzés. Ha a felhasználó újra megnyitja a szerkesztetlen eredetit, az oldalai még ott vannak

Biztonsági határok: titkosított források és linked mappák

A HotPDF lemezes render gyorsítótára szándékosan kétféle bemenetet utasít el: sosem ír lemezre titkosított PDF oldalait, és sosem követ egy junction vagy más reparse point dokumentumalszönyvtárt. Mindkét szabály gyorsítótár-találatokat ad oda azért, hogy ne szivárogjon adat és ne töröljön rossz fájlokat

Titkosított PDF-ek sosem kerülnek lemezre

Egy renderelt oldal dekódolt tartalom. Ha sima PNG-ként egy gyorsítótár-mappába írnád, jól olvasható másolata maradna egy jelszóval védett dokumentumnak a lemezen, a szerző által választott védelmen kívül (ISO 32000-1 §7.6). Ezért a HotPDF nem kap azonosságot bármely olyan forrásról, aminek a trailere /Encrypt bejegyzést hordoz, beleszámítva a jelszóval vagy üres user jelszóval nyitott fájlokat. Ezek a dokumentumok továbbra is a memóriabeli szintet használják, ami a folyamattal együtt hal

A junction alszönyvtárak v2.770.173 óta elutasítva

A gyorsítótár gyökere a te választásod, és junctionre mutatni vele megengedett. A felette lévő dokumentumalszönyvtárak már más kérdés: a gyorsítótár magától hozza létre, olvassa, érinti és törli őket, az indítási helyreállításnál (ami a maradék ideiglenes fájlokat szedi), a keresésnél (ami frissíti az időbélyegeket), a tárolásnál, az érvénytelenítésnél és a három kilakolási limitnél. Ha valaki, akinek írásjoga van a gyorsítótár gyökerére, egy dokumentummappát egy másik könyvtárra mutató junctionre cserél, mindegyik ilyen út követné, és a kilakolás ott törölne fájlokat, ahol a gyorsítótárnak sosem volt tulajdona. v2.770.173 óta mindegyik belépő ellenőrzi a reparse-point attribútumot, és átugorja a linked dokumentummappát: a keresés találatlannak számol, a tárolás íráshibának, a kilakolás pedig békén hagyja

Unicode elérési utak és megosztott gyökerek

Két rokon javítás számít, ha user profilokra telepítesz. v2.770.135 előtt a RenderCacheFolder AnsiString volt, így a rendszerkódlapon kívüli mappa (például kínai felhasználónév egy angol Windowson) veszteségesen alakult át, mielőtt a gyorsítótár meglátta volna; a property most Unicode string, és az atomi csere a széles Windows API-t használja. v2.770.52 óta több, egy folyamatban élő THotPDF példány, ami ugyanarra a gyökérre mutat (elérési út kibontása után, kisbetű-nagybetű érzéketlenül összevetve), egyetlen hivatkozásszámlált indexet és zárat oszt meg. Korábban minden példány a saját másolatával írta felül az index.txt-et, és a limiteket a részleges nézetére mérte, így a mappa a budgetjét többszörösen túl is nőhetett

Ez a megosztás a folyamathatárnál megáll. Két külön folyamat ugyanazon a gyökéren továbbra is külön memóriabeli indexeket tart, így adj minden párhuzamosan futó alkalmazásnak saját gyorsítótár-gyökeret. Azok a nézegetők, amik worker threadeken renderelnek, egy folyamaton belül rendben vannak: a PrefetchLoadedPages és a háttérrenderelés kérősorral cikkben taglalt sor ugyanazon a kapcsolt úton és ugyanazon a záron át megy

Gyorsreferencia: RenderCacheFolder ellenőrzőlista

  • Állítsd be a RenderCacheFolder-t, RenderCacheMaxDocuments-t és RenderCacheMaxBytes-t az első RenderLoadedPageToBitmapCached hívás előtt; stream és random-access betöltéseknél a mappát a betöltés előtt állítsd
  • Frissíts v2.770.140-re vagy újabbra, ha a lemezes szintre építesz; a korábbi verziók elfogadják a property-t, de normál betöltésekre sosem szolgálnak ki oldalt lemezről
  • Számolj azzal, hogy titkosított PDF-ekre, a betöltés után szerkesztett dokumentumokra, vagy amíg a RenderFallbackPolicy nem rfpIgnore, nincs lemezes kapcsolás
  • Szabadítsd fel a THotPDF példányt rendesen; v2.770.140 óta sem a Free, sem az InvalidateRenderedPageCache nem törli a lemezes bejegyzéseket
  • A PageRenderBackend vagy az ICC munkafolyamat váltása a dokumentumot másik kulcs alatt megtartja a lemezes szinten
  • Futó alkalmazásonként egy gyorsítótár-gyökeret használj; egy folyamaton belüli példányok v2.770.52 óta megosztják az indexet
  • Tartsd a gyorsítótár gyökerét userenkénti helyen; junction dokumentumalszönyvtárak v2.770.173 óta kihagyásra kerülnek

Egy perzisztens oldal-gyorsítótár leginkább olyan nézegetőben fizet ki, ami egész nap ugyanazokat a dokumentumokat nyitja újra, ami pontosan annak a egyéni PDF nézegető architektúrának Delphiben az alakja, amit a blog máshol taglal. A RenderCacheFolder, a memóriabeli rasztergyorsítótár és az oldalrenderer a HotPDF Delphi PDF component csomag része Delphihez és C++Builderhez