Techninis straipsnis

Nutolusių PDF srautas Delphi: HotPDF diapazonų sujungimas

HotPDF įkelia PDF iš bet kokio atsitiktinės prieigos šaltinio, kurį įgyvendinate, o THPDFCoalescingRandomAccessSource apgaubia tą šaltinį taip, kad analizatoriaus išsklaidyti maži skaitymai virsta ribotu podėlyje laikomų blokų diapazonų rinkiniu su asinchroniniu nuskaitymu į priekį. Dokumente, teikiamame per HTTP diapazono užklausas, tai yra skirtumas tarp kelių šimtų kelionių pirmyn-atgal ir kelių dešimčių

Analizatoriuje niekas nesikeičia. Vis tiek iškviečiate LoadFromRandomAccessSource, grįžta tas pats dokumento objektas, ir veikia ta pati puslapio API. Kas keičiasi – tai srautas žemiau

Kodėl tas pats PDF įkeliamas akimirksniu vietoje ir šliaužia per tinklą?

Todėl, kad PDF analizatorius neskaito failo, jis juo naršo. Jis peršoka į failo galą ieškodamas startxref, grįžta prie kryžminių nuorodų lentelės, išsprendžia trumpinamąjį žodyną, seka nuorodą į katalogą, tada į puslapių medžio šaknį, tada į puslapio mazgą, tada į jo išteklių žodyną. Kiekvienas iš šių žingsnių perskaito dešimtis baitų iš skirtingos padėties

Vietiniame faile toks šablonas beveik nemokamas: operacinė sistema jau turi aplinkinius 4 KiB puslapyje podėlyje, todėl antras skaitymas kainuoja tik memcpy. Per tinklo transportą tokio lokalumo nėra. Kiekvienas skaitymas yra užklausa su savo delsa, o 300 nuoseklių užklausų po 40 ms kiekviena – tai dvylika sekundžių, praleistų beveik vien laukiant. Sprendimas nėra skaityti mažiau; analizatoriui reikia lygiai to, ko jis prašo. Sprendimas – kad kiekvienas fizinis skaitymas apimtų daugiau to, ko norės kitas loginis skaitymas

Ką pakeičia sujungimas

Sujungimo šaltinis apvalina kiekvieną skaitymą iki bloko ir kešuoja tą bloką. BlockSize numatytoji reikšmė 262 144 baitai, o MaxCacheBytes – 2 097 152, todėl pagal numatytuosius nustatymus rezidentuoja aštuoni blokai, ir jie pašalinami mažiausiai neseniai naudoto tvarka pagal griežtą baitų biudžetą. Analizatoriaus 40 baitų trumpinamojo rakto skaitymas įtraukia 256 KiB aplink jį, ir sekančias tuziną skaitymų toje kaimynystėje, kur gyvena kryžminių nuorodų ir katalogo duomenys, aptarnaujami iš atminties

Jūsų pačių šaltinis lieka paprastas. Įgyvendinkite GetSize ir ReadAt, perrašykite ReadAtCancellable, jei jūsų transportas gali nutraukti vidury vykdymo, ir leiskite apvalkalui tvarkyti kešavimą, sujungimą ir nuskaitymą į priekį

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: apvalkalas atlaisvina Raw kartu su savimi
  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;

Kaip toli reikėtų skaityti į priekį?

Adaptyvus nuskaitymas į priekį atsako į šį klausimą kiekvienam dokumentui atskirai, vietoj to, kad verstų jus spėlioti. Kai AdaptiveReadAheadEnabled nustatytas, langas auga per 1, 2, 4 ir 8 blokus, kaupiantis nuosekliems skaitymams į priekį, ir jis niekada neviršija MaxReadAheadBlocks arba sukonfigūruoto podėlio talpos. Vos tik atsiranda skaitymas, kuris nėra maždaug ten, kur baigėsi ankstesnis, langas sugriūva, ir nuskaitymas į priekį sustabdomas

SequentialReadToleranceBytes, numatytoji reikšmė 4096, apibrėžia „maždaug". Skaitymai, patenkantys per tą atstumą nuo ankstesnio skaitymo pabaigos, vis dar laikomi nuosekliais, o tai svarbu, nes PDF analizatorius, naršantis turinio srautą, nesukuria tobulai gretimų poslinkių; jis praleidžia ilgio lauką čia, įterptą žodyną ten. Nustatykite toleranciją per žemai, ir įprastas nuoseklus nuskaitymas klasifikuojamas kaip atsitiktinis, todėl nuskaitymas į priekį niekada neįsijungia. Nustatykite ją per aukštai, ir tikras atsitiktinis prieigos šablonas atrodo nuoseklus, todėl parsiunčiate megabaitus, kurių niekas nenori. Numatytoji reikšmė kalibruota turinio srauto naršymui, ir statistika parodys, jei jūsų transportas nesutinka

