HotPDF učitava PDF iz bilo kog izvora sa nasumičnim pristupom koji sami implementirate, a THPDFCoalescingRandomAccessSource obavija taj izvor tako da rasuta mala čitanja parsera postaju ograničen skup keširanih blokovskih opsega sa asinhronim prefetch-om. Kod dokumenta koji se servira preko HTTP range zahteva, to je razlika između nekoliko stotina round-trip-ova i nekoliko desetina
Ništa se ne menja u parseru. I dalje pozivate LoadFromRandomAccessSource, vraća se isti objekat dokumenta, a isti API za stranice radi. Ono što se menja je saobraćaj ispod površine
Zašto se isti PDF učitava trenutno lokalno, a puzi preko mreže?
Zato što PDF parser ne čita fajl, već kroz njega navigira. Skače na kraj radi startxref, vraća se nazad na tabelu unakrsnih referenci, razrešava rečnik trailer-a, prati referencu do Catalog-a, zatim do korena stabla stranica, zatim do čvora stranice, pa do njenog rečnika resursa. Svaki od tih koraka čita desetine bajtova sa drugačijeg offset-a
Kod lokalnog fajla taj obrazac je gotovo besplatan: operativni sistem već ima keširanu okolnu 4 KiB stranicu, tako da drugo čitanje košta samo memcpy. Preko mrežnog transporta te lokalnosti nema. Svako čitanje je zahtev sa sopstvenom kašnjenjem, a 300 sekvencijalnih zahteva po 40 ms svaki je dvanaest sekundi provedenih gotovo u potpunosti u čekanju. Popravka nije čitati manje; parseru je tačno potrebno ono što traži. Popravka je da svako fizičko čitanje pokrije više od onoga što će sledeće logičko čitanje želeti
Šta menja spajanje (coalescing)
Izvor za spajanje zaokružuje svako čitanje naviše na blok i kešira taj blok. BlockSize podrazumevano iznosi 262.144 bajta, a MaxCacheBytes 2.097.152, tako da je podrazumevano osam blokova rezidentno, a izbacuju se po redosledu least-recently-used u odnosu na tvrdi budžet bajtova. Čitanje trailer ključa od 40 bajtova od strane parsera povlači 256 KiB oko sebe, a sledećih desetak čitanja u toj okolini, gde žive podaci unakrsnih referenci i Catalog-a, opslužuje se iz memorije
Vaš sopstveni izvor ostaje jednostavan. Implementirajte GetSize i ReadAt, predefinišite ReadAtCancellable ako vaš transport može da prekine u letu, i prepustite wrapper-u da rukuje keširanjem, spajanjem i prefetch-om
type
THttpRangeSource = class(THPDFRandomAccessSource)
private
FClient: TMyHttpClient;
FUrl: string;
FSize: Int64;
public
function GetSize: Int64; override;
function ReadAt(Offset: Int64; var Buffer; Count: Longint): Longint; override;
function ReadAtCancellable(Offset: Int64; var Buffer; Count: Longint;
CancellationToken: THPDFCancellationToken): Longint; override;
end;
var
Raw: THttpRangeSource;
Cached: THPDFCoalescingRandomAccessSource;
Pdf: THotPDF;
begin
Raw := THttpRangeSource.Create('https://files.example.com/contract.pdf');
// OwnsSource=True: wrapper oslobađa Raw zajedno sa sobom
Cached := THPDFCoalescingRandomAccessSource.Create(Raw, True, 262144, 8388608);
Pdf := THotPDF.Create(nil);
try
Cached.AsyncPrefetchEnabled := True;
Cached.AdaptiveReadAheadEnabled := True;
Cached.MaxReadAheadBlocks := 8;
if Pdf.LoadFromRandomAccessSource(Cached, True) = 1 then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
Koliko unapred treba da čita?
Adaptivno čitanje unapred odgovara na to pitanje za svaki dokument posebno, umesto da vas primorava da nagađate. Kada je AdaptiveReadAheadEnabled postavljen, prozor raste kroz 1, 2, 4 i 8 blokova kako se gomilaju istrajna čitanja unapred, i nikada ne premašuje MaxReadAheadBlocks ili konfigurisani kapacitet keša. U trenutku kada stigne čitanje koje ne počinje otprilike tamo gde se prethodno završilo, prozor kolabira i prefetch se suzbija
SequentialReadToleranceBytes, podrazumevano 4.096, definiše to „otprilike”. Čitanja koja padnu unutar te udaljenosti od kraja prethodnog čitanja i dalje se broje kao sekvencijalna, što je bitno jer PDF parser koji prolazi kroz tok sadržaja ne proizvodi savršeno susedne offset-e; preskoči polje dužine ovde, inline rečnik tamo. Postavite toleranciju previše nisko i normalno skeniranje unapred biće klasifikovano kao nasumično, pa se čitanje unapred nikada ne aktivira. Postavite je previsoko i pravi nasumičan pristup deluje sekvencijalno, pa preuzimate megabajte koje niko ne želi. Podrazumevana vrednost je kalibrisana za prolazak kroz tok sadržaja, a statistika će vam reći ako se vaš transport ne slaže s tim
Ta asimetrija je namerna: rast je postepen, kolaps je trenutan. Prekomerno preuzimanje kod radnog opterećenja sa nasumičnim pristupom košta stvaran propusni opseg i stvaran novac na naplativim transportima, pa se jeftina greška preferira u odnosu na skupu
Otkazivanje koje zaista zaustavlja prenos
Bazna klasa deklariše ReadAtCancellable, a izvor za spajanje ga poštuje od početka do kraja. Kada stigne prednje (foreground) čitanje za opseg koji prefetch u letu ne opslužuje, prefetch se otkazuje umesto da se pusti da se završi, tako da zahtev korisnika za stranicom ne čeka u redu iza spekulativnog saobraćaja. Podrazumevana implementacija u THPDFRandomAccessSource vraća se na obično ReadAt, što znači da je funkcija opt-in po transportu: HTTP klijenti koji podržavaju prekid zahteva dobijaju istinsko otkazivanje, a jednostavniji izvori nastavljaju da rade nepromenjeno
Kombinujte to sa tokenom za otkazivanje provučenim kroz vaš UI, i zatvaranje dokumenta od strane korisnika zaista zaustavlja mrežni saobraćaj umesto da čeka da se isprazni. Isti model tokena stoji u osnovi redosleda opisanog u renderovanju u pozadini sa redom zahteva, tako da jedan token može da pokrije čitavu putanju od viewport-a do soket-a
Čitanje statistike keša opsega
GetStatistics popunjava zapis THPDFRangeCacheStatistics koji razdvaja ono što je uradio vaš transport od onoga što je uradio keš. SourceReadCount i SourceBytesRead su fizički saobraćaj. CacheHitCount i CacheMissCount su logički saobraćaj. SequentialReadCount i RandomReadCount pokazuju kako je obrazac pristupa klasifikovan, CurrentReadAheadBlocks i PeakReadAheadBlocks pokazuju koliko se prozor otvorio, a PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount i SuppressedPrefetchCount pokazuju da li se spekulacija isplatila
var
S: THPDFRangeCacheStatistics;
begin
Cached.GetStatistics(S);
Log(Format('physical %d reads / %d bytes, hits %d, misses %d',
[S.SourceReadCount, S.SourceBytesRead, S.CacheHitCount, S.CacheMissCount]));
Log(Format('pattern: %d sequential, %d random, peak window %d blocks',
[S.SequentialReadCount, S.RandomReadCount, S.PeakReadAheadBlocks]));
Log(Format('prefetch: %d issued, %d completed, %d cancelled, %d suppressed',
[S.PrefetchRequestCount, S.PrefetchCompletedCount,
S.PrefetchCancelledCount, S.SuppressedPrefetchCount]));
end;
Tri očitavanja vam govore šta da promenite. Mnogo otkazanih prefetch-eva uz visok broj nasumičnih čitanja znači da se dokumentu pristupa van redosleda, pa smanjite MaxReadAheadBlocks i prestanite da plaćate propusni opseg koji odbacujete. Mnogo promašaja sa vršnim prozorom koji i dalje ostaje na 1 znači da tolerancija odbacuje obrazac koji je zapravo sekvencijalan, pa povećajte SequentialReadToleranceBytes. A pročitani bajtovi koji daleko premašuju veličinu fajla znače da keš trashuje, pa povećajte MaxCacheBytes pre nego što dirate bilo šta drugo
Linearizovani fajlovi menjaju računicu
Ako kontrolišete proizvođača, linearizacija dokumenta menja problem umesto da ga optimizuje. Linearizovan PDF postavlja objekte prve stranice i hint tabelu na početak fajla, tako da pregledač može da renderuje prvu stranicu iz uvodnog megabajta bez uvida u ostatak. HotPDF izlaže tu putanju direktno kroz GetProgressiveLinearizedLoadInfo i ReadProgressiveLinearizedFirstPageSection, a strana pisanja pokrivena je u generisanju linearizovanih PDF-ova sa hint tabelama
Obe tehnike se dopunjuju. Spajanje čini svaki dokument podnošljivim preko sporog linka; linearizacija čini da prva stranica brzo stigne kod dokumenata koje sami proizvodite. Za fajlove koji žive na lokalnom disku, ali su preveliki da stanu u memoriju, putanje mapiranog fajla i lenjog stream-a opisane u radnom toku direktnog File API-ja obično su bolji alat, pošto tu uopšte nema kašnjenja round-trip-a koje bi trebalo amortizovati
HotPDF je izvorna VCL PDF komponenta za Delphi i C++Builder, bez spoljnog DLL-a za parser, sa dostupnim punim izvornim kodom. API za izvor sa nasumičnim pristupom, wrapper za spajanje i ulazne tačke za progresivno učitavanje dokumentovani su na stranici HotPDF Delphi PDF komponente