Tehnički članak

HotPDF RenderCacheFolder: disk keš stranica u Delphi-ju

HotPDF RenderCacheFolder pretvara keš renderovanih stranica u memoriji HotPDF Delphi komponente u trajan disk keš stranica: renderovane stranice upisuju se kao PNG fajlovi pod folder koji izaberete, i sledeći put kad se isti PDF izvor otvori, RenderLoadedPageToBitmapCached čita ih naziv umesto da ponovo rasterizuje. Redosled potrage je memorija, pa disk, pa renderer

Diskovni sloj je u API-ju od v2.416.0, ali sve do v2.770.140 nikada nije stvarno servisirao stranicu za normalan poziv LoadFromFile ili LoadFromStream. Popravka je nametnula pitanje na koje svaki trajan keš mora odgovoriti: odakle znate da je fajl koji ste danas otvorili dokument koji ste juče renderovali, i šta se dešava sa keširanim stranicama kad nije? Dole su odgovori na koje se HotPDF pridržao, uključujući gde namerno odbija da kešira

Kako radi HotPDF disk keš rendera?

HotPDF disk keš rendera je drugi sloj iza rasterskog keša u memoriji, i učestvuje samo kad je RenderCacheFolder ne-prazan put. Poziv RenderLoadedPageToBitmapCached(PageIndex, DPI) prvo skenira unose u memoriji, ključane indeksom stranice, DPI-jem i varijantom podešavanja rendera. Na promašaj pita diskovni sloj; diskovski pogodak dekodira PNG, vraća ga u memoriju i vraća kopiju u vlasništvu pozivaoca. Tek kad oba sloja promaše stranica prolazi kroz interpreter content streama opisan u renderovanju učitane PDF stranice u TBitmap, i sveža bitmapa se zatim upisuje i na disk

HotPDF dijagram potrage keša rendera za RenderLoadedPageToBitmapCached: sloj u memoriji ključan stranicom, DPI-jem i varijantom rendera ispituje se prvo, pa RenderCacheFolder disk sloj PNG fajlova sa atomskom zamenom, pa interpreter content streama, i svaki pogodak vraća kopiju u vlasništvu pozivaoca
HotPDF gleda prvo u memoriju, pa na disk, i tek onda rasterizuje; diskovski pogodak vraća se u memoriju i svaka putanja vam uručuje kopiju koja je vaša i koju morate osloboditi

Na disku raspored je namerno dosadan. Svaki dokument dobija podfolder nazvan od 16-heksa-karakternog ključa dokumenta plus 16-heksa-karakterne varijante rendera, svaka stranica čuva se kao <page>@<dpi>.png, i index.txt u korenu drži dokumente u redosledu najskorije upotrebe iza schema oznake. Nepoklapanje šeme briše folder pri prvoj upotrebi. Upisi idu prvo u privremeni fajl i menjaju se na mesto atomskom zamenom, pa pad usred upisa ostavlja ili staru stranicu ili ništa, nikada pola PNG-a. PNG koji ne uspe da se dekodira briše se i računa kao promašaj

Tri granice vezuju folder:

  • RenderCacheMaxDocuments (podrazumevano 20) ograničava broj podfoldera dokumenata; najskorije nekorišćeni folder izbacuje se prvi
  • RenderCacheMaxBytes (podrazumevano 524288000, što je 500 MB) ograničava ukupnu veličinu svih PNG fajlova ispod korena
  • Svaki folder dokumenta drži najviše 200 slika stranica; ta granica po dokumentu fiksna je u THotPDF i nije objavljeno svojstvo

RenderCacheCapacity (podrazumevano 8) je odvojeno dugme: postavlja koliko renderovanih stranica sloj u memoriji drži, i nema ništa sa diskom

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Podesite disk sloj pre prvog keširanog rendera:
    // folder i obe granice čitaju se kad se sloj prvi put koristi
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // stranice u memoriji

    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
          // Predajte kopiju traci sličica ovde
        finally
          Bmp.Free; // keširani poziv uvek vraća kopiju u vlasništvu pozivaoca
        end;
      end;
  finally
    Pdf.Free; // od v2.770.140 ovo više ne briše disk unose
  end;
end;

Pokrenite istu proceduru dvaput i drugo izvođenje nikada ne rasterizuje stranicu koja staje u keš. Objekat disk keša stvara se lenjo pri prvom keširanom renderu i živi dok se THotPDF instanca ne oslobodi, pa promena RenderCacheFolder, RenderCacheMaxDocuments ili RenderCacheMaxBytes posle te tačke ne pomera ni ne menja veličinu već otvorenog keša. Stranice prevelike za politiku prijema u memoriji (podrazumevano jedan unos ne sme premašiti 64 MiB 32-bitnih piksela) takođe se ne trajno čuvaju, i diskovni sloj pita se samo dok RenderFallbackPolicy drži svoju podrazumevanu rfpIgnore, jer fallback dijagnostika ne čuva se uz PNG

