Teknisk artikel

HotPDF RenderCacheFolder: en disk-sidecache i Delphi

HotPDF RenderCacheFolder gør den in-memory rendered-side-cache i HotPDF Delphi-komponenten til en persistent diskens sidecache: renderede sider skrives som PNG-filer under en folder, du vælger, og næste gang samme PDF-kilde åbnes, læser RenderLoadedPageToBitmapCached dem tilbage i stedet for at rasterisere igen. Opslagsrækkefølgen er hukommelse, så disk, så rendereren

Disk-laget har været i API'et siden v2.416.0, men indtil v2.770.140 serverede det faktisk aldrig en side for et almindeligt LoadFromFile- eller LoadFromStream-kald. Fixet tvang et spørgsmål frem, enhver persistent cache må svare på: hvordan ved du, at filen, du åbnede i dag, er det dokument, du renderede i går, og hvad sker der med de cachede sider, når den ikke er det? Nedenfor er svarene, HotPDF landede på, inklusive hvor den bevidst nægter at cache

Hvordan virker HotPDF'ens disk render cache?

HotPDF'ens disk render cache er et andet lag bag den in-memory raster-cache, og den deltager kun, når RenderCacheFolder er en ikke-tom sti. Et kald til RenderLoadedPageToBitmapCached(PageIndex, DPI) scanner først de in-memory-poster, nøglet efter sideindeks, DPI og en render-indstillingsvariant. Ved et miss spørger den disk-laget; et disk-hit dekoder PNG'en, promoverer den tilbage i hukommelsen og returnerer en kalder-ejet kopi. Først når begge lag misser, går siden gennem content stream-tolken, der er beskrevet i rendering af en indlæst PDF-side til en TBitmap, og den friske bitmap skrives så også til disk

HotPDF-diagram over render cache-opslaget for RenderLoadedPageToBitmapCached: in-memory-laget, nøglet efter side, DPI og render-variant, tjekkes først, derefter RenderCacheFolder-disk-laget af PNG-filer med atomar erstatning, derefter content stream-tolken, og hvert hit returnerer en kalder-ejet kopi
HotPDF kigger først i hukommelsen, så på disken, og rasteriserer først derefter; et disk-hit promoveres tilbage i hukommelsen, og hver vej giver dig en kopi, du ejer og skal frigøre

På disken er layoutet bevidst kedeligt. Hvert dokument får en underfolder opkaldt efter en dokumentnøgle på 16 hex-tegn plus en render-variant på 16 hex-tegn, hver side gemmes som <page>@<dpi>.png, og en index.txt i roden holder dokumenter i senest-anvendt-rækkefølge bag et schema-tag. Et schema mismatch rydder folderen ved første anvendelse. Skrivninger går først til en midlertidig fil og swappes på plads med en atomar erstatning, så et crash midt i en skrivning efterlader enten den gamle side eller intet, aldrig en halv PNG. En PNG, der fejler at dekode, slettes og tæller som et miss

Tre grænser afgrænser folderen:

  • RenderCacheMaxDocuments (default 20) sætter loft over antallet af dokumentunderfoldere; den mindst nyligt anvendte folder evictes først
  • RenderCacheMaxBytes (default 524288000, hvilket er 500 MB) sætter loft over den samlede størrelse af alle PNG-filer under roden
  • Hver dokumentfolder holder højst 200 sidebilleder; det pr. dokument-loft er fastsat af THotPDF og er ikke en publiceret property

RenderCacheCapacity (default 8) er en separat knap: den sætter, hvor mange renderede sider in-memory-laget holder, og den har intet at gøre med disk-fodaftrykket

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Konfigurér disk-laget før den første cachede render:
    // folderen og begge grænser læses, når laget bruges første gang
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // sider i hukommelsen

    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
          // Giv kopien videre til thumbnail-strippen her
        finally
          Bmp.Free; // det cachede kald returnerer altid en kalder-ejet kopi
        end;
      end;
  finally
    Pdf.Free; // siden v2.770.140 sletter dette ikke længere disk-posterne
  end;
end;

