Tehnički članak

HotPDF RenderCacheFolder: predmemorija stranica na disku

HotPDF RenderCacheFolder pretvara predmemoriju renderiranih stranica u memoriji HotPDF Delphi komponente u trajnu diskovnu predmemoriju stranica: renderirane stranice zapisuju se kao PNG datoteke u mapu koju odaberete, a sljedeći put kad se isti PDF izvor otvori, RenderLoadedPageToBitmapCached čita ih natrag umjesto da ponovno rasterizira. Redoslijed pretraživanja jest memorija, pa disk, pa renderer

Diskovna razina bila je u API-ju od v2.416.0, ali do v2.770.140 nikad zapravo nije poslužila stranicu za običan poziv LoadFromFile ili LoadFromStream. Popravak je nametnuo pitanje na koje svaka trajna predmemorija mora odgovoriti: odakle znate da je datoteka koju ste danas otvorili dokument koji ste jučer renderirali, i što se događa s predmemoriranim stranicama kad nije? Dolje su odgovori na koje se HotPDF smirio, uključujući gdje namjerno odbija predmemorirati

Kako radi HotPDF diskovna render predmemorija?

HotPDF diskovna render predmemorija druga je razina iza raster predmemorije u memoriji i sudjeluje samo kad je RenderCacheFolder neprazna putanja. Poziv RenderLoadedPageToBitmapCached(PageIndex, DPI) najprije pregledava unose u memoriji, s ključem po indeksu stranice, DPI-ju i varijanti render postavki. Na promašaj pita diskovnu razinu; diskovni pogodak dekodira PNG, vraća ga u memoriju i daje kopiju u vlasništvu pozivatelja. Tek kad obje razine promaše stranica prolazi kroz interpreter content streama opisan u renderiranju učitane PDF stranice u TBitmap, a svježa se bitmapa tada zapisuje i na disk

HotPDF dijagram pretraživanja render predmemorije za RenderLoadedPageToBitmapCached: razina u memoriji s ključem po stranici, DPI-ju i render varijanti ispituje se prva, pa RenderCacheFolder diskovna razina PNG datoteka s atomičkom zamjenom, pa interpreter content streama, i svaki pogodak vraća kopiju u vlasništvu pozivatelja
HotPDF gleda prvo u memoriju, pa na disk, i tek onda rasterizira; diskovni se pogodak vraća u memoriju i svaki vam put daje kopiju koju posjedujete i morate osloboditi

Na disku raspored je namjerno dosadan. Svaki dokument dobiva podmapu imenovanu iz 16-znakovnog heksadecimalnog ključa dokumenta plus 16-znakovne heksadecimalne render varijante, svaka se stranica sprema kao <page>@<dpi>.png, a index.txt u korijenu drži dokumente u redoslijedu nedavno korištenih iza schema oznake. Nepoklapanje schemata čisti mapu pri prvoj upotrebi. Upisi idu najprije u privremenu datoteku i zamjenjuju se na mjesto atomičkom zamjenom, pa pad usred upisa ostavlja ili staru stranicu ili ništa, nikad pola PNG-a. PNG koji ne uspije dekodirati briše se i broji kao promašaj

Tri granice omeđuju mapu:

  • RenderCacheMaxDocuments (zadano 20) ograničava broj podmapa dokumenata; najdavnije korištena mapa izbacuje se prva
  • RenderCacheMaxBytes (zadano 524288000, što je 500 MB) ograničava ukupnu veličinu svih PNG datoteka pod korijenom
  • Svaka mapa dokumenta drži najviše 200 slikovnih stranica; to ograničenje po dokumentu fiksira THotPDF i nije objavljeno svojstvo

RenderCacheCapacity (zadano 8) zaseban je regulator: postavlja koliko renderiranih stranica razina u memoriji drži, i nema nikakve veze s diskovnim otiskom

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Podesite diskovnu razinu prije prvog predmemoriranog rendera:
    // mapa i obje granice čitaju se kad se razina 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 ovdje
        finally
          Bmp.Free; // predmemorirani poziv uvijek vraća kopiju u vlasništvu pozivatelja
        end;
      end;
  finally
    Pdf.Free; // od v2.770.140 ovo više ne briše diskovne unose
  end;
end;

Pokrenite istu proceduru dvaput i drugo izvođenje nikad ne rasterizira stranicu koja je stala u predmemoriju. Objekt diskovne predmemorije stvara se lijenom pri prvom predmemoriranom renderu i živi dok se THotPDF instanca ne oslobodi, pa promjena RenderCacheFolder, RenderCacheMaxDocuments ili RenderCacheMaxBytes nakon toga ne premješta ni ne mijenja veličinu već otvorene predmemorije. Stranice prevelike za politiku primanja u memoriji (po zadanim vrijednostima jedan unos ne smije prijeći 64 MiB 32-bitnih piksela) također se ne spremaju trajno, a diskovna se razina konzultira samo dok RenderFallbackPolicy drži svoju zadanu rfpIgnore, jer fallback dijagnostika ne sprema se uz PNG

