Tehnički članak

Udaljeni PDF-ovi u Delphiju: HotPDF spajanje raspona

HotPDF učitava PDF iz bilo kojeg izvora sa slučajnim pristupom koji sami implementirate, a THPDFCoalescingRandomAccessSource omata taj izvor tako da parserova raspršena mala čitanja postanu ograničen skup predmemoriranih blokova raspona uz asinkrono unaprijedno dohvaćanje. Kod dokumenta koji se poslužuje putem HTTP raspona, to je razlika između nekoliko stotina putovanja tamo-natrag i tek nekoliko desetaka

Ništa se u parseru ne mijenja. I dalje pozivate LoadFromRandomAccessSource, vraća se isti objekt dokumenta, a isti API za stranice radi kao i prije. Ono što se mijenja jest promet ispod površine

Zašto se isti PDF lokalno učitava trenutno, a puzi preko mreže?

Zato što PDF parser datoteku ne čita, nego njome navigira. Skoči na kraj radi startxref, vrati se natrag na tablicu unakrsnih referenci, razriješi rječnik trailera, slijedi referencu do kataloga, zatim do korijena stabla stranica, zatim do čvora stranice, zatim do njezinog rječnika resursa. Svaki od tih koraka čita desetke bajtova s drugačijeg pomaka

Kod lokalne datoteke taj je obrazac gotovo besplatan: operacijski sustav već ima predmemoriranu okolnu 4 KiB stranicu, pa drugo čitanje košta tek jedan memcpy. Preko mrežnog prijenosa takve lokalnosti nema. Svako je čitanje zaseban zahtjev sa svojom latencijom, a 300 sekvencijalnih zahtjeva po 40 ms svaki znači dvanaest sekundi provedenih gotovo isključivo u čekanju. Rješenje nije čitati manje; parseru treba točno ono što traži. Rješenje je da svako fizičko čitanje pokrije više onoga što će sljedeće logičko čitanje htjeti

Što spajanje raspona mijenja

Izvor sa spajanjem raspona zaokružuje svako čitanje naviše na veličinu bloka i predmemorira taj blok. BlockSize zadano iznosi 262.144 bajta, a MaxCacheBytes 2.097.152, pa je zadano prisutno osam blokova koji se izbacuju redoslijedom najmanje nedavno korišten (LRU) u odnosu na strogi proračun bajtova. Parserovo čitanje od 40 bajtova ključa trailera povuče za sobom 256 KiB okolnog prostora, a sljedećih tridesetak čitanja u tom susjedstvu, gdje žive podaci unakrsnih referenci i kataloga, poslužuje se iz memorije

Vaš vlastiti izvor ostaje jednostavan. Implementirajte GetSize i ReadAt, po potrebi predefinirajte ReadAtCancellable ako vaš prijenos može prekinuti zahtjev u letu, a omotaču prepustite predmemoriranje, spajanje raspona i unaprijedno dohvaćanje

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: omotač oslobađa Raw zajedno sa sobom
  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;

Koliko daleko unaprijed treba čitati?

Prilagodljivo čitanje unaprijed odgovara na to pitanje za svaki dokument zasebno, umjesto da vas tjera na nagađanje. Kada je AdaptiveReadAheadEnabled postavljen, prozor raste kroz 1, 2, 4 i 8 blokova dok se nastavljaju uzastopna čitanja unaprijed, i nikada ne premašuje MaxReadAheadBlocks ni konfigurirani kapacitet predmemorije. U trenutku kada stigne čitanje koje otprilike ne nastavlja ondje gdje je prethodno završilo, prozor se ruši i unaprijedno dohvaćanje se suspendira

SequentialReadToleranceBytes, zadano 4.096, definira što znači „otprilike”. Čitanja koja padnu unutar te udaljenosti od kraja prethodnog čitanja i dalje se broje kao sekvencijalna, što je važno jer PDF parser koji prolazi kroz tok sadržaja ne proizvodi savršeno susjedne pomake; tu preskoči polje duljine, ondje ugniježđeni rječnik. Postavite li toleranciju prenisko, normalno skeniranje unaprijed klasificira se kao slučajno, pa se čitanje unaprijed nikad ne aktivira. Postavite li je previsoko, pravi slučajni pristup izgleda sekvencijalno, pa dohvaćate megabajte koje nitko ne želi. Zadana vrijednost kalibrirana je za prolazak kroz tok sadržaja, a statistika će vam reći slaže li se vaš prijenos s tim

