Technikai cikk

PDF-néző lemezgyorsítótár és monitoronkénti DPI Delphi-ben

Minden beágyazott PDF-nézőt két panasz kísér: a nagy dokumentumok minden indításkor teljesen elölről renderelődnek újra, a szöveg pedig azonnal elmosódottá válik, amint az ablakot egy magas DPI-jű monitorra húzzák. A losLab PDF Library mindkettőre választ ad a TPDFlibViewer vezérlőjében: a DiskCacheFolder tulajdonság lehetővé teszi a dokumentum ujjlenyomata alapján kulcsolt, munkameneteken átívelő lemezes oldalgyorsítótárat, a ScreenDPI tulajdonság pedig – amelyet a GetDpiForWindow és egy WM_DPICHANGED kezelő támogat – valódi monitoronkénti DPI-tudatosságot biztosít a vezérlőnek.

Mindkét problémát érdemes komolyan venni, mert a felhasználók azonnal észreveszik őket. Egy 400 oldalas szkennelt szerződés, amelynek tegnap nyolc másodperc kellett ahhoz, hogy görgethetővé váljon, ma ismét nyolc másodpercet vesz igénybe, még akkor is, ha a fájlban semmi sem változott. Különböző DPI-jű monitorokból álló asztali környezetben pedig egy olyan néző, amely az elsődleges monitor 96 DPI-s felbontásával renderel, ott jól mutat, de a mellette lévő 4K-s panelen láthatóan homályos lesz. Ez a cikk végigvezeti Önt azon, hogyan oldja meg a TPDFlibViewer mindegyik problémát, és ami még hasznosabb, hogy a tervezés miért éppen így valósult meg. Ha még új Önnek a vezérlő, kezdje az interaktív PDF-néző vezérlő áttekintésével, majd térjen vissza a teljesítményre vonatkozó részhez.

Hogyan gyorsítótárazhatók a renderelt PDF-oldalak munkamenetek között Delphi-ben?

Rendeljen hozzá egy mappát a TPDFlibViewer.DiskCacheFolder tulajdonsághoz, és a vezérlő a renderelt oldalak bitképeit a lemezre menti, így a nemrég megtekintett dokumentum azonnal megnyílik az újrarenderelés helyett. A lemezen a struktúra a következő: <Folder>\PDFlibPas-PageCache\<dokumentum ujjlenyomata>\p<oldal>_z<nagyítási kulcs>.bin: dokumentumonként egy almappa, oldal- és nagyítás-kombinációnként pedig egy fájl. Minden más automatikus; nincs meghívandó gyorsítótár API, és nincs mit üríteni.

uses
  PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  // Egyetlen sor bekapcsolja a munkameneteken átívelő oldalgyorsítótárat
  FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
  FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;

A DiskCacheHits számláló jelzi, hogy a lemezes gyorsítótár hány oldalrenderelést spórolt meg az aktuális munkamenetben, ami a nyereséget mérhetővé teszi a puszta feltételezések helyett. Egy bezárt, majd újra megnyitott dokumentumnál minden olyan oldal, amely renderelés nélkül gördül be a nézetbe, növeli ezt az értéket.

Miért ujjlenyomat a gyorsítótár-kulcs, és miért nem verziószám?

A dokumentum ujjlenyomata egy 64 bites FNV-1a hash érték az útvonal|méret|utolsó módosítás ideje karakterláncból képezve, tizenhat hexadecimális számjegyként formázva. Ez a hármas alkotja a teljes érvénytelenítési stratégiát: a fájl szerkesztésekor a mérete vagy a módosítási ideje megváltozik, a hash megváltozik, és a néző egyszerűen egy másik gyorsítótár-almappát nyit meg. Az elavult mappát a rendszer soha többé nem használja, és végül az LRU eltávolítás áldozatává válik. Nincs fenntartandó verziócímke-protokoll, nincs elcsúszásra hajlamos időbélyeg-összehasonlítási logika, és kizárt, hogy egy régi bitkép jelenjen meg egy módosított dokumentumhoz.

