Držite pritisnut gumb za zumiranje u naivnom PDF pregledniku i promatrajte grafikon procesora. Jedan pritisak na kontrolu zumiranja s automatskim ponavljanjem pokreće desetak ili više koraka zumiranja u sekundi, a ako svaki korak pokrene ponovno iscrtavanje vidljive stranice u punoj kvaliteti, iscrtavanja se gomilaju brže nego što se dovršavaju. Stranica se izolirano iscrtava sasvim dobro, možda 180 ms za skenirani A4 dokument, ali sada izvodite desetak renderskih procesa od 180 ms za rad koji je korisnik već prošao. Preglednik se blokira, jedna jezgra procesora se zakuca na 100%, a dok se zaslon konačno osvježi, korisnik se zaustavio na razini zumiranja od prije četiri koraka. Rješenje nije brži rasterizator. To je predmemorija (cache) koja trenutačno vraća gotove stranice i petlja iscrtavanja koja je spremna napustiti posao onog trenutka kada postane zastario.
PDFium komponenta (PDFium Component) daje vam dijelove za oboje i ne miješa se u vašu politiku izvođenja. Dobivate bitmape u vlasništvu pozivatelja, progresivni rasterizator koji prima token za otkazivanje, načine prilagodbe koji ponovno izračunavaju zumiranje pri promjeni veličine prozora i poziv za popločavanje (tiling) za stranice koje su prevelike za iscrtavanje u cijelosti. Ono što namjerno ne nudi je sama predmemorija, jer ispravna politika izbacivanja (eviction policy) ovisi o vašem okviru prikaza (viewport), ograničenju memorije vaše platforme i načinu na koji korisnici pomiču sadržaj. Ta je odluka na vama, a posljedice pogrešne odluke su upravo blokiranje i curenje memorije.
Kamo odlaze milisekunde i megabajti
Stavite brojke na trošak prije nego što išta dizajnirate. A4 stranica pri 96 DPI iznosi otprilike 794 puta 1123 piksela, što je oko 3,5 MB kao 32-bitna bitmapa. Zumirajte na 200% i to se učetverostručuje. Na 400% na zaslonu visoke gustoće piksela (high-DPI) alocirate i popunjavate bitmapu jedne stranice od 50 do 60 MB, a preglednik s kontinuiranim pomicanjem drži nekoliko stranica aktivnima odjednom. Trošak rasterizacije prati izlazne piksele, tako da svako udvostručenje zumiranja otprilike učetverostručuje i vrijeme iscrtavanja i memoriju zajedno.
Dvije posljedice proizlaze izravno iz te aritmetike. Predmemorija čiji ključ zanemaruje razinu zumiranja je bezvrijedna, jer upravo gesta koju treba ubrzati - zumiranje - svaki put stvara novu bitmapu. A neograničena predmemorija će ostaviti 32-bitni proces bez adresnog prostora upravo na dokumentima koje korisnici najviše zumiraju: gustim skenovima vlasničkih listova, inženjerskim nacrtima, kartama velikog formata. Predmemorija mora imati ispravan ključ i biti čvrsto ograničena, i nijedno od toga nije neobavezno.
Što pripada ključu predmemorije
Predmemorirana bitmapa je sigurna za ponovnu upotrebu samo kada se svi ulazni parametri koji su oblikovali njezine piksele i dalje podudaraju. To znači broj stranice, efektivno zumiranje (or ekvivalentno tome izlazne dimenzije u pikselima), rotacija, DPI monitora i opcije iscrtavanja koje su bile na snazi kada je stvorena. Stranica iscrtana s reAnnotations je drugačija slika od iste stranice bez njih, a iscrtavanje u sivoj skali kroz reGrayscale je opet treća stvar. Ispustite bilo koji od ovih parametara iz ključa i pogreške su predvidljive: sloj anotacije koji ostaje nakon što recenzent obriše komentar, ili stranica koja postaje mutna čim korisnik povuče prozor sa zaslona prijenosnog računala na vanjski 4K monitor, a DPI se promijeni ispod zastarjele bitmape.
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;
Kod pogotka (hit) ovo se vraća u mikrosekundama, što je i cijela svrha. Teže pitanje je što se događa s bitmapama koje ispadnu iz predmemorije, a to se svodi na pitanje o tome tko je njihov vlasnik.
Tko oslobađa bitmapu
Oblik funkcije RenderPage vraća TBitmap koji je u vlasništvu pozivatelja. U jednokratnom izvozu to je vlasništvo očito i lako ga je poštovati. Unutar predmemorije to postaje najčešći uzrok curenja memorije u Delphi PDF preglednicima, jer rječnik (dictionary) sada drži jedinu referencu na svaku bitmapu, a običan TDictionary oslobađa ključeve i vrijednosti samo ako se radi o upravljanim (managed) tipovima. TBitmap to nije. Izbacite unos iz predmemorije bez pozivanja Free i pikseli ostaju alocirani iako ništa više ne upućuje na njih.
Razlog zašto ovo prolazi neopaženo je vrijeme. Brzi desetominutni test nikada ne zumira dovoljno različitih stranica da bi se to uočilo; curenje se pokazuje tek nakon što netko skrola i zumira dugačak dokument nekoliko sati, u kojem trenutku proces drži stotine siročadi od bitmapa stranica, a računalo počinje koristiti swap datoteku. Zato izbacivanje iz predmemorije pripada prvoj verziji, a ne nekoj kasnijoj. Ograničite predmemoriju procijenjenim bajtovima, izračunatim kao širina puta visina puta četiri, izbacite najmanje nedavno korištene stranice (LRU) koje se nalaze izvan okvira prikaza i prozora predviđanja (prefetch), te oslobodite svaku bitmapu pri uklanjanju. Za crtanja koja su uistinu privremena, preopterećene metode koje crtaju u TBitmap koji osigurava pozivatelj ili izravno na HDC omogućuju vam da u potpunosti izbjegnete problematiku vlasništva. Pretpregled ispisa je očigledan primjer, jer svaku stranicu iscrtavate samo jednom i predmemoriranje ne donosi nikakvu korist.
Progresivno iscrtavanje i pošteno otkazivanje
Obične metode RenderPage blokiraju izvođenje dok se stranica ne dovrši, što je točno ono ponašanje koje ne želite dok korisnik još uvijek pomiče kontrolu zumiranja. Za to posežete za RenderPageProgressive. Ona prima IPdfCancellationToken i vraća jednu od vrijednosti: prsDone, prsCancelled ili prsFailed. Detalj u ponašanju koji iznenađuje ljude jest da otkazivanje nije trenutačno. Token se provjerava na granicama blokova (chunks) unutar iscrtavanja, pa token koji signalizirate u sredini bloka stupa na snagu tek kada taj blok završi. Na složenoj stranici latencija između zahtjeva i zaustavljanja penje se na desetke milisekundi. Dizajnirajte oko tog jaza umjesto da ga ignorirate: otkažite prethodni token onog trenutka kada stigne nova vrijednost zumiranja, ali nemojte pretpostavljati da se staro iscrtavanje zaustavlja onog trenutka kada to zatražite.
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;
Tijekom interakcije, prsCancelled je uobičajen ishod, a ne iznimka. Većina iscrtavanja koja gesta zumiranja pokrene bit će zamijenjena novima prije nego što završe, stoga otkazivanje tretirajte kao rutinu i tiho odbacite rezultat. Red čekanja za iscrtavanje koji bilježi svako otkazivanje kao upozorenje zatrpat će onaj jedan neuspjeh koji je stvarno važan tisućama redaka šuma u dnevniku. Kako zaslon ne bi izgledao zamrznuto dok radi stvarno iscrtavanje, uparite progresivnu putanju s jeftinom zamjenom: skalirajte prethodnu predmemoriranu bitmapu na novo zumiranje i prikažite je odmah. Izgledat će mutno stotinu ili dvjesto milisekundi, ali djeluje trenutačno i kupuje iscrtavanju u punoj kvaliteti vrijeme koje mu je potrebno da završi ili bude otkazano sljedećom gestom.
Način prilagodbe koji zumiranje tiho isključuje
Svojstvo preglednika FitMode, postavljeno na pfmFitPage ili pfmFitWidth, ponovno izračunava zumiranje pri svakoj promjeni veličine prozora kako bi stranica ostala prilagođena. Kvaka je u tome što izravno dodjeljivanje Zoom vraća FitMode na pfmNone. Kao zadano ponašanje to je ispravno: korisnik koji je namjerno upisao 150% ne želi da sljedeća promjena veličine prozora to odbaci. Ali to iznenađuje svakoga tko poveže gumb za zumiranje kao Zoom := Zoom * 1.25 i onda ne može shvatiti zašto je prilagodba širini prestala reagirati nakon prvog klika. Ako vaša alatna traka nudi i eksplicitno zumiranje i načine prilagodbe, morate sami zapamtiti korisnikov zadnji izbor prilagodbe i ponovno ga dodijeliti kada ponovno pritisnu gumb za prilagodbu. Komponenta neće sama vratiti način rada koji je dodjeljivanje zumiranja upravo očistilo, niti bi trebala.
Proračun memorije koji možete opravdati
Proračun koji možete zapisati je proračun koji možete braniti u reviziji koda, pa krenimo od konkretnog scenarija. Recimo da kontinuirano skrolanje drži vidljivu stranicu plus jednu unaprijed učitanu stranicu iznad i ispod, uz traku sa sličicama. Na 100% na zaslonu od 96 DPI te tri bitmape pune veličine iznose oko 3,5 MB svaka, što je zanemarivo. Na 300% na 4K zaslonu te iste tri bitmape iznose otprilike 30 MB svaka, i to prije nego što je predmemorija zadržala ijednu povijesnu stranicu. Rast je u gesti, a ne u dokumentu.
Razuman zadani proračun za 32-bitni Delphi proces je 256 MB proračuna za bitmape pod LRU izbacivanjem. Na 64-bitnom sustavu možete skalirati s fizičkim RAM-om, ali bez obzira na to zadržite čvrsti strop, jer neuspjeh od kojeg se štitite nije rušenje vašeg procesa. Radi se o tome da cijelo računalo počne bjesomučno koristiti swap datoteku dok vaš preglednik tehnički i dalje radi, a korisnik se pita zašto je sve ostalo usporilo. Čvrsto ograničenje zakazuje predvidljivo; neograničena predmemorija zakazuje tako da povuče cijeli sustav sa sobom. Sličice (thumbnails) zaslužuju vlastiti tretman: iscrtajte svaku jednom u njezinoj maloj ciljanoj veličini i držite je u zasebnom bazenu koji LRU logika nikada ne dotiče. Regeneriranje sličice od 120 piksela smanjivanjem bitmape cijele stranice od 60 MB najrasipniji je mogući način stvaranja minijature.
Neke pojedinačne stranice nadmašuju bilo koji proračun. Inženjerski nacrt veličine E ili velika karta iscrtana u cijelosti na 400% predstavlja alokaciju od nekoliko stotina megabajta, i nikakva politika izbacivanja to ne može učiniti prihvatljivim. Odgovor je u tom slučaju prestati iscrtavati cijele stranice. RenderTile rasterizira samo područje na pikselnom pomaku (Left, Top) unutar stranice fiktivno skalirane na PageWidth puta PageHeight, pa iscrtavate samo vidljivi pravokutnik plus marginu od jedne pločice (tile) oko njega za glatko paniranje, a pomake pločica ugrađujete u ključ predmemorije zajedno sa zumiranjem. Držite dimenzije pločica fiksnima kroz cijeli dokument. Fiksne pločice znače da promjena DPI-ja čisto poništava cijelu mrežu, dok vas varijabilne pločice ostavljaju da tražite vidljive spojeve između regija iscrtanih u blago različitim mjerilima.
Dvije susjedne značajke tiho pridonose svemu tome. Prolazi filtara boja kao što su siva skala ili inverzija pokreću se nakon iscrtavanja i svaki put stvaraju drugu bitmapu pune veličine, udvostručujući otisak po stranici svakog prikaza koji ih koristi; taj je trošak tema članka o filtriranju boja za slabovidne u Delphi PDF preglednicima. A preglednik koji ističe riječi tijekom pretvaranja teksta u govor poništava iscrtani prikaz na svakoj izgovorenoj riječi, pa interakcija između ponovnog crtanja istaknutog dijela i brzine govora utječe više nego što se na prvu čini, kao što je pokriveno u članku o isticanju riječi po riječ za TTS.
Preopterećenja iscrtavanja, progresivni kodovi stanja i sama komponenta preglednika dokumentirani su na stranici proizvoda za PDFium komponentu (PDFium Component).