HotPDF načíta PDF z ľubovoľného zdroja s náhodným prístupom, ktorý si sami implementujete, a THPDFCoalescingRandomAccessSource tento zdroj obalí tak, že roztrúsené malé čítania parsera sa zmenia na ohraničenú množinu cachovaných blokových rozsahov s asynchrónnym prefetchom. Pri dokumente obsluhovanom cez HTTP range requesty je to rozdiel medzi niekoľkými stovkami round-tripov a niekoľkými desiatkami
Na parseri sa nič nemení. Stále voláte LoadFromRandomAccessSource, vráti sa ten istý objekt dokumentu a funguje to isté API stránok. Mení sa iba prevádzka pod povrchom
Prečo sa ten istý PDF načíta lokálne okamžite a cez sieť sa plazí?
Pretože parser PDF súbor nečíta, ale prechádza ním. Presunie sa na koniec kvôli startxref, skočí späť na tabuľku krížových odkazov, vyrieši trailer slovník, nasleduje referenciu na Catalog, potom na koreň stromu stránok, potom na uzol stránky, potom na jej slovník zdrojov. Každý z týchto krokov prečíta desiatky bajtov z iného offsetu
Na lokálnom súbore je tento vzor takmer zadarmo: operačný systém má okolitú 4 KiB stránku už v cache, takže druhé čítanie stojí len memcpy. Cez sieťový transport takáto lokalita neexistuje. Každé čítanie je požiadavka s vlastnou latenciou, a 300 sekvenčných požiadaviek po 40 ms je dvanásť sekúnd strávených takmer výhradne čakaním. Riešením nie je čítať menej; parser potrebuje presne to, o čo žiada. Riešením je, aby každé fyzické čítanie pokrylo viac z toho, čo bude chcieť ďalšie logické čítanie
Čo zlučovanie (coalescing) mení
Zlučujúci zdroj zaokrúhli každé čítanie nahor na blok a tento blok cachuje. BlockSize má predvolenú hodnotu 262 144 bajtov a MaxCacheBytes 2 097 152, takže v predvolenom nastavení je rezidentných osem blokov a vyraďujú sa v poradí najdávnejšie použitých voči pevnému bajtovému rozpočtu. 40-bajtové čítanie kľúča trailera parserom pritiahne 256 KiB okolo neho, a ďalší tucet čítaní v tomto susedstve, kde žijú dáta krížových odkazov a catalogu, sa už obslúži z pamäte
Váš vlastný zdroj zostáva jednoduchý. Implementujte GetSize a ReadAt, prepíšte ReadAtCancellable, ak váš transport dokáže prerušiť čítanie za behu, a cachovanie, zlučovanie a prefetch nechajte na wrapperi
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 uvoľní Raw spolu so sebou
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;
Ako ďaleko dopredu by sa malo čítať?
Adaptívne čítanie dopredu (read-ahead) odpovedá na túto otázku pre každý dokument zvlášť namiesto toho, aby vás nútilo hádať. Keď je nastavené AdaptiveReadAheadEnabled, okno rastie cez 1, 2, 4 a 8 blokov, ako sa hromadia trvalé čítania dopredu, a nikdy neprekročí MaxReadAheadBlocks ani nakonfigurovanú kapacitu cache. V okamihu, keď príde čítanie, ktoré sa približne nezhoduje s tým, kde skončilo predchádzajúce, okno sa zrúti a prefetch sa potlačí
SequentialReadToleranceBytes, predvolene 4 096, definuje toto „približne“. Čítania, ktoré pristanú v rámci tejto vzdialenosti od konca predchádzajúceho čítania, sa stále počítajú ako sekvenčné, čo je dôležité, pretože parser PDF prechádzajúci obsahový tok neprodukuje dokonale súvislé offsety; tu preskočí pole dĺžky, tam vložený slovník. Ak nastavíte toleranciu príliš nízko, normálny dopredný sken sa klasifikuje ako náhodný, takže čítanie dopredu sa nikdy nezapne. Ak ju nastavíte príliš vysoko, skutočný náhodný prístup vyzerá ako sekvenčný, takže načítate megabajty, ktoré nikto nechce. Predvolená hodnota je kalibrovaná na prechádzanie obsahových tokov, a štatistiky vám povedia, či sa váš transport správa inak
Táto asymetria je zámerná: rast je postupný, kolaps je okamžitý. Nadmerné načítavanie pri záťaži s náhodným prístupom stojí reálnu šírku pásma a reálne peniaze pri spoplatnených transportoch, takže sa uprednostňuje lacná chyba pred nákladnou
Zrušenie, ktoré prenos naozaj zastaví
Základná trieda deklaruje ReadAtCancellable, a zlučujúci zdroj ju rešpektuje od začiatku do konca. Keď príde popredné čítanie pre rozsah, ktorý neobsluhuje bežiaci prefetch, tento prefetch sa zruší namiesto toho, aby sa nechal dokončiť, takže požiadavka používateľa na stránku nečaká vo fronte za špekulatívnou prevádzkou. Predvolená implementácia v THPDFRandomAccessSource sa vracia k obyčajnému ReadAt, čo znamená, že táto funkcia je voliteľná (opt-in) pre každý transport zvlášť: HTTP klienti, ktorí podporujú prerušenie požiadavky, dostanú skutočné zrušenie, a jednoduchšie zdroje fungujú ďalej bez zmeny
Skombinujte to s tokenom zrušenia prevlečeným cez celé vaše UI, a keď používateľ zatvorí dokument, sieťová prevádzka sa naozaj zastaví namiesto toho, aby ste čakali, kým sa vyprázdni. Ten istý model tokenu je základom radenia opísaného v článku renderovanie na pozadí s frontou požiadaviek, takže jeden token môže pokryť celú cestu od viewportu až po socket
Čítanie štatistík cache rozsahov
GetStatistics naplní záznam THPDFRangeCacheStatistics, ktorý oddeľuje, čo urobil váš transport, od toho, čo urobila cache. SourceReadCount a SourceBytesRead sú fyzická prevádzka. CacheHitCount a CacheMissCount sú logická prevádzka. SequentialReadCount a RandomReadCount ukazujú, ako bol klasifikovaný vzor prístupu, CurrentReadAheadBlocks a PeakReadAheadBlocks ukazujú, ako ďaleko sa okno otvorilo, a PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount a SuppressedPrefetchCount ukazujú, či sa špekulácia oplatila
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 hodnoty vám povedia, čo zmeniť. Veľa zrušených prefetchov s vysokým počtom náhodných čítaní znamená, že sa k dokumentu pristupuje mimo poradia, takže znížte MaxReadAheadBlocks a prestaňte platiť za šírku pásma, ktorú zahadzujete. Veľa výpadkov s vrcholovým oknom stále na 1 znamená, že tolerancia odmieta vzor, ktorý je v skutočnosti sekvenčný, takže zvýšte SequentialReadToleranceBytes. A počet prečítaných bajtov výrazne prevyšujúci veľkosť súboru znamená, že cache trashuje, takže zvýšte MaxCacheBytes skôr, než sa dotknete čohokoľvek iného
Linearizované súbory menia aritmetiku
Ak máte kontrolu nad producentom, linearizácia dokumentu problém skôr mení, než optimalizuje. Linearizovaný PDF umiestni objekty prvej stránky a tabuľku pomôcok na začiatok súboru, takže čítačka dokáže vykresliť prvú stránku z úvodného megabajtu bez toho, aby videla zvyšok. HotPDF sprístupňuje túto cestu priamo cez GetProgressiveLinearizedLoadInfo a ReadProgressiveLinearizedFirstPageSection, a strana zápisu je opísaná v článku generovanie linearizovaných PDF s tabuľkami pomôcok
Obe techniky sa dajú skombinovať. Zlučovanie robí znesiteľným akýkoľvek dokument cez pomalé pripojenie; linearizácia zaistí, že prvá stránka dorazí rýchlo na dokumentoch, ktoré si vytvárate sami. Pri súboroch, ktoré žijú na lokálnom disku, no sú príliš veľké na to, aby sa zmestili do pamäte, sú zvyčajne lepším nástrojom cesty cez mapovaný súbor a lenivý stream opísané v článku workflow priameho súborového API, keďže tam v prvom rade nie je žiadna round-trip latencia, ktorú treba amortizovať
HotPDF je natívny VCL PDF komponent pre Delphi a C++Builder, bez externého DLL pre parser a s dostupným úplným zdrojovým kódom. API zdroja s náhodným prístupom, zlučujúci wrapper a vstupné body progresívneho načítavania sú zdokumentované na stránke HotPDF Delphi PDF component