Kør samme procedure to gange, og anden kørsel rasteriserer aldrig en side, der var plads til i cachen. Disk-cache-objektet oprettes lazy ved den første cachede render og lever, til THotPDF-instansen frigøres, så at ændre RenderCacheFolder, RenderCacheMaxDocuments eller RenderCacheMaxBytes efter det punkt flytter eller resizer ikke en allerede åben cache. Sider, der er for store til in-memory-adgangspolitikken (som default må en enkelt post ikke overstige 64 MiB af 32-bit pixels), persisteres heller ikke, og disk-laget konsulteres kun, mens RenderFallbackPolicy beholder sin default rfpIgnore, for fallback-diagnostik gemmes ikke sammen med PNG'en

Hvorfor virkede RenderCacheFolder aldrig før v2.770.140?

RenderCacheFolder havde ingen effekt før v2.770.140, fordi disk-laget nøglede dokumenter på en hash af source-bytes, som almindelige indlæsninger aldrig gemte. Dokumentnøglen kom fra en SHA-256 over en intern kopi af de rå PDF-bytes, men LoadFromFile og LoadFromStream parser kilden på stedet og beholder ikke en sådan kopi; feltet blev kun udfyldt midlertidigt på en encrypted-recovery-vej og ryddet igen lige efter. Uden bytes var nøglen altid tom, og en tom nøgle betyder, at disk-laget bypasses. Ingen fejl, ingen advarsel, bare en folder, der forblev tom

At gøre nøglen ikke-tom afslørede en anden bug, der havde gemt sig bag den første. Den gamle InvalidateRenderedPageCache slettede dokumentets disk-folder, og InvalidateRenderedPageCache kører ved starten af hvert indlæs, ved hver redigering og inde i Free. Så i det øjeblik nøglen virkede, ville hver viewer-session have ødelagt sin egen cache ved exit, og næste session ville være startet kold alligevel. Værre: nøglen blev genberegnet fra samme kilde efter en redigering, så renders af det redigerede dokument ville være blevet gemt under originalfilens nøgle og serveret til næste session, der åbnede den umodificerede PDF. v2.770.140 retter identiteten og invalidationen sammen; at rette kun én af dem ville have skibet enten en død cache eller en løgnagtig en

Hvordan HotPDF identificerer en PDF uden at læse hele filen

HotPDF identificerer en PDF indlæst fra en lokal fil ved et fingeraftryk af dens størrelse, dens last-write time og dens første og sidste 64 KiB, og identificerer en stream eller random access-kilde ved en SHA-256 over hele dens indhold. Begge fanges én gang, når et indlæs lykkes, og de første 16 hex-tegn i SHA-256-digesten (64 bit) bliver dokumentnøglen

KildeIdentitetOmkostningHvornår den optages
LoadFromFileStørrelse + LastWriteTime + første og sidste 64 KiB, hasheret med SHA-256Højst 128 KiB læst, uafhængigt af filstørrelsenHvert vellykket indlæs, selv hvis RenderCacheFolder sættes senere
LoadFromStreamSHA-256 over hele streamenÉn fuld gennemkørsel af kildenKun hvis RenderCacheFolder var sat før indlæsningen
LoadFromRandomAccessSourceSHA-256 over hele kildenÉn fuld gennemkørsel af kildenKun hvis folderen var sat først, og hele intervallet er tilgængeligt
Enhver kilde med en /Encrypt-postIngenIngenAldrig; disk-laget bypasses
HotPDF'ens kildeidentitets-kort for disk render cachen: LoadFromFile hasherer størrelse, LastWriteTime og første og sidste 64 KiB, LoadFromStream og LoadFromRandomAccessSource hasherer hele indholdet, kun når RenderCacheFolder var sat først, og enhver /Encrypt-trailer fanger slet ingen identitet
filer fingerprintes fra deres ender, for header, xref og trailer ligger dér, streams betaler kun for en fuld hash, når du bad om cachen først, og krypterede dokumenter skrives aldrig til disk

Filfingeraftrykket er en bevidst afvejning. At hashe et scannet arkiv på 400 MB fuldt ud ved hver åbning kan koste mere end at rendere de to sider, en bruger faktisk kigger på. De samples regioner er ikke vilkårlige: headeren ligger i starten af filen, og trailer og sidste cross-reference-sektion ligger i enden (ISO 32000-1 §7.5). En inkrementel opdatering tilføjer en ny body, cross-reference-sektion og trailer (§7.5.6), så den ændrer størrelsen og halen på én gang. En fuld omskrivning af ethvert almindeligt værktøj ændrer last-write time. For filer op til 128 KiB dækker de to samples hver byte, så små dokumenter hashes i praksis fuldt ud