Ez ugyanaz a „tartalom-identitás mint gyorsítótár-kulcs” elképzelés, amelyet a build rendszerek is használnak, és az általa kiküszöbölt hibaforrások révén bizonyítja be hasznosságát. Egy olyan gyorsítótárnek, amely a dokumentum egyszerű nevén tárolja a fájlkat, explicit módon kell észlelnie a szerkesztéseket, és minden ilyen explicit ellenőrzés potenciális hibaforrás: egy biztonsági mentésből visszaállított fájl régi időbélyeggel, egy olyan mentés, amely megtartja a méretet, vagy egy útvonal, amelyet az egyik gépen kis- és nagybetű-érzékenyen hasonlítanak össze, a másikon pedig nem. Mindhárom jelzés mappanévvé történő hashelése a lejártságot strukturálisan lehetetlenné teszi, ahelyett, hogy azt folyamatosan ellenőrizni kellene.

Miért maradt ki szándékosan a forgatás a lemezes kulcsból?

A gyorsítótár-fájl neve az oldalszámot és a nagyítást kódolja, és semmi mást – a nézet forgatási szöge szándékosan hiányzik. A lemezes gyorsítótár mindig a forgatás nélküli renderelési bájtokat tárolja; amikor egy gyorsítótárazott oldalt betöltenek a nézet forgatása közben, a képpontok tájolása a dekódolás után a memóriában történik meg. Ha a forgatás a lemezes kulcs része lenne, akkor a négy nézetszög között váltogató felhasználó ugyanazt az oldalt négyszer írná a lemezre, négyszerezve a gyorsítótár növekedését nulla plusz információért, mivel a forgatás olcsó pixel-átrendezés egy teljes renderelési fázishoz képest.

A memóriabeli bitkép-gyorsítótár összetett kulcsot használ (Round(Zoom * 1000) * 4 + Rotation div 90), mert ott a forgatott bitkép pontosan az, amire a rajzoló kezelőnek szüksége van. A háttérben futó előtöltő szál ugyanezt az elvet követi: mindig forgatás nélküli oldalakat renderel, és egyszerű nagyítási kulcsokat jelent, a főszál pedig az eredmények kiürítésekor alkalmazza a forgatást – ugyanaz a „főszál birtokolja a gyorsítótárat” szabály érvényesül, mint amelyet a párhuzamos oldalrenderelés és szálbiztonság című cikkben tárgyaltunk. A forgatás távol tartása a memória réteg alatti minden szinttől egyetlen kanonikus renderelést jelent oldalonként és nagyításonként, minden olyan helyen, ahol a tárolás költséges.

Hogyan tartja korlátok között az LRU eltávolítás a gyorsítótár-mappát?

A gyorsítótár legfeljebb tíz dokumentummappát tart meg, és az új dokumentumok érkezésekor eltávolítja a legrégebben használtakat. Az utolsó használat idejét egy last-used.marker fájl követi nyomon minden dokumentummappában, amelynek időbélyegét a gyorsítótárba történő íráskor és olvasáskor egyaránt frissíti a rendszer, így az a dokumentum, amelyet csak újraolvas, aktív marad anélkül, hogy újraíródna. Az éppen megnyitott dokumentumhoz tartozó mappa mentesül a törlés alól, így a tisztítás soha nem távolíthat el oldalakat az aktív nézet alól.

A gyorsítótár minden lemezművelete – az ujjlenyomat-vétel, a mappa létrehozása, a jelölőfájl frissítése, az oldalak olvasása és írása, valamint a tisztítás – elnyeli a saját kivételeit. Ez szándékos magatartás, nem pedig hanyagság: a gyorsítótár egy „legjobb törekvés” típusú gyorsító, és a megtelt lemez, az írásvédett mappa vagy az víruskereső zárolása esetén a nézőnek a „renderel, mint korábban” állapotra kell visszaesnie, nem pedig a „nem tudja megjeleníteni az oldalt” hibára. A legrosszabb eset, amit egy gyorsítótár IO-hiba okozhat, egy elszalasztott találat (missed hit).

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // Hány renderelési fázist spórolt meg a lemezes gyorsítótár ebben a munkamenetben?
  Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;

Hogyan tehető a PDF-néző DPI-tudatossá Delphi-ben?

A TPDFlibViewer minden DPI-függő számítást egyetlen mezőn keresztül irányít, és ez az egyetlen ponton történő változtatási tervezés teszi kezelhetővé a monitoronkénti támogatást. Az elrendezés, az előtöltés és a renderelő kód mind egyetlen FScreenDPI értéket olvas; 100%-os nagyításnál egy PDF-pont ScreenDPI / 72 képpontnak felel meg. A publikus ScreenDPI tulajdonság beállítója kiüríti a bitkép-gyorsítótárat, újraépíti az elrendezést, frissíti a görgetési tartományokat és érvényteleníti az ablakot, így a DPI megváltoztatása egyetlen hozzárendelés, amelyet minden fogyasztó automatikusan követ. Ugyanez a képpont-matematika számít akkor is, ha monitor helyett nyomtatóra renderel, ezt a témát a nyomtatási előnézet és eszközkontextus renderelés című cikk tárgyalja.

