Arhiv, skeniran v 2 GB, živi v vedru S3 in uporabnik želi stran 900. PDFlibPas lahko servira to stran brez nalaganja datoteke: LoadFromRangeSource zgradi tok za branje samo z iskanjem prek vašega lastnega povratnega klica bajtnega obsega in ga da TPDFDocument, zato razčlenjevalnik povleče tabele navzkrižnih sklicev, eno vejo drevesa strani in en tok vsebine
Transportna stran tega je stara in dolgočasna. Strežniki HTTP oglašujejo bajtne obsege že desetletja, zdaj določene v RFC 9110 §14, vsaka objektna shramba pa govori isti jezik. Stran PDF je enako urejena: ISO 32000-1 §7.5.8 definira linearizacijo natanko zato, da bralnik lahko izriše prvo stran s sprednjega dela datoteke. Kar je v Delphiju manjkalo, je kos na sredini, del, ki odloča, katere obsege vprašati, koliko jih obdržati in kako se izogniti dvema vprašanjema
Kaj LoadFromRangeSource potrebuje od vašega transporta?
Dve stvari, in nobena ni tok. PDFlibPas vpraša za avtoritativni SourceSize in sinhroni povratni klic branja vrste TPDFlibRangeReadEvent, deklariran kot function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Navznoter par postane TCallbackByteRangeSource, ki izpostavlja SourceSize in ReadRange, ovit v tok, katerega lastništvo gre dokumentu. Vaš cilj povratnega klica in njegovo zaledje ostajata vaša: dokument sprosti ovijalnik ob zaprtju, čiščenju ali ponovnem nalaganju, nikoli pa ne dotakne transportnega objekta za kazalcem metode
Pogodba je namenoma odpuščajoča v eni smeri in stroga v drugi. Krajše branje je zakonito in preprosto pomeni, da razčlenjevalnik vpraša znova. Povratni klic, ki sproži izjemo, se pretvori v krajše branje in konvergira skozi normalno pot odpovedi nalaganja. Povratni klic, ki trdi, da je zapisal več kot Count bajtov, se prijme, ker pokvarjen ponudnik ne sme preplaviti medpomnilnika predpomnilnika. Poskusi gesla znova zgradijo svež obsežni tok in sveže stanje razčlembe nad istim virskim povratnim klicem, tako da neuspešen poskus ne more pustiti zastarelega položaja, okna ali stanja 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
{ en blokirajoč GET z 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; { sprosti ovijalni tok }
Src.Free; { vaš transport, vaša življenjska doba }
end;
Koliko obsežni predpomnilnik dejansko drži?
Privzeto 4 MiB, razporejeno čez okna poravnana na kose in izpodrinjena LRU. Zgodnja zasnova z enim oknom je rasla na kakršno koli dolžino, ki jo je klicatelj zahteval, zato je eno veliko zaporedno branje lahko pihnilo čez nominalno velikost kosa, medtem ko je naključni skok prejšnje okno takoj vrgel proč. Trenutni predpomnilnik poravna vsak virski odmik na ChunkSize, pridobi natanko en kos na zamudo in uveljavi trdi bajtni proračun čez več oken. Vsak izrecni proračun, ki ga podate, se dvigne na vsaj en cel kos, tako da eno branje vedno napreduje kos za kosom in vršna obremenitev predpomnilnika ostane napovedljiva. ChunkSize pod 4096 pade nazaj na privzeti 64 KiB
Knjigovodstvo ponovnih branj je del, vreden povezave v vašo telemetrijo. PDFlibPas prepozna ponovitev po poravnanem začetku kosa in drži urejene sosednje intervale, kar loči pravo prvo pridobitev od ponovne pridobitve po izpodrivu, hkrati pa knjigovodstvu preprečuje rast linearno z velikostjo datoteke. GetRangeSourceCacheInfo vrne celotno sliko kot JSON, SetRangeSourceCacheLimit spremeni velikost proračuna med zagonom, ClearRangeSourceCache pa spusti okna in ponastavi statistiko skupaj. Krčenje proračuna med zagonom obdrži zgodovino in šteje praznjenja, ki jih poganja proračun, kot izpodrive, zato je naraščajoči repeatedReads ob ravnem hits vaš signal, da delovna množica ne gre več noter
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;
Kaj se zgodi, ko več niti hoče isti kos?
Čakajo na eno zahtevo, ne na več. Klasični TStream ima en sam kazalec položaja, in dve niti, ki pravilno zaklepata, lahko vseeno doživita prepis tega položaja med Seek in Read, zato leni objekti in segmentirana branja v PDFlibPas uporabljajo absolutni ReadAt, ki nikoli ne premakne kazalca. Vsak poravnan kos dobi eno prošnjo v letu, ki jo vsak klicatelj za ta kos deli, sosednji čakajoči kosi se združijo, preden se začne virsko branje, eno fizično branje pa je zvezano na 16 MiB, tako da izbruh vzporednega dela na straneh ne poživi ne podvojenih majhnih zahtev ne ene absurdno velike. Okno združevanja je privzeto 2 ms in velja le za prvi manjkajoči kos vsakega ReadAt; pozicijski Read nanj nikoli ne čaka, podajanje nič pa odstrani začetno zbiranje zamikov v celoti, kar šteje pri dolgih zaporednih skeniranjih, ki bi sicer nabirala čakanje kos za kosom. Položaj, metapodatki predpomnilnika in virska branja sedijo za tremi ločenimi zaklepi, virski povratni klic pa je serializiran, kar omogoča, da adapter zbirke podatkov ali objektne shrambe brez notranje zaštite niti ostane nespremenjen. Čakajoči prejmejo svojo kopijo podatkov, zato kasnejši izpodriv LRU ne more razveljaviti medpomnilnika, ki je bil že izročen
Ali lahko vprašate, ali je stran 900 pripravljena, brez da jo pridobite?
Da, in natanko zato je tu izbirni povratni klic razpoložljivosti. Navaden povratni klic branja ne more ločiti bajtov, ki so že pristali, od bajtov, ki zahtevajo blokirajoč povratni krog, in preizkušanje s poskusnim branjem bi sprožilo ravno tisto nalaganje, ki se mu izogibate. TPDFlibRangeAvailabilityEvent odgovarja na eno vprašanje samo, ali se celoten obseg da brati takoj, in mu je prepovedano kaj pridobiti; bajti, ki jih predpomnilnik že pokriva, vedno štejejo kot razpoložljivi. GetRangeSourceDataAvailability preslika posredne objekte na fizične obsege shrambe, zabeležene v vnosih navzkrižnih sklicev, razreši stisnjene objekte na njihov vsebnik toka objektov, popravi za prestavljeno glavo PDF in razčleni objekt šele, ko celoten obseg gre čez preizkus brez pridobivanja, tako da manjkajoča pot nikoli ne pokliče vašega povratnega klica branja
Prehajanje je obsegano in ne izčrpno. Poizvedba po strani prehodi le vejo drevesa strani, ki vsebuje ciljno stran, nato doda vsebino strani, vire, pripombe in podedovane atribute strani, preskoči povratne povezave Parent in P, tako da ena sama stran ali pripomoček ne more nazaj razširiti v celoten dokument. Graf objektov je zvezan na 100000 zahtevanih objektov in globino 256, tokovni objekti se razčlenijo najprej po slovarju, zasilna pot polne razčlembe pa je dovoljena le za shranjene objekte do 4 MiB. Poročilo JSON stiče prekrivajoče in sosednje intervale pred štetjem, zato se requiredBytes in missingBytes računata iz stičenih tabel requiredRanges in missingRanges, katerih end je vključujočka končna točka. Poizvedba po objektu, ki je že razpoložljiv, lahko napolni obsežni predpomnilnik; poizvedba po manjkajočem pusti statistiko branja nedotaknjeno
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 stičene "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { npr. datoteka sploh nima AcroForm }
end;
Zakaj mora predvleka iterirati
Ker branje trenutnih missingRanges enkrat ne naredi strani razpoložljive. Manjkajoče vozlišče drevesa strani ali tok objektov razkrije naslednjo plast odvisnosti šele, ko prispe, zato delo predvleke v PDFlibPas poganja zanko poizvedba, pridobitev, ponovna poizvedba, dokler stran, obrazec ali graf objektov ni popolnoma razpoložljiv ali ga ne ustavi meja bajtov ali prehodov. Delo uporablja svojega bralnika in majhen sekundarni predpomnilnik, katerega vir podatkov posreduje absolutna branja izvornemu obsežnemu toku, kar drži stanje razčlembe ločeno od osprednega TSmartPDFReader, bajti, ki jih resnično naloži, pa vseeno pristanejo v skupnem glavnem predpomnilniku. Ena delovna nit obstaja na obsežni tok, kar se ujema s serializacijo, ki jo virski povratni klic že zahteva, vrsta pa izbira po štirih ravneh prioritete in nato po vrstnem redu oddaje znotraj ravni. MaxBytes se bremeni v fizičnih bajtov kosov, zato razčlenjevalnik, ki vpraša po enem bajtu znotraj nepredpomnjenega kosa, vseeno plača cel kos, kosi, ki so že v skupnem predpomnilniku, pa delo ne stanejo nič. Preklic čakajočega dela doseže končno stanje z nič virskimi branjmi; tekoče delo se preveri pred vsakim prehodom odvisnosti in vsakim virskim kosom, sprostitev obsežnega toka pa počaka, da se povratni klic v letu vrne, namesto da bi ga poskusila prekiniti
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" in zadnji
celoten poročilo o razpoložljivosti, tako da LIMIT_REACHED ostane
razločljivo od FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Kje to nazaduje v nalaganje cele datoteke
Obsežno nalaganje je stava na razporeditev datoteke, nekatere datoteke pa je ne častijo. Linearizirana datoteka po ISO 32000-1 §7.5.8 je dober primer: razdelek prve strani se segreje ob odprtju, zvezano z obstoječim varnostnim pragom 4 MiB in trenutnim proračunom predpomnilnika, tako da ogrevanje ne more takoj izpodrivati večine sebe. Nelinearizirana datoteka se še vedno razreši skozi priklopnik in verigo navzkrižnih sklicev blizu konca, kar stane par dodatnih povratnih krogov in ne katastrofo. Pravi peč je poškodovana datoteka, ki vsili pot popravila, ker obnova tabele navzkrižnih sklicev pomeni iskanje glav objektov čez celoten dokument, in to je polno nalaganje, ki prispe kos za kosom. Zakasnitev je druga iskrena meja: pri 60 ms na zahtevo naključno dostopna razčlemba, ki potrebuje štirideset nepredpomnjenih kosov, preživi več kot dve sekundi v prevozu, ne glede na to, kako dober je predpomnilnik, kar je ravno tisto, kar argument branja vnaprej in vrsta prioritet obstajata, da to skrijeta. Ista disciplina se pokaže v pristopu neposrednega dostopa za združevanje in rezanje velikih PDFov, ta predpomnilnik pa sedi pod vzporednim izrisovanjem strani in diskovnim predpomnilnikom strani bralnika enako
API obsežnega vira, poizvedba po razpoložljivosti in razporejevalnik predvleke so del standardne PDFlibPas Delphi PDF Library za Delphi, C++Builder in Free Pascal; produkcijska stran nosi celoten sklic parametrov za LoadFromRangeSource skupaj s konstantami prioritete in stanja predvleke