Tehnični članak

HotPDF RenderCacheFolder: diskovni predpomnilnik v Delphiju

HotPDF RenderCacheFolder spremeni predpomnilnik izrisanih strani v pomnilniku komponente HotPDF Delphi v obstojen diskovni predpomnilnik strani: izrisane strani se zapisujejo kot datoteke PNG pod mapo po vaši izbiri, ob naslednjem odprtju istega vira PDF pa RenderLoadedPageToBitmapCached prebere nazaj, namesto da bi rasteriziral znova. Vrstni red iskanja je pomnilnik, nato disk, nato izrisovalnik

Diskovna plast je v API-ju od v2.416.0, do v2.770.140 pa nikoli ni dejansko postregla strani za navaden klic LoadFromFile ali LoadFromStream. Popravek je vsilil vprašanje, na katerega mora odgovoriti vsak obstoječ predpomnilnik: kako veste, da je datoteka, ki ste jo odprli danes, dokument, ki ste ga izrisali včeraj, in kaj se zgodi s predpomnjenimi stranmi, kadar ni? Spodaj so odgovori, na katere se je odločil HotPDF, vključno z mesti, kjer se namenoma odkloni od predpomnjenja

Kako deluje diskovni izrisovalni predpomnilnik HotPDF?

Diskovni izrisovalni predpomnilnik HotPDF je druga plast za rastrskim predpomnilnikom v pomnilniku in sodeluje samo, kadar je RenderCacheFolder neprazna pot. Klic RenderLoadedPageToBitmapCached(PageIndex, DPI) najprej preišče vnose v pomnilniku, indeksirane po indeksu strani, DPI in različici nastavitev izrisovanja. Ob zgrešitvi vpraša diskovno plast; diskovni zadetek dekodira PNG, ga povrne v pomnilnik in vrne kopijo v lasti klicalca. Šele, ko zgrešita obe plasti, gre stran skozi interpretator tokov vsebine, opisan v izrisovanju naložene strani PDF v TBitmap, sveža bitna slika pa se nato zapiše tudi na disk

HotPDF shema iskanja v izrisovalnem predpomnilniku za RenderLoadedPageToBitmapCached: plast v pomnilniku, indeksirana po strani, DPI in različici izrisovanja, se preveri najprej, nato diskovna plast RenderCacheFolder iz datotek PNG z atomsko zamenjavo, nato interpretator tokov vsebine, vsak zadetek pa vrne kopijo v lasti klicalca
HotPDF pogleda najprej v pomnilnik, nato na disk in šele potem rasterizira; diskovni zadetek se povrne v pomnilnik in vsaka pot vam izroči kopijo, ki je vaša in jo morate sprostiti

Na disku je postavitev namenoma dolgočasna. Vsak dokument dobi podmapo, poimenovano iz 16-hex-znakovnega dokumentnega ključa plus 16-hex-znakovne različice izrisovanja, vsaka stran se shrani kot <page>@<dpi>.png, index.txt pri korenu pa hrani dokumente v vrstnem redu najbolj nazadnje uporabljenih za oznako sheme. Neujemanje sheme počisti mapo ob prvi uporabi. Zapisi gredo najprej v začasno datoteko in se na svoje mesto zamenjajo z atomsko zamenjavo, tako da sesutje med pisanjem pusti ali staro stran ali nič, nikoli pol PNG. PNG, ki se ne da dekodirati, je izbrisan in šteje kot zgrešitev

Mapo omejujejo tri meje:

  • RenderCacheMaxDocuments (privzeto 20) omeji število podmap dokumentov; najdlje neuporabljena mapa je izpodrinjena prva
  • RenderCacheMaxBytes (privzeto 524288000, kar je 500 MB) omeji skupno velikost vseh datotek PNG pod korenom
  • Vsaka mapa dokumenta hrani največ 200 slik strani; ta omejitev na dokument je fiksirana v THotPDF in ni objavljena lastnost

