Technický článek

Disková mezipaměť prohlížeče PDF a DPI pro jednotlivé monitory v Delphi

Každý vestavěný prohlížeč PDF provázejí dvě stížnosti: velké dokumenty se při každém spuštění vykreslují znovu od začátku a text se rozmaže v okamžiku, kdy je okno přetaženo na monitor s vysokým DPI. losLab PDF Library řeší obojí ve svém ovládacím prvku TPDFlibViewer: vlastnost DiskCacheFolder umožňuje diskovou mezipaměť stránek mezi relacemi klíčovanou otiskem (fingerprint) dokumentu a vlastnost ScreenDPI, podporovaná funkcí GetDpiForWindow a obsluhou zpráv WM_DPICHANGED, poskytuje ovládacímu prvku skutečnou podporu DPI pro jednotlivé monitory (per-monitor DPI)

Oba problémy stojí za to brát vážně, protože si jich uživatelé všimnou okamžitě. Naskenovaná smlouva o 400 stránkách, jejíž posouvání včera trvalo osm sekund, dnes trvá opět osm sekund, i když se v souboru nic nezměnilo. A na ploše s kombinovaným DPI vypadá prohlížeč, který se vykresluje při 96 DPI primárního monitoru, na něm dobře, ale na vedlejším 4K panelu je viditelně neostrý. Tento článek vás provede tím, jak TPDFlibViewer řeší každý z těchto problémů, a co je užitečnější, proč návrh skončil právě u tohoto řešení. Pokud je pro vás samotný ovládací prvek nový, začněte přehledem o interaktivním komponentu prohlížeče PDF a poté se vraťte k této výkonnostní vrstvě

Jak v Delphi ukládat vykreslené stránky PDF do mezipaměti mezi relacemi?

Přiřaďte složku vlastnosti TPDFlibViewer.DiskCacheFolder a ovládací prvek uloží vykreslené bitmapy stránek na disk, takže nedávno prohlížený dokument se otevře okamžitě namísto opětovného vykreslování. Struktura na disku je <Folder>\PDFlibPas-PageCache\<otisk dokumentu>\p<stranka>_z<klic_priblizeni>.bin: jedna podsložka pro každý dokument, jeden soubor pro každou kombinaci stránky a přiblížení. Vše ostatní je automatické; není třeba volat žádné rozhraní API mezipaměti ani nic vyprazdňovat

uses
  PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  // One line turns on the cross-session page cache
  FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
  FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;

Počítadlo DiskCacheHits hlásí, kolik vykreslení stránek ušetřila disková mezipaměť v aktuální relaci, což činí přínos měřitelným a nikoli jen domnělým. U dokumentu, který jste zavřeli a znovu otevřeli, jej zvyšuje každá stránka, která se posune do zobrazení bez nutnosti vykreslení

Proč je klíčem mezipaměti otisk, a nikoli číslo verze

Otisk (fingerprint) dokumentu je 64bitový hash FNV-1a z řetězce cesta|velikost|čas posledního zápisu, formátovaný jako šestnáctková čísla o délce 16 znaků. Tato trojice představuje celou strategii zneplatnění mezipaměti: při úpravě souboru se změní jeho velikost nebo čas zápisu, změní se hash a prohlížeč jednoduše otevře jinou podsložku mezipaměti. Stará složka již není nikdy konzultována a časem podlehne prořezávání LRU. Není třeba udržovat žádný protokol verzí, žádná porovnávací logika časových razítek se nemůže odchýlit a stará bitmapa nemůže být nikdy použita pro upravený dokument

Jedná se o stejnou myšlenku „identita obsahu jako klíč mezipaměti“, kterou používají sestavovací systémy, a svou hodnotu prokazuje v chybových stavech, které eliminuje. Mezipaměť, která ukládá soubory pod prostým názvem dokumentu, musí úpravy detekovat explicitně a každá explicitní kontrola je chybou čekající na okrajový případ: soubor obnovený ze zálohy se starým časovým razítkem, uložení zachovávající velikost, cesta porovnávaná s ohledem na velikost písmen na jednom stroji a bez ohledu na ni na druhém. Zašifrování všech tří signálů do názvu složky činí zastaralost strukturálně nemožnou, namísto abychom ji procedurálně kontrolovali

Proč je otočení záměrně vynecháno z diskového klíče

Název souboru mezipaměti kóduje číslo stránky a přiblížení, nic jiného — úhel otočení pohledu záměrně chybí. Disková mezipaměť vždy ukládá neotočené vykreslené bajty; při načtení stránky z mezipaměti během otočeného pohledu se pixely po dekódování přeorientují v paměti. Pokud by otočení bylo součástí diskového klíče, uživatel procházející čtyři úhly pohledu by zapsal stejnou stránku na disk čtyřikrát, což by zečtyřnásobilo růst mezipaměti bez jakékoli dodatečné informace, protože otočení je ve srovnání s plným vykreslením pouze nenáročné přeskupení pixelů

Mezipaměť bitmap v paměti používá složený klíč Round(Zoom * 1000) * 4 + Rotation div 90, protože tam je otočená bitmapa přesně tím, co obsluha vykreslování potřebuje. Prefetch vlákno na pozadí dodržuje stejnou zásadu: vždy vykresluje neotočené stránky a hlásí prosté klíče přiblížení, a hlavní vlákno aplikuje otočení při zpracování výsledků — stejné pravidlo, že hlavní vlákno vlastní mezipaměť, které je rozebráno v článku o paralelním vykreslování stránek a bezpečnosti vláken. Ponechání otočení mimo vše pod paměťovou vrstvou znamená jedno kanonické vykreslení na stránku a přiblížení všude tam, kde je ukládání náročné

