Technický článek

Prohlížeč PDFium v Delphi: Vyrovnávací paměť a plynulé přiblížení

Podržte tlačítko zvětšení v naivním prohlížeči PDF a sledujte graf CPU. Jediné stisknutí automaticky opakujícího se ovládání zvětšení vypálí tucet nebo více kroků zvětšení za sekundu, a pokud každý krok spustí plnohodnotné překreslení viditelné stránky, vykreslování se hromadí rychleji, než stihne doběhnout. Stránka se izolovaně vykreslí v pořádku, možná za 180 ms u naskenovaného A4, ale nyní běží tucet 180ms vykreslení proti práci, kterou už uživatel dávno opustil. Prohlížeč se zasekne, jedno jádro se zafixuje na 100 %, a než obrazovka dožene dění, uživatel se zastavil na úrovni zvětšení, která je čtyři vykreslení stará. Lékem není rychlejší rastrovač. Je jím mezipaměť, která vrací hotové stránky okamžitě, a vykreslovací smyčka ochotná opustit práci ve chvíli, kdy zastará

PDFium Component vám dá díly pro obojí a nezasahuje do politiky. Dostanete bitmapy vlastněné volajícím, progresivní vykreslovač, který přijímá zrušovací token, režimy přizpůsobení, jež při změně velikosti přepočítávají zvětšení, a volání pro dlaždicování stránek příliš velkých na vykreslení vcelku. Co komponenta záměrně neposkytuje, je samotná mezipaměť, protože správná politika vyřazování závisí na vašem zobrazovaném okně, paměťovém stropu vaší platformy a na tom, jak vaši uživatelé posouvají obsah. Toto rozhodnutí musíte správně učinit sami, a důsledky jeho špatného učinění jsou přesně to zaseknutí a ten únik paměti

Kam mizí milisekundy a megabajty

Než začnete cokoliv navrhovat, vyjádřete náklady čísly. Stránka A4 při 96 DPI má zhruba 794 na 1123 pixelů, tedy asi 3,5 MB jako 32bitová bitmapa. Zvětšíte na 200 % a to se zečtyřnásobí. Při 400 % na displeji s vysokým DPI alokujete a plníte jedinou bitmapu stránky o velikosti 50 až 60 MB, a prohlížeč s plynulým posouváním udržuje živých několik stránek najednou. Náklady na rastrování se řídí počtem výstupních pixelů, takže každé zdvojnásobení zvětšení zhruba zečtyřnásobí jak dobu vykreslení, tak paměť dohromady

Z této aritmetiky přímo plynou dva důsledky. Mezipaměť, jejíž klíč ignoruje úroveň zvětšení, je bezcenná, protože právě to gesto, které má urychlovat, tedy zvětšování, produkuje pokaždé novou bitmapu. A neomezená mezipaměť vyčerpá 32bitovému procesu adresní prostor přesně na těch dokumentech, kde lidé zvětšují nejvíce: husté skeny listin vlastnictví, technické výkresy, velkoformátové mapy. Mezipaměť musí mít správně sestavený klíč a pevně nastavený strop, a ani jedno z toho není volitelné

Co patří do klíče mezipaměti

Uloženou bitmapu je bezpečné znovu použít pouze tehdy, když se shoduje každý vstup, který formoval její pixely. To znamená číslo stránky, efektivní zvětšení (nebo ekvivalentně rozměry výstupních pixelů), rotace, DPI monitoru a vykreslovací volby, které platily v okamžiku jejího vzniku. Stránka vykreslená s reAnnotations je jiný obrázek než tatáž stránka bez nich, a průchod v odstínech šedi přes reGrayscale je zase jiný. Vynechejte z klíče kterýkoli z nich a chyby jsou předvídatelné: překryv anotace, který přetrvává poté, co recenzent smaže komentář, nebo stránka, která se rozmaže ve chvíli, kdy uživatel přetáhne okno z panelu notebooku na externí 4K monitor a DPI se pod zastaralou bitmapou změní

Vyhledání render cache PDFium v prohlížeči Delphi, kde klíč cache kombinuje stranu, zoom, rotaci, DPI monitoru a render volby, zásah vrátí bitmapu v mikrosekundách a eviction uvolní každou bitmapu, kterou zahodí
Klíč cache pokrývá každý vstup, který tvaruje pixely, a evikce uvolní bitmapy, jež vyhodí
function TPageCache.Acquire(Pdf: TPdf; PageNo: Integer; ZoomPct: Single;
  Rotation: TRotation; Opts: TRenderOptions): TBitmap;
var
  Key: string;
begin
  Key := Format('%d|%.0f|%d|%d|%d',
    [PageNo, ZoomPct, Ord(Rotation), Screen.PixelsPerInch, OptionsMask(Opts)]);
  if FBitmaps.TryGetValue(Key, Result) then
    Exit;

  Pdf.PageNumber := PageNo;
  Result := Pdf.RenderPage(0, 0, OutputWidth(PageNo, ZoomPct),
    OutputHeight(PageNo, ZoomPct), Rotation, Opts);
  FBitmaps.Add(Key, Result);   // mezipaměť nyní vlastní tuto bitmapu