Ši asimetrija sąmoninga: augimas laipsniškas, griūtis akimirksniu. Perteklinis parsisiuntimas atsitiktinės prieigos darbo apkrovoje kainuoja realų pralaidumą ir realius pinigus matuojamuose transportuose, todėl pigi klaida renkasi prieš brangią

Atšaukimas, kuris iš tikrųjų sustabdo perdavimą

Bazinė klasė deklaruoja ReadAtCancellable, ir sujungimo šaltinis jo laikosi iki galo. Kai pirmame plane atsiranda skaitymas diapazonui, kurio nebeaptarnauja vykdomas nuskaitymas į priekį, tas nuskaitymas atšaukiamas, o ne paliekamas baigti, todėl naudotojo puslapio užklausa nelieka eilėje už spekuliatyvaus srauto. Numatytoji implementacija THPDFRandomAccessSource grįžta prie paprasto ReadAt, o tai reiškia, kad funkcija pasirenkama atskirai kiekvienam transportui: HTTP klientai, palaikantys užklausos nutraukimą, gauna tikrą atšaukimą, o paprastesni šaltiniai veikia nepakitę

Sujunkite tai su atšaukimo žetonu, nutiestu per jūsų vartotojo sąsają, ir naudotojas, uždarantis dokumentą, iš tikrųjų sustabdo tinklo srautą, o ne laukia, kol jis nutekės. Tas pats žetono modelis yra pagrindas eiliavimui, aprašytam straipsnyje apie foninį atvaizdavimą su užklausų eile, todėl vienas žetonas gali apimti visą kelią nuo peržiūros lango iki lizdo

Diapazono podėlio statistikos skaitymas

GetStatistics užpildo THPDFRangeCacheStatistics įrašą, kuris atskiria, ką padarė jūsų transportas, nuo to, ką padarė podėlis. SourceReadCount ir SourceBytesRead yra fizinis srautas. CacheHitCount ir CacheMissCount yra loginis srautas. SequentialReadCount ir RandomReadCount rodo, kaip buvo klasifikuotas prieigos šablonas, CurrentReadAheadBlocks ir PeakReadAheadBlocks rodo, kaip plačiai atsivėrė langas, o PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount ir SuppressedPrefetchCount rodo, ar spekuliacija atsipirko

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;

Trys rodmenys pasako, ką keisti. Daug atšauktų iš anksto nuskaitymų su aukštu atsitiktinių skaitymų skaičiumi reiškia, kad dokumentas pasiekiamas ne tvarka, todėl sumažinkite MaxReadAheadBlocks ir nebemokėkite už pralaidumą, kurį atmetate. Daug nepataikymų, o didžiausias langas vis dar 1, reiškia, kad tolerancija atmeta šabloną, kuris iš esmės nuoseklus, todėl padidinkite SequentialReadToleranceBytes. O jei nuskaitytų baitų kiekis gerokai viršija failo dydį, tai reiškia, kad podėlis blaškosi, todėl padidinkite MaxCacheBytes, kol dar nelietėte nieko kito

Linearizuoti failai keičia aritmetiką

Jei kontroliuojate generatorių, dokumento linearizavimas pakeičia problemą, o ne ją optimizuoja. Linearizuotas PDF patalpina pirmojo puslapio objektus ir užuominų lentelę failo priekyje, todėl peržiūros programa gali atvaizduoti pirmą puslapį iš atidarymo megabaito, nematydama likusios dalies. HotPDF atskleidžia šį kelią tiesiogiai per GetProgressiveLinearizedLoadInfo ir ReadProgressiveLinearizedFirstPageSection, o rašymo pusė aprašyta straipsnyje apie linearizuotų PDF su užuominų lentelėmis generavimą

Abi technikos derinasi. Sujungimas padaro bet kokį dokumentą pakenčiamu per lėtą ryšį; linearizavimas priverčia pirmą puslapį atvykti greitai dokumentuose, kuriuos kuriate patys. Failams, gyvenantiems vietiniame diske, bet per dideliems tilpti atmintyje, atvaizduoto failo ir tinginio srauto keliai, aprašyti straipsnyje apie tiesioginio failų API darbo eigą, paprastai yra geresnis įrankis, nes tada iš viso nėra kelionės pirmyn-atgal delsos, kurią reikėtų amortizuoti

HotPDF yra natyvus VCL PDF komponentas Delphi ir C++Builder, be jokios išorinės DLL analizatoriui ir su pilnu prieinamu šaltiniu. Atsitiktinės prieigos šaltinio API, sujungimo apvalkalas ir progresyvaus įkėlimo funkcijos dokumentuotos HotPDF Delphi PDF komponento puslapyje