Technisch artikel

Font-subsets op schijf cachen met HotPDF in Delphi

HotPDF kan TrueType- en OpenType-font-subsets op schijf houden en hergebruiken over documenten en over proces-runs heen, zodat een batch die tienduizend afschriften rendert met dezelfde drie fonts die fonts één keer subset in plaats van tienduizend keer. De cache wordt met twee eigenschappen geconfigureerd, met één record geïnspecteerd, en is veilig om aan te laten staan: een cachefout valt terug op normaal in-geheugen subsetting en stopt nooit de productie van een document

Subsetting is om een reden duur. Een subset bouwen betekent de glyph-afsluiting doorlopen, loca en glyf herschrijven, cmap en hmtx herbouwen, en een CID-mapping emitteeren die de PDF kan adresseren. Bij één document verdwijnt die kosten in de ruis. Bij een rapportserver die documenten in een lus produceert, is het vaak het grootste enkele blok CPU-tijd in de run

Wat een cache-hit mogelijk maakt

Vier dingen moeten overeenkomen: de fontinhoud, de verzameling gebruikte glyphs, de subset-mode en het cache-schema. Mis er een en HotPDF subset vanaf nul, want een subset is alleen herbruikbaar wanneer hij toch byte-identiek zou zijn geweest

De glyph-verzameling is de voorwaarde die mensen verrast. Twee facturen die verschillen in één klantnaam gebruiken verschillende glyph-verzamelingen, en produceren daarom verschillende subsets en verschillende cache-entries. De cache loont wanneer documenten een glyph-repertoire delen — afschriften uit een vast sjabloon, formulieren waarvan de variabele gegevens numeriek zijn, catalogi uit één productdatabase — en levert niets op wanneer elk document een andere plak van een groot CJK-font tekent. Meet vóór je aanneemt in welk geval je zit

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;

Hoe weet je dat de cache iets doet?

GetFontSubsetCacheInfo retourneert negen tellers, en de verhouding tussen de eerste twee beantwoordt de vraag direct. HitCount en MissCount geven de hit-rate. WriteCount en EvictionCount tonen of entries lang genoeg overleven om hergebruikt te worden of worden weggedrukt door een budget dat te klein is. CurrentBytes en FileCount rapporteren wat er nu op schijf staat

De overige drie zijn de moeite waard om op te alerten. CorruptCount telt entries die validatie faalden en werden verwijderd — een paar na een onschone shutdown zijn normaal, een gestage stroom betekent dat de opslag onbetrouwbaar is. RejectedCount telt entries geweigerd vóór gebruik. WriteFailureCount telt entries die helemaal niet konden worden geschreven, wat meestal een rechtenprobleem op de map betekent in plaats van iets over fonts. Geen van deze drie stopt documentgeneratie, wat precies is waarom je ernaar moet kijken: een cache die stilzwijgend nooit schrijft ziet er van buiten hetzelfde uit als een cache die werkt, behalve de CPU-rekening

Verwijdering, budgetten en het moment waarop je er een verkleint

FontSubsetCacheMaxBytes standaard is 268435456 bytes, dat is 256 MiB, en kan tijdens runtime worden verlaagd. Verlagen triggert onmiddellijke least-recently-used-verwijdering in plaats van te wachten op de volgende schrijfactie, zodat een service die op schijfdruk reageert ruimte kan vrijmaken op het moment dat het beslist, niet op een later moment dat het niet beheerst

FontSubsetCacheFolder op een lege tekenreeks zetten schakelt de schijflaag uit zonder iets te wissen wat al is opgeslagen, en zonder een enkele byte font-uitvoer te wijzigen. Dat is de eigenschap om naar te grijpen wanneer je de cache tijdens probleemoplossing wilt isoleren: zet hem uit, rendeer dezelfde batch, en vergelijk de geproduceerde PDF-bestanden. Ze zouden identiek moeten zijn, want de cache slaat een resultaat op, geen beleid

Wat de cache doet wanneer een entry is beschadigd

Hij verwijdert hem en subset normaal. Misvormde of afgekapte entries worden afgewezen vóór de subset een PDF-stream kan bereiken, wat het deel van het ontwerp is dat het meest telt: een beschadigde cache-entry die in een document terechtkwam, zou een PDF produceren met een gebroken font-program, en dat falen zou ver van zijn oorzaak opduiken — in een viewer, op een machine van een klant, weken later

Schrijfacties zijn atomair, zodat een lezer nooit een half geschreven entry waarneemt, en een crash tijdens het schrijven laat de cache consistent achter in plaats van vergiftigd. Compacte subset-entries behouden de CID-remapping-gegevens die PDF/A-font-dictionaries vereisen, zodat een gecachte subset nog steeds een conformerende subset is — archiefuitvoer hoeft de cache niet te omzeilen om geldig te blijven

// 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');

Waar de map in een echte implementatie thuishoort

Drie eigenschappen beslissen dit: de map moet beschrijfbaar zijn door de account waaronder de service draait, hij moet op lokale opslag staan in plaats van op een netwerkshare, en hij mag niet in een map staan die een uitrolstap wist. Een cache op een share verandert elke miss in een round-trip en elke hit in twee; een cache onder een applicatiemap die het installatieprogramma hercreëert is een cache die na elke update koud start

Voor multi-instance-services, geef elke instance zijn eigen map tenzij je hebt bevestigd dat de opslag gelijktijdige atomische vervanging afhandelt zoals je verwacht. De kosten van een gedupliceerde entry zijn één extra subset-pass; de kosten van het debuggen van een shared-cache-race zijn een middag

Wanneer naar iets anders grijpen

De cache vermindert herhaald werk. Hij vermindert niet het werk van het eerste document, en hij helpt geen workload waarvan glyph-verzamelingen nooit herhalen. Als je uitvoer wordt gedomineerd door één enorm CJK-font gebruikt over onvoorspelbare tekst, dan is de effectievere hefboom de subset-afsluiting zelf — welke glyphs worden binnengehaald, en waarom — behandeld in de notities over font-subset-afsluiting en shaping-glyphs. Als je batch traag is om redenen die uiteindelijk helemaal geen fonts zijn, toont de doorloop van rapportuitvoer met fonts en afbeeldingen waar de andere tijd meestal zit, en de casestudy over de EndDoc-font-subset-volgordebug is een herinnering dat subset-correctheid en subset-snelheid afzonderlijke problemen zijn

HotPDF is een native VCL PDF-component voor Delphi en C++Builder, en de subset-cache is onderdeel van de bibliotheek in plaats van een add-on-service, zodat een rapportserver hem krijgt door één mappad in te stellen — zie de HotPDF-componentpagina voor de volledige font- en prestatiefunctielijst