RenderCacheCapacity (privzeto 8) je ločen regulator: določa, koliko izrisanih strani hrani plast v pomnilniku, z diskovnim odtisom pa nima ničesar

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Nastavite diskovno plast pred prvim predpomnjenim izrisom:
    // mapa in obe omejitvi sta prebrani, ko se plast prvič uporabi
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // strani v pomnilniku

    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
          // Kopijo tu izročite vrstici sličic
        finally
          Bmp.Free; // predpomnjen klic vedno vrne kopijo v lasti klicalca
        end;
      end;
  finally
    Pdf.Free; // od v2.770.140 to ne izbriše več diskovnih vnosov
  end;
end;

Pognajte isto proceduro dvakrat in drugi pogon ne rasterizira strani, ki je spravila v predpomnilnik. Objekt diskovnega predpomnilnika se ustvari lenobno ob prvem predpomnjenem izrisu in živi, dokler se instanca THotPDF ne sprosti, tako da sprememba RenderCacheFolder, RenderCacheMaxDocuments ali RenderCacheMaxBytes po tej točki ne premakne ali predimenzionira že odprtega predpomnilnika. Strani, prevelike za politiko sprejemanja v pomnilnik (privzeto posamezen vnos ne sme presegati 64 MiB 32-bitnih pikslov), se prav tako ne obstojijo, diskovna plast pa se obravnava samo, dokler RenderFallbackPolicy obdrži privzeti rfpIgnore, ker rezervna diagnostika ni shranjena ob strani PNG

Zakaj RenderCacheFolder pred v2.770.140 nikoli ni deloval?

RenderCacheFolder je pred v2.770.140 brez učinka, ker je diskovna plast indeksirala dokumente po hashu izvornih bajtov, ki ga navadna nalaganja nikoli ni hranila. Dokumentni ključ je prišel iz SHA-256 nad notranjo kopijo surovih bajtov PDF, LoadFromFile in LoadFromStream pa razčlenita vir na mestu in take kopije ne obdržita; polje je bilo zapolnjeno samo začasno na poti šifrirane obnove in ponovno počiščeno takoj zatem. Brez bajtov je bil ključ vedno prazen, prazen ključ pa pomeni, da se diskovna plast obide. Nobene napake, nobenega opozorila, samo mapa, ki je ostala prazna

Narediti ključ neprazen je razkrilo drugi hrošč, skritega za prvim. Stari InvalidateRenderedPageCache je izbrisal diskovno mapo dokumenta, InvalidateRenderedPageCache pa teče na začetku vsakega nalaganja, ob vsakem urejanju in znotraj Free. Torej bi v trenutku, ko bi ključ deloval, vsaka seja pregledovalnika uničila svoj predpomnilnik ob izhodu, naslednja seja pa bi vseeno začela hladna. Še huje, ključ bi bil po urejanju ponovno izračunan iz istega vira, tako da bi bili izrisi urejenega dokumenta shranjeni pod ključem izvirne datoteke in postreženi naslednji seji, ki odpre nespremenjeni PDF. v2.770.140 popravi identiteto in razveljavitev skupaj; popravek samo enega od njiju bi poslal ali mrtv ali lažnivec predpomnilnik

Kako HotPDF prepozna PDF, ne da bi prebral celo datoteko

HotPDF prepozna PDF, naložen iz lokalne datoteke, po prstnem odtisu njene velikosti, časa zadnjega zapisa ter prvih in zadnjih 64 KiB, vir tok ali naključno-dostopni vir pa po SHA-256 celotne vsebine. Oboje se zajame enkrat, ko nalaganje uspe, prvih 16 hex znakov povzetka SHA-256 (64 bitov) pa postane dokumentni ključ