Ta je asimetrija namjerna: rast je postupan, a rušenje trenutno. Prekomjerno dohvaćanje kod opterećenja sa slučajnim pristupom stoji stvarne propusnosti i stvarnog novca kod naplativih prijenosa, pa se jeftinija pogreška preferira nad skupljom

Otkazivanje koje stvarno zaustavlja prijenos

Bazna klasa deklarira ReadAtCancellable, a izvor sa spajanjem raspona to poštuje od početka do kraja. Kada stigne čitanje u prvom planu za raspon koji unaprijedno dohvaćanje u letu ne poslužuje, to se dohvaćanje otkazuje umjesto da mu se dopusti da završi, tako da korisnikov zahtjev za stranicom ne čeka u redu iza špekulativnog prometa. Zadana implementacija u THPDFRandomAccessSource vraća se na obični ReadAt, što znači da je značajka opcionalna po prijenosu: HTTP klijenti koji podržavaju prekid zahtjeva dobivaju istinsko otkazivanje, a jednostavniji izvori nastavljaju raditi nepromijenjeno

Kombinirajte to s tokenom za otkazivanje provedenim kroz vaše korisničko sučelje, i korisnik koji zatvori dokument stvarno zaustavlja mrežni promet umjesto da čeka da se isprazni. Isti model tokena stoji iza reda čekanja opisanog u članku pozadinsko renderiranje s redom čekanja zahtjeva, pa jedan token može pokriti cijelu putanju od prikaznog područja do utičnice

Čitanje statistike predmemorije raspona

GetStatistics popunjava zapis THPDFRangeCacheStatistics koji razdvaja ono što je učinio vaš prijenos od onoga što je učinila predmemorija. SourceReadCount i SourceBytesRead predstavljaju fizički promet. CacheHitCount i CacheMissCount predstavljaju logički promet. SequentialReadCount i RandomReadCount pokazuju kako je obrazac pristupa klasificiran, CurrentReadAheadBlocks i PeakReadAheadBlocks pokazuju koliko se prozor proširio, a PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount i SuppressedPrefetchCount pokazuju je li se špekulacija isplatila

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;

Tri očitanja govore vam što treba promijeniti. Mnogo otkazanih unaprijednih dohvaćanja uz visok broj slučajnih čitanja znači da se dokumentu pristupa izvan redoslijeda, pa smanjite MaxReadAheadBlocks i prestanite plaćati propusnost koju odbacujete. Mnogo promašaja uz vršni prozor koji ostaje na 1 znači da tolerancija odbacuje obrazac koji je zapravo sekvencijalan, pa povećajte SequentialReadToleranceBytes. A bajtovi pročitani daleko iznad veličine datoteke znače da se predmemorija grči, pa prije bilo čega drugog povećajte MaxCacheBytes

Linearizirane datoteke mijenjaju računicu

Ako kontrolirate proizvođača dokumenta, linearizacija dokumenta mijenja problem umjesto da ga samo optimizira. Linearizirani PDF smješta objekte prve stranice i tablicu naputaka na početak datoteke, pa preglednik može renderirati prvu stranicu iz otvarajućeg megabajta bez potrebe da vidi ostatak. HotPDF tu putanju izlaže izravno kroz GetProgressiveLinearizedLoadInfo i ReadProgressiveLinearizedFirstPageSection, a strana pisanja pokrivena je u članku generiranje lineariziranih PDF-ova s tablicama naputaka

Obje se tehnike mogu kombinirati. Spajanje raspona čini svaki dokument podnošljivim preko sporog spoja; linearizacija čini da prva stranica stigne brzo kod dokumenata koje sami proizvodite. Za datoteke koje žive na lokalnom disku, ali su prevelike da bi stale u memoriju, putanje preslikane datoteke i lijenog toka opisane u članku tijek rada s Direct File API-jem obično su bolji alat, jer ondje uopće nema latencije putovanja tamo-natrag koju bi trebalo amortizirati

HotPDF je izvorna VCL PDF komponenta za Delphi i C++Builder, bez vanjskog DLL-a za parser i s dostupnim potpunim izvornim kodom. API za izvor sa slučajnim pristupom, omotač za spajanje raspona i ulazne točke za progresivno učitavanje dokumentirani su na stranici HotPDF Delphi PDF komponente