Skenirani arhiv od 2 GB živi u S3 bucketu i korisnik želi stranicu 900. PDFlibPas može poslužiti tu stranicu bez preuzimanja datoteke: LoadFromRangeSource gradi tok samo za čitanje s pozicioniranjem nad vašim vlastitim byte-range povratnim pozivom i predaje ga TPDFDocument, pa parser povlači tablice unakrsnih referenci, jednu granu stabla stranica i jedan tok sadržaja
Transportna strana ovoga stara je i dosadna. HTTP serveri oglašavaju byte raspone desetljećima, sada specificirano u RFC 9110 §14, i svaki object store govori isti dijalekt. PDF strana jednako je sređena: ISO 32000-1 §7.5.8 definira linearizaciju upravo da čitač može prikazati prvu stranicu s početka datoteke. Ono što je u Delphiju nedostajalo jest komad u sredini, dio koji odlučuje koje raspone tražiti, koliko ih držati i kako izbjeći dvostruko traženje
Što LoadFromRangeSource treba od vašeg transporta?
Dvije stvari, i nijedna nije tok. PDFlibPas traži autoritativni SourceSize i sinkroni povratni poziv čitanja tipa TPDFlibRangeReadEvent, deklariran kao function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Izvodno par postaje TCallbackByteRangeSource koji izlaže SourceSize i ReadRange, omotan u tok čije vlasništvo prelazi na dokument. Vaš cilj povratnog poziva i njegova pozadina ostaju vaši: dokument oslobađa omot pri zatvaranju, brisanju ili ponovnom učitavanju, ali nikad ne dodiruje transportni objekt iza pokazivača metode
Ugovor je namjerno popustljiv u jednom smjeru i strog u drugom. Kraće čitanje legalno je i jednostavno znači da parser pita ponovno. Povratni poziv koji baci iznimku pretvara se u kraće čitanje i konvergira kroz normalni put neuspjeha učitavanja. Povratni poziv koji tvrdi da je napisao više od Count bajtova steže se, jer pokvaren davatelj ne smije moći pregaziti međuspremnik keša. Pokušaji lozinke ponovno grade svježi range tok i svježe stanje parsiranja nad istim izvorom povratnog poziva, pa neuspjeli pokušaj ne može ostaviti za sobom zastarjelu poziciju, prozor ili stanje dešifriranja
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 s 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 tok }
Src.Free; { vaš transport, vaš vijek trajanja }
end;
Koliko range keš stvarno drži?
Po zadanom 4 MiB, raspršeno po chunk-poravnatim prozorima i izbačeno LRU-om. Raniji dizajn s jednim prozorom rastao bi na koju duljinu pozivatelj zamoli, pa bi jedno veliko sekvencijalno čitanje moglo prijeći nominalnu veličinu chunka dok bi nasumični skok odmah bacio prethodni prozor. Trenutačni keš poravnava svaki izvorni ofset na ChunkSize, dohvaća točno jedan chunk po promašaju i provodi kruti bajtni budžet kroz više prozora. Svaki izričit budžet koji pošaljete podiže se na najmanje jedan puni chunk, pa jedno čitanje uvijek napreduje chunk po chunk i vršno opterećenje keša ostaje predvidivo. ChunkSize ispod 4096 vraća se na zadane 64 KiB
Evidencija ponovljenih čitanja dio je koji vrijedi povezati s vašom telemetrijom. PDFlibPas prepoznaje ponavljanje po poravnatom početku chunka i drži uređene neprekinute intervale, što razdvaja pravi prvi dohvat od ponovnog dohvata nakon izbacivanja uz knjigovodstvo koje ne raste linearno s veličinom datoteke. GetRangeSourceCacheInfo vraća cijelu sliku kao JSON, SetRangeSourceCacheLimit mijenja budžet tijekom izvođenja, a ClearRangeSourceCache baca prozore i resetira statistiku zajedno. Smanjenje budžeta tijekom izvođenja čuva povijest i broji otpuštanja uzrokovana budžetom kao izbacivanja, pa rastući repeatedReads uz ravan hits signal je 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;
Što se dogodi kad nekoliko niti želi isti chunk?
Čekaju na jedan zahtjev, a ne na više njih. Klasični TStream ima jedan pokazivač pozicije, i dvije niti koje svaka zaključava ispravno i dalje mogu imati tu poziciju prepisanom između Seek i Read, pa lijeni objekti i segmentirana čitanja u PDFlibPasu koriste apsolutni ReadAt koji nikad ne pomjera pokazivač. Svaki poravnati chunk dobiva jedan zahtjev u letu koji svaki pozivatelj za taj chunk dijeli, susjedni chunkovi u redu čekanja spajaju se prije nego što počne izvorno čitanje, i jedno fizičko čitanje ograničeno je na 16 MiB, pa se nalet paralelnog rada na stranicama ne pojačava ni u dvostruke male zahtjeve ni u jedan apsurdno veliki. Spajajući prozor zadano je 2 ms i odnosi se samo na prvi nedostajući chunk svakog ReadAt-a; pozicijski Read nikad ne čeka na njega, a slanje nule uklanja početno kašnjenje sakupljanja sasvim, što je važno za duga sekvencijalna skeniranja koja bi inače nakupljala čekanje chunk po chunk. Pozicija, metapodaci keša i izvorna čitanja stoje iza tri odvojena ključa, a sam izvorni povratni poziv serijaliziran je, što omogućuje da se adapter baze podataka ili object storea bez unutarnje zaštite niti koristi nepromijenjen. Čekatelji primaju vlastitu kopiju podataka, pa kasnije LRU izbacivanje ne može poništiti međuspremnik koji je već predan
Možete li pitati je li stranica 900 spremna bez dohvaćanja?
Možete, i upravo za to služi neobavezan povratni poziv dostupnosti. Običan povratni poziv čitanja ne razlikuje bajtove koji su već stigli od bajtova koji traže blokirajući odlazak i povratak, i ispitivanje probnim čitanjem pokrenulo bi upravo preuzimanje kojemu pokušavate umaknuti. TPDFlibRangeAvailabilityEvent odgovara na jedno pitanje, može li se potpun raspon pročitati odmah, i zabranjeno mu je dohvaćati bilo što; bajtove koje keš već pokriva uvijek računa dostupnima. GetRangeSourceDataAvailability preslikava indirektne objekte na fizičke raspone pohrane zabilježene u stavkama unakrsnih referenci, razrješuje kompresirane objekte na njihov object stream spremnik, ispravlja pomaknuto PDF zaglavlje i parsira objekt tek nakon što puni raspon prođe nepovlačeću probu, pa nedostajući put nikad ne poziva vaš povratni poziv čitanja
Obilazak ima opseg, a nije iscrpan. Upit stranice prolazi samo granu stabla stranica koja sadrži ciljnu stranicu i zatim dodaje sadržaj stranice, resurse, anotacije i naslijeđene atribute stranice, preskačući Parent i P povratne grane da jedna stranica ili widget ne može unatrag proširiti se na cijeli dokument. Graf objekta ograničen je na 100000 traženih objekata i dubinu 256, tok objekti parsiraju se rječnikom najprije, a fallback potpunog parsiranja dopušten je samo za pohranjene objekte do 4 MiB. JSON izvještaj spaja preklapajuće i susjedne intervale prije brojanja, pa se requiredBytes i missingBytes računaju iz spojenih polja requiredRanges i missingRanges, čiji je end uključiva krajnja točka. Upit nad objektom koji je već dostupan može napuniti range keš; upit nad nedostajućim 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" uz spojene "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { npr. datoteka uopće nema AcroForm }
end;
Zašto prefetch mora iterirati
Jer jedno čitanje trenutačnih missingRanges ne čini stranicu dostupnom. Nedostajući čvor stabla stranica ili object stream otkriva sljedeći sloj ovisnosti tek kad stigne, pa PDFlibPas prefetch posao izvodi petlju upit, dohvat, ponovni upit dok stranica, form ili graf objekta nije potpuno dostupan ili dok ga bajtno ili prolazno ograničenje ne zaustavi. Posao koristi vlastiti čitač i mali sekundarni keš čiji izvor podataka prosljeđuje apsolutna čitanja izvornom range toku, što stanje parsiranja drži izoliranim od prednjeg TSmartPDFReader-a dok bajtovi koje stvarno preuzme i dalje padaju u dijeljeni glavni keš. Jedna radna nit postoji po range toku, u skladu sa serijalizacijom koju izvorni povratni poziv već traži, i red biranja ide po četiri razine prioriteta pa po redoslijedu predaje unutar razine. MaxBytes naplaćuje se u fizičkim bajtovima chunka, pa parser koji traži jedan bajt unutar nekeširanog chunka i dalje plaća cijeli chunk, dok chunkovi već u dijeljenom kešu posao ne koštaju ništa. Otkazivanje posla u redu čekanja dolazi do završnog stanja s nula izvornih čitanja; posao u tijeku provjerava se prije svakog prolaza ovisnosti i svakog izvornog chunka, a oslobađanje range toka čeka da povratni poziv u letu vrati umjesto da pokuša prekinuti
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 zadnji
potpuni izvještaj dostupnosti, pa LIMIT_REACHED ostaje razlučit
od FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Gdje se to degradira u preuzimanje cijele datoteke
Range učitavanje kladion je na raspored datoteke, i neke datoteke ga ne poštuju. Linearizirana datoteka po ISO 32000-1 §7.5.8 dobar je slučaj: odjeljak prve stranice ugrijava se pri otvaranju, ograničen i postojećim 4 MiB sigurnosnim pragom i trenutačnim keš budžetom da zagrijavanje ne može odmah izbaciti većinu samog sebe. Nelinearizirana datoteka i dalje se razrješuje kroz trailer i lanac unakrsnih referenci kraj kraja, što košta par dodatnih odlazaka i povratka, a ne katastrofu. Prava litica je oštećena datoteka koja forsira put popravka, jer obnova tablice unakrsnih referenci znači skeniranje zaglavlja objekata preko cijelog dokumenta, a to je potpuno preuzimanje koje stiže jedan chunk po chunk. Latencija je druga poštena granica: pri 60 ms po zahtjevu, parsiranje nasumičnog pristupa koje treba četrdeset nekeširanih chunkova provesti će preko dvije sekunde u prijenosu koljegod keš dobar bio, i upravo to argument read-ahead i prioritetni red postoje sakriti. Ista disciplina pojavljuje se u pristupu izravnog pristupa spajanju i dijeljenju velikih PDF-ova, i ovaj keš leži ispod paralelnog prikazivanja stranica i diskovnog keša stranica preglednika jednako
API izvora raspona, upit dostupnosti i prefetch planer dio su standardne PDFlibPas Delphi PDF Library za Delphi, C++Builder i Free Pascal; stranica proizvoda nosi potpunu referencu parametara za LoadFromRangeSource uz prefetch prioritetne i stanje konstante