Technischer Artikel

Font-Subsets auf der Platte cachen mit HotPDF in Delphi

HotPDF kann TrueType- und OpenType-Font-Subsets auf der Platte halten und über Dokumente und Prozessläufe hinweg wiederverwenden, sodass ein Batch, der zehntausend Kontoauszüge mit denselben drei Schriften rendert, diese Schriften einmal statt zehntausendmal subsettet. Der Cache wird über zwei Eigenschaften konfiguriert, über einen Datensatz inspiziert und kann gefahrlos aktiviert bleiben: Ein Cache-Fehlschlag fällt auf normales In-Memory-Subsetting zurück und hält ein Dokument niemals davon ab, erzeugt zu werden

Subsetting ist aus gutem Grund teuer. Ein Subset zu bauen bedeutet, den Glyphen-Abschluss abzugehen, loca und glyf neu zu schreiben, cmap und hmtx neu aufzubauen und ein CID-Mapping zu emittieren, das das PDF adressieren kann. Bei einem Dokument verschwindet diese Kosten im Rauschen. Bei einem Report-Server, der Dokumenten in einer Schleife erzeugt, ist sie oft der größte einzelne CPU-Zeitblock im Lauf

Was einen Cache-Hit möglich macht

Vier Dinge müssen übereinstimmen: der Schrift-Inhalt, die Menge der verwendeten Glyphen, der Subset-Modus und das Cache-Schema. Fehlt einer, subsettet HotPDF von Grund auf neu, denn ein Subset ist nur dann wiederverwendbar, wenn es ohnehin byte-identisch gewesen wäre

Die Glyphen-Menge ist die Bedingung, die Leute überrascht. Zwei Rechnungen, die sich in einem einzigen Kundennamen unterscheiden, verwenden unterschiedliche Glyphen-Mengen und erzeugen daher unterschiedliche Subsets und unterschiedliche Cache-Einträge. Der Cache zahlt sich aus, wenn Dokumente ein gemeinsames Glyphen-Repertoire teilen — Auszüge aus einer festen Vorlage, Formulare mit numerischen variablen Daten, Kataloge aus einer Produkt-Datenbank — und bringt nichts, wenn jedes Dokument einen anderen Ausschnitt aus einer großen CJK-Schrift zieht. Messen, bevor man annimmt, in welchem Fall man sich befindet

var
  Pdf: THotPDF;
  Info: THPDFFontSubsetCacheInfo;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.EnableFontSubsetting := True;
    Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
    Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024;   // 64 MiB, default is 256
    // ... generate the batch ...
    Info := Pdf.GetFontSubsetCacheInfo;
    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;

Woher weiß man, dass der Cache etwas tut?

GetFontSubsetCacheInfo liefert neun Zähler zurück, und das Verhältnis der ersten beiden beantwortet die Frage direkt. HitCount und MissCount geben die Hit-Rate. WriteCount und EvictionCount zeigen, ob Einträge lange genug überleben, um wiederverwendet zu werden, oder von einem zu kleinen Budget herausgedrängt werden. CurrentBytes und FileCount melden, was gerade auf der Platte liegt

Die übrigen drei sind die, auf die es sich zu alarmieren lohnt. CorruptCount zählt Einträge, die die Validierung nicht bestanden haben und entfernt wurden — einige nach einem unsauberen Shutdown sind normal, ein stetiger Strom bedeutet, dass der Speicher unzuverlässig ist. RejectedCount zählt Einträge, die vor der Verwendung abgelehnt wurden. WriteFailureCount zählt Einträge, die sich gar nicht schreiben ließen, was meistens ein Berechtigungsproblem des Ordners bedeutet statt irgendetwas mit Schriften. Keiner dieser drei hält die Dokument-Erzeugung an, und genau deshalb muss man sie ansehen: ein Cache, der stillschweigend nie schreibt, sieht von außen genauso aus wie ein funktionierender Cache, nur mit einer anderen CPU-Rechnung

Verdrängung, Budgets und der Moment, in dem man eines verkleinert

FontSubsetCacheMaxBytes ist standardmäßig 268435456 Bytes, also 256 MiB, und lässt sich zur Laufzeit absenken. Ein Absenken löst sofortige Least-Recently-Used-Verdrängung aus, statt auf den nächsten Schreibvorgang zu warten, sodass ein Dienst, der auf Plattendruck reagiert, Platz freigeben kann, sobald er es entscheidet, und nicht zu einem späteren Zeitpunkt, den er nicht kontrolliert