VirIdentitetaCenaZajeto kdaj
LoadFromFileSize + LastWriteTime + vodilnih in končnih 64 KiB, hashirano s SHA-256Največ 128 KiB branja, neodvisno od velikosti datotekeVsako uspešno nalaganje, tudi če je RenderCacheFolder nastavljen kasneje
LoadFromStreamSHA-256 celega tokaEn poln prehod čez virSamo, kadar je bil RenderCacheFolder nastavljen pred nalaganjem
LoadFromRandomAccessSourceSHA-256 celega viraEn poln prehod čez virSamo, kadar je bila mapa nastavljena najprej in je celoten obseg na voljo
Kateri koli vir z vnosom /EncryptBrezBrezNikoli; diskovna plast se obide
HotPDF zemljevid identitete vira za diskovni izrisovalni predpomnilnik: LoadFromFile hashira velikost, LastWriteTime ter prve in zadnje 64 KiB, LoadFromStream in LoadFromRandomAccessSource hashirata celotno vsebino samo, kadar je bil RenderCacheFolder nastavljen najprej, vsak priklopnik /Encrypt pa ne zajame nobene identitete
Datoteke so prstno odtisnjene z njihovih koncev, ker glava, xref in priklopnik živijo tam, tokovi plačajo poln hash samo, kadar ste predpomnilnik zahtevali najprej, šifrirani dokumenti pa se nikoli ne zapisujejo na disk

Prstni odtis datoteke je namerno trgovanje. Poln hash 400 MB skeniranega arhiva ob vsakem odprtju lahko stane več kot izris dveh strani, ki si ju uporabnik dejansko ogleda. Vzorčeni območja niso poljubna: glava leži na začetku datoteke, priklopnik in zadnji odsek navzkrižnih sklicev pa na koncu (ISO 32000-1 §7.5). Inkrementalna posodobitev doda novo telo, odsek navzkrižnih sklicev in priklopnik (§7.5.6), tako da hkrati spremeni velikost in rep. Poln prepis s katerim koli navadnim orodjem spremeni čas zadnjega zapisa. Za datoteke do 128 KiB vzorca pokrijeta vsak bajt, tako da so majhni dokumenti učinkovito hashirani v celoti

Preostalo tveganje je sprememba enake velikosti na mestu sredi velike datoteke, katere zapisovalec nato povrne izvirni časovni žig. To potrebuje orodje, ki namenoma ohranja čase sprememb med urejanjem vsebine — redko, a ne nemogoče — in v tem primeru predpomnilnik postreže zastarele strani. Nasprotna stran je neÅ¡kodljiva: kopiranje datoteke na Windows običajno ohrani čas zadnjega zapisa, tako da kopija dokumenta, ki je že v predpomnilniku, zadene iste vnose — kar je prav, ker so bajti identični

Tokovi nimajo časa spremembe sploh, zato je edina poštena identiteta vsebina. HotPDF plača ta poln prehod SHA-256 samo, kadar ste diskovni predpomnilnik zahtevali pred nalaganjem; vsak drug klicalec LoadFromStream ne vidi dodatne cene. Vrstni red dodeljevanja lastnosti je zato ključen:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Napačen vrstni red za tokove: vsebinski hash se izračuna samo, kadar je
  // mapa že nastavljena, tako da bi ta dokument obšel diskovno plast
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

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

Naključno-dostopni vir, ki se še prenaša (nekateri obsegi še niso na voljo), ne dobi identitete, namesto hasha delne vsebine, in če izračun identitete iz kakršnega koli razloga odpove, nalaganje še vedno uspe; dokument preprosto izrisuje brez diskovne plasti

Kaj razveljavi vnos diskovnega predpomnilnika HotPDF?

Vnos diskovnega predpomnilnika HotPDF ni nikoli razveljavljen z brisanjem ob urejanju; namesto tega urejanje naloženega dokumenta spusti identiteto dokumenta, tako da se diskovna plast obide za preostanek tega nalaganja, shranjene strani pa ostanejo veljavne za nespremenjeni vir. Vnosi zapustijo disk samo skozi LRU in bajtne meje, pokvarjen PNG ali spremembo sheme

