Articolo tecnico

Memorizzare i sottoinsiemi dei font su disco con HotPDF

Due fatture renderizzate dallo stesso template con lo stesso font TrueType, che differiscono solo nel cognome del cliente, lasciano due file separati in una cache di subset dei font. Il cognome aggiunge una Q maiuscola e una z minuscola che la prima fattura non usava mai, quindi il set di glifi conservati cambia, e con esso il subset e la chiave di cache. Una cache di subset persistente ripaga quando il repertorio di glifi dei vostri documenti si ripete, e i contatori che espone vi dicono nel giro di un batch se il vostro è uno di questi casi

Diagramma delle quattro chiavi di cache dei subset dei font in HotPDF: contenuto del font, set di glifi, modalità di subset e versione dello schema, con due fatture che differiscono di un nome e finiscono in voci diverse
due fatture che differiscono per un solo nome cliente sono due voci di cache — la cache ripaga solo quando i documenti condividono un repertorio di glifi

Attivare la cache su disco in HotPDF

HotPDF tiene la cache dei subset spenta finché non vi date una cartella. Due proprietà su THotPDF la controllano: FontSubsetCacheFolder nomina la directory, e FontSubsetCacheMaxBytes pone un tetto a quanto può contenere, con un predefinito di 268435456 byte (256 MiB). La cache è attiva solo mentre la cartella è non vuota e il budget è sopra zero. Assegnare la cartella crea la directory se manca e ritaglia subito tutto ciò che già contiene fino al budget

Il subsetting merita la cache perché è lavoro vero. Costruire un subset TrueType significa percorrere la chiusura dei glifi composti, riscrivere loca e glyf, ricostruire le tabelle che indicizzano i glifi, e produrre una mappatura CID che il PDF possa indirizzare. Per un singolo documento il costo svanisce nel rumore. In un server di report che renderizza migliaia di estratti conto in un loop con gli stessi tre font, gli stessi subset vengono costruiti ancora e ancora, e quella è la CPU che la cache vi restituisce

var
  Pdf: THotPDF;
  Info: THPDFFontSubsetCacheInfo;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.EnableFontSubsetting := True;
    // percorso solo ASCII: la proprietà è un AnsiString
    Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
    Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024;   // 64 MiB invece del predefinito di 256 MiB
    // ... BeginDoc / disegno / EndDoc per ogni documento del batch ...
    Info := Pdf.GetFontSubsetCacheInfo;
    // LogFmt è la vostra routine di logging
    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;

La cache sta dietro a entrambi i percorsi di subsetting: i font subsettati tramite EnableFontSubsetting, che copre i font TrueType e OpenType-CFF, e il font registrato con RegisterUnicodeTTF. Null'altro nel vostro codice di disegno cambia. Il percorso della cartella è dichiarato come AnsiString, quindi un nome di directory con caratteri fuori dalla code page di sistema chiede guai; un percorso ASCII semplice sotto ProgramData o in una directory dati di servizio evita del tutto la questione

Cosa entra in una chiave di cache

Quattro input decidono se un subset memorizzato può essere riusato: i byte esatti del font sorgente, il set di glifi che il documento conserva, la modalità di subset, e la versione del formato di cache. Ogni chiave è costruita da un hash SHA-256 dei byte del font e un hash SHA-256 di una bitmap degli ID dei glifi usati, con prefisso v1- e suffisso -s per un subset sparso o -c per uno compatto. Le voci atterrano su disco come <key>.hpdfsubset, quindi un elenco della directory vi mostra a colpo d'occhio quale modalità il vostro batch sta producendo

Hashare il contenuto del font invece del suo nome file ha una conseguenza pratica. Aggiornare Arial o sostituire una build più recente di un font aziendale produce nuove chiavi automaticamente, e le vecchie voci semplicemente smettono di essere usate finché l'evacuazione non le rimuove. Non dovete svuotare la cache dopo un aggiornamento del font per ottenere output corretto. Svuotarla recupera solo lo spazio prima

La modalità di subset è l'input che la gente dimentica. CompactFontSubsetting rinumera i glifi conservati in un intervallo denso e scrive una /CIDToGIDMap esplicita (ISO 32000-1 §9.7.4.2), e il font registrato riceve sempre un subset compatto quando PDFACompliance è impostato. CompressDocument attiva i subset compatti anche per la durata del documento. Così un servizio che scrive lo stesso estratto conto una volta come PDF semplice e una volta come PDF/A riempie due set di voci per glifi identici, uno che finisce in -s e uno in -c. I font OpenType-CFF prendono sempre il percorso sparso

