O arhivă scanată de 2 GB stă într-un bucket S3, iar utilizatorul vrea pagina 900. PDFlibPas poate livra pagina respectivă fără să descarce fișierul: LoadFromRangeSource construiește un flux seekabil doar în citire peste propriul dumneavoastră callback de intervale de octeți și îl predă lui TPDFDocument, astfel încât parserul trage doar tabelele de referințe încrucișate, o ramură din arborele de pagini și un flux de conținut
Partea de transport e veche și plictisitoare. Serverele HTTP publică intervale de octeți de decenii, acum specificate în RFC 9110 §14, iar orice object store vorbește același dialect. Partea PDF este la fel de așezată: ISO 32000-1 §7.5.8 definește liniarizarea tocmai pentru ca un cititor să poată reda prima pagină din fața fișierului. Ce a lipsit în Delphi este piesa din mijloc, partea care decide ce intervale cere, câte reține și cum evită să ceară de două ori
Ce cere LoadFromRangeSource de la transportul dumneavoastră?
Două lucruri, și niciunul nu este un flux. PDFlibPas cere un SourceSize autoritar și un callback sincron de citire de tipul TPDFlibRangeReadEvent, declarat ca function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Intern, perechea devine un TCallbackByteRangeSource care expune SourceSize și ReadRange, învelit într-un flux a cărui proprietate trece la document. Ținta callback-ului și backendul ei rămân ale dumneavoastră: documentul eliberează învelișul la close, clear sau reload, dar nu atinge niciodată obiectul de transport din spatele pointerului de metodă
Contractul este, în mod deliberat, îngăduitor într-o direcție și strict în cealaltă. O citire scurtă este legală și înseamnă pur și simplu că parserul cere din nou. Un callback care ridică o excepție este convertit într-o citire scurtă și converge pe calea obișnuită a eșecului de încărcare. Un callback care pretinde că a scris mai mult de Count octeți este limitat, pentru că un furnizor cu erori nu trebuie să poată depăși bufferul cache. Reîncercările de parolă reconstruiesc un flux de intervale nou și o stare de parsare nouă peste aceeași sursă de callback, astfel încât o tentativă eșuată nu poate lăsa în urmă poziție, fereastră sau stare de decriptare perimate
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
{ un singur GET blocant cu 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; { eliberează fluxul-înveliș }
Src.Free; { transportul tău, durata ta de viață }
end;
Cât deține de fapt cache-ul de intervale?
În mod implicit 4 MiB, răspândite pe ferestre aliniate la chunk și eliminate LRU. Proiectul anterior cu o singură fereastră creștea la orice lungime cerea apelantul, astfel încât o citire secvențială mare putea depăși dimensiunea nominală a chunk-ului, în timp ce un salt aleator arunca imediat fereastra anterioară. Cache-ul actual aliniază fiecare offset al sursei la ChunkSize, aduce exact un chunk per miss și impune un buget riguros de octeți pe mai multe ferestre. Orice buget explicit pe care îl transmiteți este ridicat la cel puțin un chunk întreg, astfel încât o citire avansează întotdeauna chunk cu chunk, iar încărcarea de vârf a cache-ului rămâne previzibilă. Un ChunkSize sub 4096 revine la implicitul de 64 KiB
Contabilizarea citirilor repetate este partea care merită legată de telemetria dumneavoastră. PDFlibPas identifică o repetare după începutul de chunk aliniat și păstrează intervale contigue ordonate, ceea ce separă o aducere genuină de la prima încercare de o readucere după eliminare, ținând contabilitatea să nu crească liniar cu dimensiunea fișierului. GetRangeSourceCacheInfo returnează imaginea completă ca JSON, SetRangeSourceCacheLimit redimensionează bugetul în timpul rulării, iar ClearRangeSourceCache aruncă ferestrele și resetează statisticile împreună. Reducerea bugetului în timpul rulării păstrează istoricul și contorizează eliberările dictate de buget ca eliminări, astfel încât un repeatedReads în creștere contra unui hits plat este semnalul că setul de lucru nu mai încape
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;
Ce se întâmplă când mai multe fire vor același chunk?
Așteaptă pe o singură cerere, nu pe mai multe. Un TStream clasic are un singur cursor de poziție, iar două fire care fiecare își iau corect blocada pot avea totuși poziția rescrisă între un Seek și un Read, de aceea obiectele leneșe și citirile segmentate din PDFlibPas folosesc un ReadAt absolut care nu mută niciodată cursorul. Fiecare chunk aliniat primește o singură cerere în curs, partajată de toți apelanții acelui chunk, chunk-urile alăturate aflate în coadă sunt contopite înainte să înceapă citirea sursei, iar o citire fizică este plafonată la 16 MiB, astfel încât o explozie de lucru paralel pe pagini nu se amplifică nici în cereri mici duplicate, nici într-una absurd de mare. Fereastra de contopire implicită este de 2 ms și se aplică doar primului chunk lipsă al fiecărui ReadAt; Read-ul pozițional nu așteaptă niciodată după ea, iar trecerea lui zero elimină complet întârzierea inițială de colectare, ceea ce contează pentru scanările secvențiale lungi care altfel ar acumula așteptarea chunk cu chunk. Poziția, metadatele cache și citirile din sursă stau în spatele a trei blocări separate, iar callback-ul sursei în sine este serializat, ceea ce permite ca un adaptor de bază de date sau object store fără protecție internă de fire să fie folosit neschimbat. Cei care așteaptă primesc propria copie a datelor, astfel încât o eliminare LRU ulterioară nu poate invalida un buffer deja predat
Puteți întreba dacă pagina 900 e gata fără să o aduceți?
Da, și exact pentru asta există callback-ul opțional de disponibilitate. Un callback de citire obișnuit nu poate distinge octeții care au aterizat deja de octeții care necesită un dus-întors blocant, iar o sondare printr-o citire de probă ar declanșa chiar descărcarea pe care încercați să o evitați. TPDFlibRangeAvailabilityEvent răspunde la o singură întrebare, dacă un interval complet poate fi citit imediat, și îi este interzis să aducă ceva; octeții acoperiți deja de cache contează întotdeauna ca disponibili. GetRangeSourceDataAvailability mapează obiectele indirecte la intervalele fizice de stocare consemnate în intrările de referințe încrucișate, rezolvă obiectele comprimate la containerul lor object stream, corectează un antet PDF deplasat și parsează un obiect doar după ce întregul interval trece de sondarea care nu aduce nimic, astfel încât calea pentru ce lipsește nu vă cheamă niciodată callback-ul de citire
Traversarea are scop delimitat, nu exhaustiv. O interogare de pagină parcurge doar ramura arborelui de pagini care conține pagina țintă și apoi adaugă conținutul paginii, resursele, adnotările și atributele de pagină moștenite, sărind muchiile înapoi Parent și P astfel încât o singură pagină sau un widget să nu se poată extinde înapoi în tot documentul. Graful de obiecte este limitat la 100000 de obiecte cerute și la o adâncime de 256, obiectele de tip stream sunt parsate întâi din dicționar, iar o repliere la parsare completă este permisă doar pentru obiecte stocate de până la 4 MiB. Raportul JSON contopește intervalele suprapuse și adiacente înainte de numărare, astfel încât requiredBytes și missingBytes sunt calculate din tablourile contopite requiredRanges și missingRanges, ai căror end este un capăt inclusiv. Interogarea unui obiect deja disponibil poate popula cache-ul de intervale; interogarea unuia care lipsește lasă statisticile de citire neatinse
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 poartă "missingBytes" plus "missingRanges" contopite }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { de ex. fișierul nu are deloc AcroForm }
end;
De ce prefetch-ul trebuie să itereze
Pentru că citirea o singură dată a missingRanges curente nu face pagina disponibilă. Un nod lipsă din arborele de pagini sau un object stream dezvăluie următorul strat de dependențe abia după ce sosește, de aceea un job de prefetch PDFlibPas rulează o buclă de interogare, aducere, reinterogare până când pagina, formularul sau graful de obiecte este complet disponibil sau o limită de octeți ori de treceri îl oprește. Jobul folosește propriul cititor și un cache secundar mic a cărui sursă de date redirecționează citirile absolute către fluxul original de intervale, ceea ce ține starea de parsare izolată de TSmartPDFReader din prim-plan, în timp ce octeții pe care îi descarcă cu adevărat aterizează tot în cache-ul principal partajat. Există un singur fir de lucru per flux de intervale, potrivind serializarea pe care callback-ul sursei o cere oricum, iar coada alege după patru niveluri de prioritate și apoi după ordinea de trimitere în cadrul unui nivel. MaxBytes se taxează în octeți fizici de chunk, astfel încât un parser care cere un singur octet dintr-un chunk necache-uit plătește oricum tot chunk-ul, în timp ce chunk-urile aflate deja în cache-ul partajat nu costă jobul nimic. Anularea unui job din coadă ajunge într-o stare terminală cu zero citiri din sursă; un job în rulare este verificat înainte de fiecare trecere de dependențe și de fiecare chunk din sursă, iar eliberarea fluxului de intervale așteaptă ca un callback în curs să se întoarcă în loc să încerce să-l întrerupă
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 ultimul
raport complet de disponibilitate, astfel încât LIMIT_REACHED
rămâne distingebil de FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Unde se degradează asta într-o descărcare a întregului fișier
Încărcarea pe intervale este un pariu pe aranjarea fișierului, iar unele fișiere nu îl onorează. Un fișier liniarizat conform ISO 32000-1 §7.5.8 este cazul bun: secțiunea primei pagini este încălzită la deschidere, delimitată atât de pragul de siguranță existent de 4 MiB, cât și de bugetul actual de cache, astfel încât încălzirea să nu se poată elimina imediat pe sine în cea mai mare parte. Un fișier neliniarizat se rezolvă totuși prin trailer și lanțul de referințe încrucișate aflat aproape de sfârșit, ceea ce costă câteva dus-întorsuri în plus, nu un dezastru. Prăpastia reală este un fișier deteriorat care forțează calea de reparare, pentru că reconstruirea unui tabel de referințe încrucișate înseamnă scanarea antetelor de obiecte în tot documentul, iar asta este o descărcare completă care sosește câte un chunk. Latența este cealaltă limită onestă: la 60 ms per cerere, o parsare cu acces aleator care are nevoie de patruzeci de chunk-uri necache-uite petrece peste două secunde în tranzit, indiferent cât de bun este cache-ul, iar tocmai pentru a ascunde asta există argumentul read-ahead și coada de priorități. Aceeași disciplină apare în abordarea cu acces direct pentru fuziunea și împărțirea PDF-urilor mari, iar acest cache stă la bază atât sub randarea paralelă de pagini, cât și sub cache-ul de pagini pe disc al vizualizatorului
API-ul de sursă pe intervale, interogarea de disponibilitate și planificatorul de prefetch fac parte din PDFlibPas Delphi PDF Library standard pentru Delphi, C++Builder și Free Pascal; pagina produsului poartă referința completă de parametri pentru LoadFromRangeSource împreună cu constantele de prioritate și stare ale prefetch-ului