Restrisikoen er en ligestørrelses-ændring på stedet midt i en stor fil, hvis writer bagefter gendanner det oprindelige tidsstempel. Det behøver et værktøj, der bevidst bevarer ændringstidspunkter, mens det redigerer indhold, hvilket er sjældent, men ikke umuligt, og i det tilfælde serverer cachen forældede sider. Den anden side er benign: at kopiere en fil på Windows bevarer normalt dens last-write time, så en kopi af et dokument, der allerede er i cachen, rammer samme poster, hvilket er korrekt, fordi bytes er identiske

Streams har slet ingen ændringstid, så den eneste ærlige identitet er indholdet. HotPDF betaler kun for den fulde SHA-256-gennemkørsel, når du har bedt om en disk-cache før indlæsning; alle andre kaldere af LoadFromStream ser ingen ekstra omkostning. Det gør property-tildelingens rækkefølge load-bearing:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Forkert rækkefølge for streams: indholdshashen beregnes kun, når
  // folderen allerede er sat, så dette dokument ville bypass-e disk-laget
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

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

En random access-kilde, der stadig downloader (nogle intervaller endnu ikke tilgængelige), får ingen identitet frem for en hash af delvist indhold, og fejler beregningen af identiteten af en eller anden grund, lykkes indlæsningen stadig; dokumentet renderer simpelthen uden disk-laget

Hvad invaliderer en HotPDF disk cache-post?

En HotPDF disk cache-post invalideres aldrig ved at blive slettet ved redigering; i stedet kaster redigering af det indlæste dokument dokumentidentiteten, så disk-laget bypasses for resten af det indlæs, og de gemte sider forbliver gyldige for den umodificerede kilde. Poster forlader disken kun gennem LRU- og byte-grænserne, en korrupt PNG eller et schema-skifte

Nøglen beskriver en kilde på disk, ikke objektgrafen i hukommelsen. Når du først stempler en side eller ændrer en annotation, matcher dokumentet ikke længere den kilde, så hverken at læse eller skrive under dens nøgle ville være korrekt. Siden v2.770.140 rydder både dokumentniveau- og sideniveau-invalidation identiteten i stedet for at røre folderen, og der er en anden vagt for redigeringer, der ikke kaldte InvalidateRenderedPageCache: før disk-laget bruges, tjekker THotPDF, om noget indlæst objekt er dirty, og behandler et dirty dokument som uden identitet

Render-indstillinger virker den anden vej. At skifte PageRenderBackend (eller kalde UseNativeGDIRenderBackend), og at kalde ConfigureRenderICCWorkflow eller ClearRenderICCWorkflow, flusher siderne i hukommelsen men beholder identiteten, for dokumentet matcher stadig sin kilde. De indstillinger ændrer pixels uden at være en del af in-memory-varianten, så disk-nøglen folder backend-navnet, black-point compensation-flagget og SHA-256-digests af ICC proof- og output-profilerne ind. Varianten dækker allerede color intent, output dithering, overprint preview, luminosity mask mode, fallback-politik og synligheden af hver valgfri content group, så at toggle et lag renderer ind i en anden folder i stedet for at overskrive default-visningen

HotPDF'ens invalidations-semantik for RenderCacheFolder-disk-cachen: redigering af det indlæste dokument eller et dirty objekt kaster kildeidentiteten, så laget bypasses, skift af render-backend eller ICC-workflow beholder identiteten under en ny variantnøgle, og gemning plus genindlæsning giver dokumentet en ny nøgle
en redigering sletter aldrig den gemte folder, et indstillingsskifte renderer under en anden nøgle, og kun gemning plus genindlæsning giver det redigerede dokument en frisk identitet

For at få et redigeret dokument tilbage på disk-laget, giv det en ny kildeidentitet ved at gemme det og indlæse resultatet:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Efter redigering af det indlæste dokument: genopfrisk siderne i hukommelsen.
  // Kildeidentiteten er allerede væk, så intet læses fra eller
  // skrives til originaldokumentets disk-folder
  Pdf.InvalidateRenderedPageCache;

  // En gemt fil har ny størrelse og ny last-write time, altså en ny
  // identitet; renders efter dette indlæs caches under den nye nøgle
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Originaldokumentets folder bliver stående og ældes ud gennem RenderCacheMaxDocuments og RenderCacheMaxBytes som enhver anden post. Genåbner brugeren den uredigerede original, er dens sider der stadig