Zašto RenderCacheFolder nikada nije radio pre v2.770.140?

RenderCacheFolder nije imao dejstva pre v2.770.140 jer je diskovni sloj ključao dokumente hešom izvornih bajtova koje obična učitavanja nikada nisu čuvala. Ključ dokumenta dolazio je iz SHA-256 nad internom kopijom sirovih PDF bajtova, ali LoadFromFile i LoadFromStream parsiraju izvor u mestu i ne zadržavaju takvu kopiju; polje je popunjavano samo privremeno na putanji oporavka šifrovanih i ponovo brisano odmah posle. Bez bajtova, ključ je uvek bio prazan, a prazan ključ znači da se diskovni sloj zaobilazi. Bez greške, bez upozorenja, samo folder koji je ostajao prazan

Činjenje ključa ne-praznim otkrilo je drugi bug koji se krio iza prvog. Stari InvalidateRenderedPageCache brišao je disk folder dokumenta, a InvalidateRenderedPageCache radi na početku svakog učitavanja, pri svakoj izmeni i unutar Free. Tako bi u trenutku kad ključ proradi svaka sesija pregledača uništila sopstveni keš na izlazu, i sledeća sesija ionako bi počela hladna. Gore, ključ bi se preračunao iz istog izvora posle izmene, pa bi renderi uređenog dokumenta bili čuvani pod ključem originalnog fajla i servisirani sledećoj sesiji koja otvori neizmenjeni PDF. v2.770.140 popravlja identitet i poništavanje zajedno; popraviti samo jedno isporučilo bi ili mrtav keš ili keš koji laže

Kako HotPDF identifikuje PDF bez čitanja celog fajla

HotPDF identifikuje PDF učitan sa lokalnog fajla otiskom njegove veličine, vremena poslednjeg upisa i njegovih prvih i poslednjih 64 KiB, a tok ili random-access izvor identifikuje SHA-256 njegovog celog sadržaja. Oba se beleže jednom, kad učitavanje uspe, i prvih 16 heksa karaktera SHA-256 sažetka (64 bita) postaje ključ dokumenta

IzvorIdentitetCenaBeleži se kad
LoadFromFileVeličina + LastWriteTime + vodećih i završnih 64 KiB, heširano sa SHA-256Najviše 128 KiB čitanja, nezavisno od veličine fajlaSvako uspešno učitavanje, čak i ako se RenderCacheFolder postavi kasnije
LoadFromStreamSHA-256 celog tokaJedan pun prolaz preko izvoraSamo ako je RenderCacheFolder postavljen pre učitavanja
LoadFromRandomAccessSourceSHA-256 celog izvoraJedan pun prolaz preko izvoraSamo ako je folder postavljen prvo i ceo opseg je dostupan
Svaki izvor sa /Encrypt unosomNijedanNijednaNikada; diskovni sloj se zaobilazi
HotPDF mapa identiteta izvora za disk keš rendera: LoadFromFile hešira veličinu, LastWriteTime i prvih i poslednjih 64 KiB, LoadFromStream i LoadFromRandomAccessSource heširaju ceo sadržaj samo kad je RenderCacheFolder postavljen prvo, i bilo koji /Encrypt trailer ne beleži nikakav identitet
fajlovi se otiskuju sa svojih krajeva jer header, xref i trailer žive tamo, tokovi plaćaju pun heš samo kad ste tražili keš prvo, i šifrovani dokumenti nikada se ne upisuju na disk

Otisak fajla je namerno prilagođavanje. Heširanje 400 MB skeniranog arhiva u celosti pri svakom otvaranju može koštati više nego renderovanje dve stranice koje korisnik zaista gleda. Uzorčeni regioni nisu proizvoljni: header sedi na početku fajla, a trailer i poslednja cross-reference sekcija sede na kraju (ISO 32000-1 §7.5). Inkrementalna dopuna dodaje novo telo, cross-reference sekciju i trailer (§7.5.6), pa menja veličinu i rep odjednom. Potpuni prepis bilo kog normalnog alata menja vreme poslednjeg upisa. Za fajlove do 128 KiB dva uzorka pokrivaju svaki bajt, pa se mali dokumenti efektivno heširaju u celosti

