Deux factures produites depuis le même modèle avec la même police TrueType, ne différant que par le nom du client, laissent deux fichiers distincts dans un cache de sous-ensembles de polices. Le nom ajoute un Q majuscule et un z minuscule que la première facture n'utilisait jamais, donc l'ensemble des glyphes conservés change, et avec lui le sous-ensemble et la clé de cache. Un cache de sous-ensembles persistant rapporte quand le répertoire de glyphes de vos documents se répète, et les compteurs qu'il expose vous diront en un seul lot si c'est votre cas
Activer le cache disque dans HotPDF
HotPDF garde le cache de sous-ensembles éteint tant que vous ne lui donnez pas un dossier. Deux propriétés de THotPDF le contrôlent : FontSubsetCacheFolder nomme le répertoire, et FontSubsetCacheMaxBytes plafonne ce qu'il peut contenir, avec une valeur par défaut de 268435456 octets (256 MiB). Le cache n'est actif que si le dossier est non vide et le budget supérieur à zéro. Affecter le dossier crée le répertoire s'il manque et réduit immédiatement ce qui s'y trouve déjà au budget
Le sous-ensemblage mérite un cache parce que c'est du vrai travail. Construire un sous-ensemble TrueType signifie parcourir la fermeture des glyphes composites, réécrire loca et glyf, reconstruire les tables qui indexent les glyphes, et produire un mappage CID adressable par le PDF. Pour un document isolé, ce coût se perd dans le bruit. Dans un serveur de rapports qui enchaîne des milliers de relevés avec les mêmes trois polices, les mêmes sous-ensembles sont reconstruits en boucle, et c'est précisément ce CPU que le cache vous rend
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
// chemin ASCII exclusivement : la propriété est une AnsiString
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 MiB au lieu des 256 MiB par défaut
// ... BeginDoc / dessin / EndDoc pour chaque document du lot ...
Info := Pdf.GetFontSubsetCacheInfo;
// LogFmt est votre propre routine de journalisation
LogFmt('subset cache: %d hits, %d misses, %d bytes in %d files',
[Info.HitCount, Info.MissCount, Info.CurrentBytes, Info.FileCount]);
finally
Pdf.Free;
end;
end;
Le cache se trouve derrière les deux chemins de sous-ensemblage : les polices sous-ensemblées via EnableFontSubsetting, qui couvre les polices TrueType et OpenType-CFF, et la police enregistrée avec RegisterUnicodeTTF. Rien d'autre ne change dans votre code de dessin. Le chemin du dossier est déclaré en AnsiString, donc un nom de répertoire avec des caractères hors de la page de codes système demande des ennuis ; un chemin ASCII simple sous ProgramData ou dans un répertoire de données de service évite la question entièrement
Ce qui entre dans une clé de cache
Quatre entrées décident si un sous-ensemble stocké peut être réutilisé : les octets exacts de la police source, l'ensemble des glyphes que le document conserve, le mode de sous-ensemble et la version du format de cache. Chaque clé est construite à partir d'un hachage SHA-256 des octets de la police et d'un hachage SHA-256 d'un bitmap des identifiants de glyphes utilisés, préfixée par v1- et suffixée par -s pour un sous-ensemble épars ou -c pour un compact. Les entrées atterrissent sur disque sous la forme <key>.hpdfsubset, donc un listage du répertoire vous montre d'un coup d'œil quel mode votre lot produit
Hacher le contenu de la police plutôt que son nom de fichier a une conséquence pratique. Passer Arial à une version plus récente ou remplacer une police d'entreprise par un build plus récent produit de nouvelles clés automatiquement, et les anciennes entrées cessent simplement d'être touchées jusqu'à ce que l'éviction les retire. Vous n'avez pas à vider le cache après une mise à jour de police pour obtenir une sortie correcte. Vider ne fait que récupérer l'espace plus tôt
Le mode de sous-ensemble est l'entrée que l'on oublie. CompactFontSubsetting renumérote les glyphes conservés en une plage dense et écrit un /CIDToGIDMap explicite (ISO 32000-1 §9.7.4.2), et la police enregistrée obtient toujours un sous-ensemble compact quand PDFACompliance est actif. CompressDocument active aussi les sous-ensembles compacts pendant toute la durée du document. Un service qui écrit le même relevé une fois en PDF simple et une fois en PDF/A remplit donc deux jeux d'entrées pour des glyphes identiques, l'un finissant par -s et l'autre par -c. Les polices OpenType-CFF prennent toujours le chemin épars
L'ensemble de glyphes décide du taux de succès. Des relevés issus d'un modèle fixe, des formulaires dont les données variables sont surtout des chiffres, des catalogues tirés d'une base produits : tout cela réutilise les mêmes glyphes de document en document. Une charge où chaque document tire une tranche différente d'une police CJK à des dizaines de milliers de glyphes rate surtout, car le bitmap de presque chaque document est nouveau. Lancez un lot représentatif et lisez les compteurs avant de décider à quel type de charge vous avez affaire
Comment savoir si le cache sert à quelque chose ?
GetFontSubsetCacheInfo renvoie un enregistrement THPDFFontSubsetCacheInfo à neuf champs, et HitCount face à MissCount répond à la question directement. WriteCount et EvictionCount montrent si les entrées vivent assez longtemps pour être réutilisées ou se font pousser dehors par un budget trop serré. CurrentBytes et FileCount décrivent le dossier tel qu'il est à l'instant
Deux détails comptent quand vous branchez ces compteurs dans la supervision. Les sept compteurs d'événements appartiennent à l'instance THotPDF et repartent de zéro à la création du composant, donc un service qui crée un composant par document voit l'activité d'un document par lecture. CurrentBytes et FileCount, en revanche, viennent d'une énumération du dossier au moment de l'appel, ce qui les fait refléter tous les processus utilisant ce dossier et rend chaque appel coûteux d'un scan de répertoire. Lisez l'enregistrement une fois par lot, à la fin, et gardez la boucle par document à l'écart
Les trois compteurs restants méritent une alerte. CorruptCount compte les entrées qui ont échoué à la validation au chargement et ont été supprimées ; chacune compte aussi comme un échec. Une poignée après une coupure de courant est attendue, et un filet constant pointe vers le stockage. RejectedCount compte les sous-ensembles que le cache a refusé de stocker, typiquement parce qu'un seul sous-ensemble dépasse tout le budget. WriteFailureCount compte les écritures qui n'ont pas abouti, ce qui en pratique signifie presque toujours un dossier que le compte de service ne peut pas écrire. Aucun des trois n'interrompt la génération de documents, donc un cache avec un problème de droits d'écriture produit exactement les mêmes PDF qu'aucun cache du tout. Il ne devient simplement jamais plus rapide
// SubsetCacheNeedsAttention est une fonction d'appui que vous ajoutez vous-même ;
// les seuils sont des exemples, ajustez-les à la taille de votre lot
function SubsetCacheNeedsAttention(const Info: THPDFFontSubsetCacheInfo;
out Reason: string): Boolean;
begin
Reason := '';
if Info.WriteFailureCount > 0 then
Reason := 'cache folder is not writable for this account'
else if Info.RejectedCount > Info.WriteCount then
Reason := 'subsets exceed FontSubsetCacheMaxBytes, raise the budget'
else if (Info.EvictionCount > Info.WriteCount div 2) and (Info.HitCount < Info.MissCount) then
Reason := 'entries are evicted before they are reused'
else if Info.CorruptCount > 10 then
Reason := 'repeated damaged entries, check the storage';
Result := Reason <> '';
end;
Budgets, éviction et le commutateur qui vide le dossier
L'éviction est en LRU selon l'heure de modification des fichiers. Chaque chargement réussi touche l'horodatage de l'entrée, chaque écriture est suivie d'une réduction, et les fichiers les plus anciens partent en premier jusqu'à ce que le dossier tienne à nouveau dans le budget. Abaisser FontSubsetCacheMaxBytes à l'exécution réduit sur place au lieu d'attendre la prochaine écriture, donc un service réagissant à une alerte d'espace disque peut réduire le cache au moment où il le décide
Cette immédiateté a un tranchant. Mettre le budget à zéro alors qu'un dossier est configuré réduit à zéro, ce qui supprime chaque fichier .hpdfsubset qu'il contient. Mettre FontSubsetCacheFolder à une chaîne vide, en revanche, éteint le cache et laisse les fichiers exactement où ils sont. Les deux commutateurs désactivent le cache pour le document suivant ; un seul conserve une semaine d'entrées chaudes. Pour du débogage, videz la propriété du dossier et laissez le budget tranquille
// Mettre le cache en pause pour une exécution de diagnostic, garder chaque entrée stockée
Pdf.FontSubsetCacheFolder := '';
// Danger : avec un dossier configuré, un budget zéro réduit le dossier à rien
// Pdf.FontSubsetCacheMaxBytes := 0;
// Réinitialisation délibérée : supprime les *.hpdfsubset et les fichiers temporaires restants,
// les compteurs d'événements gardent leurs valeurs
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;
Une exécution de diagnostic avec le cache en pause est la manière la plus propre de l'écarter. Le tag de sous-ensemble à six lettres dans /BaseFont est dérivé de l'ensemble des glyphes conservés, donc les mêmes glyphes produisent le même tag avec ou sans cache, et les programmes de polices incorporés devraient correspondre. Les hachages de fichiers entiers différeront toujours à cause des dates de création et de l'identifiant de document, donc comparez les flux de polices, pas les fichiers
Que devient une entrée endommagée ou à moitié écrite ?
Une entrée endommagée est supprimée et le sous-ensemble est construit normalement, avant que quoi que ce soit de cette entrée n'atteigne un flux PDF. Au chargement, HotPDF vérifie la signature du fichier, la version du format, le drapeau de mode de sous-ensemble, le nombre de glyphes face à la police source, les longueurs déclarées face à la taille réelle du fichier, et l'en-tête sfnt du programme de police stocké. Toute discordance jette l'entrée, incrémente CorruptCount, et retombe sur le sous-ensemblage en mémoire. Glisser un programme de police cassé dans le PDF d'un client se verrait des semaines plus tard dans un lecteur sur la machine de quelqu'un d'autre, loin de sa cause, et c'est précisément l'échec que cette validation existe pour prévenir
Les entrées à moitié écrites n'apparaissent pas, justement. Une écriture passe par un fichier temporaire dont le nom porte l'identifiant du processus et l'instance du composant, puis le déplace sur le nom final par un renommage replace-and-flush unique. Un crash en cours d'écriture laisse un fichier .tmp- égaré, que ClearFontSubsetCache balaye, et l'entrée réelle est soit la version ancienne complète, soit la version nouvelle complète. Les entrées compactes stockent aussi la table de remappage ancien-vers-nouveau à côté du programme de police, donc un sous-ensemble compact en cache reconstruit le même /CIDToGIDMap et la sortie PDF/A peut utiliser le cache sans le contourner
Où le dossier doit-il vivre ?
Trois conditions le décident : le compte de service doit pouvoir y écrire, le dossier devrait être sur un stockage local, et aucune étape de déploiement ne devrait l'effacer. Un partage réseau transforme chaque recherche en aller-retour et confie la sémantique de renommage au serveur de fichiers. Un dossier dans le répertoire de l'application que l'installateur recrée signifie un cache froid après chaque mise à jour, ce qui efface silencieusement le bénéfice exactement le jour où vous observez les performances de plus près
Plusieurs composants et plusieurs processus peuvent partager un dossier local. Les noms temporaires ne se collisionnent jamais, le renommage final est atomique, et une course perdue ne coûte rien de pire qu'un échec ou une écriture à refaire au document suivant. Le partage est le meilleur défaut sur une machine, car chaque instance réchauffe le cache pour les autres. Le seul effet de bord visible est que les compteurs de chaque instance ne décrivent que son propre trafic, tandis que CurrentBytes et FileCount décrivent celui de tout le monde, et un WriteFailureCount occasionnel peut venir de deux processus remplaçant la même entrée en même temps. Un dossier en lecture seule sert quand même les entrées existantes ; il ne peut simplement pas rafraîchir leurs horodatages ni en ajouter
Quand le cache est le mauvais levier
Le cache supprime du travail répété. Il ne fait rien pour le premier document d'un lot et peu pour les charges dont les jeux de glyphes ne se répètent jamais. Si la sortie est dominée par une grande police CJK employée sur du texte imprévisible, regardez quels glyphes sont tirés dans chaque sous-ensemble et pourquoi, traité dans les notes sur la fermeture de sous-ensembles et la mise en forme des glyphes. Si l'objectif est la taille de fichier plutôt que le CPU, l'article sur CompressDocument et les sous-ensembles compacts explique ce que le mode compact change dans la police incorporée. Quand un lot s'avère lent pour des raisons sans rapport avec les polices, la visite guidée de la sortie de rapports avec polices et images montre où passe habituellement le reste du temps, et le dossier sur l'ordre des sous-ensembles à EndDoc couvre le moment où les sous-ensembles sont construits pendant la finalisation du document
Comme le cache de sous-ensembles vit dans le composant, un serveur de rapports Delphi ou C++Builder le récupère en posant un chemin de dossier et un budget, sans service supplémentaire à déployer ni à surveiller. L'incorporation de polices, le sous-ensemblage et le reste des fonctionnalités de génération PDF sont listés sur la page du composant HotPDF Delphi