Sikkerhedsgrænser: krypterede kilder og linkede foldere

HotPDF'ens disk render cache nægter to slags input med vilje: den skriver aldrig sider af en krypteret PDF til disk, og den følger aldrig en dokumentunderfolder, der er en junction eller et andet reparse point. Begge regler bytter cache-hit for ikke at lække data eller slette de forkerte filer

Krypterede PDF'er caches aldrig på disk

En renderet side er dekrypteret indhold. At skrive den som en ren PNG ind i en cache-folder ville efterlade en læsbar kopi af et adgangskodebeskyttet dokument på disken, uden for den beskyttelse, forfatteren valgte (ISO 32000-1 §7.6). HotPDF fanger derfor ingen identitet for nogen kilde, hvis trailer bærer en /Encrypt-post, inklusive filer åbnet med en adgangskode eller med en tom brugeradgangskode. De dokumenter bruger stadig in-memory-laget, som dør med processen

Junction-underfoldere afvises siden v2.770.173

Cache-roden er dit valg, og at pege den på en junction er tilladt. Dokumentunderfolderne under den er en anden sag: cachen opretter, læser, rører og sletter dem på egen hånd, under startup recovery (som fjerner resterende midlertidige filer), opslag (som opdaterer tidsstempler), gemning, invalidation og de tre eviction-grænser. Erstatter en med skriveadgang til cache-roden en dokumentfolder med en junction til en anden mappe, ville hver af de veje følge den, og eviction ville slette filer et sted, cachen aldrig ejede. Siden v2.770.173 tjekker hver af de indgange reparse-point-attributten og springer en linket dokumentfolder over: et opslag tæller et miss, en gemning tæller en skrivefejl, og eviction lader den være

Unicode-stier og delte rødder

To relaterede fixes betyder noget, hvis du deployer til brugerprofiler. Før v2.770.135 var RenderCacheFolder en AnsiString, så en folder uden for systemets kodepage (et kinesisk brugernavn på en engelsk Windows-installation, for eksempel) blev konverteret med tab, inden cachen så den; propertyen er nu en Unicode string, og den atomare erstatning bruger den brede Windows API. Siden v2.770.52 deler flere THotPDF-instanser i én proces, der peger på samme rod (efter sti-udvidelse, sammenlignet case-insensitivt), ét enkelt reference-talt indeks og lock. Tidligere overskrev hver instans index.txt med sin egen kopi og håndhævede grænserne mod sit delvise view, så folderen kunne vokse flere gange forbi sit budget

Delingen stopper ved procesgrænsen. To separate processer på samme rod holder stadig separate in-memory-indekser, så giv hver samtidigt kørende applikation sin egen cache-rod. Viewers, der renderer på worker-threads, er fine inden for én proces: PrefetchLoadedPages og køen, der er dækket i background rendering med en request-kø, går begge gennem samme cachede vej og samme lock

Hurtig reference: RenderCacheFolder-tjekliste

  • Sæt RenderCacheFolder, RenderCacheMaxDocuments og RenderCacheMaxBytes før det første kald til RenderLoadedPageToBitmapCached; for stream- og random access-indlæsninger, sæt folderen før indlæsning
  • Opgradér til v2.770.140 eller senere, hvis du er afhængig af disk-laget; tidligere versioner accepterer propertyen men serverer aldrig en side fra disken ved almindelige indlæsninger
  • Forvent ingen disk-caching for krypterede PDF'er, for dokumenter redigeret efter indlæsningen, eller mens RenderFallbackPolicy ikke er rfpIgnore
  • Frigør THotPDF-instansen normalt; siden v2.770.140 sletter hverken Free eller InvalidateRenderedPageCache disk-poster
  • At skifte PageRenderBackend eller ICC-workflowet beholder dokumentet på disk-laget under en anden nøgle
  • Brug én cache-rod pr. kørende applikation; instanser inden for én proces deler indekset siden v2.770.52
  • Hold cache-roden et sted pr. bruger; dokumentunderfoldere, der er junctions, skippes siden v2.770.173

En persistent sidecache betaler sig mest i en viewer, der genåbner de samme dokumenter hele dagen, hvilket er præcis formen på den tilpassede PDF-viewer-arkitektur i Delphi, beskrevet andre steder på denne blog. RenderCacheFolder, den in-memory raster-cache og side-rendereren følger med HotPDF Delphi PDF-komponenten til Delphi og C++Builder