Il set di glifi è dove si decide l'hit rate. Estratti conto da un template fisso, moduli i cui dati variabili sono per lo più cifre, cataloghi tratti da un unico database prodotti: questi riusano gli stessi glifi documento dopo documento. Un carico di lavoro in cui ogni documento pesca una fetta diversa di un font CJK con decine di migliaia di glifi per lo più sbaglia, perché la bitmap di quasi ogni documento è nuova. Eseguite un batch rappresentativo e leggete i contatori prima di decidere che tipo di carico avete

Come sapete se la cache sta facendo qualcosa?

GetFontSubsetCacheInfo restituisce un record THPDFFontSubsetCacheInfo con nove campi, e HitCount contro MissCount risponde alla domanda direttamente. WriteCount e EvictionCount mostrano se le voci vivono abbastanza da essere riusate o vengono spinte fuori da un budget troppo stretto. CurrentBytes e FileCount descrivono la cartella così com'è in questo momento

Pannello HotPDF dei nove contatori di GetFontSubsetCacheInfo raggruppati in hit rate, sopravvivenza e conteggi di fallimento, con tre letture che nominano l'impostazione da cambiare
il rapporto tra HitCount e MissCount risponde se la cache sta facendo qualcosa, e gli altri contatori dicono perché no

Due dettagli contano quando collegate questi valori al monitoring. I sette contatori di evento appartengono all'istanza THotPDF e partono da zero quando il componente viene creato, quindi un servizio che crea un componente per documento vede l'attività di un solo documento per lettura. CurrentBytes e FileCount, al contrario, arrivano dall'enumerare la cartella al momento della chiamata, il che li fa riflettere ogni processo che usa quella cartella e rende anche ogni chiamata costosa per la scansione della directory. Leggete il record una volta per batch, alla fine, e tenete il loop per documento libero da esso

I restanti tre contatori sono quelli che meritano un alert. CorruptCount conta le voci che hanno fallito la validazione al caricamento e sono state eliminate; ognuna conta anche come miss. Qualche unità dopo un black-out è atteso, e un flusso costante punta allo storage. RejectedCount conta i subset che la cache ha rifiutato di memorizzare, tipicamente perché un singolo subset è più grande dell'intero budget. WriteFailureCount conta le scritture non completate, che in pratica è quasi sempre una cartella su cui l'account di servizio non può scrivere. Nessuno dei tre interrompe la generazione del documento, quindi una cache con un problema di permessi di scrittura produce esattamente gli stessi PDF di nessuna cache. Semplicemente non diventa mai più veloce

// SubsetCacheNeedsAttention è un helper che aggiungete voi;
// le soglie sono esempi, taratele sulla dimensione del vostro batch
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;

Budget, evacuazione e l'interruttore che svuota la cartella

L'evacuazione è least-recently-used per tempo di modifica del file. Ogni caricamento riuscito tocca il timestamp della voce, ogni memorizzazione è seguita da un trim, e i file più vecchi vanno per primi finché la cartella rientra nel budget. Abbassare FontSubsetCacheMaxBytes a runtime ritaglia sul posto invece di aspettare la prossima scrittura, così un servizio che reagisce a un alert di spazio su disco può ridurre la cache nel momento in cui lo decide

Questa immediatezza ha un filo tagliente. Impostare il budget a zero mentre una cartella è configurata ritaglia a zero, il che cancella ogni file .hpdfsubset al suo interno. Impostare FontSubsetCacheFolder a una stringa vuota, al contrario, spegne la cache e lascia i file esattamente dove sono. Entrambi gli interruttori disattivano la cache per il documento successivo; solo uno di essi conserva una settimana di voci calde. Per il troubleshooting, svuotate la proprietà della cartella e lasciate stare il budget

// Mettete in pausa la cache per un run diagnostico, conservate ogni voce memorizzata
Pdf.FontSubsetCacheFolder := '';

// Pericolo: con una cartella impostata, un budget a zero ritaglia la cartella a nulla
// Pdf.FontSubsetCacheMaxBytes := 0;

// Reset deliberato: cancella *.hpdfsubset e i file temporanei residui,
// i contatori di evento conservano i loro valori
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;

