Arhiva skeniranih dokumenata od 2 GB leži u S3 bucket-u i korisnik traži stranicu 900. PDFlibPas može poslužiti tu stranicu bez preuzimanja fajla: LoadFromRangeSource gradi read-only seekable stream preko vašeg sopstvenog byte-range callback-a i predaje ga TPDFDocument-u, pa parser povlači tabele unakrsnih referenci, jednu granu page tree-a i jedan content stream
Transportna strana priče stara je i dosadna. HTTP serveri oglašavaju byte range-ove decenijama, danas specificirane u RFC 9110 §14, i svaki object store govori isti dijalekat. PDF strana jednako je rešena: ISO 32000-1 §7.5.8 definiše linearizaciju upravo zato da čitač može renderovati prvu stranicu sa početka fajla. Ono što je u Delphi-ju nedostajalo je komad u sredini, deo koji odlučuje koje range-ove da traži, koliko da zadrži i kako da ne pita dvaput
Šta LoadFromRangeSource traži od vašeg transporta?
Dve stvari, i nijedna nije stream. PDFlibPas traži autoritativan SourceSize i sinhroni read callback tipa TPDFlibRangeReadEvent, deklarisan kao function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Iznutra par postaje TCallbackByteRangeSource koji izlaže SourceSize i ReadRange, umotan u stream čije vlasništvo prelazi na dokument. Vaš callback cilj i njegov backend ostaju vaši: dokument oslobađa omot pri close, clear ili reload, ali nikad ne dodiruje transportni objekat iza method pokazivača
Ugovor je namerno popustljiv u jednom smeru i strog u drugom. Kraće čitanje je legalno i samo znači da parser pita ponovo. Callback koji baci izuzetak pretvara se u kraće čitanje i konvergira kroz normalan put greške pri učitavanju. Callback koji tvrdi da je upisao više od Count bajtova se steže, jer pokvaren provajder ne sme da prepiše preko keš bafera. Ponovni pokušaji lozinke grade svež range stream i sveže parse stanje nad istim callback izvorom, pa neuspešan pokušaj ne može za sobom da ostavi zastarelu poziciju, prozor ili stanje dešifrovanja
type
TObjectStoreSource = class
private
FClient: TRangeHttpClient;
FSize: Int64;
public
function ReadRange(Sender: TObject; Offset: Int64;
Buffer: Pointer; Count: LongInt): LongInt;
function IsResident(Sender: TObject; Offset: Int64;
Count: LongInt): Integer;
property Size: Int64 read FSize;
end;
function TObjectStoreSource.ReadRange(Sender: TObject; Offset: Int64;
Buffer: Pointer; Count: LongInt): LongInt;
begin
{ jedan blokirajući GET sa Range: bytes=Offset-(Offset+Count-1) }
Result := FClient.FetchInto(Offset, Count, Buffer);
end;
{ ... }
Lib := TPDFlib.Create;
Src := TObjectStoreSource.Create(BucketUrl);
try
if Lib.LoadFromRangeSource(Src.Size, Src.ReadRange, '',
65536, 8 * 1024 * 1024, 2, Src.IsResident) = 1 then
Lib.SelectPage(900);
finally
Lib.Free; { oslobađa omotni stream }
Src.Free; { vaš transport, vaš vek trajanja }
end;
Koliko range keš zaista drži?
Podrazumevano 4 MiB, raspoređeno po chunk-poravnatim prozorima i izbačeno po LRU. Raniji dizajn sa jednim prozorom rastao je na dužinu koju je pozivalac tražio, pa je jedno veliko sekvencijalno čitanje moglo da pregazi nominalnu chunk veličinu dok je slučajan skok odmah bacao prethodni prozor. Trenutni keš poravnava svaki izvorni ofset na ChunkSize, dobavlja tačno jedan chunk po promašaju i sprovodi krut bajtni budžet kroz više prozora. Svaki eksplicitni budžet koji prosledite podiže se na najmanje jedan pun chunk, pa jedno čitanje uvek napreduje chunk po chunk i vršno opterećenje keša ostaje predvidivo. ChunkSize ispod 4096 vraća se na podrazumevanih 64 KiB
Računovodstvo ponovljenih čitanja deo je koji vredi povezati sa vašom telemetrijom. PDFlibPas prepoznaje ponavljanje po poravnatom početku chunk-a i drži uređene kontigualne intervale, čime razdvaja pravo prvo dobavljanje od ponovnog posle izbacivanja, a da knjigovodstvo ne raste linearno sa veličinom fajla. GetRangeSourceCacheInfo vraća celu sliku kao JSON, SetRangeSourceCacheLimit menja budžet u toku rada, a ClearRangeSourceCache baca prozore i resetuje statistiku zajedno. Smanjenje budžeta u toku rada čuva istoriju i izbacivanja vođena budžetom broji kao evictions, pa rastući repeatedReads uz ravan hits je vaš signal da radni skup više ne staje
var
Info: WideString;
begin
Lib.SetRangeSourceCacheLimit(16 * 1024 * 1024);
Lib.SelectPage(900);
if Lib.GetRangeSourceCacheInfo(Info) = 1 then
{ "windowCount", "cacheLimitBytes", "cachedBytes", "hits", "misses",
"evictions", "sourceReads", "sourceBytes", "repeatedReads",
"coalescedRequests", "coalescedSourceReads" }
LogRangeStats(Info);
end;
Šta se dešava kada više niti želi isti chunk?
Čekaju na jedan zahtev, ne na više. Klasičan TStream ima jedan kursor pozicije, i dve niti koje svaka ispravno zaključavaju i dalje mogu da dožive prepisivanje te pozicije između Seek i Read, pa lazy objekti i segmentirana čitanja u PDFlibPas-u koriste apsolutni ReadAt koji nikad ne pomera kursor. Svaki poravnati chunk dobija jedan in-flight zahtev koji svi pozivaoci tog chunk-a dele, susedni zakazani chunk-ovi spajaju se pre početka izvornog čitanja, a jedno fizičko čitanje ograničeno je na 16 MiB, pa se nalet paralelnog rada na stranicama ne pojačava ni u duple male zahteve ni u jedan apsurdno veliki. Prozor spajanja podrazumeva 2 ms i odnosi se samo na prvi nedostajući chunk svakog ReadAt; pozicioni Read nikad ne čeka na njega, a nula potpuno uklanja početno kašnjenje sakupljanja, što je bitno za duga sekvencijalna skeniranja koja bi inače nagomilala čekanje chunk po chunk. Pozicija, metapodaci keša i izvorna čitanja stoje iza tri odvojena brava, a sam izvorni callback je serializovan, što omogućava da adapter baze podataka ili object store-a bez unutrašnje zaštite niti bude korišćen nepromenjen. Čekaoci primaju sopstveni primerak podataka, pa kasnije LRU izbacivanje ne može da poništi bafer koji je već isporučen
Možete li pitati da li je stranica 900 spremna, a da je ne dobavljate?
Da, i upravo za to služi opciona availability callback funkcija. Običan read callback ne može da razlikuje bajtove koji su već stigli od bajtova koji zahtevaju blokirajući odlazak i povratak, a ispitivanje probnim čitanjem okidalo bi baš ono preuzimanje koje pokušavate da izbegnete. TPDFlibRangeAvailabilityEvent odgovara na jedno pitanje, da li kompletan range može da se pročita odmah, i zabranjeno mu je da išta dobavlja; bajtove koje keš već pokriva uvek računa kao dostupne. GetRangeSourceDataAvailability mapira indirektne objekte na fizičke storage range-ove zabeležene u unosima unakrsnih referenci, razrešava kompresovane objekte na njihov object stream kontejner, ispravlja pomeren PDF header i parsira objekat tek kada ceo range prođe nedobavljajuću probu, pa put nedostajućeg nikad ne poziva vaš read callback
Obilazak je opsežan, a ne iscrpan. Upit stranice prelazi samo granu page tree-a koja sadrži ciljnu stranicu, a zatim dodaje sadržaj stranice, resurse, anotacije i nasleđene atribute stranice, preskačući Parent i P povratne grane, da jedna stranica ili vidžet ne može unazad da se proširi na ceo dokument. Graf objekata ograničen je na 100000 traženih objekata i dubinu 256, stream objekti parsiraju se rečnikom prvo, a fallback punog parsiranja dozvoljen je samo za uskladištene objekte do 4 MiB. JSON izveštaj spaja preklapajuće i susedne intervale pre brojanja, pa se requiredBytes i missingBytes računaju iz spojenih nizova requiredRanges i missingRanges, čiji je end uključiva krajnja tačka. Upit objekta koji je već dostupan može da napuni range keš; upit nedostajućeg ostavlja statistiku čitanja netaknutom
var
Report: WideString;
Status: Integer;
begin
Status := Lib.GetRangeSourceDataAvailability(PDF_RANGE_DATA_PAGE, 900,
Report);
if Status = PDF_RANGE_DATA_AVAILABLE then
RenderPageNow
else if Status = PDF_RANGE_DATA_NOT_AVAILABLE then
{ Report nosi "missingBytes" plus spojene "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { npr. fajl uopšte nema AcroForm }
end;
Zašto prefetch mora da iterira
Zato jedno čitanje trenutnih missingRanges ne čini stranicu dostupnom. Nedostajući čvor page tree-a ili object stream otkriva sledeći sloj zavisnosti tek kad stigne, pa PDFlibPas prefetch posao vrti petlju upit, dobavljanje, ponovni upit dok stranica, forma ili graf objekata ne bude potpuno dostupan ili dok ga bajtni ili prolazni limit ne zaustavi. Posao koristi sopstveni čitač i mali sekundarni keš čiji izvor podataka prosleđuje apsolutna čitanja originalnom range stream-u, čime je parse stanje izolovano od prednjeg TSmartPDFReader-a, dok bajtovi koje zaista preuzme ipak dospevaju u deljeni glavni keš. Po jedan radni thread postoji po range stream-u, u skladu sa serializacijom koju izvorni callback već zahteva, a red bira po četiri nivoa prioriteta pa po redu prijave unutar nivoa. MaxBytes se naplaćuje u fizičkim chunk bajtovima, pa parser koji traži jedan bajt unutar nekeširanog chunk-a i dalje plaća ceo chunk, dok chunk-ovi već u deljenom kešu posao ne koštaju ništa. Otkazivanje posla u redu dolazi do terminalnog stanja sa nula izvornih čitanja; posao u toku proverava se pre svakog prolaza zavisnosti i pre svakog izvornog chunk-a, a oslobađanje range stream-a čeka da in-flight callback vrati rezultat umesto da pokuša da ga prekine
var
Job: Integer;
Info: WideString;
begin
Job := Lib.StartRangeSourcePrefetch(PDF_RANGE_DATA_PAGE, 901,
PDF_RANGE_PREFETCH_PRIORITY_HIGH, 8 * 1024 * 1024, 65536);
if Lib.WaitForRangeSourcePrefetch(Job, 5000) =
PDF_RANGE_PREFETCH_STATE_COMPLETED then
PrepareNextPage
else
Lib.CancelRangeSourcePrefetch(Job);
{ "passes", "plannedRanges", "sourceReads", "fetchedBytes" i poslednji
pun izveštaj dostupnosti, pa LIMIT_REACHED ostaje razlučiv
od FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Gde se ovo degradira u preuzimanje celog fajla
Range učitavanje je opklada o rasporedu fajla, i neki fajlovi je ne poštuju. Linearizovan fajl po ISO 32000-1 §7.5.8 je dobar slučaj: sekcija prve stranice zagreva se pri otvaranju, ograničena i postojećim sigurnosnim pragom od 4 MiB i trenutnim keš budžetom, da zagrevanje ne može odmah da izbaci većinu samog sebe. Nelinearizovan fajl i dalje se razrešava kroz trailer i lanac unakrsnih referenci pri kraju, što košta par dodatnih odlazaka i povratka umesto katastrofe. Prava litica je oštećen fajl koji forsira put popravke, jer obnova tabele unakrsnih referenci znači skeniranje headera objekata kroz ceo dokument, a to je puno preuzimanje koje stiže chunk po chunk. Latencija je druga poštena granica: pri 60 ms po zahtevu, parsiranje slučajnim pristupom koje traži četrdeset nekeširanih chunk-ova provede preko dve sekunde u transportu ma koliko dobar keš bio, što je upravo ono što read-ahead argument i red sa prioritetima postoje da sakriju. Ista disciplina pokazuje se u pristupu spajanju i razdvajanju velikih PDF-ova direktnim pristupom, a ovaj keš leži ispod paralelnog renderovanja stranica i disk keša stranica pregledača
Range source API, upit dostupnosti i prefetch raspoređivač deo su standardne PDFlibPas Delphi PDF Library za Delphi, C++Builder i Free Pascal; stranica proizvoda nosi kompletnu referencu parametara za LoadFromRangeSource uz prefetch prioritete i konstante stanja