Jak prořezávání LRU udržuje velikost složky mezipaměti pod kontrolou?

Mezipaměť uchovává až deset složek dokumentů a při příchodu nových dokumentů prořezává ty nejméně používané. Aktuálnost se sleduje pomocí souboru last-used.marker v každé složce dokumentu, jehož časové razítko se aktualizuje při zápisu i čtení z mezipaměti, takže dokument, který pouze znovu čtete, zůstává aktivní, aniž by musel být znovu zapisován. Složka patřící aktuálně otevřenému dokumentu je z mazání vyloučena, takže prořezávání nemůže nikdy smazat stránky pod aktivním zobrazením

Každá disková operace v mezipaměti — vytváření otisků, vytváření složek, aktualizace značek, čtení a zápis stránek, prořezávání — zachytává své vlastní výjimky. To je záměrný postoj, nikoli nedbalost: mezipaměť je akcelerátor s nejlepším úsilím a plný disk, složka pouze pro čtení nebo zámek antiviru musí prohlížeč degradovat na „vykresluje se jako dříve“, nikdy na „selhání zobrazení stránky“. Nejhorším výsledkem, který může chyba IO mezipaměti způsobit, je neúspěšný zásah do mezipaměti (missed hit)

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // How many render passes did the disk cache save this session?
  Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;

Jak v Delphi vytvořit prohlížeč PDF s podporou DPI?

Komponenta TPDFlibViewer směřuje každý výpočet závislý na DPI přes jediné pole a tento návrh s jediným místem změny činí podporu pro jednotlivé monitory snadno zvládnutelnou. Kód pro rozvržení, prefetch a vykreslování načítá jedinou hodnotu FScreenDPI; při 100% přiblížení se jeden bod PDF mapuje na ScreenDPI / 72 pixelů. Veřejný setter vlastnosti ScreenDPI vymaže mezipaměť bitmap, znovu sestaví rozvržení, aktualizuje rozsahy posouvání a zneplatní okno, takže změna DPI představuje jedno přiřazení, po kterém všichni spotřebitelé následují automaticky. Stejná matematika pixelů je důležitá, když vykreslujete pro tiskárnu místo monitoru, což je téma popsané v článku o náhledu tisku a vykreslování kontextu zařízení

Detekce probíhá v metodě CreateWnd namísto konstruktoru, protože GetDpiForWindow vyžaduje platný popisovač okna (handle) a konstruktor se spouští dlouho předtím, než nějaký existuje. Dokud se nespustí CreateWnd, ovládací prvek udržuje výchozí hodnotu 96, aby matematika rozvržení zůstala konzistentní. Samotná funkce GetDpiForWindow je API systému Windows 10 verze 1607+, takže ji ovládací prvek váže dynamicky pomocí GetProcAddress a na starších systémech se vrací k GetDeviceCaps(LOGPIXELSY) — unita se tak stále načte všude a starší systémy Windows jednoduše hlásí systémové DPI

Co se stane, když se okno přesune na 4K monitor?

Systém Windows odešle zprávu WM_DPICHANGED, když okno podporující DPI pro jednotlivé monitory přejde na monitor s jiným měřítkem, a TPDFlibViewer ji zpracuje přímo. Obsluha přečte nové DPI z nižšího slova wParam, přiřadí jej prostřednictvím SetScreenDPI — čímž zahodí nyní nesprávnou mezipaměť bitmap a přepočítá rozvržení stránek — a poté přijme navrhovaný obdélník okna, který Windows předávají v lParam pomocí SetWindowPos, což udržuje logickou velikost klienta během přesunu stabilní. Uživatel vidí, že se dokument na novém měřítku vykresluje ostře, namísto toho, aby byl operačním systémem roztažen jako bitmapa

// Normally you never touch ScreenDPI: CreateWnd detects the host monitor
// and WM_DPICHANGED tracks moves. Override it only for special targets,
// e.g. rendering the layout as if for a 150% display:
FViewer.ScreenDPI := 144;  // clears the bitmap cache and rebuilds layout

Omezení, o kterých byste měli vědět před nasazením

Ve vašem posouzení návrhu si zaslouží místo tři praktická omezení. Za prvé, zpráva WM_DPICHANGED přichází pouze v případě, že se k ní proces přihlásí: manifest vaší aplikace musí deklarovat podporu DPI pro jednotlivé monitory (per-monitor v2 v aktuálním Windows), což je v Delphi IDE nastavení DPI awareness v možnostech aplikace (Application options). Proces podporující pouze systémové DPI tuto zprávu nikdy neobdrží a ovládací prvek se pak vykresluje při DPI detekovaném při vytváření okna. Za druhé, disková mezipaměť mění diskový prostor za rychlost — bitmapy stránek pro deset dokumentů napříč několika úrovněmi přiblížení představují reálné úložiště, takže nasměrujte DiskCacheFolder do umístění, kde vám nevadí jeho růst, nikoli do cestovního profilu (roaming profile). Za třetí, složka mezipaměti vyžaduje oprávnění k zápisu pro spuštěného uživatele; bez něj prohlížeč stále funguje, tiše, ale s nulovým počtem zásahů (hits) — pokud máte podezření, že se mezipaměť nezapojuje, zkontrolujte při testování hodnotu DiskCacheHits

Obě funkce dohromady mění vnímanou kvalitu dokumentově náročné aplikace více než většina optimalizací vykreslování: dokumenty, které uživatelé navštěvují opakovaně, se otevírají okamžitě a zobrazení zůstává ostré na kterémkoli monitoru, na kterém přistane. Obojí se dodává jako běžné vlastnosti komponentu TPDFlibViewer v knihovně losLab PDF Library, so adopting them is an afternoon of wiring rather than a rendering-engine project