Setzt man FontSubsetCacheFolder auf eine leere Zeichenfolge, wird die Platten-Stufe deaktiviert, ohne etwas zu löschen, was bereits gespeichert ist, und ohne ein einziges Byte der Schrift-Ausgabe zu ändern. Das ist die Eigenschaft, nach der man greifen sollte, wenn man den Cache bei der Fehlersuche isolieren will: ausschalten, denselben Batch laufen lassen und die erzeugten PDFs vergleichen. Sie sollten identisch sein, denn der Cache speichert ein Ergebnis, keine Policy

Was der Cache tut, wenn ein Eintrag beschädigt ist

Er entfernt ihn und subsettet normal. Fehlgeformte oder abgeschnittene Einträge werden abgelehnt, bevor das Subset einen PDF-Stream erreichen kann, und das ist der Teil des Entwurfs, der am meisten zählt: Ein beschädigter Cache-Eintrag, der es in ein Dokument geschafft hätte, würde ein PDF mit einem kaputten Schriften-Programm erzeugen, und dieser Fehl schlage weit von seiner Ursache entfernt auf — in einem Betrachter, auf der Maschine eines Kunden, Wochen später

Schreibvorgänge sind atomar, sodass ein Leser nie einen halb geschriebenen Eintrag beobachtet, und ein Absturz mittendrin lässt den Cache konsistent statt vergiftet zurück. Kompakte Subset-Einträge behalten die CID-Remapping-Daten, die PDF/A-Schrift-Dictionaries verlangen, sodass ein gecachtes Subset nach wie vor ein konformes Subset ist — Archiv-Ausgabe muss den Cache nicht umgehen, um gültig zu bleiben

// Reset the disk tier after a font upgrade or a schema change
Pdf.ClearFontSubsetCache;

// Or move it somewhere writable and let the budget apply immediately
Pdf.SetFontSubsetCacheFolder('D:\cache\fonts');

Wo der Ordner in einem echten Deployment hingehört

Drei Eigenschaften entscheiden darüber: Der Ordner muss durch das Konto beschreibbar sein, als das der Dienst läuft, er sollte auf lokalem Speicher liegen statt auf einer Netzwerk-Freigabe, und er sollte nicht in einem Verzeichnis liegen, das ein Deployment-Schritt löscht. Ein Cache auf einer Freigabe macht aus jedem Miss einen Roundtrip und aus jedem Hit zwei; ein Cache in einem Anwendungs-Ordner, den der Installer neu anlegt, ist ein Cache, der nach jedem Update kalt startet

Für Dienste mit mehreren Instanzen sollte man jeder Instanz ihren eigenen Ordner geben, es sei denn, man hat bestätigt, dass der Speicher nebenläufiges atomares Ersetzen so handhabt, wie man es erwartet. Die Kosten eines duplizierten Eintrags ist ein zusätzlicher Subsetting-Durchlauf; die Kosten der Fehlersuche in einer Shared-Cache-Race ist ein Nachmittag

Wann man nach etwas anderem greifen sollte

Der Cache reduziert wiederkehrende Arbeit. Er reduziert nicht die Arbeit des ersten Dokuments, und er hilft einer Arbeitslast nicht, deren Glyphen-Mengen sich nie wiederholen. Wenn die Ausgabe von einer einzigen riesigen CJK-Schrift dominiert ist, die über unvorhersehbarbaren Text verteilt ist, ist der wirksamere Hebel der Subsetting-Abschluss selbst — welche Glyphen hineingezogen werden und warum —, behandelt in den Hinweisen zum Font-Subset-Abschluss und zu Shaping-Glyphen. Wenn ein Batch aus Gründen langsam ist, die sich am Ende gar nicht um Schriften drehen, zeigt die Erläuterung zu Report-Ausgabe mit Schriften und Bildern, wo die übrige Zeit üblicherweise hingeht, und die Fallstudie zum EndDoc-Font-Subset-Reihenfolge-Fehler ist eine Erinnerung daran, dass Subsetting-Korrektheit und Subsetting-Tempo zwei getrennte Probleme sind

HotPDF ist eine native VCL-PDF-Komponente für Delphi und C++Builder, und der Subset-Cache ist Teil der Bibliothek statt eines Add-on-Dienstes, sodass ein Report-Server ihn durch das Setzen eines einzigen Ordner-Pfads erhält — siehe die HotPDF-Komponenten-Seite für die vollständige Schrift- und Performance-Funktionsliste