HotPDF rend de nombreuses pages PDF simultanément via deux points d'entrée : RenderLoadedPagesParallel, qui retourne un tableau de bitmaps, et RenderLoadedPagesParallelOrdered, qui livre chaque page à un callback dans l'ordre d'entrée dès qu'elle est prête. La forme ordonnée est celle qui monte en charge, car elle ne conserve jamais en mémoire plus qu'un nombre borné de bitmaps, quel que soit le nombre de pages demandées
La distinction fait tout l'article. Rendre 10 000 pages à 300 DPI dans un tableau signifie 10 000 bitmaps vivants simultanément, ce qui n'est pas un problème de débit mais un plantage par manque de mémoire. Les rendre via un callback ordonné avec une profondeur de file de deux signifie deux bitmaps vivants, et le travail s'exécute dans une empreinte fixe quelle que soit la longueur du document
Pourquoi le rendu parallèle a-t-il besoin de deux étapes ?
Rendre une page PDF, c'est deux travaux différents portant un seul nom. D'abord le flux de contenu est tokenisé et compilé en une liste d'affichage, ce qui est limité par le CPU et sollicite des caches partagés. Ensuite la liste d'affichage est rejouée dans un bitmap, ce qui est aussi limité par le CPU mais alloue massivement. Traiter les deux comme une unité indivisible force à choisir entre sérialiser la moitié qui touche le cache ou dupliquer le travail
HotPDF les sépare. Le verrou du cache de listes d'affichage ne protège que la comptabilité de fréquence, l'ordre LRU, les compteurs d'usage et les décisions d'admission ; la compilation elle-même s'exécute hors du verrou. Après la compilation, le worker requête à nouveau le cache : si un autre worker a publié la même page entre-temps, il libère sa propre copie et prend un compteur d'usage sur l'entrée publiée. Cela borne le travail dupliqué et empêche deux entrées pour la même page, sans jamais sérialiser la compilation entre les pages
L'effet mesuré sur un fixture complexe de 32 pages est la partie qui mérite d'être retenue. Le temps total sur Win64 est passé de 1 565 ms avec un worker à 696 ms avec quatre, soit environ 2,25 fois plus rapide. Le temps jusqu'au premier résultat est passé de 1 255 ms à 63 ms, soit environ 95 %, car les workers commencent à rejouer les listes d'affichage terminées pendant que d'autres compilent encore, au lieu d'attendre à une barrière de compilation complète
Livraison ordonnée et l'emplacement réservé
L'API ordonnée garantit que votre callback voit les pages dans l'ordre où vous les avez demandées, tandis que les workers terminent dans l'ordre où ils terminent. Les bitmaps achevés sont publiés dans un emplacement de sortie indexé par la position d'entrée, et le callback s'exécute de façon sérielle sur le thread appelant
Deux règles rendent cela borné plutôt que simplement ordonné. Le compteur de la file de sortie inclut les bitmaps actuellement utilisés par le callback, si bien qu'un callback lent ne peut pas laisser les producteurs dépasser la profondeur de file. Et les pages ultérieures ne peuvent occuper au maximum que queueDepth - 1 emplacements, car un emplacement est réservé en permanence pour la prochaine page à livrer. Sans cette réservation, une première page lente peut être bloquée par des pages ultérieures terminées qui remplissent la file, et le pipeline s'interbloque en tête de ligne avec tous les workers inactifs
uses
HPDFDoc;
procedure TExportJob.PageReady(Sender: TObject; InputIndex,
PageIndex: Integer; Bitmap: TBitmap; var Cancel: boolean);
begin
// Le bitmap est emprunté : valide pour cet appel seulement. Consommez-le ici
// (écriture disque, encodage, hachage) et ne conservez pas la référence
Bitmap.SaveToFile(Format('page-%.4d.bmp', [InputIndex + 1]));
Cancel := FUserCancelled;
end;
procedure TExportJob.Run;
var
Pdf: THotPDF;
Pages: array of Integer;
Info: THPDFParallelRenderPipelineInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('manual.pdf') <= 0 then
Exit;
SetLength(Pages, Pdf.LoadedPageCount);
for I := 0 to High(Pages) do
Pages[I] := I;
Pdf.RenderLoadedPagesParallelOrdered(Pages, 200, 4, 2,
PageReady, Info);
Writeln(Format('workers=%d peak queued=%d peak bytes=%d',
[Info.WorkerCount, Info.PeakQueuedPageCount, Info.PeakQueuedBytes]));
Writeln(Format('first result=%d ms total=%d ms backpressure waits=%d',
[Info.FirstResultMilliseconds, Info.TotalMilliseconds,
Info.BackpressureWaitCount]));
finally
Pdf.Free;
end;
end;
Le contrat de bitmap emprunté est la raison pour laquelle l'empreinte reste plate. La bibliothèque libère le bitmap dès que votre callback retourne, si bien qu'un fixture de 10 000 pages avec une profondeur de file de 2 plafonne exactement à deux bitmaps, tant sur Win32 que sur Win64. Si vous avez besoin du bitmap après le callback, copiez-le, et alors vous possédez la mémoire et la comptabilité
Comment le budget mémoire choisit un nombre de workers
Plus de workers n'est pas toujours plus rapide et c'est souvent fatal. Six workers rendant des pages A3 à 600 DPI ont besoin de plusieurs centaines de mégaoctets de bitmaps vivants, quel que soit le nombre de cœurs disponibles, et la machine qui a huit cœurs peut ne pas disposer d'autant d'espace d'adressage libre
HotPDF normalise donc le nombre de workers demandé en fonction de la taille de la requête, du nombre de pages et d'un plafond strict, puis divise le budget global actuellement disponible par la plus grande estimation prudente d'une seule page dans la requête. Au moins un worker survit toujours, si bien qu'une page surdimensionnée se rend quand même, en exclusivité. Dans la mesure de référence, un travail de six pages à 600 DPI sous un budget de 512 Mio s'est exécuté avec trois workers plutôt que six, avec un pic de réservation de 504 583 296 octets
Chaque worker prend une réservation de mémoire après avoir réclamé une page et avant de compiler, et la réservation couvre compilation et lecture ensemble. Réserver uniquement autour de l'étape de rastérisation manquerait la liste d'affichage et l'ensemble de travail du flux de contenu, qui sur des pages riches en graphiques constitue la moitié la plus lourde. Les deux API diffèrent ensuite : la forme en tableau libère sa réservation une fois le bitmap terminé, car l'appelant possède le résultat et l'ordonnanceur ne peut plus en tenir compte, tandis que la forme ordonnée transfère la réservation avec le bitmap dans l'emplacement de sortie et la conserve jusqu'à ce que le callback retourne. C'est une raison supplémentaire de préférer l'API ordonnée quand l'ensemble de sortie est volumineux
Annulation sans interblocage
Définir Cancel dans le callback arrête tout nouveau travail et réveille tout worker en attente sur la contre-pression. Les pages déjà à l'intérieur du moteur de rendu se terminent et sont nettoyées plutôt qu'abandonnées, et seul le premier diagnostic d'échec est conservé, levé une fois que chaque worker a rejoint
L'ordre ici est structurant. L'état d'annulation est défini en premier, les réservations de sortie non publiées sont libérées en second, et ce n'est qu'ensuite que les workers sont rejoints. Inverser les deux dernières étapes crée une boucle fermée : un worker attend l'admission dans un budget qui ne sera pas libéré tant que le consommateur n'a pas terminé, tandis que le consommateur attend que ce worker se termine
Les applications interactives doivent également connaître la préemption au premier plan. Le préchargement de pages s'exécute sur ses propres jetons d'annulation, et les appels de rendu au premier plan annulent et rejoignent les workers de préchargement avant de faire leur propre travail, si bien que le débit en arrière-plan n'ajoute jamais de latence à la page qu'un utilisateur attend. L'annulation est vérifiée à des intervalles bornés par 4 Kio de balayage d'octets ou 256 jetons, bien qu'un codec atomique ou un appel système ne puisse pas être interrompu en plein vol — la garantie de réactivité couvre les boucles appartenant à la bibliothèque, pas les décodeurs tiers. L'approche par file de requêtes pour le rendu interactif est décrite dans le rendu en arrière-plan avec une file de requêtes
Lire les statistiques du pipeline
THPDFParallelRenderPipelineInfo sépare des chiffres qu'il est facile de confondre. RequestedWorkerCount comparé à WorkerCount et MemoryBudgetWorkerLimit indique si le budget a réduit votre parallélisme. CompiledPageCount comparé à CacheHitPageCount indique combien le cache de listes d'affichage a économisé. CompiledPagesAtFirstResult montre si le pipeline a réellement chevauché compilation et lecture ou s'il a dégénéré en barrière
Pour le réglage, les deux indicateurs les plus utiles sont BackpressureWaitCount et MemorySchedulerWaitMilliseconds. Des attentes de contre-pression élevées signifient que votre callback est le goulot d'étranglement, donc soit accélérez-le, soit augmentez la profondeur de file si la mémoire le permet. Des attentes de planificateur élevées signifient que le budget est le goulot d'étranglement, donc réduisez le DPI, réduisez le parallélisme, ou augmentez le budget. Augmenter le nombre de workers dans les deux cas dégrade le débit, ce qui est la partie contre-intuitive
Un point de départ pratique pour l'export en lot : un parallélisme égal aux cœurs physiques moins un, une profondeur de file de deux ou trois, et un DPI choisi selon le besoin de sortie réel plutôt que par habitude. Puis lisez les statistiques d'un document réel et ajustez une fois, plutôt que de deviner deux fois. Pour les travaux où le document lui-même est trop volumineux pour tenir en mémoire, combinez ceci avec le modèle d'accès en flux décrit dans l'API de fichier direct pour les flux de travail sur de gros PDF
Le rendu parallèle, le préchargement et le cache de listes d'affichage font partie de la même pile de rendu pour Delphi et C++Builder ; la liste complète des fonctionnalités se trouve sur la page du composant PDF HotPDF Delphi