Preostali rizik je izmena iste veličine u mestu na sredinu velikog fajla čiji pisac zatim vrati originalnu vremensku oznaku. To traži alat koji namerno čuva vremena izmene dok uređuje sadržaj, što je retko ali ne nemoguće, i u tom slučaju keš servira zastarele stranice. Druga strana je blaga: kopiranje fajla na Windows-u normalno čuva vreme poslednjeg upisa, pa kopija dokumenta već u kešu pogađa iste unose, što je ispravno jer su bajtovi identični

Tokovi uopšte nemaju vreme izmene, pa je jedini iskren identitet sadržaj. HotPDF plaća taj pun SHA-256 prolaz samo kad ste tražili disk keš pre učitavanja; svaki drugi pozivalac LoadFromStream ne vidi dodatnu cenu. To čini redosled dodele svojstava nosećim:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Pogrešan redosled za tokove: heš sadržaja računa se samo kad je
  // folder već postavljen, pa bi ovaj dokument zaobišao disk sloj
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

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

Random-access izvor koji se još preuzima (neki opsezi još nedostupni) ne dobija identitet umesto heša delimičnog sadržaja, i ako računanje identiteta padne iz bilo kog razloga učitavanje i dalje uspeva; dokument jednostavno renderuje bez disk sloja

Šta poništava unos HotPDF disk keša?

Unos HotPDF disk keša nikada se ne poništava brisanjem pri izmeni; umesto toga, uređivanje učitanog dokumenta otpušta identitet dokumenta, pa se diskovni sloj zaobilazi do kraja tog učitavanja i pohranjene stranice ostaju važeće za neizmenjeni izvor. Unosi napuštaju disk samo kroz LRU i bajt granice, oštećeni PNG ili promenu šeme

Ključ opisuje izvor na disku, a ne graf objekata u memoriji. Kad jednom štampate stranicu ili promenite anotaciju, dokument više ne odgovara tom izvoru, pa ni čitanje ni upis pod njegovim ključem ne bi bili ispravni. Od v2.770.140, poništavanje na nivou dokumenta i na nivou stranice briše identitet umesto da dira folder, i postoji druga čuvarka za izmene koje nisu zvale InvalidateRenderedPageCache: pre upotrebe disk sloja, THotPDF proverava da li je bilo koji učitani objekat prljav i tretira prljav dokument kao bez identiteta

Podešavanja rendera rade obrnuto. Prelazak na drugi PageRenderBackend (ili poziv UseNativeGDIRenderBackend), i pozivanje ConfigureRenderICCWorkflow ili ClearRenderICCWorkflow, ispiraju stranice u memoriji ali čuvaju identitet, jer dokument i dalje odgovara svom izvoru. Ta podešavanja menjaju piksele a nisu deo varijante u memoriji, pa disk ključ uplifuje ime beka, zastavicu black-point kompenzacije i SHA-256 sažetke ICC proof i izlaznih profila. Varijanta sama već pokriva namenu boja, dithering izlaza, overprint pregled, režim luminositet maske, fallback politiku i vidljivost svake grupe opcionog sadržaja, pa prebacivanje sloja renderuje u drugi folder umesto da prepiše podrazumevani prikaz

HotPDF semantika poništavanja za RenderCacheFolder disk keš: uređivanje učitanog dokumenta ili bilo kog prljavog objekta otpušta identitet izvora pa se sloj zaobilazi, promena render beka ili ICC toka čuva identitet pod novim ključem varijante, a snimanje plus ponovno učitavanje daje dokumentu novi ključ
izmena nikada ne briše pohranjeni folder, promena podešavanja renderuje pod drugim ključem, i samo snimanje plus ponovno učitavanje zaslužuje uređenom dokumentu svež identitet

Da vratite uređen dokument na disk sloj, dajte mu novi identitet izvora snimanjem i učitavanjem rezultata:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Posle uređivanja učitanog dokumenta: osvežite stranice u memoriji.
  // Identitet izvora je već otišao, pa se ništa ne čita iz niti
  // upisuje u disk folder originalnog dokumenta
  Pdf.InvalidateRenderedPageCache;

  // Sačuvan fajl ima novu veličinu i vreme poslednjeg upisa, dakle novi
  // identitet; renderi posle ovog učitavanja keširaju se pod novim ključem
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Folder originalnog dokumenta ostavljen je na miru i stari kroz RenderCacheMaxDocuments i RenderCacheMaxBytes kao svaki drugi unos. Ako korisnik ponovo otvori neuređeni original, njegove stranice su još tamo

Bezbednosne granice: šifrovani izvori i povezani folderi