Zašto RenderCacheFolder nikad nije radio prije v2.770.140?

RenderCacheFolder nije imao učinka prije v2.770.140 jer je diskovna razina ključala dokumente hashom izvornih bajtova koje obična učitavanja nikad nisu zadržavala. Ključ dokumenta dolazio je iz SHA-256 nad internom kopijom sirovih PDF bajtova, ali LoadFromFile i LoadFromStream parsiraju izvor na mjestu i ne zadržavaju takvu kopiju; polje je popunjavano samo privremeno na putanji oporavka od šifriranja i odmah poslije ponovno čistjeno. Bez bajtova ključ je uvijek bio prazan, a prazan ključ znači da se diskovna razina zaobilazi. Bez pogreške, bez upozorenja, samo mapa koja je ostala prazna

Činjenica da ključ nije prazan otkrila je drugi bug koji se krio iza prvoga. Stari InvalidateRenderedPageCache brisao je diskovnu mapu dokumenta, a InvalidateRenderedPageCache izvodi se na početku svakog učitavanja, pri svakoj izmjeni i unutar Free. Pa bi onog trenutka kad bi ključ proradio svaka sesija preglednika uništila vlastitu predmemoriju pri izlasku, i sljedeća bi sesija ionako počela hladna. Gore od toga, ključ je ponovno računan iz istog izvora nakon izmjene, pa bi renderi uređenog dokumenta bili spremljeni pod ključem izvorne datoteke i posluženi sljedećoj sesiji koja otvori neizmijenjeni PDF. v2.770.140 popravlja identitet i poništavanje zajedno; popraviti samo jedno isporučilo bi ili mrtvu predmemoriju ili lažnu

Kako HotPDF identificira PDF bez čitanja cijele datoteke

HotPDF identificira PDF učitan iz lokalne datoteke otiskom njegove veličine, vremena posljednjeg zapisa te njegovih prvih i posljednjih 64 KiB, a tok ili random-access izvor identificira SHA-256 njegovog cijelog sadržaja. Oboje se bilježi jednom, kad učitavanje uspije, i prvih 16 heksadecimalnih znakova SHA-256 sažetka (64 bita) postaje ključ dokumenta

IzvorIdentitetTrošakBilježi se kad
LoadFromFileVeličina + LastWriteTime + vodećih i repnih 64 KiB, hashirano SHA-256Najviše 128 KiB čitanja, neovisno o veličini datotekeSvako uspješno učitavanje, i ako se RenderCacheFolder postavi kasnije
LoadFromStreamSHA-256 cijelog tokaJedan potpuni prolaz kroz izvorSamo ako je RenderCacheFolder bio postavljen prije učitavanja
LoadFromRandomAccessSourceSHA-256 cijelog izvoraJedan potpuni prolaz kroz izvorSamo ako je mapa bila postavljena prva i cijeli raspon je dostupan
Svaki izvor s /Encrypt unosomNijedanNijedanNikad; diskovna se razina zaobilazi
HotPDF mapa identiteta izvora za diskovnu render predmemoriju: LoadFromFile hashira veličinu, LastWriteTime i prvih i posljednjih 64 KiB, LoadFromStream i LoadFromRandomAccessSource hashiraju cijeli sadržaj samo kad je RenderCacheFolder bio postavljen prvi, i svaki /Encrypt trailer ne bilježi nikakav identitet
datoteke se otiskuju s krajeva jer se tamo nalaze zaglavlje, xref i trailer, tokovi plaćaju potpuni hash samo kad ste predmemoriju tražili prvi, i šifrirani dokumenti nikad se ne zapisuju na disk

Otisak datoteke namjerna je razmjena. Hashiranje 400 MB skeniranog arhiva u cijelosti pri svakom otvaranju može koštati više nego renderiranje dviju stranica koje korisnik stvarno gleda. Uzorkovana područja nisu proizvoljna: zaglavlje sjedi na početku datoteke, a trailer i posljednja cross-reference sekcija na kraju (ISO 32000-1 §7.5). Inkrementalna nadogradnja dodaje novo tijelo, cross-reference sekciju i trailer (§7.5.6), pa mijenja veličinu i rep u jednom. Potpuni prepis bilo kojim normalnim alatom mijenja vrijeme posljednjeg zapisa. Za datoteke do 128 KiB dva uzorka pokrivaju svaki bajt, pa su mali dokumenti djelotvorno hashirani u cijelosti

