Tehnični članak

Pretakanje oddaljenih PDF v Delphiju: HotPDF

HotPDF naloži PDF iz kateregakoli vira z naključnim dostopom, ki ga sami vgradite, THPDFCoalescingRandomAccessSource pa ta vir zavije tako, da razpršena majhna branja razčlenjevalnika postanejo omejena množica predpomnjenih obsegov blokov s sočasnim vnaprejšnjim nalaganjem. Pri dokumentu, ki je postrežen prek zahtev za obsege HTTP, je to razlika med nekaj sto krožnimi potovanji in nekaj deseti

Na razčlenjevalniku se ne spremeni nič. Še vedno pokličete LoadFromRandomAccessSource, vrne se isti dokumentni objekt, deluje pa isti API za strani. Kar se spremeni, je promet pod njim

Zakaj se isti PDF lokalno naloži takoj, prek omrežja pa vleče?

Ker razčlenjevalnik PDF datoteke ne bere, temveč po njej navigira. Poišče konec za startxref, skoči nazaj na tabelo navzkrižnih sklicev, razreši slovar zaključka, sledi referenci do kataloga, nato do korena drevesa strani, nato do vozlišča strani, nato do njegovega slovarja virov. Vsak od teh korakov prebere deset ali dvajset bajtov z drugega odmika

Na lokalni datoteki je ta vzorec skoraj brezplačen: operacijski sistem že ima okoliških 4 KiB predpomnjenih, zato drugo branje stane le memcpy. Prek omrežnega prenosa take lokalnosti ni. Vsako branje je zahteva s svojo latenco, 300 zaporednih zahtev po 40 ms pa je dvanajst sekund, porabljenih skoraj v celoti za čakanje. Popravek ni brati manj; razčlenjevalnik potrebuje natanko to, kar zahteva. Popravek je, da vsako fizično branje pokrije več tistega, kar bo naslednje logično branje želelo

Kaj spremeni združevanje

Vir za združevanje vsako branje zaokroži navzgor na blok in blok predpomni. BlockSize je privzeto 262.144 bajtov, MaxCacheBytes pa 2.097.152, tako da je privzeto prisotnih osem blokov, ki se izmetavajo po vrstnem redu najmanj nedavno uporabljenih glede na trd proračun bajtov. 40-bajtno branje ključa zaključka s strani razčlenjevalnika povleče noter 256 KiB okolice, naslednji ducat branj v tej soseščini, kjer živijo podatki navzkrižnih sklicev in kataloga, pa je postrežen iz pomnilnika

Vaš lastni vir ostane preprost. Vgradite GetSize in ReadAt, prepišite ReadAtCancellable, če lahko vaš prenos prekine med letom, in prepustite ovoju predpomnjenje, združevanje in vnaprejšnje nalaganje

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: ovoj z lastnim uničenjem sprosti tudi Raw
  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;

Kako daleč vnaprej naj bere?

Prilagodljivo vnaprejšnje branje na to vprašanje odgovori za vsak dokument posebej, namesto da bi vas prisililo v ugibanje. Z omogočenim AdaptiveReadAheadEnabled okno raste skozi 1, 2, 4 in 8 blokov, ko se kopičijo vztrajna branja naprej, in nikoli ne preseže MaxReadAheadBlocks ali nastavljene zmogljivosti predpomnilnika. V trenutku, ko prispe branje, ki ni približno tam, kjer se je končalo prejšnje, se okno sesuje in vnaprejšnje nalaganje se zatre

SequentialReadToleranceBytes, privzeto 4.096, določa "približno". Branja, ki pristanejo znotraj te razdalje od konca prejšnjega branja, še vedno štejejo za zaporedna, kar je pomembno, ker razčlenjevalnik PDF, ki hodi skozi vsebinski tok, ne ustvarja popolnoma sosednjih odmikov; tu preskoči polje dolžine, tam vgnezden slovar. Nastavite toleranco prenizko in normalen pregled naprej je razvrščen kot naključen, zato se vnaprejšnje branje nikoli ne vklopi. Nastavite jo previsoko in pravi naključni dostop izgleda zaporeden, zato pridobite megabajte, ki jih nihče ne želi. Privzeta vrednost je umerjena za prehod skozi vsebinski tok, statistika pa vam bo povedala, če se vaš prenos s tem ne strinja