end;

Při zásahu se to vrátí v mikrosekundách, což je celý smysl věci. Těžší otázkou je, co se stane s bitmapami, které z mezipaměti vypadnou, a to se ukazuje být otázkou toho, kdo je vlastní

Kdo uvolňuje bitmapu

Funkční forma RenderPage vrací TBitmap, kterou vlastní volající. Při jednorázovém exportu je toto vlastnictví zřejmé a snadno dodržitelné. Uvnitř mezipaměti se z toho stává zdaleka nejčastější únik paměti v prohlížečích PDF pro Delphi, protože slovník nyní drží jedinou referenci na každou bitmapu, a obyčejný TDictionary uvolňuje klíče a hodnoty za vás pouze tehdy, jsou-li to spravované typy. TBitmap jím není. Vyřaďte položku bez volání Free a pixely zůstanou alokované, aniž by na ně cokoli ukazovalo

Důvodem, proč tohle propadne sítem, je časování. Desetiminutový kouřový test nikdy nezvětší dost odlišných stránek, aby si toho všiml; únik se projeví až poté, co někdo posouval a zvětšoval dlouhý dokument několik hodin, a v tu chvíli proces drží stovky osiřelých bitmap stránek a stroj začíná stránkovat paměť na disk. Proto vyřazování patří už do první verze mezipaměti, ne do pozdější. Omezte mezipaměť podle odhadovaných bajtů, vypočtených jako šířka krát výška krát čtyři, vyřaďte nejméně nedávno použité stránky, které leží mimo zobrazované okno a okno předběžného načítání, a při odstranění uvolněte každou bitmapu. Pro kresby, které jsou opravdu přechodné, vám přetížení, jež vykreslují do bitmapy TBitmap poskytnuté volajícím nebo přímo do HDC, umožní zcela přeskočit tanec kolem vlastnictví. Náhled tisku je zjevný případ, protože každý list vykreslíte jednou a jeho ukládání do mezipaměti nic nepřinese

Progresivní vykreslování a poctivé rušení

Obyčejná přetížení RenderPage blokují, dokud stránka není hotová, což je přesně to chování, které nechcete, zatímco uživatel stále pohybuje ovládáním zvětšení. Na to sáhnete po RenderPageProgressive. Přijímá IPdfCancellationToken a vrací jeden z prsDone, prsCancelled nebo prsFailed. Detail chování, který lidi zaskočí, je, že zrušení není okamžité. Token se kontroluje na hranicích bloků uvnitř vykreslování, takže token, který signalizujete uprostřed bloku, se projeví až po dokončení tohoto bloku. Na složité stránce se latence mezi žádostí a zastavením pohybuje v desítkách milisekund. Navrhujte s ohledem na tuto mezeru, místo abyste si přáli, aby neexistovala: zrušte předchozí token ve chvíli, kdy přijde nová hodnota zvětšení, ale nepředpokládejte, že se staré vykreslování zastaví ve chvíli, kdy o to požádáte

Časová osa progresivního renderování PDFium v Delphi, kde každá nová žádost o zoom zruší předchozí token, rušení dopadne na hranici chunku, nahrazené rendery vrátí prsCancelled a poslední pokus vrátí prsDone
Každý nový požadavek na zoom zruší předchozí render token a zrušení dopadne na hranici chunku
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
  Status: TPdfProgressiveStatus;
begin
  if FTokenSource <> nil then
    FTokenSource.Cancel;           // opustit předchozí probíhající vykreslování
  FTokenSource := TPdfCancellationTokenSource.New;  // modul FPdfAsync

  Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
    FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
    ro0, [reAnnotations]);

  case Status of
    prsDone:      PresentBackBuffer;
    prsCancelled: ;                // nahrazeno novějším požadavkem: tiše zahodit
    prsFailed:    ShowRenderFailure;
  end;
end;

Během interakce je prsCancelled běžný výsledek, nikoli výjimečný. Většina vykreslení, která spustí gesto zvětšení, bude nahrazena dřív, než doběhnou, takže zrušení berte jako běžnou rutinu a výsledek tiše zahoďte. Fronta vykreslování, která zaznamenává každé zrušení jako varování, pohřbí tu jedinou chybu, na které opravdu záleží, pod tisíci řádky šumu. Aby obrazovka nepůsobila mrtvě, zatímco skutečné vykreslování běží, spárujte progresivní cestu s levnou náhradou: zvětšete předchozí uloženou bitmapu na novou úroveň a okamžitě ji zobrazte. Sto nebo dvě stě milisekund vypadá měkce, ale působí to jako okamžité, a získává to plnohodnotnému vykreslení čas, který potřebuje buď na dokončení, nebo na zrušení dalším gestem

Režim přizpůsobení, který zvětšení potichu vypíná