Preostali rizik jest promjena iste veličine na mjestu usred velike datoteke čiji pisac zatim vrati izvornu vremensku oznaku. To treba alat koji namjerno čuva vremena izmjena dok uređuje sadržaj, što je retko ali ne nemoguće, i u tom slučaju predmemorija poslužuje zastarjele stranice. Obrnuta strana je benigna: kopiranje datoteke na Windowsima obično čuva vrijeme posljednjeg zapisa, pa kopija dokumenta koji je već u predmemoriji pogađa iste unose, što je ispravno jer su bajtovi identični

Tokovi uopće nemaju vrijeme izmjene, pa je jedini pošteni identitet sadržaj. HotPDF plaća taj potpuni SHA-256 prolaz samo ako ste diskovnu predmemoriju zatražili prije učitavanja; svaki drugi pozivatelj LoadFromStream ne vidi dodatni trošak. To redoslijed dodjele svojstava čini nosivim:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Pogrešan redoslijed za tokove: hash sadržaja računa se samo kad je
  // mapa već postavljena, pa bi ovaj dokument zaobišao diskovnu razinu
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

  Pdf.RenderCacheFolder := CacheRoot; // postavi prvi
  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 rasponi još nisu dostupni) ne dobiva identitet, a ne hash djelomičnog sadržaja, i ako računanje identiteta padne iz bilo kojeg razloga učitavanje i dalje uspijeva; dokument jednostavno renderira bez diskovne razine

Što poništava unos HotPDF diskovne predmemorije?

HotPDF diskovni predmemorinski unos nikad se ne poništava brisanjem pri izmjeni; umjesto toga, uređivanje učitanog dokumenta ispada identitet dokumenta, pa se diskovna razina zaobilazi do kraja tog učitavanja, a spremljene stranice ostaju valjane za neizmijenjeni izvor. Unosi napuštaju disk samo kroz LRU i bajtne granice, pokvareni PNG ili promjenu schemata

Ključ opisuje izvor na disku, a ne graf objekata u memoriji. Jednom kad poštampite stranicu ili promijenite anotaciju, dokument se više ne poklapa s tim izvorom, pa ni čitanje ni pisanje pod njegovim ključem ne bi bilo ispravno. Od v2.770.140 i poništavanje na razini dokumenta i na razini stranice čisti identitet umjesto da dira mapu, i postoji drugi čuvar za izmjene koje nisu pozvale InvalidateRenderedPageCache: prije upotrebe diskovne razine THotPDF provjerava je li bilo koji učitani objekt prljav i tretira prljav dokument kao da nema identiteta

Render postavke rade obrnuto. Prebacivanje PageRenderBackend (ili poziv UseNativeGDIRenderBackend), i pozivanje ConfigureRenderICCWorkflow ili ClearRenderICCWorkflow, ispuštaju stranice u memoriji ali zadržavaju identitet, jer se dokument i dalje poklapa sa svojim izvorom. Te postavke mijenjaju piksele ne čineći dio varijante u memoriji, pa diskovni ključ upliće ime backenda, zastavicu black-point kompenzacije i SHA-256 sažetke ICC proof i izlaznih profila. Varijanta sama već pokriva namjeru boja, izlazno dithering, overprint pregled, način luminosity maske, fallback politiku i vidljivost svake neobavezne content grupe, pa prebacivanje sloja renderira u drugu mapu umjesto da prepiše zadani prikaz

HotPDF semantika poništavanja za RenderCacheFolder diskovnu predmemoriju: uređivanje učitanog dokumenta ili bilo kojeg prljavog objekta ispada identitet izvora pa se razina zaobilazi, promjena render backenda ili ICC workflowa zadržava identitet pod novim varijantnim ključem, a spremanje plus ponovno učitavanje dokumentu daje novi ključ
izmjena nikad ne briše spremljenu mapu, promjena postavki renderira pod drugim ključem, i samo spremanje plus ponovno učitavanje zaslužuje uređenom dokumentu svjež identitet

Da vratite uređeni dokument na diskovnu razinu, dajte mu novi identitet izvora spremanjem i učitavanjem rezultata:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Nakon uređivanja učitanog dokumenta: osvježite stranice u memoriji.
  // Identitet izvora već je nestao, pa se ništa ne čita iz niti
  // piše u diskovnu mapu izvornog dokumenta
  Pdf.InvalidateRenderedPageCache;

  // Spremljena datoteka ima novu veličinu i vrijeme posljednjeg zapisa, dakle novi
  // identitet; renderi nakon ovog učitavanja predmemoriraju se pod novim ključem
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Mapa izvornog dokumenta ostavljena je na miru i stari kroz RenderCacheMaxDocuments i RenderCacheMaxBytes poput bilo kojeg drugog unosa. Ako korisnik ponovno otvori neuređeni original, njegove su stranice još tamo

Sigurnosne granice: šifrirani izvori i povezane mape