Un run diagnostico con la cache in pausa è il modo più pulito per escluderla. Il tag di subset a sei lettere in /BaseFont deriva dal set di glifi conservati, quindi gli stessi glifi producono lo stesso tag con o senza cache, e i programmi font incorporati dovrebbero corrispondere. Gli hash dell'intero file continueranno a differire per via delle date di creazione e del document ID, quindi confrontate gli stream dei font, non i file

Cosa succede a una voce danneggiata o scritta a metà?

Una voce danneggiata viene eliminata e il subset viene costruito normalmente, prima che qualsiasi cosa di quella voce raggiunga uno stream PDF. Al caricamento HotPDF controlla la firma del file, la versione del formato, il flag della modalità di subset, il conteggio dei glifi contro il font sorgente, le lunghezze dichiarate contro la dimensione effettiva del file, e l'header sfnt del programma font memorizzato. Qualsiasi mancata corrispondenza butta via la voce, incrementa CorruptCount, e ripiega sul subsetting in memoria. Mettere un programma font rotto nel PDF di un cliente affiorerebbe settimane dopo in un viewer sulla macchina di qualcun altro, lontano dalla sua causa, e quello è il fallimento che questa validazione esiste per prevenire

Diagramma di flusso della gestione delle voci di cache danneggiate in HotPDF: validazione prima dell'uso, rifiuto e rimozione prima di qualsiasi stream PDF, ripiego sul subsetting in memoria
le voci danneggiate falliscono aperte — il documento viene comunque prodotto, e l'output PDF/A mantiene la cache perché le voci conservano il remapping CID

Le voci scritte a metà non compaiono nemmeno. Una memorizzazione scrive su un file temporaneo il cui nome porta il process ID e l'istanza del componente, poi lo sposta sul nome finale con una singola rename replace-and-flush. Un crash a metà scrittura lascia un file .tmp- vagante, che ClearFontSubsetCache spazza via, e la voce reale è o la versione completa vecchia o quella completa nuova. Le voci compatte memorizzano anche la mappa glifi vecchio-a-nuovo accanto al programma font, così un subset compatto in cache ricostruisce la stessa /CIDToGIDMap e l'output PDF/A può usare la cache senza bypassarla

Dove dovrebbe vivere la cartella?

Tre condizioni lo decidono: l'account di servizio deve poterci scrivere, la cartella dovrebbe stare su storage locale, e nessun passaggio di deployment dovrebbe ripulirla. Una condivisione di rete trasforma ogni lookup in un round trip e mette la semantica delle rename nelle mani del file server. Una cartella dentro la directory dell'applicazione che l'installer ricrea significa che la cache riparte fredda dopo ogni aggiornamento, il che cancella in silenzio il beneficio esattamente nel giorno in cui state osservando le performance più da vicino

Più componenti e più processi possono condividere una cartella locale. I nomi temporanei non collidono mai, la rename finale è atomica, e una corsa persa non costa nulla di peggio di un miss o di una scrittura da riprovare sul documento successivo. La condivisione è il predefinito migliore su una macchina, perché ogni istanza scalda la cache per le altre. L'unico effetto collaterale visibile è che i contatori di ogni istanza descrivono solo il proprio traffico, mentre CurrentBytes e FileCount descrivono quello di tutti, e un tick occasionale di WriteFailureCount può arrivare da due processi che sostituiscono insieme la stessa voce. Una cartella read-only continua a servire le voci esistenti; semplicemente non può rinfrescarne i timestamp né aggiungerne di nuove

Quando la cache è la leva sbagliata

La cache elimina il lavoro ripetuto. Non fa nulla per il primo documento di un batch e poco per i carichi di lavoro i cui set di glifi non si ripetono mai. Se l'output è dominato da un grande font CJK usato su testo imprevedibile, guardate quali glifi vengono pescati in ogni subset e perché, trattato nelle note su chiusura del subset dei font e glifi con shaping. Se l'obiettivo è la dimensione del file più che la CPU, l'articolo su CompressDocument e subset di font compatti spiega cosa cambia la modalità compatta nel font incorporato. Quando un batch risulta lento per motivi non legati ai font, la guida di output di report con font e immagini mostra dove va di solito il resto del tempo, e l'approfondimento su ordinamento dei subset dei font in EndDoc copre quando i subset vengono costruiti durante la finalizzazione del documento

Perché la cache dei subset vive dentro il componente, un server di report Delphi o C++Builder la adotta impostando un percorso di cartella e un budget, senza servizi extra da deployare o monitorare. Incorporamento dei font, subsetting e il resto delle funzionalità di generazione PDF sono elencati nella pagina del componente Delphi HotPDF