HotPDF charge un PDF depuis n'importe quelle source à accès aléatoire que vous implémentez, et THPDFCoalescingRandomAccessSource enveloppe cette source afin que les petites lectures éparpillées du parseur deviennent un ensemble borné de plages de blocs mises en cache avec préchargement asynchrone. Sur un document servi via des requêtes de plage HTTP, c'est la différence entre quelques centaines d'allers-retours et quelques dizaines
Rien ne change du côté du parseur. Vous appelez toujours LoadFromRandomAccessSource, le même objet document revient, et la même API de page fonctionne. Ce qui change, c'est le trafic en dessous
Pourquoi le même PDF se charge-t-il instantanément en local et rampe-t-il sur le réseau ?
Parce qu'un parseur PDF ne lit pas un fichier, il le navigue. Il se positionne à la fin pour trouver startxref, revient à la table de références croisées, résout le dictionnaire de fin (trailer), suit une référence vers le Catalog, puis vers la racine de l'arbre des pages, puis vers un nœud de page, puis vers son dictionnaire de ressources. Chacune de ces étapes lit des dizaines d'octets à un décalage différent
Sur un fichier local, ce schéma est presque gratuit : le système d'exploitation a déjà mis en cache la page de 4 Kio environnante, si bien que la seconde lecture ne coûte qu'un memcpy. Sur un transport réseau, cette localité n'existe pas. Chaque lecture est une requête avec sa propre latence, et 300 requêtes séquentielles à 40 ms chacune représentent douze secondes passées presque entièrement à attendre. La solution n'est pas de lire moins ; le parseur a besoin exactement de ce qu'il demande. La solution consiste à faire en sorte que chaque lecture physique couvre davantage de ce que la prochaine lecture logique voudra
Ce que change la fusion
La source de fusion arrondit chaque lecture à un bloc et met le bloc en cache. BlockSize vaut 262 144 octets par défaut et MaxCacheBytes vaut 2 097 152, si bien que huit blocs sont résidents par défaut et sont évincés selon l'ordre du moins récemment utilisé face à un budget d'octets strict. La lecture de 40 octets d'une clé de trailer par le parseur ramène les 256 Kio qui l'entourent, et la douzaine de lectures suivantes dans ce voisinage, là où résident les données de références croisées et de Catalog, sont servies depuis la mémoire
Votre propre source reste simple. Implémentez GetSize et ReadAt, redéfinissez ReadAtCancellable si votre transport peut interrompre une opération en vol, et laissez l'enveloppe gérer la mise en cache, la fusion et le préchargement
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 : l'enveloppe libère Raw avec elle-même
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;
Jusqu'où faut-il lire à l'avance ?
La lecture anticipée adaptative répond à cette question document par document au lieu de vous forcer à deviner. Quand AdaptiveReadAheadEnabled est activé, la fenêtre croît par paliers de 1, 2, 4 et 8 blocs à mesure que s'accumulent des lectures vers l'avant soutenues, et elle ne dépasse jamais MaxReadAheadBlocks ni la capacité de cache configurée. Dès qu'une lecture arrive qui ne se situe pas à peu près là où la précédente s'est terminée, la fenêtre s'effondre et le préchargement est suspendu
SequentialReadToleranceBytes, dont la valeur par défaut est 4 096, définit ce « à peu près ». Les lectures qui arrivent dans cette distance de la fin de la lecture précédente comptent tout de même comme séquentielles, ce qui compte car un parseur PDF qui parcourt un flux de contenu ne produit pas des décalages parfaitement contigus ; il saute un champ de longueur ici, un dictionnaire en ligne là. Réglez la tolérance trop bas et un balayage normal vers l'avant est classé comme aléatoire, si bien que la lecture anticipée ne s'enclenche jamais. Réglez-la trop haut et un véritable accès aléatoire paraît séquentiel, si bien que vous récupérez des mégaoctets dont personne ne veut. La valeur par défaut est calibrée pour la traversée de flux de contenu, et les statistiques vous diront si votre transport n'est pas d'accord
Cette asymétrie est délibérée : la croissance est progressive, l'effondrement est immédiat. Sur-récupérer sur une charge de travail à accès aléatoire coûte de la bande passante réelle et de l'argent réel sur des transports facturés à l'usage, l'erreur bon marché est donc préférée à l'erreur coûteuse
Une annulation qui arrête réellement le transfert
La classe de base déclare ReadAtCancellable, et la source de fusion l'honore de bout en bout. Quand une lecture au premier plan arrive pour une plage qu'un préchargement en vol ne dessert pas, ce préchargement est annulé plutôt que laissé à se terminer, si bien que la requête de page de l'utilisateur n'est pas mise en file derrière du trafic spéculatif. L'implémentation par défaut de THPDFRandomAccessSource se rabat sur un simple ReadAt, ce qui signifie que la fonctionnalité est opt-in par transport : les clients HTTP qui prennent en charge l'interruption de requête obtiennent une véritable annulation, et les sources plus simples continuent de fonctionner sans changement
Combinez cela avec un jeton d'annulation propagé à travers votre interface utilisateur, et un utilisateur qui ferme un document arrête réellement le trafic réseau au lieu d'attendre qu'il s'écoule. Le même modèle de jeton sous-tend la mise en file décrite dans le rendu en arrière-plan avec une file de requêtes, si bien qu'un seul jeton peut couvrir tout le chemin depuis la fenêtre d'affichage jusqu'à la socket
Lire les statistiques du cache de plages
GetStatistics remplit un enregistrement THPDFRangeCacheStatistics qui sépare ce qu'a fait votre transport de ce qu'a fait le cache. SourceReadCount et SourceBytesRead représentent le trafic physique. CacheHitCount et CacheMissCount représentent le trafic logique. SequentialReadCount et RandomReadCount montrent comment le motif d'accès a été classé, CurrentReadAheadBlocks et PeakReadAheadBlocks montrent jusqu'où la fenêtre s'est ouverte, et PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount et SuppressedPrefetchCount montrent si la spéculation a payé
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;
Trois lectures de valeurs vous indiquent quoi changer. Beaucoup de préchargements annulés avec un nombre élevé de lectures aléatoires signifie que le document est accédé dans le désordre, donc abaissez MaxReadAheadBlocks et cessez de payer pour de la bande passante que vous jetez. Beaucoup d'échecs avec une fenêtre maximale toujours à 1 signifie que la tolérance rejette un motif qui est en réalité séquentiel, donc augmentez SequentialReadToleranceBytes. Et des octets lus dépassant largement la taille du fichier signifient que le cache thrashe, donc augmentez MaxCacheBytes avant de toucher à autre chose
Les fichiers linéarisés changent la donne
Si vous contrôlez le producteur, linéariser le document change le problème plutôt que de l'optimiser. Un PDF linéarisé place les objets de la première page et une table d'indices en tête de fichier, si bien qu'une visionneuse peut rendre la page un à partir du premier mégaoctet sans voir le reste. HotPDF expose ce chemin directement via GetProgressiveLinearizedLoadInfo et ReadProgressiveLinearizedFirstPageSection, et le côté écriture est couvert dans la génération de PDF linéarisés avec tables d'indices
Les deux techniques se combinent. La fusion rend tout document tolérable sur une liaison lente ; la linéarisation fait arriver la première page rapidement sur les documents que vous produisez vous-même. Pour les fichiers qui résident sur un disque local mais sont trop volumineux pour tenir en mémoire, les chemins de fichier mappé et de flux paresseux décrits dans le flux de travail Direct File API sont généralement le meilleur outil, puisqu'il n'y a de toute façon aucune latence d'aller-retour à amortir
HotPDF est un composant VCL PDF natif pour Delphi et C++Builder, sans DLL externe pour le parseur et avec le code source complet disponible. L'API de source à accès aléatoire, l'enveloppe de fusion et les points d'entrée de chargement progressif sont documentés sur la page HotPDF Delphi PDF component