HotPDF diskovna render predmemorija odbija dvije vrste ulaza namjerno: nikad ne zapisuje stranice šifriranog PDF-a na disk i nikad ne slijedi podmapu dokumenta koja je junction ili drugi reparse point. Oba pravila trguju pogocima predmemorije za necurenje podataka ili brisanje pogrešnih datoteka

Šifrirani PDF-ovi nikad se ne predmemoriraju na disku

Renderirana stranica dešifriran je sadržaj. Zapisivanje kao običan PNG u mapu predmemorije ostavilo bi čitljivu kopiju dokumenta zaštićenog lozinkom na disku, izvan zaštite koju je autor odabrao (ISO 32000-1 §7.6). HotPDF stoga ne bilježi identitet za nijedan izvor čiji trailer nosi /Encrypt unos, uključujući datoteke otvorene s lozinkom ili s praznom korisničkom lozinkom. Ti dokumenti i dalje koriste razinu u memoriji, koja umire s procesom

Junction podmape odbijaju se od v2.770.173

Korijen predmemorije vaš je izbor, i usmjeravanje na junction dopušteno je. Podmape dokumenata ispod njega druga su priča: predmemorija ih sama stvara, čita, dodiruje i briše, tijekom oporavka pri pokretanju (koji uklanja zaostale privremene datoteke), pretraživanja (koje ažurira vremenske oznake), spremanja, poništavanja i tri granice izbacivanja. Da netko s pravom pisanja na korijen predmemorije zamijeni mapu dokumenta junctionom na drugi direktorij, svaka bi od tih putanja slijedila njega, i izbacivanje bi brisalo datoteke negdje gdje predmemorija nikad nije vladala. Od v2.770.173 svaka od tih ulaznih točaka provjerava reparse-point atribut i preskače povezanu mapu dokumenta: pretraživanje broji promašaj, spremanje broji neuspjeh upisa, a izbacivanje je ostavlja na miru

Unicode putanje i dijeljeni korijeni

Dva povezana popravka važna su ako se raspoređujete na korisničke profile. Prije v2.770.135 RenderCacheFolder bio je AnsiString, pa je mapa izvan sistemske kodne stranice (kinesko korisničko ime na engleskoj Windows instalaciji, na primjer) gubivo pretvorena prije nego ju je predmemorija vidjela; svojstvo je sada Unicode string, a atomička zamjena koristi široki Windows API. Od v2.770.52 nekoliko THotPDF instanci u jednom procesu koje pokazuju na isti korijen (nakon proširenja putanje, uspoređeno bez razlikovanja velikih i malih slova) dijele jedan indeks s brojanjem referenci i jedan lock. Ranije je svaka instanca prepravljala index.txt svojom kopijom i provodila granice nad svojim djelomičnim prikazom, pa je mapa mogla narasti nekoliko puta preko proračuna

To dijeljenje staje na granici procesa. Dva odvojena procesa na istom korijenu i dalje drže odvojene indekse u memoriji, pa dajte svakoj istovremeno pokrenutoj aplikaciji vlastiti korijen predmemorije. Preglednici koji renderiraju na worker dretvama u redu su unutar jednog procesa: PrefetchLoadedPages i red opisan u pozadinskom renderiranju s redom zahtjeva oboje prolaze kroz isti predmemorirani put i isti lock

Brza referenca: RenderCacheFolder kontrolna lista

  • Postavite RenderCacheFolder, RenderCacheMaxDocuments i RenderCacheMaxBytes prije prvog poziva RenderLoadedPageToBitmapCached; za učitavanja tokova i random-access postavite mapu prije učitavanja
  • Nadogradite na v2.770.140 ili kasniji ako se oslanjate na diskovnu razinu; ranije verzije prihvaćaju svojstvo ali nikad ne poslužuju stranicu s diska za obična učitavanja
  • Očekujte bez diskovnog predmemoriranja za šifrirane PDF-ove, za dokumente uređene nakon učitavanja, ili dok RenderFallbackPolicy nije rfpIgnore
  • Oslobodite THotPDF instancu normalno; od v2.770.140 ni Free ni InvalidateRenderedPageCache ne brišu diskovne unose
  • Promjena PageRenderBackend ili ICC workflowa zadržava dokument na diskovnoj razini pod drugim ključem
  • Koristite jedan korijen predmemorije po pokrenutoj aplikaciji; instance unutar jednog procesa dijele indeks od v2.770.52
  • Držite korijen predmemorije na lokaciji po korisniku; podmape dokumenata koje su junctioni preskaču se od v2.770.173

Trajna predmemorija stranica najviše se isplati u pregledniku koji cijeli dan ponovno otvara iste dokumente, što je točno oblik arhitekture prilagođenog PDF preglednika u Delphiju opisanog drugdje na ovom blogu. RenderCacheFolder, raster predmemorija u memoriji i renderer stranica isporučuju se s HotPDF Delphi PDF komponentom za Delphi i C++Builder