Az észlelés a CreateWnd felülírásában történik a konstruktor helyett, mivel a GetDpiForWindow-nak érvényes ablakkezelőre (window handle) van szüksége, a konstruktor pedig jóval ennek létezése előtt fut le. Amíg a CreateWnd le nem fut, a vezérlő megtartja a hagyományos, alapértelmezett 96-os értéket, hogy az elrendezés matematikája ésszerű maradjon. Maga a GetDpiForWindow egy Windows 10 1607+ API, így a vezérlő dinamikusan köti azt a GetProcAddress segítségével, és a régebbi rendszereken a GetDeviceCaps(LOGPIXELSY)-re esik vissza – az egység így továbbra is mindenhol betöltődik, a régebbi Windows pedig egyszerűen a rendszer DPI-jét jelenti.

Mi történik, ha az ablak egy 4K-s monitorra kerül?

A Windows WM_DPICHANGED üzenetet küld, amikor egy monitoronkénti DPI-tudatos ablak átkerül egy eltérő méretezési tényezőjű monitorra, és a TPDFlibViewer ezt közvetlenül kezeli. A kezelő kiolvassa az új DPI-t a wParam alsó szavából, hozzárendeli a SetScreenDPI-n keresztül – ami eldobja a most már hibás bitkép-gyorsítótárat és újratervezi az oldalakat –, majd elfogadja a javasolt ablaktéglalapot, amelyet a Windows az lParam paraméterben ad át a SetWindowPos segítségével, ami stabilan tartja a logikai ügyfélméretet a mozgatás során. A felhasználó azt látja, hogy a dokumentum élesen renderelődik le az új méretarányban az operációs rendszer által nyújtott bitkép helyett.

// Normális esetben soha nem kell hozzányúlni a ScreenDPI-hez: a CreateWnd észleli a gazdagépet
// és a WM_DPICHANGED követi a mozgásokat. Csak speciális célok esetén írja felül,
// pl. az elrendezés renderelése úgy, mintha egy 150%-os kijelzőre történne:
FViewer.ScreenDPI := 144;  // kiüríti a bitkép-gyorsítótárat és újraépíti az elrendezést

Kiszállítás előtt érdemes ismerni a korlátokat

Három gyakorlati korlát érdemel helyet a tervezési felülvizsgálatban. Először is, a WM_DPICHANGED csak akkor érkezik meg, ha a folyamat hozzájárul: az alkalmazás manifestjének deklarálnia kell a monitoronkénti DPI-tudatosságot (per-monitor v2 a jelenlegi Windows rendszereken), ami a Delphi IDE-ben az Application beállítások alatti DPI-tudatosság beállítást jelenti. A rendszer-DPI-tudatos folyamat soha nem kapja meg ezt az üzenetet, és a vezérlő ekkor az ablak létrehozásakor észlelt DPI-vel renderel. Másodszor, a lemezes gyorsítótár a lemezterületet cseréli el sebességre – tíz dokumentumnyi oldalbitkép több nagyítási szinten valós tárhelyet jelent, ezért a DiskCacheFolder-t olyan helyre irányítsa, amelynek növekedésével elégedett, és ne egy roaming profilra. Harmadszor, a gyorsítótár-mappának írási engedélyre van szüksége a futó felhasználó számára; ennek hiányában a néző továbbra is működik, csendben, nulla találattal – teszteléskor ellenőrizze a DiskCacheHits értéket, ha azt gyanítja, hogy a gyorsítótár nem lép működésbe.

A két funkció együttesen jobban megváltoztatja a dokumentum-intenzív alkalmazások észlelt minőségét, mint a legtöbb renderelési optimalizálás: a felhasználók által újra meglátogatott dokumentumok azonnal megnyílnak, és a nézet éles marad bármelyik monitoron is jelenjen meg. Mindkét funkció a TPDFlibViewer szokásos tulajdonságaként érhető el a losLab PDF Library termékben, így bevezetésük egy délutáni bekötési munka, nem pedig egy renderelőmotor-projekt.