Vlastnost FitMode prohlížeče, nastavená na pfmFitPage nebo pfmFitWidth, při každé změně velikosti přepočítá zvětšení, aby se stránka nadále vešla, jak se okno mění. Háček je v tom, že přímé přiřazení do Zoom resetuje FitMode zpátky na pfmNone. Jako výchozí chování je to správné: uživatel, který záměrně zadal 150 %, nechce, aby to příští změna velikosti okna zahodila. Ale překvapí to každého, kdo zapojí tlačítko přiblížení jako Zoom := Zoom * 1.25 a pak nemůže přijít na to, proč přizpůsobení šířce po prvním kliknutí přestalo reagovat. Pokud váš panel nástrojů nabízí jak explicitní zvětšení, tak režimy přizpůsobení, musíte si sami pamatovat poslední volbu přizpůsobení uživatele a znovu ji přiřadit, když znovu stiskne tlačítko přizpůsobení. Komponenta neobnoví režim, který přiřazení zvětšení právě vymazalo, a ani by neměla

Paměťový rozpočet, který dokážete obhájit

Rozpočet, který si dokážete napsat na papír, je rozpočet, který dokážete obhájit při revizi kódu, takže začněte konkrétním scénářem. Řekněme, že plynulé posouvání udržuje viditelnou stránku plus jednu předem načtenou stránku nad a pod ní, vedle pruhu s náhledy. Při 100 % na displeji s 96 DPI vyjdou tyto tři bitmapy v plné velikosti zhruba na 3,5 MB každá, což je zanedbatelné. Při 300 % na 4K displeji jsou stejné tři bitmapy zhruba 30 MB každá, a to ještě předtím, než mezipaměť podržela jedinou historickou stránku. Růst je v gestu, ne v dokumentu

Bitmapová paměťová aritmetika PDFium pro prohlížeč v Delphi, kde každé zdvojnásobení zoom čtyřnásobí paměť strany, plynulý posuv drží tři strany naživu, stropovaný LRU rozpočet brání cache a RenderTile obslouží přes rozměrné kresby
Každé zdvojnásobení zoom čtyřnásobí bitmapovou paměť, takže cache potřebuje tvrdý strop a tiles pro předimenzované stránky

Rozumným výchozím nastavením pro 32bitový proces Delphi je rozpočet bitmap 256 MB pod vyřazováním LRU. Na 64 bitech můžete škálovat podle fyzické RAM, ale i tak si ponechte pevný strop, protože chyba, před kterou se chráníte, není pád vašeho procesu. Je to celý stroj mlátící svým stránkovacím souborem, zatímco váš prohlížeč technicky vzato dál běží a uživatel se diví, proč se všechno ostatní zpomalilo. Pevný strop selže předvídatelně; neomezená mezipaměť selže tak, že s sebou strhne celou pracovní plochu. Náhledy si zaslouží vlastní zacházení: vykreslete každý jednou v jeho malé cílové velikosti a držte ho v samostatném fondu, kterého se logika LRU nikdy nedotkne. Regenerovat 120pixelový náhled zmenšením 60MB bitmapy celé stránky je nejplýtvavější možný způsob, jak vyrobit poštovní známku

Některé jednotlivé stránky přemůžou jakýkoli rozpočet. Technický výkres formátu E nebo velká mapa vykreslená celá při 400 % je alokace v řádu stovek megabajtů, a žádná politika vyřazování to neučiní přijatelným. Odpovědí je tam přestat vykreslovat celé stránky. RenderTile rastruje pouze oblast na pixelovém posunu (Left, Top) uvnitř stránky pomyslně škálované na PageWidth krát PageHeight, takže vykreslíte jen viditelný obdélník plus okraj o velikosti jedné dlaždice kolem něj kvůli plynulému posouvání, a posuny dlaždic zapojíte do klíče mezipaměti vedle zvětšení. Udržujte rozměry dlaždic v celém dokumentu neměnné. Pevné dlaždice znamenají, že změna DPI čistě zneplatní celou mřížku, zatímco proměnlivé dlaždice vás nechají honit se za viditelnými švy mezi oblastmi vykreslenými v mírně odlišných měřítkách

K tomu všemu potichu přidávají dvě sousední funkce. Průchody barevnými filtry, jako jsou odstíny šedi nebo inverze, běží po vykreslení a pokaždé vyprodukují druhou bitmapu v plné velikosti, čímž zdvojnásobí paměťovou stopu na stránku u každého zobrazení, které je používá; touto cenou se zabývá barevné filtrování pro slabozraké v prohlížečích PDF pro Delphi. A prohlížeč, který zvýrazňuje slova během převodu textu na řeč, zneplatňuje vykreslené zobrazení při každém vysloveném slově, takže interakce mezi překreslováním zvýraznění a rychlostí řeči má větší význam, než se na první pohled zdá, jak je popsáno v zvýrazňování slovo po slově u TTS

Přetížení vykreslování, kódy progresivního stavu a samotná komponenta prohlížeče jsou zdokumentovány na produktové stránce pro PDFium Component