Podržte tlačidlo priblíženia v jednoduchom prehliadači PDF a sledujte graf vyťaženia CPU. Jediné stlačenie ovládacieho prvku priblíženia s automatickým opakovaním vyvolá desiatku alebo viac krokov za sekundu. Ak každý z nich spustí opätovné vykreslenie viditeľnej stránky v plnej kvalite, vykresľovacie úlohy sa hromadia rýchlejšie, než sa stíhajú dokončovať. Stránka sa sama o sebe rasterizuje rýchlo, napríklad za 180 ms pri skene formátu A4, no vy teraz spúšťate tucet 180 ms vykreslení pre zobrazenia, ktoré používateľ už dávno preskočil. Prehliadač zamrzne, jadro procesora sa vyťaží na 100 % a kým obrazovka dobehne požiadavky, používateľ skončí na úrovni priblíženia spred štyroch vykreslení. Riešením nie je rýchlejšia rasterizácia, ale vyrovnávacia pamäť (cache), ktorá okamžite vracia dokončené stránky, a vykresľovací cyklus schopný okamžite zahodiť prácu, ktorá už nie je aktuálna.
PDFium Component vám dáva nástroje pre obe riešenia, no nevnucuje vám konkrétne pravidlá. Získate bitové mapy vlastnené volajúcim, progresívny render s tokenom zrušenia (cancellation token), režimy prispôsobenia prepočítavajúce priblíženie pri zmene veľkosti a dlaždicové vykresľovanie (tiling) pre stránky príliš veľké na to, aby sa dali rasterizovať celé. Čo však komponent zámerne neposkytuje, je samotná vyrovnávacia pamäť. Správna stratégia vyraďovania položiek (eviction policy) totiž závisí od vášho zobrazenia, pamäťového limitu platformy a spôsobu, akým používatelia posúvajú stránky. Toto rozhodnutie musíte urobiť vy a dôsledkami nesprávneho postupu sú práve zamŕzanie a úniky pamäte.
Kam sa strácajú milisekundy a megabajty
Pred návrhom čohokoľvek si vyčíslite reálne náklady. Stránka A4 pri 96 DPI má približne 794 x 1123 pixelov, čo je asi 3,5 MB pri 32-bitovej bitovej mape. Priblížte na 200 % a táto hodnota sa zoštvornásobí. Pri 400 % na displeji s vysokým DPI už alokujete a vypĺňate bitovú mapu jednej stránky s veľkosťou 50 až 60 MB, pričom prehliadač s plynulým posúvaním udržiava niekoľko stránok aktívnych súčasne. Náklady na rasterizáciu kopírujú výstupné pixely, takže každé zdvojnásobenie priblíženia zhruba zoštvornásobí čas vykresľovania aj spotrebu pamäte.
Z tejto aritmetiky priamo vyplývajú dva dôsledky. Vyrovnávacia pamäť, ktorej kľúč ignoruje úroveň priblíženia, je bezcenná, pretože práve gesto, ktoré má urýchliť – priblíženie –, zakaždým vytvára novú bitovú mapu. A neobmedzená vyrovnávacia pamäť vyčerpá adresný priestor 32-bitového procesu presne pri dokumentoch, kde ľudia najviac približujú: husté skeny listov vlastníctva, technické výkresy či veľkoformátové mapy. Cache musí byť správne kľúčovaná a pevne obmedzená – ani jedno z toho nie je voliteľné.
Čo patrí do kľúča cache
Uloženú bitovú mapu je bezpečné znova použiť iba vtedy, ak sa zhodujú všetky vstupy, ktoré formovali jej pixely. To znamená číslo stránky, efektívne priblíženie (alebo zodpovedajúce rozmery výstupných pixelov), otočenie, DPI monitora a možnosti vykresľovania platné pri jej vytváraní. Stránka vykreslená s voľbou reAnnotations je iným obrazom ako rovnaká stránka bez nej, a prechod na stupne sivej cez reGrayscale je opäť odlišný. Vynechajte ktorúkoľvek z týchto hodnôt z kľúča a chyby na seba nenechajú dlho čakať: prekrytie anotáciami zostane viditeľné aj po tom, čo recenzent vymaže komentár, alebo stránka zostane rozmazaná v momente, keď používateľ presunie okno z obrazovky notebooku na externý 4K monitor a hodnota DPI sa zmení pod zastaranou bitovou mapou.
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); // the cache now owns this bitmap
end;
Pri nájdení zhody (cache hit) sa výsledok vráti v mikrosekundách, čo je hlavným zmyslom. Zložitejšou otázkou je, čo sa stane s bitovými mapami, ktoré z vyrovnávacej pamäte vypadnú, a to je otázka vlastníctva.
Kto uvoľňuje bitovú mapu
Funkčná podoba metódy RenderPage vracia objekt TBitmap, ktorý vlastní volajúci. Pri jednorazovom exporte je toto vlastníctvo zrejmé a ľahko sa dodržiava. Vnútri vyrovnávacej pamäte sa to však stáva najčastejším zdrojom únikov pamäte v prehliadačoch PDF v Delphi, pretože slovník (dictionary) teraz drží jedinú referenciu na každú bitovú mapu. Štandardný TDictionary uvoľňuje kľúč a hodnoty automaticky iba vtedy, ak ide o spravované (managed) typy. TBitmap ním však nie je. Vyraďte položku bez volania Free a pixely zostanú alokované bez toho, aby na ne čokoľvek odkazovalo. Dôvodom, prečo sa to často prehliadne, je načasovanie. Desaťminútový dymový test (smoke test) nikdy neotestuje priblíženie toľkých strán, aby si to niekto všimol. Únik sa prejaví až po tom, čo niekto niekoľko hodín posúva a približuje dlhý dokument. Vtedy už proces drží stovky osamotených bitových máp stránok a systém začína swapovať na disk. Preto vyraďovanie položiek patrí už do prvej verzie vyrovnávacej pamäte, nie až do niektorej z neskorších. Obmedzte cache podľa odhadovaných bajtov (šírka krát výška krát štyri), vyraďte najmenej používané stránky (LRU), ktoré sú mimo viditeľnej oblasti a okna prednačítania (prefetch), a uvoľnite každú bitovú mapu pri jej odstránení. Pri krátkodobých vykresleniach vám preťaženia, ktoré vykresľujú do volajúcim poskytnutého objektu TBitmap alebo priamo na HDC, umožňujú úplne sa vyhnúť riešeniu vlastníctva. Tlačový náhľad (print preview) je jasným príkladom, keďže každý list vykresľujete iba raz a ukladanie do vyrovnávacej pamäte tu nemá zmysel.
Progresívne vykresľovanie a poctivé zrušenie úlohy
Bežné preťaženia RenderPage blokujú beh programu, kým sa stránka nedokončí, čo je presne to správanie, ktoré nechcete, keď používateľ stále pohybuje ovládaním priblíženia. Vtedy siahnite po metóde RenderPageProgressive. Tá prijíma rozhranie IPdfCancellationToken a vracia jeden zo stavov prsDone, prsCancelled alebo prsFailed. Detail správania, ktorý mnohých prekvapí, je, že zrušenie nie je okamžité. Token sa kontroluje na hraniciach blokov (chunks) vnútri vykresľovania, takže token, ktorý aktivujete uprostred bloku, sa prejaví až po jeho dokončení. Pri zložitej stránke môže oneskorenie medzi požiadavkou a zastavením dosiahnuť desiatky milisekúnd. Navrhnite systém s ohľadom na tento časový posun namiesto jeho ignorovania: zrušte predchádzajúci token v momente príchodu novej hodnoty priblíženia, ale nepredpokladajte, že staré vykresľovanie sa zastaví v sekunde, kedy ste o to požiadali.
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
Status: TPdfProgressiveStatus;
begin
if FTokenSource <> nil then
FTokenSource.Cancel; // abandon the previous in-flight render
FTokenSource := TPdfCancellationTokenSource.New; // FPdfAsync unit
Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
ro0, [reAnnotations]);
case Status of
prsDone: PresentBackBuffer;
prsCancelled: ; // superseded by a newer request: drop silently
prsFailed: ShowRenderFailure;
end;
end;
Počas interakcie je stav prsCancelled bežným výsledkom, nie výnimočným. Väčšina vykreslení spustených gestom priblíženia bude nahradená novými požiadavkami skôr, než sa stihnú dokončovať. Pristupujte preto k zrušeniu ako k bežnej rutine a výsledok potichu zahoďte. Vykresľovací rad, ktorý loguje každé zrušenie ako varovanie, pochová jedno dôležité zlyhanie pod tisíckami riadkov zbytočného šumu. Aby obrazovka nevyzerala zamrznuto počas behu skutočného vykresľovania, skombinujte progresívnu cestu s jednoduchou náhradou: zväčšite alebo zmenšite predchádzajúcu uloženú bitovú mapu na nové priblíženie a okamžite ju zobrazte. Obraz bude na sto či dvesto milisekúnd mäkší (neostrý), no pre používateľa bude reakcia pôsobiť okamžite, čo poskytne vykresľovaniu v plnej kvalite čas na dokončenie alebo zrušenie nasledujúcim gestom.
Režim prispôsobenia, ktorý priblíženie potichu vypne
Vlastnosť prehliadača FitMode nastavená na pfmFitPage alebo pfmFitWidth prepočítava priblíženie pri každej zmene veľkosti okna, aby stránka stále vypĺňala vyhradený priestor. Záludnosť spočíva v tom, že priame priradenie hodnoty Zoom vráti vlastnosť FitMode späť na pfmNone. Ako predvolené správanie je to správne: používateľ, ktorý si sám nastavil 150 %, nechce, aby nasledujúca zmena veľkosti okna toto nastavenie zrušila. To však prekvapí každého, kto napíše tlačidlo priblíženia ako Zoom := Zoom * 1.25 a potom nechápe, prečo prispôsobenie šírke prestalo po prvom kliknutí fungovať. Ak váš panel nástrojov ponúka explicitné priblíženie aj režimy prispôsobenia, musíte si poslednú voľbu prispôsobenia používateľa pamätať sami a znova ju priradiť, keď znova stlačí príslušné tlačidlo. Komponent neobnoví režim, ktorý priradenie priblíženia práve vymazalo – a ani by to robiť nemal.
Pamäťový limit, ktorý dokážete obhájiť
Pamäťový limit, ktorý si dokážete spísať, je limitom, ktorý môžete obhájiť pri kontrole kódu. Začnime reálnym scenárom. Predpokladajme, že plynulé posúvanie udržiava aktívnu viditeľnú stránku plus jednu prednačítanú stránku nad a pod ňou, spolu s pásom náhľadov. Pri 100 % na 96-DPI monitore majú tieto tri veľké bitové mapy približne 3,5 MB každá, čo je zanedbateľné. Pri 300 % na 4K monitore má rovnaká trojica bitových máp už okolo 30 MB každá – a to ešte vyrovnávacia pamäť neuložila žiadnu historickú stránku. Nárast spotreby pamäte spôsobuje gesto používateľa, nie samotný dokument. Rozumným predvoleným nastavením pre 32-bitový proces v Delphi je limit 256 MB pre bitové mapy pri vyraďovaní typu LRU. Na 64-bitovom systéme ho môžete škálovať s veľkosťou fyzickej RAM, no napriek tomu zachovajte pevný strop. Problém, pred ktorým sa chránite, totiž nie je len pád vášho procesu, ale stav, kedy celý počítač začne intenzívne využívať swapovací súbor na disku, hoci váš prehliadač technicky stále beží a používateľ sa čuduje, prečo všetko ostatné spomalilo. Pevný limit zlyhá predvídateľne; neobmedzená cache zlyhá tak, že so sebou stiahne celý operačný systém. Náhľady (thumbnails) si zaslúžia vlastné zaobchádzanie: vykreslite každý z nich iba raz v jeho cieľovej malej veľkosti a držte ho v samostatnej pamäti, ktorej sa logika LRU vôbec nedotýka. Regenerovať 120-pixelový náhľad zmenšovaním 60 MB bitovej mapy celej stránky je najplytvavejším možným spôsobom tvorby poštovej známky.
Niektoré jednotlivé stránky však porazia akýkoľvek rozpočet. Technický výkres formátu E alebo veľká mapa vykreslená celá pri 400 % predstavuje alokáciu stoviek megabajtov a žiadne pravidlá vyraďovania položiek to nedokáže urobiť prijateľným. Riešením je prestať vykresľovať celé stránky. Metóda RenderTile rasterizuje iba oblasť na pixelovom posune (Left, Top) v rámci stránky s pomyselnou mierkou PageWidth ku PageHeight. Vykresľujete tak len viditeľný obdĺžnik plus okraj o veľkosti jednej dlaždice okolo neho pre hladké posúvanie, a posuny dlaždíc pridáte do kľúča cache spolu s priblížením. Udržujte rozmery dlaždíc v celom dokumente konštantné. Fixné dlaždice znamenajú, že zmena DPI čisto zneplatní celú mriežku, zatiaľ čo pri premenlivých dlaždiciach by ste museli riešiť viditeľné švy medzi oblasťami vykreslenými v mierne odlišných mierkach.
K tomu všetkému sa potichu pripájajú dve súvisiace funkcie. Farebné filtre, ako sú stupne sivej alebo inverzia, bežia po vykreslení a zakaždým vytvárajú druhú plnohodnotnú bitovú mapu, čím zdvojnásobujú pamäťovú stopu každej stránky, ktorá ich používa. Týmto nákladom sa zaoberá článok o farebných filtroch pre slabozrakých v Delphi. A prehliadač, ktorý zvýrazňuje slová počas čítania textu (text-to-speech), zneplatňuje vykreslené zobrazenie pri každom vyslovenom slove, takže interakcia medzi prekresľovaním zvýraznení a rýchlosťou reči je dôležitejšia, než sa na prvý pohľad zdá, ako popisuje článok o zvýrazňovaní slov pri TTS.
Preťaženia vykresľovania, progresívne stavové kódy a samotný komponent prehliadača sú zdokumentované na produktovej stránke PDFium Component.