Ključ opisuje vir na disku, ne grafa objektov v pomnilniku. Ko žigosate stran ali spremenite anotacijo, se dokument ne ujema več s tem virom, tako da ne bi bilo prav niti brati niti pisati pod njegovim ključem. Od v2.770.140 razveljavitev na ravni dokumenta in na ravni strani počistita identiteto, namesto da bi se dotaknili mape, in obstaja druga straža za urejanja, ki niso klicala InvalidateRenderedPageCache: pred uporabo diskovne plasti THotPDF preveri, ali je kateri koli naloženi objekt umazan, in umazan dokument obravnava kot brez identitete

Nastavitve izrisovanja delujejo obratno. Preklop PageRenderBackend (ali klic UseNativeGDIRenderBackend) ter klic ConfigureRenderICCWorkflow ali ClearRenderICCWorkflow spraznijo strani v pomnilniku, a obdržijo identiteto, ker se dokument še vedno ujema s svojim virom. Te nastavitve spremenijo piksle, ne da bi bile del različice v pomnilniku, zato diskovni ključ zvije noter ime zaledja, zastavico kompenzacije črne točke in povzetke SHA-256 ICC profilov dokaza in izhoda. Različica sama že pokriva barvni namen, rastriranje izhoda, predogled pretiske, način maske svetlosti, rezervno politiko in vidnost vsake skupine neobvezne vsebine, tako da preklop plasti izrisuje v drugačno mapo, namesto da bi prepisal privzeti pogled

HotPDF semantika razveljavitve za diskovni predpomnilnik RenderCacheFolder: urejanje naloženega dokumenta ali katerega koli umazanega objekta spusti identiteto vira, tako da se plast obide, sprememba izrisovalnega zaledja ali ICC delovnega toka obdrži identiteto pod novim ključem različice, shranjevanje in ponovno nalaganje pa dokumentu dajo nov ključ
Urejanje nikoli ne izbriše shranjene mape, sprememba nastavitev izrisuje pod drugačnim ključem, samo shranjevanje plus ponovno nalaganje pa zaslužita urejenemu dokumentu svežo identiteto

Da urejeni dokument spet pridete na diskovno plast, mu dajte novo identiteto vira tako, da ga shranite in naložite rezultat:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Po urejanju naloženega dokumenta: osvežite strani v pomnilniku.
  // Identiteta vira je že izgubljena, tako da se iz izvornega dokumenta
  // nič ne bere in nič ne zapisuje v njegovo diskovno mapo
  Pdf.InvalidateRenderedPageCache;

  // Shranjena datoteka ima novo velikost in čas zadnjega zapisa, torej novo
  // identiteto; izrisi po tem nalaganju so predpomnjeni pod novim ključem
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Mapa izvornega dokumenta je puščena pri miru in stari skozi RenderCacheMaxDocuments in RenderCacheMaxBytes kot kateri koli drug vnos. Če uporabnik znova odpre neurejeni izvirnik, so njegove strani še vedno tam

Varnostne meje: šifrirani viri in povezane mape

Diskovni izrisovalni predpomnilnik HotPDF se namenoma odkloni od dveh vrst vnosa: nikoli ne zapisuje strani šifriranega PDF-ja na disk in nikoli ne sledi podmani dokumenta, ki je junction ali druga točka ponovne razčlenitve. Obe pravili trgujeta zadetke predpomnilnika za to, da ne puščata podatkov ali ne izbrišeta napačnih datotek

Šifrirani PDF-ji se nikoli ne predpomnijo na disk

Izrisana stran je dešifrirana vsebina. Njen zapis kot navaden PNG v mapo predpomnilnika bi pustil berljivo kopijo dokumenta, zaščitenega z geslom, na disku, izven zaščite, ki jo je izbral avtor (ISO 32000-1 §7.6). HotPDF zato ne zajame identitete za noben vir, katerega priklopnik nosi vnos /Encrypt, vključno z datotekami, odprtimi z geslom ali s praznim uporabniškim geslom. Ti dokumenti še vedno uporabljajo plast v pomnilniku, ki pogine s procesom

Podmane junction so zavrnjene od v2.770.173

