HotPDF laadt een PDF vanuit elke random-access-bron die u zelf implementeert, en THPDFCoalescingRandomAccessSource omhult die bron zodat de verspreide kleine reads van de parser een begrensde set gecachte blokbereiken worden met asynchrone prefetch. Bij een document dat via HTTP-range-requests wordt aangeboden, is dat het verschil tussen een paar honderd rondgangen en een paar dozijn
Er verandert niets aan de parser. U roept nog altijd LoadFromRandomAccessSource aan, hetzelfde documentobject komt terug, en dezelfde pagina-API werkt. Wat verandert, is het verkeer eronder
Waarom laadt dezelfde PDF lokaal instantaan en kruipt ze over het netwerk?
Omdat een PDF-parser een bestand niet leest, maar erdoorheen navigeert. Ze zoekt naar het einde voor startxref, springt terug naar de cross-referentietabel, lost de trailer-dictionary op, volgt een verwijzing naar de catalogus, dan naar de wortel van de paginaboom, dan naar een paginanode, dan naar diens resource-dictionary. Elk van die stappen leest tientallen bytes vanaf een andere offset
Bij een lokaal bestand is dat patroon vrijwel gratis: het besturingssysteem heeft de omringende 4 KiB-pagina al gecachet, dus de tweede read kost een memcpy. Over een netwerktransport bestaat die lokaliteit niet. Elke read is een verzoek met zijn eigen latentie, en 300 opeenvolgende verzoeken van 40 ms elk is twaalf seconden bijna volledig besteed aan wachten. De oplossing is niet minder lezen; de parser heeft precies nodig wat hij opvraagt. De oplossing is elke fysieke read meer laten dekken van wat de volgende logische read zal willen
Wat coalescing verandert
De coalescing-bron rondt elke read af naar boven op een blok en cachet het blok. BlockSize staat standaard op 262.144 bytes en MaxCacheBytes op 2.097.152, dus acht blokken zijn standaard resident en worden verdrongen in least-recently-used-volgorde tegen een hard bytebudget. De read van 40 bytes van de parser voor een trailersleutel haalt de 256 KiB eromheen binnen, en de daaropvolgende tientallen reads in die buurt, waar cross-referentie- en catalogusgegevens zich bevinden, worden vanuit het geheugen bediend
Uw eigen bron blijft eenvoudig. Implementeer GetSize en ReadAt, override ReadAtCancellable als uw transport onderweg kan afbreken, en laat de wrapper caching, coalescing en prefetch afhandelen
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: de wrapper geeft Raw samen met zichzelf vrij
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;
Hoe ver vooruit moet er gelezen worden?
Adaptief vooruitlezen beantwoordt die vraag per document in plaats van u te dwingen te gokken. Met AdaptiveReadAheadEnabled ingesteld, groeit het venster door 1, 2, 4 en 8 blokken naarmate aanhoudende voorwaartse reads zich opstapelen, en het overschrijdt nooit MaxReadAheadBlocks of de geconfigureerde cachecapaciteit. Zodra er een read binnenkomt die niet ongeveer daar ligt waar de vorige eindigde, stort het venster in en wordt prefetch onderdrukt
SequentialReadToleranceBytes, standaard 4.096, bepaalt "ongeveer". Reads die binnen die afstand van het einde van de vorige read vallen, tellen nog steeds als sequentieel, wat van belang is omdat een PDF-parser die een content stream doorloopt geen perfect aaneengesloten offsets produceert; hij slaat hier een lengteveld over, daar een inline dictionary. Zet de tolerantie te laag en een normale voorwaartse scan wordt als willekeurig geclassificeerd, zodat vooruitlezen nooit ingrijpt. Zet ze te hoog en echte willekeurige toegang lijkt sequentieel, zodat u megabytes ophaalt die niemand wil. De standaard is gekalibreerd voor het doorlopen van content streams, en de statistieken vertellen u of uw transport het daarmee oneens is
Die asymmetrie is bewust: groei is geleidelijk, instorting is onmiddellijk. Overmatig ophalen bij een willekeurige-toegangswerklast kost echte bandbreedte en echt geld op gemeten transporten, dus de goedkope vergissing krijgt de voorkeur boven de dure
Annulering die het transport werkelijk stopt
ReadAtCancellable is gedeclareerd in de basisklasse, en de coalescing-bron eerbiedigt het volledig. Wanneer een voorgrondread binnenkomt voor een bereik dat een lopende prefetch niet bedient, wordt de prefetch geannuleerd in plaats van tot voltooiing gelaten te worden, zodat het paginaverzoek van de gebruiker niet achter speculatief verkeer in de wachtrij komt. De standaardimplementatie op THPDFRandomAccessSource valt terug op een gewone ReadAt, wat betekent dat de functie opt-in is per transport: HTTP-clients die verzoekafbreking ondersteunen, krijgen echte annulering, en eenvoudigere bronnen blijven ongewijzigd werken
Combineer dat met een annuleringstoken dat door uw UI loopt, en een gebruiker die een document sluit, stopt daadwerkelijk het netwerkverkeer in plaats van te wachten tot het leegloopt. Hetzelfde tokenmodel ligt onder de wachtrij beschreven in achtergrondrendering met een verzoekwachtrij, zodat één token het volledige pad van de viewport tot de socket kan dekken
De bereikcachestatistieken lezen
GetStatistics vult een THPDFRangeCacheStatistics-record dat scheidt wat uw transport deed van wat de cache deed. SourceReadCount en SourceBytesRead zijn fysiek verkeer. CacheHitCount en CacheMissCount zijn logisch verkeer. SequentialReadCount en RandomReadCount tonen hoe het toegangspatroon geclassificeerd werd, CurrentReadAheadBlocks en PeakReadAheadBlocks tonen hoe ver het venster opende, en PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount en SuppressedPrefetchCount tonen of speculatie zich uitbetaalde
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;
Drie metingen vertellen u wat u moet veranderen. Veel geannuleerde prefetches met een hoog aantal willekeurige reads betekent dat het document buiten volgorde benaderd wordt, dus verlaag MaxReadAheadBlocks en stop met betalen voor bandbreedte die u weggooit. Veel misses met een piekvenster dat nog op 1 staat, betekent dat de tolerantie een patroon afwijst dat feitelijk sequentieel is, dus verhoog SequentialReadToleranceBytes. En bytes gelezen die de bestandsgrootte ver overschrijden, betekent dat de cache thrasht, dus verhoog MaxCacheBytes voordat u iets anders aanraakt
Gelineariseerde bestanden veranderen de rekensom
Als u de producent beheert, verandert linearisatie van het document het probleem in plaats van het te optimaliseren. Een gelineariseerde PDF plaatst de objecten van de eerste pagina en een hinttabel vooraan in het bestand, zodat een viewer pagina één kan renderen vanuit de openingsmegabyte zonder de rest te zien. HotPDF legt dat pad rechtstreeks bloot via GetProgressiveLinearizedLoadInfo en ReadProgressiveLinearizedFirstPageSection, en de schrijfkant wordt behandeld in gelineariseerde PDF's genereren met hinttabellen
Beide technieken combineren goed. Coalescing maakt elk document draaglijk over een trage verbinding; linearisatie zorgt dat de eerste pagina snel aankomt bij documenten die u zelf produceert. Voor bestanden die op een lokale schijf staan maar te groot zijn om in het geheugen te passen, zijn de mapped-file- en lazy-stream-paden beschreven in de workflow van de Direct File API meestal het betere gereedschap, aangezien er dan om te beginnen geen rondgangslatentie is om af te schrijven
HotPDF is een native VCL PDF-component voor Delphi en C++Builder, zonder externe DLL voor de parser en met volledige broncode beschikbaar. De random-access-bron-API, de coalescing-wrapper en de progressieve laadpunten staan gedocumenteerd op de HotPDF Delphi PDF-componentpagina