HotPDF disk keš rendera odbija dve vrste unosa namerno: nikada ne upisuje stranice šifrovanog PDF-a na disk, i nikada ne prati podfolder dokumenta koji je junction ili druga reparse tačka. Oba pravila trguju keš pogocima za ne-curenje podataka ili brisanje pogrešnih fajlova

Šifrovani PDF-ovi se nikada ne keširaju na disku

Renderovana stranica je dešifrovani sadržaj. Upisati je kao običan PNG u keš folder ostavilo bi čitljivu kopiju dokumenta zaštićenog lozinkom na disku, van zaštite koju je autor izabrao (ISO 32000-1 §7.6). HotPDF zato ne beleži identitet za bilo koji izvor čiji trailer nosi /Encrypt unos, uključujući fajlove otvorene sa lozinkom ili sa praznom korisničkom lozinkom. Ti dokumenti i dalje koriste sloj u memoriji, koji umire sa procesom

Junction podfolderi se odbijaju od v2.770.173

Koren keša je vaš izbor, i usmeriti ga na junction dopušteno je. Podfolderi dokumenata ispod njega su druga stvar: keš ih stvara, čita, dodiruje i briše sam, tokom oporavka pri startu (koji uklanja zaostale privremene fajlove), potrage (koja ažurira vremenske oznake), upisa, poništavanja i tri granice izbacivanja. Da neko sa pravom upisa na koren keša zameni folder dokumenta junction-om ka drugom direktorijumu, svaka od tih putanja pratila bi ga, i izbacivanje brisalo bi fajlove negde gde keš nikada nije posedovao. Od v2.770.173 svaki od tih ulaza proverava reparse-point atribut i preskače povezan folder dokumenta: potraga računa promašaj, upis računa neuspeh upisa, i izbacivanje ga ostavlja na miru

Unicode putanje i deljeni koreni

Dve srodne popravke su bitne ako se raspoređuje na korisničke profile. Pre v2.770.135, RenderCacheFolder bio je AnsiString, pa je folder van sistemskog koda stranice (kinesko korisničko ime na engleskoj Windows instalaciji na primer) pretvaran sa gubicima pre nego što ga je keš video; svojstvo je sada Unicode string, i atomska zamena koristi široki Windows API. Od v2.770.52, više THotPDF instanci u jednom procesu koje pokazuju na isti koren (posle proširenja putanje, poređeno neosećljivo na velika i mala slova) dele jedan indeks i bravu sa brojanjem referenci. Ranije je svaka instanca prepisivala index.txt svojom kopijom i sprovodila granice nad svojim delimičnim pogledom, pa je folder mogao narasti nekoliko puta preko budžeta

To deljenje staje na granici procesa. Dva odvojena procesa na istom korenu i dalje drže odvojene indekse u memoriji, pa dajte svakoj istovremeno pokrenutoj aplikaciji sopstveni koren keša. Pregledači koji renderuju na radnim nitima u redu su unutar jednog procesa: PrefetchLoadedPages i red pokriven u renderovanju u pozadini sa redom zahteva oba idu kroz istu keširanu putanju i istu bravu

Brzi pregled: RenderCacheFolder proverna lista

  • Postavite RenderCacheFolder, RenderCacheMaxDocuments i RenderCacheMaxBytes pre prvog poziva RenderLoadedPageToBitmapCached; za učitavanja toka i random-access, postavite folder pre učitavanja
  • Nadogradite na v2.770.140 ili noviji ako se oslanjate na disk sloj; ranije verzije primaju svojstvo ali nikada ne serviraju stranicu sa diska za normalna učitavanja
  • Očekujte bez disk keširanja za šifrovane PDF-ove, za dokumente uređene posle učitavanja, ili dok RenderFallbackPolicy nije rfpIgnore
  • Oslobodite THotPDF instancu normalno; od v2.770.140 ni Free ni InvalidateRenderedPageCache ne brišu disk unose
  • Promena PageRenderBackend ili ICC toka drži dokument na disk sloju pod drugim ključem
  • Koristite jedan koren keša po pokrenutoj aplikaciji; instance unutar jednog procesa dele indeks od v2.770.52
  • Držite koren keša na lokaciji po korisniku; podfolderi dokumenata koji su junction-i preskaču se od v2.770.173

Trajni keš stranica najviše se isplati u pregledaču koji ceo dan otvara iste dokumente, što je upravo oblik arhitekture prilagođenog PDF pregledača u Delphi-ju opisan drugde na ovom blogu. RenderCacheFolder, rasterski keš u memoriji i renderer stranica isporučuju se sa HotPDF Delphi PDF komponentom za Delphi i C++Builder