Koren predpomnilnika je vaša izbira in kazati ga na junction je dovoljeno. Podmane dokumentov pod njim pa so druga zadeva: predpomnilnik jih sam ustvarja, bere, se jih dotika in jih briše — med zagonom obnove (ki odstrani preostale začasne datoteke), iskanja (ki posodablja časovne žige), shranjevanja, razveljavitve in treh mej izpodrivanja. Če bi nekdo z dostopom za pisanje do korena predpomnilnika zamenjal mapo dokumenta z junction na drug imenik, bi ji sledila vsaka od teh poti in izpodrivanje bi brisalo datoteke nekje, kjer jih predpomnilnik nikoli ni imel v lasti. Od v2.770.173 vsak od teh vstopnih točk preveri atribut točke ponovne razčlenitve in preskoči povezano mapo dokumenta: iskanje šteje zgrešitev, shranjevanje neuspel zapis, izpodrivanje pa jo pusti pri miru

Poti Unicode in deljeni koreni

Dva povezana popravka štejeta, kadar nameščate v uporabniške profile. Pred v2.770.135 je bil RenderCacheFolder AnsiString, tako da je bila mapa izven sistemske kodne strani (kitajsko uporabniško ime na angleški namestitvi Windows, na primer) pretvorjena z izgubo, preden jo je videl predpomnilnik; lastnost je zdaj Unicode string in atomska zamenjava uporablja široki Windows API. Od v2.770.52 si več instanc THotPDF v enem procesu, ki kažejo na isti koren (po razširitvi poti, primerjano neobčutljivo na velike črke), delijo en sam indeks in zaklep, štet po sklicih. Prej je vsaka instanca prepisala index.txt s svojo kopijo in uveljavljala meje proti svojemu delnemu pogledu, tako da je lahko mapa zrasla nekajkrat čez svoj proračun

To deljenje se ustavi na meji procesa. Dva ločena procesa na istem korenu še vedno držita ločena indeksa v pomnilniku, zato dajte vsaki sočasno tekoči aplikaciji svoj koren predpomnilnika. Pregledovalniki, ki izrisujejo na delovnih nitih, so znotraj enega procesa v redu: PrefetchLoadedPages in čakalna vrsta, pokrita v izrisovanju v ozadju s čakalno vrsto zahtev, oba greta skozi isto predpomnjeno pot in isti zaklep

Hiter pregled: kontrolni seznam RenderCacheFolder

  • Nastavite RenderCacheFolder, RenderCacheMaxDocuments in RenderCacheMaxBytes pred prvim klicem RenderLoadedPageToBitmapCached; za nalaganja tok in naključnega dostopa nastavite mapo pred nalaganjem
  • Nadgradite na v2.770.140 ali novejši, kadar se zanašate na diskovno plast; starejše različice sprejmejo lastnost, a nikoli ne postrežejo strani z diska za navadna nalaganja
  • Pričakujte brez diskovnega predpomnjenja za šifrirane PDF-je, za dokumente, urejene po nalaganju, ali medtem ko RenderFallbackPolicy ni rfpIgnore
  • Sprostite instanco THotPDF navadno; od v2.770.140 ne Free ne InvalidateRenderedPageCache ne briše diskovnih vnosov
  • Sprememba PageRenderBackend ali ICC delovnega toka obdrži dokument na diskovni plasti pod drugačnim ključem
  • Uporabite en koren predpomnilnika na tekočo aplikacijo; instance znotraj enega procesa si delijo indeks od v2.770.52
  • Držite koren predpomnilnika na mestu na uporabnika; podmane dokumentov, ki so junction, so preskočene od v2.770.173

Obstoječ predpomnilnik strani se najbolj izplača v pregledovalniku, ki ves dan znova odpira iste dokumente — točno to je oblika arhitekture pregledovalnika PDF po meri v Delphiju, opisane drugje na tem blogu. RenderCacheFolder, rastrski predpomnilnik v pomnilniku in izrisovalnik strani prihajajo s komponento HotPDF Delphi PDF za Delphi in C++Builder