Naskenovaný archiv o 2 GB leží v kbelíku S3 a uživatel chce stránku 900. PDFlibPas ji dodá bez stahování celého souboru: LoadFromRangeSource postaví read-only vyhledávatelný proud nad vaším vlastním callbackem bajtových rozsahů a předá ho TPDFDocument, takže parser vytáhne tabulky křížových odkazů, jednu větev stromu stránek a jeden proud obsahu
Dopravná strana téhle stavby je stará a nudná. HTTP servery inzerují bajtové rozsahy celá desetiletí, dnes popsané v RFC 9110 §14, a každé objektové úložiště mluví tímtéž nářečím. Strana PDF je stejně vyřešená: ISO 32000-1 §7.5.8 definuje linearizaci přesně proto, aby čtenář mohl vykreslit první stránku z přední části souboru. Chybí-li v Delphi něco, je to kus uprostřed, část, která rozhoduje, které rozsahy si vyžádat, kolik jich držet a jak se neptat dvakrát
Co LoadFromRangeSource potřebuje od vaší dopravy?
Dvě věci a ani jedna není proud. PDFlibPas žádá autoritativní SourceSize a synchronní read callback typu TPDFlibRangeReadEvent, deklarovaný jako function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Uvnitř se dvojice stane TCallbackByteRangeSource vystavujícím SourceSize a ReadRange, zabaleným v proudu, jehož vlastnictví přechází na dokument. Cíl vašeho callbacku i jeho backend zůstávají vaše: dokument uvolní obálku při uzavření, vyčištění nebo znovunačtení, ale nikdy nesáhne na transportní objekt za ukazatelem metody
Smlouva je úmyslně shovívavá jedním směrem a přísná druhým. Kratší čtení je legální a prostě znamená, že se parser zeptá znovu. Callback, který vyhodí výjimku, se převede na kratší čtení a konverguje normální cestou selhání načítání. Callback, který tvrdí, že zapsal víc než Count bajtů, se zalamuje, protože pokazený provider nesmí přetéct vyrovnávací buffer cache. Pokusy o heslo staví čerstvý rozsahový proud a čerstvý stav parsování nad tímtéž zdrojem callbacku, takže neúspěšný pokus nemůže zanechat zastaralou pozici, 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
{ jeden blokující 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; { uvolní obálkový proud }
Src.Free; { vaše doprava, váš životní cyklus }
end;
Kolik ve skutečnosti drží cache rozsahů?
Ve výchozím stavu 4 MiB, rozprostřené po oknech zarovnaných na bloky a vytěsněných LRU. Starší návrh s jedním oknem rostl na libovolnou délku, o kterou volající požádal, takže jedno velké sekvenční čtení mohlo přeletět nominální velikost bloku, zatímco náhodný skok ihned vyhodil předchozí okno. Současná cache zarovnává každý zdrojový offset na ChunkSize, stahuje při každém miss přesně jeden blok a vymáhá tvrdý bajtový rozpočet napříč několika okny. Jakýkoli explicitní rozpočet, který předáte, se zvedne alespoň na jeden celý blok, takže jediné čtení postupuje blok po bloku a špičková zátěž cache zůstává předvídatelná. ChunkSize pod 4096 padá zpět na výchozích 64 KiB
Účtování opakovaných čtení je kus, který stojí za napojení na vaši telemetrii. PDFlibPas rozezná opakování podle zarovnaného začátku bloku a drží uspořádané souvislé intervaly, což odděluje skutečné první stažení od refetch po vytěsnění, aniž by účetnictví rostlo lineárně s velikostí souboru. GetRangeSourceCacheInfo vrací celý obraz jako JSON, SetRangeSourceCacheLimit mění rozpočet za běhu a ClearRangeSourceCache shodí okna a vynuluje statistiky dohromady. Zmenšení rozpočtu za běhu drží historii a počítá rozpočtem vynucená uvolnění jako vytěsnění, takže rostoucí repeatedReads při plochém hits je váš signál, že pracovní množina už se nevejde
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;
Co se stane, když několik vláken chce tentýž blok?
Čekají na jeden požadavek, ne na několik. Klasický TStream má jediný kurzor pozice a dvě vlákna, která se každé správně zamknou, si přesto mohou tu pozici přepsat mezi Seek a Read, takže líné objekty a segmentované čtení v PDFlibPas používají absolutní ReadAt, který kurzor nikdy nepohne. Každý zarovnaný blok dostane jedinou žádost v letu, o niž se dělí každý volající toho bloku, sousední frontované bloky se sloučí, než zdrojové čtení nastartuje, a jedno fyzické čtení se uzavírá na 16 MiB, takže závan paralelní práce na stránkách se nerozmnoží ani v duplicitní malé požadavky, ani v jeden absurdně velký. Slučovací okno má výchozích 2 ms a vztahuje se jen na první chybějící blok každého ReadAt; poziční Read na něj nikdy nečeká a nula odstraní počáteční sběrnou prodlevu úplně, což zajímá dlouhé sekvenční skeny, které by jinak hromadily čekání blok po bloku. Pozice, metadata cache i zdrojová čtení sedí za třemi oddělenými zámky a samotný zdrojový callback se serializuje, což dovolí použít adapter databáze či objektového úložiště bez vnitřní ochrany vláken beze změny. Čekající dostávají vlastní kopii dat, takže pozdější vytěsnění LRU nemůže zneplatnit buffer, který už byl předán
Dá se zeptat, zda je stránka 900 hotová, aniž by se stahovala?
Ano a přesně k tomu slouží volitelný callback dostupnosti. Obyčejný read callback nerozezná bajty, které už doletěly, od bajtů, které vyžadují blokující okružní cestu, a píchnutí zkušebním čtením by spustilo přesně to stahování, kterému se snažíte vyhnout. TPDFlibRangeAvailabilityEvent odpovídá na jedinou otázku, zda lze celý rozsah přečíst okamžitě, a je zakázáno, aby cokoli stahovalo; bajty, které cache už kryje, se počítají jako dostupné vždy. GetRangeSourceDataAvailability mapuje nepřímé objekty na fyzické rozsahy úložiště zapsané v položkách křížových odkazů, rozliší komprimované objekty na jejich kontejner objektového proudu, opraví posunutou PDF hlavičku a parsuje objekt až poté, co celý rozsah projde ne-stahující sondou, takže chybějící cesta nikdy nezavolá váš read callback
Průchod je zúžený, ne vyčerpávající. Dotaz na stránku projde jen větví stromu stránek obsahující cílovou stránku a pak přidá obsah stránky, zdroje, anotace a děděné atributy stránky, s přeskočením zpětných hran Parent a P, takže jediná stránka ani widget se nerozexpanduje zpět do celého dokumentu. Graf objektů se uzavírá na 100000 vyžádaných objektů a hloubku 256, proudové objekty se parsují nejdřív po slovníku a plné parsování se dovoluje jen pro uložené objekty do 4 MiB. JSON hlášení sloučí překrývající se a sousedící intervaly před počítáním, takže requiredBytes a missingBytes se počítají ze sloučených polí requiredRanges a missingRanges, jejichž end je hraniční bod včetně. Dotaz na objekt, který už je dostupný, může naplnit rozsahovou cache; dotaz na chybějící nechá statistiky čtení nedotčené
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 nese "missingBytes" plus sloučené "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { např. soubor nemá vůbec žádné AcroForm }
end;
Proč prefetch musí iterovat
Protože jednorázové přečtení aktuálních missingRanges stránku dostupnou neučiní. Chybějící uzel stromu stránek nebo objektový proud odhalí další vrstvu závislostí až po svém příchodu, takže prefetch job PDFlibPas běží ve smyčce dotaz–stažení–dotaz, dokud není stránka, formulář nebo graf objektů kompletně dostupný, nebo dokud ho nezastaví limit bajtů či průchodů. Job používá vlastního čtenáře a malou sekundární cache, jejíž zdroj dat přeposílá absolutní čtení původnímu rozsahovému proudu, což drží stav parsování izolovaný od popředního TSmartPDFReader, zatímco bajty, které skutečně stáhne, dopadnou do sdílené hlavní cache. Na každý rozsahový proud připadá jedno pracovní vlákno, v souladu se serializací, kterou už zdrojový callback vyžaduje, a fronta vybírá podle čtyř prioritních úrovní a pak podle pořadí podání v rámci úrovně. MaxBytes se účtuje ve fyzických bajtech bloků, takže parser, který si řekne o jediný bajt uvnitř necachovaného bloku, zaplatí za celý blok, zatímco bloky už sedící ve sdílené cache joba nic nestojí. Zrušení frontovaného jobu dospěje do koncového stavu s nulou zdrojových čtení; běžící job se kontroluje před každým průchodem závislostmi a každým zdrojovým blokem a uvolnění rozsahového proudu čeká, až žádost v letu vrátí, místo aby se ji pokoušelo přerušit
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é hlášení dostupnosti, takže LIMIT_REACHED zůstane rozeznatelné
od FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Kde se tohle degraduje na stažení celého souboru
Rozsahové načítání je sázka na uspořádání souboru a některé soubory ji nesplatí. Linearizovaný soubor podle ISO 32000-1 §7.5.8 je dobrý případ: sekce první stránky se ohřeje při otevření, ohraničená jak stávajícím 4 MiB bezpečnostním prahem, tak aktuálním rozpočtem cache, takže ohřev nemůže hned vytěsnit sám sebe. Nelinearizovaný soubor se pořád rozliší přes trailer a řetěz křížových odkazů poblíž konce, což stojí pár okružních cest navíc, ne katastrofu. Skutečný útes je poškozený soubor, který vnutí cestu opravy, protože rekonstrukce tabulky křížových odkazů znamená skenovat hlavičky objektů napříč celým dokumentem, a to je stažení celého souboru, které přichází po jednom bloku. Latence je druhá čestná mez: při 60 ms na požadavek stráví náhodně přistupující parse potřeba čtyřiceti necachovaných bloků přes dvě sekundy v tranzitu, ať je cache sebelepší, a přesně proto tu jsou argument předčítání a prioritní fronta. Tatáž disciplína se ukazuje v přístupu s přímým přístupem ke slučování a dělení velkých PDF a tahle cache sedí pod paralelním vykreslováním stránek i pod diskovou cache stránek prohlížeče
Rozsahové zdrojové API, dotaz dostupnosti i plánovač prefetch jsou součástí standardní PDFlibPas Delphi PDF Library pro Delphi, C++Builder a Free Pascal; produktová stránka nese kompletní referenci parametrů LoadFromRangeSource spolu s konstantami priorit a stavů prefetch