Een gescand archief van 2 GB ligt in een S3-bucket en de gebruiker wil pagina 900. PDFlibPas kan die pagina leveren zonder het bestand te downloaden: LoadFromRangeSource bouwt een alleen-lezen, doorzoekbare stream over uw eigen byte-range-callback en geeft die aan TPDFDocument, dus de parser trekt de kruisverwijzingstabellen, één paginaboomtak en één contentstream
De transportkant hiervan is oud en saai. HTTP-servers adverteren al decennia byte ranges, nu gespecificeerd in RFC 9110 §14, en elke objectopslag spreekt hetzelfde dialect. De PDF-kant is net zo afgehandeld: ISO 32000-1 §7.5.8 definieert linearisatie juist zodat een reader de eerste pagina vanaf de voorkant van het bestand kan renderen. Wat in Delphi ontbrak is het stuk in het midden, het deel dat beslist welke ranges u vraagt, hoeveel u bewaart, en hoe u dubbel vragen vermijdt
Wat vraagt LoadFromRangeSource aan uw transport?
Twee dingen, en geen van beide is een stream. PDFlibPas vraagt een autoritatieve SourceSize en een synchrone lees-callback van het type TPDFlibRangeReadEvent, gedeclareerd als function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Intern wordt het paar een TCallbackByteRangeSource die SourceSize en ReadRange blootlegt, verpakt in een stream waarvan het eigendom naar het document gaat. Uw callback-doel en zijn backend blijven van u: het document geeft de wrapper vrij bij close, clear of reload, maar raakt nooit het transportobject achter de methodewijzer aan
Het contract is bewust coulant in de ene richting en streng in de andere. Een korte lezing is legaal en betekent simpelweg dat de parser opnieuw vraagt. Een callback die een exceptie gooit wordt omgezet in een korte lezing en convergeert via het normale load-failure-pad. Een callback die beweert meer dan Count bytes te hebben geschreven wordt afgeklempt, want een foutieve provider mag de cachebuffer niet kunnen overschrijven. Wachtwoordhertoetsen bouwen een verse range-stream en een verse parsetoestand op over dezelfde callbackbron, dus een mislukte poging kan geen verouderde positie, window of decryptietoestand achterlaten
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
{ één blokkerende GET met 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; { geeft de wrapperstream vrij }
Src.Free; { jouw transport, jouw levensduur }
end;
Hoeveel houdt de range-cache werkelijk vast?
Standaard 4 MiB, verspreid over chunk-gealigneerde windows en LRU weggegooid. Het eerdere single-window-ontwerp groeide naar elke lengte die de aanroeper vroeg, dus één grote sequentiële lezing kon voorbij de nominale chunkgrootte schieten terwijl een sprong in het wilde weg het vorige window onmiddellijk wegooide. De huidige cache aligneert elke bronoffset op ChunkSize, haalt bij elke miss exact één chunk op, en handhaaft een hard bytebudget over meerdere windows. Elk expliciet budget dat u meegeeft wordt verhoogd naar minstens één volle chunk, dus een enkele lezing schrijdt altijd chunk voor chunk voort en de piekbelasting van de cache blijft voorspelbaar. Een ChunkSize onder 4096 valt terug op de standaard van 64 KiB
Herhalingslees-boekhouding is het deel dat de moeite waard is om in uw telemetrie te vatten. PDFlibPas herkent een herhaling aan de gealigneerde chunkstart en houdt geordende aaneengesloten intervallen bij, wat een echte eerste ophaal scheidt van een herhaling na eviction terwijl de boekhouding niet lineair met de bestandsgrootte meegroeit. GetRangeSourceCacheInfo geeft het hele beeld terug als JSON, SetRangeSourceCacheLimit stelt het budget tijdens runtime bij, en ClearRangeSourceCache gooit de windows weg en reset de statistieken samen. Het verkleinen van het budget tijdens runtime bewaart de geschiedenis en telt de budgetgedreven vrijgaven als evictions, dus een stijgende repeatedReads tegenover een vlakke hits is uw signaal dat de working set niet meer past
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;
Wat gebeurt er als meerdere threads dezelfde chunk willen?
Ze wachten op één verzoek, niet op meerdere. Een klassieke TStream heeft één positiecursor, en twee threads die elk correct locken kunnen die positie nog steeds herschreven krijgen tussen een Seek en een Read, dus lazy objects en gesegmenteerde lezingen in PDFlibPas gebruiken een absolute ReadAt die de cursor nooit verplaatst. Elke gealigneerde chunk krijgt één in-flight verzoek dat elke aanroeper voor die chunk deelt, aangrenzende wachtrijchunks worden samengevoegd voordat de bronlezing begint, en één fysieke lezing is afgekapt op 16 MiB, dus een uitbarsting van parallel paginawerk versterkt zich in geen dubbele kleine verzoeken noch in één absurd grote. Het mergewindow staat standaard op 2 ms en geldt alleen voor de eerste ontbrekende chunk van elke ReadAt; positionele Read wacht er nooit op, en nul meegeven schrapt de initiële verzamelvertraging volledig, wat telt voor lange sequentiële scans die anders chunk voor chunk zouden opstapelen. Positie, cachemetadata en bronlezingen zitten achter drie aparte locks, en de broncallback zelf wordt geserialiseerd, wat het mogelijk maakt een database- of objectopslag-adapter zonder interne threadbescherming ongewijzigd te gebruiken. Wachtenden ontvangen hun eigen kopie van de data, dus een latere LRU-eviction kan een buffer niet ongeldig maken die al is uitgehand
Kunt u vragen of pagina 900 klaar is zonder haar op te halen?
Ja, en dat is precies waarom de optionele availability-callback bestaat. Een kale lees-callback kan bytes die al aangekomen zijn niet onderscheiden van bytes die een blokkerende rondtrip vereisen, en peilen met een proeflezing zou precies die download triggeren die u probeert te vermijden. TPDFlibRangeAvailabilityEvent beantwoordt één vraag, of een complete range onmiddellijk leesbaar is, en heeft geen toestemming om iets op te halen; bytes die de cache al dekt tellen altijd als beschikbaar. GetRangeSourceDataAvailability mapt indirecte objecten op de fysieke opslagranges die in de kruisverwijsinvoeren staan, herleidt gecomprimeerde objecten naar hun object stream-container, corrigeert voor een verschoven PDF-header, en parst een object pas nadat de volledige range de niet-ophalende peiling doorstaat, dus het ontbrekende pad roept uw lees-callback nooit aan
De traversering is begrensd in plaats van uitputtend. Een paginaquery loopt alleen door de tak van de paginaboom die de doelpagina bevat en voegt daarna paginacontent, resources, annotaties en geërfde pagina-attributen toe, waarbij de terugranden Parent en P worden overgeslagen zodat één pagina of widget zich niet achterwaarts kan uitbreiden naar het hele document. De objectgrafiek is begrensd op 100000 opgevraagde objecten en een diepte van 256, streamobjecten worden woordenboek-eerst geparsd, en een full-parse-fallback is alleen toegestaan voor opgeslagen objecten tot 4 MiB. Het JSON-rapport voegt overlappende en aangrenzende intervallen samen voordat het telt, dus requiredBytes en missingBytes worden berekend uit de samengevoegde arrays requiredRanges en missingRanges, waarvan end een inclusief eindpunt is. Het bevragen van een object dat al beschikbaar is kan de range-cache vullen; het bevragen van een ontbrekend laat de leesstatistieken onaangetast
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 bevat "missingBytes" plus de samengevoegde "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { bijv. het bestand heeft helemaal geen AcroForm }
end;
Waarom prefetch moet herhalen
Omdat de huidige missingRanges één keer lezen de pagina niet beschikbaar maakt. Een ontbrekende paginaboomknoop of object stream onthult pas de volgende laag afhankelijkheden nadat hij aankomt, dus een prefetch-job van PDFlibPas draait een query-, ophaal-, hernieuwde-query-lus tot de pagina, het formulier of de objectgrafiek volledig beschikbaar is of een byte- of passlimiet hem stopt. De job gebruikt zijn eigen reader en een kleine secundaire cache waarvan de databron absolute lezingen doorstuurt naar de originele range-stream, wat de parsetoestand geïsoleerd houdt van de TSmartPDFReader op de voorgrond terwijl de bytes die hij echt downloadt toch in de gedeelde hoofdcache belanden. Er bestaat één werkthread per range-stream, passend bij de serialisatie die de broncallback al vereist, en de wachtrij kiest op vier prioriteitsniveaus en daarna op indieningsvolgorde binnen een niveau. MaxBytes wordt gerekend in fysieke chunkbytes, dus een parser die om één byte in een niet-gecachte chunk vraagt betaalt toch de hele chunk, terwijl chunks die al in de gedeelde cache zitten de job niets kosten. Een wachtrijjob annuleren bereikt een terminale toestand met nul bronlezingen; een draaiende job wordt getoetst vóór elke afhankelijkheidspas en elke bronchunk, en het vrijgeven van de range-stream wacht tot een in-flight callback terugkeert in plaats van te proberen hem te onderbreken
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" en het laatste
volledige beschikbaarheidsrapport, zodat LIMIT_REACHED onderscheidbaar
blijft van FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Waar dit degradeert tot een download van het hele bestand
Range loading is een gok op de bestandsindeling, en sommige bestanden eren die niet. Een gelineariseerd bestand volgens ISO 32000-1 §7.5.8 is het goede geval: de eerste-paginasectie wordt warm gemaakt bij het openen, begrensd door zowel de bestaande veiligheidsdrempel van 4 MiB als het huidige cachebudget, zodat de opwarming niet meteen het grootste deel van zichzelf weggooit. Een niet-gelineariseerd bestand herleidt nog steeds via de trailer en de kruisverwijsketen vlak voor het einde, wat een paar extra rondtrips kost in plaats van een ramp. De echte klif is een beschadigd bestand dat het reparatiepad afdwingt, want het reconstrueren van een kruisverwijstabel betekent het hele document scannen naar objectheaders, en dat is een volledige download die chunk voor chunk aankomt. Latentie is de andere eerlijke grens: bij 60 ms per verzoek brengt een random-access-parse die veertig niet-gecachte chunks nodig heeft meer dan twee seconden onderweg door, hoe goed de cache ook is, en precies dat is wat het read-ahead-argument en de prioriteitswachtrij bestaan om te verhullen. Dezelfde discipline verschijnt in de direct-access-aanpak voor het samenvoegen en splitsen van grote PDFs, en deze cache ligt onder parallelle paginarendering en de viewer-schijfpaginacache al net zo
De range source-API, de availability-query en de prefetch-planner horen bij de standaard PDFlibPas Delphi PDF Library voor Delphi, C++Builder en Free Pascal; de productpagina voert de volledige parameterreferentie voor LoadFromRangeSource samen met de prefetch-prioriteits- en toestandsconstanten