HotPDF încarcă un PDF din orice sursă cu acces aleatoriu pe care o implementezi, iar THPDFCoalescingRandomAccessSource încapsulează acea sursă astfel încât citirile mici și împrăștiate ale parserului devin un set limitat de intervale de blocuri cache cu prefetch asincron. Pe un document servit prin cereri HTTP range, aceasta este diferența dintre câteva sute de round trip-uri și câteva zeci
Nimic din parser nu se schimbă. Tot apelezi LoadFromRandomAccessSource, se întoarce același obiect document, iar același API de pagini funcționează. Ce se schimbă este traficul de dedesubt
De ce același PDF se încarcă instant local și se târăște pe rețea?
Pentru că un parser PDF nu citește un fișier, ci îl navighează. Sare la sfârșit pentru startxref, revine la tabela cross-reference, rezolvă dicționarul trailer, urmează o referință către Catalog, apoi către rădăcina arborelui de pagini, apoi către un nod de pagină, apoi către dicționarul lui de resurse. Fiecare dintre acești pași citește zeci de bytes de la un offset diferit
Pe un fișier local acest tipar este aproape gratuit: sistemul de operare are deja pagina de 4 KiB din jur în cache, așa că a doua citire costă un memcpy. Printr-un transport de rețea nu există o astfel de localitate. Fiecare citire este o cerere cu propria latență, iar 300 de cereri secvențiale la 40 ms fiecare înseamnă douăsprezece secunde petrecute aproape în întregime așteptând. Soluția nu este să citești mai puțin; parserul are nevoie exact de ce cere. Soluția este ca fiecare citire fizică să acopere mai mult din ce va cere următoarea citire logică
Ce schimbă coalescing-ul
Sursa de coalescing rotunjește fiecare citire în sus până la un bloc și pune blocul în cache. BlockSize are implicit 262.144 bytes, iar MaxCacheBytes are implicit 2.097.152, astfel încât opt blocuri sunt rezidente implicit și sunt evacuate în ordine least-recently-used față de un buget strict de bytes. Citirea de 40 de bytes a parserului pentru o cheie trailer aduce cu ea cei 256 KiB din jur, iar următoarele câteva zeci de citiri din acea vecinătate, unde locuiesc datele cross-reference și catalog, sunt servite din memorie
Propria ta sursă rămâne simplă. Implementează GetSize și ReadAt, suprascrie ReadAtCancellable dacă transportul tău poate abandona în timp ce citirea este în curs, și lasă wrapper-ul să gestioneze cache-ul, coalescing-ul și prefetch-ul
type
THttpRangeSource = class(THPDFRandomAccessSource)
private
FClient: TMyHttpClient;
FUrl: string;
FSize: Int64;
public
function GetSize: Int64; override;
function ReadAt(Offset: Int64; var Buffer; Count: Longint): Longint; override;
function ReadAtCancellable(Offset: Int64; var Buffer; Count: Longint;
CancellationToken: THPDFCancellationToken): Longint; override;
end;
var
Raw: THttpRangeSource;
Cached: THPDFCoalescingRandomAccessSource;
Pdf: THotPDF;
begin
Raw := THttpRangeSource.Create('https://files.example.com/contract.pdf');
// OwnsSource=True: wrapper-ul eliberează Raw odată cu el însuși
Cached := THPDFCoalescingRandomAccessSource.Create(Raw, True, 262144, 8388608);
Pdf := THotPDF.Create(nil);
try
Cached.AsyncPrefetchEnabled := True;
Cached.AdaptiveReadAheadEnabled := True;
Cached.MaxReadAheadBlocks := 8;
if Pdf.LoadFromRandomAccessSource(Cached, True) = 1 then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
Cât de mult înainte ar trebui să citească?
Read-ahead-ul adaptiv răspunde la această întrebare pentru fiecare document, în loc să te oblige să ghicești. Cu AdaptiveReadAheadEnabled activat, fereastra crește prin 1, 2, 4 și 8 blocuri pe măsură ce se acumulează citiri înainte susținute, și nu depășește niciodată MaxReadAheadBlocks sau capacitatea de cache configurată. În momentul în care sosește o citire care nu se află aproximativ acolo unde s-a terminat cea anterioară, fereastra se prăbușește și prefetch-ul este suprimat
SequentialReadToleranceBytes, implicit 4.096, definește „aproximativ”. Citirile care ajung în acea distanță de sfârșitul citirii anterioare încă mai contează ca secvențiale, ceea ce contează pentru că un parser PDF care parcurge un content stream nu produce offset-uri perfect contigue; sare peste un câmp de lungime ici, un dicționar inline colo. Setează toleranța prea jos și o baleiere normală înainte este clasificată ca aleatorie, astfel încât read-ahead-ul nu se activează niciodată. Setează-o prea sus și accesul cu adevărat aleatoriu pare secvențial, astfel încât aduci megabytes pe care nimeni nu îi vrea. Valoarea implicită este calibrată pentru parcurgerea content stream-urilor, iar statisticile îți vor spune dacă transportul tău nu este de acord
Această asimetrie este deliberată: creșterea este graduală, prăbușirea este imediată. Supra-aducerea pe o încărcare de lucru cu acces aleatoriu costă lățime de bandă reală și bani reali pe transporturi taxate la volum, așa că greșeala ieftină este preferată în locul celei scumpe
Anularea care chiar oprește transferul
Clasa de bază declară ReadAtCancellable, iar sursa de coalescing o respectă de la un capăt la altul. Când sosește o citire în prim-plan pentru un interval pe care un prefetch aflat în desfășurare nu îl deservește, prefetch-ul este anulat în loc să fie lăsat să se termine, astfel încât cererea de pagină a utilizatorului nu stă la coadă în spatele traficului speculativ. Implementarea implicită din THPDFRandomAccessSource revine la un simplu ReadAt, ceea ce înseamnă că funcționalitatea este opt-in per transport: clienții HTTP care suportă abandonarea cererii primesc o anulare autentică, iar sursele mai simple continuă să funcționeze neschimbate
Combină asta cu un token de anulare propagat prin UI-ul tău, iar utilizatorul care închide un document chiar oprește traficul de rețea, în loc să aștepte să se scurgă. Același model de token stă la baza cozii descrise în randarea în fundal cu o coadă de cereri, astfel încât un singur token poate acoperi întregul traseu de la viewport până la socket
Interpretarea statisticilor cache-ului de interval
GetStatistics completează o înregistrare THPDFRangeCacheStatistics care separă ce a făcut transportul tău de ce a făcut cache-ul. SourceReadCount și SourceBytesRead sunt trafic fizic. CacheHitCount și CacheMissCount sunt trafic logic. SequentialReadCount și RandomReadCount arată cum a fost clasificat tiparul de acces, CurrentReadAheadBlocks și PeakReadAheadBlocks arată cât de mult s-a deschis fereastra, iar PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount și SuppressedPrefetchCount arată dacă specularea a meritat
var
S: THPDFRangeCacheStatistics;
begin
Cached.GetStatistics(S);
Log(Format('physical %d reads / %d bytes, hits %d, misses %d',
[S.SourceReadCount, S.SourceBytesRead, S.CacheHitCount, S.CacheMissCount]));
Log(Format('pattern: %d sequential, %d random, peak window %d blocks',
[S.SequentialReadCount, S.RandomReadCount, S.PeakReadAheadBlocks]));
Log(Format('prefetch: %d issued, %d completed, %d cancelled, %d suppressed',
[S.PrefetchRequestCount, S.PrefetchCompletedCount,
S.PrefetchCancelledCount, S.SuppressedPrefetchCount]));
end;
Trei citiri îți spun ce să schimbi. Multe prefetch-uri anulate cu un număr mare de citiri aleatorii înseamnă că documentul este accesat dezordonat, așa că scade MaxReadAheadBlocks și oprește-te din plătit pentru lățime de bandă pe care o arunci. Multe rateuri cu o fereastră de vârf încă la 1 înseamnă că toleranța respinge un tipar care este efectiv secvențial, așa că crește SequentialReadToleranceBytes. Iar bytes citiți depășind cu mult dimensiunea fișierului înseamnă că cache-ul se zbate, așa că crește MaxCacheBytes înainte de a atinge orice altceva
Fișierele liniarizate schimbă aritmetica
Dacă controlezi producătorul, liniarizarea documentului schimbă problema, nu doar o optimizează. Un PDF liniarizat plasează obiectele primei pagini și o tabelă hint la începutul fișierului, astfel încât un viewer poate randa pagina unu din primul megabyte fără să vadă restul. HotPDF expune acest traseu direct prin GetProgressiveLinearizedLoadInfo și ReadProgressiveLinearizedFirstPageSection, iar partea de scriere este acoperită în generarea de PDF-uri liniarizate cu tabele hint
Ambele tehnici se combină. Coalescing-ul face orice document tolerabil pe o legătură lentă; liniarizarea face ca prima pagină să sosească rapid pentru documentele pe care le produci chiar tu. Pentru fișierele care trăiesc pe un disc local, dar sunt prea mari pentru a încăpea în memorie, traseele mapped-file și lazy-stream descrise în workflow-ul API-ului de fișiere direct sunt de obicei instrumentul mai bun, întrucât oricum nu există latență de round-trip de amortizat
HotPDF este o componentă VCL nativă pentru Delphi și C++Builder, fără niciun DLL extern pentru parser și cu sursă completă disponibilă. API-ul sursei cu acces aleatoriu, wrapper-ul de coalescing și punctele de intrare pentru încărcarea progresivă sunt documentate pe pagina componentei HotPDF Delphi PDF