HotPDF läser in en PDF från vilken slumpmässig åtkomstkälla du än implementerar, och THPDFCoalescingRandomAccessSource omsluter den källan så att tolkarens utspridda små läsningar blir en begränsad mängd cachade blockintervall med asynkron förhandshämtning. På ett dokument som serveras över HTTP-intervallbegäranden är det skillnaden mellan några hundra tur-och-retur-anrop och några dussin
Ingenting i tolkaren ändras. Du anropar fortfarande LoadFromRandomAccessSource, samma dokumentobjekt kommer tillbaka, och samma sid-API fungerar. Det som ändras är trafiken under ytan
Varför läses samma PDF in blixtsnabbt lokalt men kryper över nätverket?
Därför att en PDF-tolkare inte läser en fil, den navigerar i en. Den söker till slutet för startxref, hoppar tillbaka till korsreferenstabellen, löser upp trailer-ordboken, följer en referens till Catalog, sedan till sidträdets rot, sedan till en sidnod, sedan till dess resursordbok. Vart och ett av de stegen läser tiotals byte från ett annat offset
På en lokal fil är det mönstret nästan gratis: operativsystemet har redan de omgivande 4 KiB sidan cachad, så den andra läsningen kostar en memcpy. Över en nätverkstransport finns ingen sådan lokalitet. Varje läsning är en förfrågan med sin egen latens, och 300 sekventiella förfrågningar à 40 ms var är tolv sekunder tillbringade nästan uteslutande med att vänta. Lösningen är inte att läsa mindre; tolkaren behöver exakt det den ber om. Lösningen är att få varje fysisk läsning att täcka mer av det nästa logiska läsning kommer att vilja ha
Vad sammanslagning ändrar
Den sammanslående källan rundar varje läsning uppåt till ett block och cachar blocket. BlockSize har standardvärdet 262 144 byte och MaxCacheBytes 2 097 152, så åtta block är residenta som standard och vräks i minst-nyligen-använd-ordning mot en hård bytebudget. Tolkarens 40-byte-läsning av en trailer-nyckel drar in de omgivande 256 KiB, och nästa dussin läsningar i det grannskapet, där korsreferens- och catalog-data bor, betjänas från minnet
Din egen källa förblir enkel. Implementera GetSize och ReadAt, åsidosätt ReadAtCancellable om din transport kan avbryta under flykt, och låt omslaget hantera cachning, sammanslagning och förhandshämtning
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: omslaget frigör Raw tillsammans med sig själv
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;
Hur långt fram bör den läsa?
Adaptiv förhandsläsning besvarar den frågan per dokument i stället för att tvinga dig att gissa. Med AdaptiveReadAheadEnabled satt växer fönstret genom 1, 2, 4 och 8 block när uthålliga framåtläsningar ackumuleras, och det överskrider aldrig MaxReadAheadBlocks eller den konfigurerade cachekapaciteten. Ögonblicket en läsning kommer in som inte ungefär ligger där den föregående slutade kollapsar fönstret och förhandshämtning undertrycks
SequentialReadToleranceBytes, standardvärde 4 096, definierar ”ungefär”. Läsningar som hamnar inom det avståndet från föregående läsnings slut räknas ändå som sekventiella, vilket spelar roll eftersom en PDF-tolkare som vandrar genom en innehållsström inte producerar helt sammanhängande offset; den hoppar över ett längdfält här, en infogad ordbok där. Sätts toleransen för lågt klassas en normal framåtskanning som slumpmässig, så förhandsläsning aktiveras aldrig. Sätts den för högt ser genuin slumpmässig åtkomst sekventiell ut, så du hämtar megabyte ingen vill ha. Standardvärdet är kalibrerat för traversering av innehållsströmmar, och statistiken talar om för dig om din transport är av annan åsikt
Den asymmetrin är medveten: tillväxt är gradvis, kollaps är omedelbar. Överhämtning på en slumpmässig åtkomstbelastning kostar riktig bandbredd och riktiga pengar på mätta transporter, så det billiga misstaget föredras framför det dyra
Avbrytning som faktiskt stoppar överföringen
Basklassen deklarerar ReadAtCancellable, och den sammanslående källan respekterar det fullt ut. När en förgrundsläsning kommer in för ett intervall som en pågående förhandshämtning inte betjänar avbryts förhandshämtningen i stället för att lämnas att slutföras, så att användarens sidbegäran inte köas bakom spekulativ trafik. Standardimplementationen på THPDFRandomAccessSource faller tillbaka till en vanlig ReadAt, vilket betyder att funktionen är opt-in per transport: HTTP-klienter som stöder avbruten begäran får genuin avbrytning, och enklare källor fortsätter fungera oförändrat
Kombinera det med en avbrytningstoken trädd genom ditt användargränssnitt, och en användare som stänger ett dokument stoppar faktiskt nätverkstrafiken i stället för att vänta på att den tömmer ut. Samma tokenmodell ligger till grund för köhanteringen som beskrivs i bakgrundsrendering med en begärandekö, så en enda token kan täcka hela vägen från visningsområdet till sockeln
Att läsa intervallcachens statistik
GetStatistics fyller i en THPDFRangeCacheStatistics-post som skiljer på vad din transport gjorde och vad cachen gjorde. SourceReadCount och SourceBytesRead är fysisk trafik. CacheHitCount och CacheMissCount är logisk trafik. SequentialReadCount och RandomReadCount visar hur åtkomstmönstret klassificerades, CurrentReadAheadBlocks och PeakReadAheadBlocks visar hur långt fönstret öppnades, och PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount och SuppressedPrefetchCount visar om spekulationen lönade sig
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;
Tre avläsningar talar om för dig vad du ska ändra. Många avbrutna förhandshämtningar med ett högt antal slumpmässiga läsningar betyder att dokumentet nås i fel ordning, så sänk MaxReadAheadBlocks och sluta betala för bandbredd du kastar bort. Många missar med ett toppfönster fortfarande på 1 betyder att toleransen avvisar ett mönster som i praktiken är sekventiellt, så höj SequentialReadToleranceBytes. Och lästa byte som vida överstiger filstorleken betyder att cachen trashar, så höj MaxCacheBytes innan du rör något annat
Linjariserade filer ändrar räknestycket
Om du styr producenten ändrar linjarisering av dokumentet problemet snarare än att optimera det. En linjariserad PDF placerar första sidans objekt och en hint-tabell längst fram i filen, så en visare kan rendera sida ett från den inledande megabyten utan att se resten. HotPDF exponerar den vägen direkt genom GetProgressiveLinearizedLoadInfo och ReadProgressiveLinearizedFirstPageSection, och skrivsidan täcks i att generera linjariserade PDF:er med hint-tabeller
Båda teknikerna går att kombinera. Sammanslagning gör vilket dokument som helst uthärdligt över en långsam länk; linjarisering får första sidan att anlända snabbt för dokument du producerar själv. För filer som lever på en lokal disk men är för stora för att rymmas i minnet är de mappade fil- och lat-ström-vägarna som beskrivs i direktfil-API-arbetsflödet vanligtvis det bättre verktyget, eftersom det inte finns någon tur-och-retur-latens att amortera från första början
HotPDF är en nativ VCL PDF-komponent för Delphi och C++Builder, utan extern DLL för tolkaren och med fullständig källkod tillgänglig. API:et för slumpmässig åtkomstkälla, det sammanslående omslaget och ingångspunkterna för progressiv inläsning dokumenteras på sidan för HotPDF Delphi PDF-komponent