Zwei Rechnungen aus derselben Vorlage mit demselben TrueType-Schnitt, die sich nur im Nachnamen des Kunden unterscheiden, hinterlassen zwei getrennte Dateien in einem Font-Subset-Cache. Der Nachname bringt ein großes Q und ein kleines z mit, die die erste Rechnung nie verwendet hat, also ändert sich die Menge der behaltenen Glyphen – und damit das Subset und der Cache-Schlüssel. Ein persistenter Subset-Cache zahlt sich aus, wenn sich das Glyphen-Repertoire Ihrer Dokumente wiederholt, und die Zähler, die er bereitstellt, verraten Ihnen innerhalb eines Batch, ob das bei Ihnen der Fall ist
Den Platten-Cache in HotPDF einschalten
HotPDF hält den Subset-Cache aus, bis Sie ihm einen Ordner geben. Zwei Eigenschaften an THotPDF steuern ihn: FontSubsetCacheFolder nennt das Verzeichnis, und FontSubsetCacheMaxBytes begrenzt, wie viel er aufnehmen darf, mit einem Standard von 268435456 Bytes (256 MiB). Aktiv ist der Cache nur, solange der Ordner nicht leer ist und das Budget über null liegt. Das Zuweisen des Ordners erzeugt das Verzeichnis, falls es fehlt, und stutzt sofort, was bereits darin liegt, aufs Budget zusammen
Subsetting ist das Cachen wert, weil es echte Arbeit ist. Ein TrueType-Subset zu bauen bedeutet, die Glyphen-Closure zusammengesetzter Glyphen abzugehen, loca und glyf neu zu schreiben, die Tabellen neu aufzubauen, die Glyphen indizieren, und ein CID-Mapping zu erzeugen, das das PDF adressieren kann. Bei einem einzelnen Dokument verschwindet diese Kosten im Rauschen. In einem Report-Server, der Tausende von Auszügen in einer Schleife mit denselben drei Schnitten rendert, werden dieselben Subsets immer und immer wieder gebaut, und genau diese CPU gibt der Cache zurück
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
// Nur-ASCII-Pfad: die Eigenschaft ist ein AnsiString
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 MiB statt der 256 MiB als Standard
// ... BeginDoc / zeichnen / EndDoc für jedes Dokument im Batch ...
Info := Pdf.GetFontSubsetCacheInfo;
// LogFmt ist Ihre eigene Logging-Routine
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;
Der Cache sitzt hinter beiden Subsetting-Pfaden: Schnitte, die über EnableFontSubsetting gesubsettet werden — das deckt TrueType- und OpenType-CFF-Schriften ab —, und der Schnitt, der per RegisterUnicodeTTF registriert wird. An Ihrem Zeichencode ändert sich sonst nichts. Der Ordner-Pfad ist als AnsiString deklariert, also ist ein Verzeichnisname mit Zeichen außerhalb der System-Codepage eine Einladung an Schwierigkeiten; ein schlichter ASCII-Pfad unter ProgramData oder in einem Dienst-Datenverzeichnis stellt die Frage erst gar nicht
Was in einen Cache-Schlüssel eingeht
Vier Eingaben entscheiden, ob ein gespeichertes Subset wiederverwendet werden kann: die exakten Bytes der Quellschrift, die Menge der Glyphen, die das Dokument behält, der Subset-Modus und die Cache-Formatversion. Jeder Schlüssel wird aus einem SHA-256-Hash der Font-Bytes und einem SHA-256-Hash einer Bitmap der verwendeten Glyphen-IDs gebaut, mit dem Präfix v1- und dem Suffix -s für ein spärliches (sparse) Subset oder -c für ein kompaktes. Einträge landen als <key>.hpdfsubset auf der Platte, sodass eine Verzeichnisauflistung auf einen Blick zeigt, welchen Modus Ihr Batch produziert
Den Schriftinhalt statt des Dateinamens zu hashen hat eine praktische Folge. Ein Update von Arial oder der Austausch gegen einen neueren Build eines Firmen-Schnitts erzeugt automatisch neue Schlüssel, und die alten Einträge werden einfach nicht mehr getroffen, bis die Verdrängung sie entfernt. Nach einem Font-Update müssen Sie den Cache nicht leeren, um korrekte Ausgabe zu bekommen. Leeren holt nur den Platz früher zurück
Der Subset-Modus ist die Eingabe, die Leute vergessen. CompactFontSubsetting nummeriert die behaltenen Glyphen in einen dichten Bereich um und schreibt eine explizite /CIDToGIDMap (ISO 32000-1 §9.7.4.2), und der registrierte Schnitt bekommt immer ein kompaktes Subset, wenn PDFACompliance gesetzt ist. CompressDocument stellt kompakte Subsets für die Dauer des Dokuments ebenfalls an. So füllt ein Dienst, der denselben Auszug einmal als gewöhnliches PDF und einmal als PDF/A schreibt, zwei Sätze von Einträgen für identische Glyphen, einer endet auf -s, einer auf -c. OpenType-CFF-Schnitte nehmen immer den spärlichen (sparse) Pfad
Über die Glyphenmenge wird die Trefferquote entschieden. Auszüge aus einer festen Vorlage, Formulare, deren variable Daten meist Ziffern sind, Kataloge aus einer Produkt-Datenbank: all das verwendet dieselben Glyphen, Dokument für Dokument. Eine Arbeitslast, bei der jedes Dokument einen anderen Ausschnitt aus einer CJK-Schrift mit Zehntausenden Glyphen zieht, verfehlt meistens, weil fast jede Bitmap neu ist. Fahren Sie einen repräsentativen Batch und lesen Sie die Zähler, bevor Sie entscheiden, welche Art von Arbeitslast Sie haben
Woher wissen Sie, dass der Cache überhaupt etwas tut?
GetFontSubsetCacheInfo gibt einen THPDFFontSubsetCacheInfo-Datensatz mit neun Feldern zurück, und HitCount gegen MissCount beantwortet die Frage direkt. WriteCount und EvictionCount zeigen, ob Einträge lange genug überleben, um wiederverwendet zu werden, oder von einem zu knappen Budget herausgedrängt werden. CurrentBytes und FileCount beschreiben den Ordner, wie er gerade aussieht
Zwei Details zählen, wenn Sie diese Zähler ins Monitoring anbinden. Die sieben Ereignis-Zähler gehören zur THotPDF-Instanz und starten bei null, wenn die Komponente erzeugt wird, also sieht ein Dienst, der pro Dokument eine Komponente erzeugt, in jeder Ablesung genau die Aktivität eines Dokuments. CurrentBytes und FileCount hingegen stammen aus dem Durchlaufen des Ordners im Moment des Aufrufs, was sie jeden Prozess widerspiegeln lässt, der diesen Ordner verwendet, und jedem Aufruf einen Verzeichnis-Scan kostet. Lesen Sie den Datensatz einmal pro Batch, am Ende, und halten Sie die Dokument-Schleife frei davon
Die übrigen drei Zähler sind die, auf die sich ein Alarm lohnt. CorruptCount zählt Einträge, die beim Laden die Validierung nicht bestanden und gelöscht wurden; jeder zählt zugleich als Miss. Eine Handvoll nach einem Stromausfall ist normal, ein stetiges Tröpfeln zeigt auf den Speicher. RejectedCount zählt Subsets, die der Cache nicht speichern wollte, typischerweise, weil ein einzelnes Subset größer ist als das gesamte Budget. WriteFailureCount zählt Schreibvorgänge, die nicht abgeschlossen wurden, was in der Praxis fast immer ein Ordner ist, den das Dienst-Konto nicht beschreiben kann. Keiner der drei unterbricht die Dokumenterzeugung, also erzeugt ein Cache mit einem Schreibrechte-Problem exakt dieselben PDFs wie gar kein Cache. Er wird nur nie schneller
// SubsetCacheNeedsAttention ist ein Helfer, den Sie selbst ergänzen;
// die Schwellen sind Beispiele, stimmen Sie sie auf Ihre Batch-Größe ab
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, Verdrängung und der Schalter, der den Ordner leert
Verdrängt wird nach dem Least-Recently-Used-Prinzip anhand der Datei-Änderungszeit. Jedes erfolgreiche Laden berührt den Zeitstempel des Eintrags, auf jeden Speichervorgang folgt eine Stutzung, und die ältesten Dateien gehen zuerst, bis der Ordner wieder ins Budget passt. Das Absenken von FontSubsetCacheMaxBytes zur Laufzeit stutzt auf der Stelle, statt auf den nächsten Schreibvorgang zu warten, sodass ein Dienst, der auf eine Plattenplatz-Warnung reagiert, den Cache genau in dem Moment schrumpfen kann, in dem er sich dafür entscheidet
Diese Unmittelbarkeit hat eine scharfe Kante. Das Budget auf null setzen, während ein Ordner konfiguriert ist, stutzt auf null, also löscht es jede .hpdfsubset-Datei darin. FontSubsetCacheFolder auf eine leere Zeichenfolge setzen schaltet dagegen den Cache aus und lässt die Dateien genau dort, wo sie sind. Beide Schalter deaktivieren das Cachen für das nächste Dokument; nur einer von beiden bewahrt eine Woche warmer Einträge. Für die Fehlersuche: die Ordner-Eigenschaft leeren und das Budget in Ruhe lassen
// Cachen für einen Diagnose-Lauf anhalten, jeden gespeicherten Eintrag behalten
Pdf.FontSubsetCacheFolder := '';
// Gefahr: mit gesetztem Ordner stutzt ein Budget von null den Ordner auf nichts
// Pdf.FontSubsetCacheMaxBytes := 0;
// Bewusstes Zurücksetzen: löscht *.hpdfsubset und übrige Temp-Dateien,
// die Ereignis-Zähler behalten ihre Werte
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;
Ein Diagnose-Lauf mit pausiertem Cache ist der sauberste Weg, ihn auszuschließen. Das sechs Buchstaben lange Subset-Tag in /BaseFont wird aus der Menge der behaltenen Glyphen abgeleitet, also liefern dieselben Glyphen dasselbe Tag mit oder ohne Cache, und die eingebetteten Font-Programme sollten übereinstimmen. Hashes über die ganze Datei werden trotzdem differieren, wegen Erstellungsdaten und Dokument-ID, also vergleichen Sie die Font-Streams, nicht die Dateien
Was passiert mit einem beschädigten oder halb geschriebenen Eintrag?
Ein beschädigter Eintrag wird gelöscht und das Subset wird normal gebaut, bevor irgendetwas aus diesem Eintrag einen PDF-Stream erreicht. Beim Laden prüft HotPDF die Datei-Signatur, die Formatversion, das Subset-Modus-Flag, die Glyphenanzahl gegen die Quellschrift, die deklarierten Längen gegen die tatsächliche Dateigröße und den sfnt-Header des gespeicherten Font-Programms. Jede Unstimmigkeit wirft den Eintrag weg, erhöht CorruptCount und fällt auf In-Memory-Subsetting zurück. Ein kaputtes Font-Programm im PDF eines Kunden würde Wochen später in einem Viewer auf fremder Maschine auftauchen, weit weg von seiner Ursache, und genau diesen Ausfall will diese Validierung verhindern
Halb geschriebene Einträge erscheinen erst gar nicht. Ein Schreibvorgang geht in eine temporäre Datei, deren Name die Prozess-ID und die Komponenten-Instanz trägt, und wird dann mit einem einzigen Replace-and-Flush-Rename über den finalen Namen bewegt. Ein Absturz mittendrin lässt eine verwaiste .tmp--Datei zurück, die ClearFontSubsetCache einsammelt, und der echte Eintrag ist entweder die vollständige alte oder die vollständige neue Version. Kompakte Einträge legen zusätzlich die Alt-zu-Neu-Glyphen-Zuordnung neben dem Font-Programm ab, sodass ein gecachtes kompaktes Subset dieselbe /CIDToGIDMap neu aufbaut und PDF/A-Ausgabe den Cache nutzen kann, ohne ihn zu umgehen
Wo sollte der Ordner liegen?
Drei Bedingungen entscheiden es: Das Dienst-Konto muss dorthin schreiben können, der Ordner sollte auf lokalem Speicher liegen, und kein Deployment-Schritt darf ihn ausradieren. Eine Netzwerk-Freigabe macht aus jedem Nachschlagen einen Roundtrip und legt Rename-Semantik in die Hände des Dateiservers. Ein Ordner im Anwendungsverzeichnis, den der Installer neu anlegt, bedeutet, dass der Cache nach jedem Update kalt startet, was den Nutzen ausgerechnet an dem Tag leise auslöscht, an dem Sie die Performance am genauesten beobachten
Mehrere Komponenten und mehrere Prozesse können sich einen lokalen Ordner teilen. Temporäre Namen kollidieren nie, das finale Rename ist atomar, und ein verlorenes Rennen kostet nichts Schlimmeres als einen Miss oder einen Schreibvorgang, der beim nächsten Dokument wiederholt werden muss. Teilen ist die bessere Voreinstellung auf einer Maschine, denn jede Instanz wärmt den Cache für die anderen. Der eine sichtbare Nebeneffekt: Die Zähler jeder Instanz beschreiben nur ihren eigenen Verkehr, während CurrentBytes und FileCount die aller beschreiben, und ein gelegentlicher WriteFailureCount-Tick kann von zwei Prozessen stammen, die denselben Eintrag gleichzeitig ersetzen. Ein nur lesbarer Ordner liefert weiterhin bestehende Einträge; er kann nur ihre Zeitstempel nicht auffrischen und keine neuen hinzufügen
Wann der Cache der falsche Hebel ist
Der Cache entfernt wiederkehrende Arbeit. Für das erste Dokument eines Batch tut er nichts, und für Arbeitslasten, deren Glyphenmengen sich nie wiederholen, wenig. Wenn die Ausgabe von einem einzigen großen CJK-Schnitt dominiert wird, der über unvorhersehbaren Text verteilt ist, sehen Sie nach, welche Glyphen in jedes Subset gezogen werden und warum, behandelt in den Hinweisen zu Font-Subset-Closure und Shaping-Glyphen. Wenn Dateigröße statt CPU das Ziel ist, erklärt der Artikel zu CompressDocument und kompakten Font-Subsets, was der Kompakt-Modus an der eingebetteten Schrift ändert. Wenn ein Batch sich aus Gründen langsam erweist, die nichts mit Schriften zu tun haben, zeigt der Durchgang durch Report-Ausgabe mit Schriften und Bildern, wohin die übrige Zeit üblicherweise geht, und der Beitrag zu EndDoc-Font-Subset-Reihenfolge behandelt, wann Subsets während der Dokument-Finalisierung gebaut werden
Weil der Subset-Cache in der Komponente lebt, nimmt ein Delphi- oder C++Builder-Report-Server ihn mit, indem er einen Ordner-Pfad und ein Budget setzt, ohne zusätzlichen Dienst zum Deployen oder Überwachen. Font-Einbettung, Subsetting und der Rest der PDF-Generierungs-Features sind auf der HotPDF-Delphi-Komponentenseite aufgeführt