Ta asimetrija je namerna: rast je postopna, sesutje pa takojšnje. Preveliko pridobivanje na obremenitvi z naključnim dostopom stane resnično pasovno širino in resnični denar na merjenih prenosih, zato je poceni napaka zaželena pred drago

Preklic, ki dejansko ustavi prenos

Osnovni razred deklarira ReadAtCancellable, vir za združevanje pa ga upošteva od začetka do konca. Ko prispe ospredje branje za obseg, ki ga vnaprejšnje nalaganje v letu ne streže, se vnaprejšnje nalaganje prekliče namesto da bi ga pustili dokončati, tako da zahteva uporabnika po strani ni v vrsti za špekulativnim prometom. Privzeta izvedba na THPDFRandomAccessSource pade nazaj na navaden ReadAt, kar pomeni, da je funkcija izbirna po posameznem prenosu: odjemalci HTTP, ki podpirajo prekinitev zahteve, dobijo pravi preklic, preprostejši viri pa delujejo naprej nespremenjeni

Združite to s žetonom za preklic, speljanim skozi vaš uporabniški vmesnik, in uporabnik, ki zapre dokument, dejansko ustavi omrežni promet namesto da bi čakal, da se izprazni. Isti model žetona podpira čakalno vrsto, opisano v upodabljanju v ozadju s čakalno vrsto zahtev, tako da en žeton lahko pokrije celotno pot od pogleda do vtičnice

Branje statistike predpomnilnika obsegov

GetStatistics napolni zapis THPDFRangeCacheStatistics, ki loči, kaj je storil vaš prenos, od tega, kaj je storil predpomnilnik. SourceReadCount in SourceBytesRead sta fizični promet. CacheHitCount in CacheMissCount sta logični promet. SequentialReadCount in RandomReadCount pokažeta, kako je bil razvrščen vzorec dostopa, CurrentReadAheadBlocks in PeakReadAheadBlocks pokažeta, kako daleč se je okno odprlo, PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount in SuppressedPrefetchCount pa pokažejo, ali se je špekulacija izplačala

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;

Trije podatki vam povedo, kaj spremeniti. Veliko preklicanih vnaprejšnjih nalaganj z visokim številom naključnih branj pomeni, da do dokumenta dostopate izven vrstnega reda, zato znižajte MaxReadAheadBlocks in nehajte plačevati za pasovno širino, ki jo zavržete. Veliko zgrešitev z vrhom okna, ki ostaja na 1, pomeni, da toleranca zavrača vzorec, ki je dejansko zaporeden, zato zvišajte SequentialReadToleranceBytes. Bajti, prebrani daleč čez velikost datoteke, pa pomenijo, da se predpomnilnik trese, zato pred vsem drugim zvišajte MaxCacheBytes

Linearizirane datoteke spremenijo račun

Če nadzorujete izdelovalca, lineariziranje dokumenta spremeni problem namesto da bi ga zgolj optimiziralo. Lineariziran PDF postavi objekte prve strani in tabelo namigov na začetek datoteke, tako da lahko pregledovalnik izriše prvo stran iz uvodnega megabajta, ne da bi videl preostanek. HotPDF to pot izpostavi neposredno prek GetProgressiveLinearizedLoadInfo in ReadProgressiveLinearizedFirstPageSection, pisalna stran pa je obravnavana v ustvarjanju lineariziranih PDF s tabelami namigov

Obe tehniki se dopolnjujeta. Združevanje naredi vsak dokument znosen prek počasne povezave; lineariziranje pospeši prihod prve strani na dokumentih, ki jih ustvarite sami. Za datoteke, ki živijo na lokalnem disku, a so prevelike za pomnilnik, sta preslikana pot datoteke in leni tok, opisana v delovnem toku neposrednega API za datoteke, ponavadi boljše orodje, saj tam sploh ni latence krožnega potovanja, ki bi jo bilo treba amortizirati

HotPDF je izvorna komponenta VCL PDF za Delphi in C++Builder, brez zunanjega DLL za razčlenjevalnik in s celotno izvorno kodo. API za vir naključnega dostopa, ovoj za združevanje in vstopne točke progresivnega nalaganja so dokumentirani na strani izdelka HotPDF