2 GB naskenovaný archív leží v S3 buckete a používateľ chce stranu 900. PDFlibPas môže obslúžiť tú stranu bez stiahnutia súboru: LoadFromRangeSource stavia read-only seekovateľný stream nad váš vlastný byte-range callback a odovzdá ho TPDFDocument, takže parser potiahne cross-reference tabuľky, jednu vetvu stromu strán a jeden obsahový stream
Transportná strana je stará a nudná. HTTP servery reklamujú byte range desaťročia, teraz špecifikované v RFC 9110 §14, a každý object store hovorí ten istý dialekt. PDF strana je rovnako usadená: ISO 32000-1 §7.5.8 definuje linearizáciu presne preto, aby čítačka mohla renderovať prvú stranu z prednej časti súboru. To, čo v Delphi chýbalo, je diel uprostred, časť, ktorá rozhoduje, ktoré rozsahy si vyžiadať, koľko ich držať a ako sa vyhnúť dvojitej žiadosti
Čo LoadFromRangeSource potrebuje od vášho transportu?
Dve veci a ani jedna nie je stream. PDFlibPas si žiada autoritatívny SourceSize a synchrónny čítací callback typu TPDFlibRangeReadEvent, deklarovaný ako function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Interně sa pár stáva TCallbackByteRangeSource vystavujúcim SourceSize a ReadRange, zabaleným do streamu, ktorého vlastníctvo prechádza na dokument. Váš callback cieľ a jeho backend ostávajú vaše: dokument uvoľní obal pri zatvorení, vyčistení alebo opätovnom načítaní, ale nikdy sa nesiahne na transportný objekt za metódovým ukazovateľom
Kontrakt je zámerne zhovievavý jedným smerom a prísny druhým. Krátke čítanie je legálne a jednoducho znamená, že parser si znova požiada. Callback, ktorý vyvolá výnimku, sa prevedie na krátke čítanie a konverguje cez normálnu cestu zlyhania načítania. Callback, ktorý tvrdí, že zapísal viac než Count bajtov, sa svorkuje, pretože chybný provider nesmie pretekať cache buffer. Opakovania hesla prestavajú čerstvý range stream a čerstvý parse stav nad tým istým callback zdrojom, takže neúspešný pokus nemôže nechať za sebou zastaraný pozíciu, okno ani dešifrovací stav
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
{ jedno blokujúce 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; { uvoľňuje obalový stream }
Src.Free; { váš transport, váš životný cyklus }
end;
Koľko cache rozsahov naozaj drží?
Štandardne 4 MiB, rozložené cez okná zarovnané na chunky a vysídľované LRU. Skorší návrh s jediným oknom rástol na akúkoľvek dĺžku, o ktorú volajúci požiadal, takže jedno veľké sekvenčné čítanie mohlo prefúkať nominálnu veľkosť chunku, zatiaľ čo náhodný skok zahodil predchádzajúce okno okamžite. Súčasná cache zarovnáva každý zdrojový offset na ChunkSize, fetchuje presne jeden chunk na každý miss a vynucuje pevný bajtový rozpočet naprieč viacerými oknami. Akýkoľvek explicitný rozpočet, ktorý pošlete, sa zdvíha na aspoň jeden celý chunk, takže jediné čítanie vždy postupuje chunk po chunku a špičková záťaž cache zostáva predvídateľná. ChunkSize pod 4096 pada späť na predvolených 64 KiB
Účtovníctvo opakovaných čítaní je časť, ktorú stojí za to napojiť do vašej telemetrie. PDFlibPas identifikuje opakovanie zarovnaným začiatkom chunku a drží usporiadané súvislé intervaly, čo oddeľuje skutočný prvý fetch od refetchu po vysídlení, a pritom drží účtovníctvo od lineárneho rastu s veľkosťou súboru. GetRangeSourceCacheInfo vracia celý obraz ako JSON, SetRangeSourceCacheLimit mení rozpočet za behu a ClearRangeSourceCache zahodí okná a resetuje štatistiky spolu. Zmenšenie rozpočtu za behu drží históriu a počíta rozpočtom riadené uvoľnenia ako vysídlenia, takže rastúce repeatedReads proti plochému hits je váš signál, že pracovná množina sa už nezmestí
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;
Čo sa stane, keď viacero vlákien chce ten istý chunk?
Čakajú na jednu žiadosť, nie na viacero. Klasický TStream má jediný kurzor pozície a dve vlákna, ktoré sa každé správne zamknú, môžu mať stále tú pozíciu prepísanú medzi Seek a Read, takže lazy objekty a segmentované čítania v PDFlibPas používajú absolútne ReadAt, ktoré nikdy nepohnú kurzor. Každý zarovnaný chunk dostane jedinú in-flight žiadosť, ktorú zdieľa každý volajúci toho chunku, susedné frontované chunky sa zlúčia pred štartom zdrojového čítania a jedno fyzické čítanie sa svorkuje na 16 MiB, takže dávková paralelná práca so stranami sa zosilní ani do duplikovaných malých žiadostí, ani do jednej absurdne veľkej. Zlučovacie okno má predvolene 2 ms a aplikuje sa len na prvý chýbajúci chunk každého ReadAt; pozičné Read nikdy nečaká naň a posunutie nuly odstráni počiatočné oneskorenie zberu úplne, čo záleží pri dlhých sekvenčných skenoch, ktoré by inak akumulovali čakanie chunk po chunku. Pozícia, metadata cache a zdrojové čítania sedia za tromi samostatnými zámkami a samotný zdrojový callback je serializovaný, čo je to, čo dovoľuje databázovému alebo object-store adaptéru bez internej ochrany vlákien byť použitý nezmenený. Čakajúci dostanú svoju vlastnú kópiu dát, takže neskoršie LRU vysídlenie nemôže zneplatniť buffer, ktorý už bol odovzdaný
Možete sa opýtať, či je strana 900 hotová, bez jej fetchu?
Áno a presne na to je voliteľný callback dostupnosti. Obyčajný čítací callback nedokáže odlíšiť bajty, ktoré už pristáli, od bajtov, ktoré vyžadujú blokujúci okruh, a prieskum skúšobným čítaním by spustil presne to stiahnutie, ktorému sa snažíte vyhnúť. TPDFlibRangeAvailabilityEvent odpovedá na jedinú otázku, či celý rozsah sa dá prečítať okamžite, a je zakázané fetchovať čokoľvek; bajty, ktoré cache už pokrýva, sa vždy počítajú ako dostupné. GetRangeSourceDataAvailability mapuje nepriame objekty na fyzické rozsahy úložiska zaznamenané v cross-reference položkách, rozlíši komprimované objekty na ich objektový stream kontajner, koriguje posunutú PDF hlavičku a parsuje objekt až po tom, ako celý rozsah prejde nefetchujúcim prieskumom, takže chýbajúca cesta nikdy nezavolá váš čítací callback
Prechod je šcopeovaný, nie vyčerpávajúci. Dotaz strany prechádza len vetvou stromu strán obsahujúcou cieľovú stranu a potom pridá obsah strany, zdroje, anotácie a zdedené atribúty strany, preskočí spätné hrany Parent a P, takže jediná strana alebo widget sa nedokáže roztiahnuť dozadu do celého dokumentu. Graf objektov je ohraničený na 100000 požadovaných objektov a hĺbku 256, streamové objekty sa parsujú najprv slovník a plný parse fallback je dovolený len pre uložené objekty do 4 MiB. JSON správa zlúči prekrývajúce sa a susedné intervaly pred počítaním, takže requiredBytes a missingBytes sa počítajú z zlúčených polí requiredRanges a missingRanges, ktorých end je inkluzívny koncový bod. Dotazovanie objektu, ktorý je už dostupný, môže naplniť cache rozsahov; dotazovanie chýbajúceho nechá štatistiky čítania nedotknuté
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 nesie "missingBytes" plus zlúčené "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { napr. súbor vôbec nemá AcroForm }
end;
Prečo musí prefetch iterovať
Pretože prečítanie aktuálnych missingRanges raz neurobí stranu dostupnou. Chýbajúci uzol stromu strán alebo objektový stream odhalí až po svojom príchode ďalšiu vrstvu závislostí, takže prefetch práca PDFlibPas beží vo vzore dotaz, fetch, znovu-dopyt, až kým strana, formulár alebo graf objektov nie je úplne dostupný, alebo bajt či priebehový limit nezastaví. Práca používa vlastného čítačku a malú sekundárnu cache, ktorej dátový zdroj preposiela absolútne čítania pôvodnému range streamu, čo drží parse stav izolovaný od popredného TSmartPDFReader, zatiaľ čo bajty, ktoré skutočne stiahne, pristanú v zdieľanej hlavnej cache. Jedno robotnícke vlákno existuje na range stream, zodpovedajúc serializácii, ktorú zdrojový callback už vyžaduje, a front vyberá podľa štyroch úrovní priority a potom podľa poradia odoslania v rámci úrovne. MaxBytes sa účtuje vo fyzických bajtoch chunkov, takže parser, ktorý si žiada jediný bajt vo vnútri neuloženého chunku, aj tak zaplatí celý chunk, zatiaľ čo chunky už v zdieľanej cache stoja prácu nič. Zrušenie frontovanej práce dosiahne terminálny stav s nulovými zdrojovými čítaniami; bežiaca práca sa kontroluje pred každou závislostnou passou a každým zdrojovým chunkom a uvoľnenie range streamu čaká na in-flight callback, aby sa vrátil, namiesto pokusu o prerušenie
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" a posledná
plná správa dostupnosti, takže LIMIT_REACHED ostáva odlíšiteľný
od FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Kde sa to degraduje na stiahnutie celého súboru
Range načítanie je stávka na usporiadanie súboru a niektoré súbory si ju nevážia. Linearizovaný súbor podľa ISO 32000-1 §7.5.8 je dobrý prípad: sekcia prvej strany sa zohreje pri otvorení, ohraničená jak existujúcim bezpečnostným prahom 4 MiB, tak aktuálnym cache rozpočtom, takže zahrievanie nemôže okamžite vysídiť väčšinu seba samého. Nelinearizovaný súbor sa aj tak rozlíši cez trailer a reťaz cross-referencií pri konci, čo stojí pár extra okruhov namiesto katastrofy. Skutočný útes je poškodený súbor, ktorý vynúti cestu opravy, pretože rekonštrukcia cross-reference tabuľky znamená skenovanie hlavičiek objektov naprieč celým dokumentom a to je plné stiahnutie prichádzajúce jeden chunk po chunku. Latencia je druhý úprimný limit: pri 60 ms na žiadosť strávi náhodný prístup parse potrebujúci štyridsať neuložených chunkov viac než dve sekundy v tranzite, nezáleží, aká dobrá je cache, čo je presne to, čo argument read-ahead a prioritný front existujú skryť. Rovnaká disciplína sa objavuje v prístupe priameho prístupu k zlučovaniu a deleniu veľkých PDF a táto cache sedí pod paralelným renderovaním strán aj diskovou cache strán prehliadača
API zdroja rozsahov, dotaz dostupnosti a prefetch plánovač sú časťou štandardnej PDFlibPas Delphi PDF Library pre Delphi, C++Builder a Free Pascal; produktová stránka nesie kompletnú referenciu parametrov LoadFromRangeSource spolu s konstantami priority a stavu prefetchu