Dos facturas renderizadas desde la misma plantilla con la misma fuente TrueType, que difieren solo en el apellido del cliente, dejan dos archivos separados en una caché de subconjuntos de fuentes. El apellido agrega una Q mayúscula y una z minúscula que la primera factura nunca usó, así que el conjunto de glifos conservados cambia, y con él el subconjunto y la clave de caché. Una caché de subconjuntos persistente rinde cuando el repertorio de glifos de sus documentos se repite, y los contadores que expone le dicen en un solo lote si el suyo lo hace
Activar la caché en disco en HotPDF
HotPDF mantiene la caché de subconjuntos apagada hasta que usted le da una carpeta. Dos propiedades de THotPDF la controlan: FontSubsetCacheFolder nombra el directorio y FontSubsetCacheMaxBytes acota cuánto puede contener, con un valor predeterminado de 268435456 bytes (256 MiB). La caché está activa solo mientras la carpeta no esté vacía y el presupuesto sea mayor que cero. Asignar la carpeta crea el directorio si falta y de inmediato recorta lo que ya haya hasta el presupuesto
El subconjunto vale la pena cachear porque es trabajo real. Construir un subconjunto TrueType significa recorrer la clausura de glifos compuestos, reescribir loca y glyf, reconstruir las tablas que indexan glifos y producir un mapeo CID direccionable por el PDF. Para un documento el costo se pierde en el ruido. En un servidor de informes que renderiza miles de estados en un bucle con las mismas tres fuentes, los mismos subconjuntos se construyen una y otra vez, y esa es la CPU que la caché le devuelve
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
// ruta solo ASCII: la propiedad es un AnsiString
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 MiB en lugar de los 256 MiB predeterminados
// ... BeginDoc / dibujar / EndDoc para cada documento del lote ...
Info := Pdf.GetFontSubsetCacheInfo;
// LogFmt es su propia rutina de 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 caché está detrás de ambas rutas de subconjunto: las fuentes con subconjunto vía EnableFontSubsetting, que cubre fuentes TrueType y OpenType-CFF, y la fuente registrada con RegisterUnicodeTTF. Nada más cambia en su código de dibujo. La ruta de carpeta está declarada como AnsiString, así que un nombre de directorio con caracteres fuera de la página de códigos del sistema está pidiendo problemas; una ruta ASCII sencilla bajo ProgramData o un directorio de datos de servicio evita la cuestión por completo
Qué entra en una clave de caché
Cuatro entradas deciden si un subconjunto almacenado puede reutilizarse: los bytes exactos de la fuente de origen, el conjunto de glifos que el documento conserva, el modo de subconjunto y la versión del formato de caché. Cada clave se construye a partir de un hash SHA-256 de los bytes de la fuente y un hash SHA-256 de un bitmap de los ID de glifo usados, con el prefijo v1- y el sufijo -s para un subconjunto disperso o -c para uno compacto. Las entradas aterrizan en disco como <key>.hpdfsubset, de modo que un listado del directorio le muestra de un vistazo qué modo está produciendo su lote
Hashear el contenido de la fuente en lugar de su nombre de archivo tiene una consecuencia práctica. Actualizar Arial o cambiar a una compilación más nueva de una fuente corporativa produce claves nuevas automáticamente, y las entradas viejas simplemente dejan de recibir aciertos hasta que la expulsión las elimina. No tiene que limpiar la caché tras una actualización de fuente para obtener salida correcta. Limpiar solo reclama el espacio antes
El modo de subconjunto es la entrada que la gente olvida. CompactFontSubsetting renumera los glifos conservados en un rango denso y escribe un /CIDToGIDMap explícito (ISO 32000-1 §9.7.4.2), y la fuente registrada siempre recibe un subconjunto compacto cuando PDFACompliance está activo. CompressDocument también activa los subconjuntos compactos por la duración del documento. Así, un servicio que escribe el mismo estado una vez como PDF sencillo y otra como PDF/A llena dos conjuntos de entradas para glifos idénticos, uno terminado en -s y otro en -c. Las fuentes OpenType-CFF siempre toman la ruta dispersa
El conjunto de glifos es donde se decide la tasa de aciertos. Estados desde una plantilla fija, formularios cuyos datos variables son mayormente dígitos, catálogos dibujados desde una sola base de datos de productos: todos reutilizan los mismos glifos documento tras documento. Una carga de trabajo donde cada documento jala un segmento distinto de una fuente CJK con decenas de miles de glifos casi siempre falla, porque el bitmap de casi cada documento es nuevo. Corra un lote representativo y lea los contadores antes de decidir qué tipo de carga tiene
¿Cómo saber si la caché está haciendo algo?
GetFontSubsetCacheInfo devuelve un registro THPDFFontSubsetCacheInfo con nueve campos, y HitCount frente a MissCount responde la pregunta directamente. WriteCount y EvictionCount muestran si las entradas viven lo suficiente para reutilizarse o las saca un presupuesto demasiado apretado. CurrentBytes y FileCount describen la carpeta tal como está ahora mismo
Dos detalles importan cuando conecta estos contadores a su monitoreo. Los siete contadores de eventos pertenecen a la instancia de THotPDF y arrancan en cero cuando se crea el componente, así que un servicio que crea un componente por documento ve la actividad de un solo documento por lectura. CurrentBytes y FileCount, en cambio, provienen de enumerar la carpeta en el momento de la llamada, lo que hace que reflejen todos los procesos que usan esa carpeta y también que cada llamada cueste un escaneo de directorio. Lea el registro una vez por lote, al final, y mantenga el bucle por documento libre de él
Los tres contadores restantes son los que vale la pena alertar. CorruptCount cuenta las entradas que fallaron la validación al cargar y fueron eliminadas; cada una cuenta también como fallo. Un puñado tras un corte de energía es lo esperado, y un goteo constante apunta al almacenamiento. RejectedCount cuenta los subconjuntos que la caché se negó a almacenar, típicamente porque un solo subconjunto es más grande que todo el presupuesto. WriteFailureCount cuenta escrituras que no se completaron, lo que en la práctica casi siempre es una carpeta a la que la cuenta de servicio no puede escribir. Ninguno de los tres interrumpe la generación de documentos, así que una caché con problema de permisos de escritura produce exactamente los mismos PDF que ninguna caché. Solo nunca se vuelve más rápida
// SubsetCacheNeedsAttention es una ayuda que usted agrega;
// los umbrales son ejemplos, ajústelos al tamaño de su lote
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;
Presupuestos, expulsión y el interruptor que vacía la carpeta
La expulsión es least-recently-used por fecha de modificación del archivo. Cada carga exitosa toca la marca de tiempo de la entrada, cada almacenamiento va seguido de un recorte, y los archivos más viejos salen primero hasta que la carpeta vuelva a caber en el presupuesto. Bajar FontSubsetCacheMaxBytes en tiempo de ejecución recorta en el momento en lugar de esperar la próxima escritura, así que un servicio que reacciona a una alerta de espacio en disco puede encoger la caché justo cuando decide hacerlo
Esa inmediatez tiene un filo afilado. Poner el presupuesto en cero mientras hay una carpeta configurada recorta a cero, lo que borra cada archivo .hpdfsubset dentro. Poner FontSubsetCacheFolder en una cadena vacía, en cambio, apaga la caché y deja los archivos exactamente donde están. Ambos interruptores desactivan el cacheo para el siguiente documento; solo uno de ellos conserva una semana de entradas calientes. Para diagnóstico, vacíe la propiedad de carpeta y no toque el presupuesto
// Pausar el cacheo para una corrida de diagnóstico, conservando cada entrada almacenada
Pdf.FontSubsetCacheFolder := '';
// Peligro: con una carpeta configurada, un presupuesto cero recorta la carpeta a nada
// Pdf.FontSubsetCacheMaxBytes := 0;
// Reinicio deliberado: borra *.hpdfsubset y archivos temporales sobrantes,
// los contadores de eventos conservan sus valores
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;
Una corrida de diagnóstico con la caché en pausa es la forma más limpia de descartarla. La etiqueta de subconjunto de seis letras en /BaseFont se deriva del conjunto de glifos conservados, así que los mismos glifos producen la misma etiqueta con o sin caché, y los programas de fuente incrustados deberían coincidir. Los hashes de archivo completo seguirán difiriendo por las fechas de creación y el ID del documento, así que compare los streams de fuente, no los archivos
¿Qué pasa con una entrada dañada o escrita a medias?
Una entrada dañada se elimina y el subconjunto se construye de forma normal, antes de que nada de esa entrada llegue a un stream PDF. Al cargar, HotPDF verifica la firma del archivo, la versión del formato, la bandera de modo de subconjunto, el conteo de glifos contra la fuente de origen, las longitudes declaradas contra el tamaño real del archivo y el encabezado sfnt del programa de fuente almacenado. Cualquier discrepancia desecha la entrada, incrementa CorruptCount y cae al subconjunto en memoria. Meter un programa de fuente roto en el PDF de un cliente aparecería semanas después en un visor en la máquina de otra persona, lejos de su causa, y ese es el fallo que esta validación existe para prevenir
Las entradas escritas a medias ni siquiera llegan a existir. Un almacenamiento escribe en un archivo temporal cuyo nombre lleva el ID de proceso y la instancia del componente, y luego lo mueve sobre el nombre final con un solo rename de reemplazar-y-vaciar. Un fallo a mitad de escritura deja un archivo .tmp- suelto, que ClearFontSubsetCache barre, y la entrada real es o la versión vieja completa o la nueva completa. Las entradas compactas también almacenan el mapa de glifos viejo-a-nuevo junto al programa de fuente, así que un subconjunto compacto cacheado reconstruye el mismo /CIDToGIDMap y la salida PDF/A puede usar la caché sin rodearla
¿Dónde debería vivir la carpeta?
Tres condiciones lo deciden: la cuenta de servicio debe poder escribir ahí, la carpeta debería estar en almacenamiento local y ningún paso de despliegue debería borrarla. Un recurso compartido de red convierte cada búsqueda en un viaje de ida y vuelta y pone la semántica de rename en manos del servidor de archivos. Una carpeta dentro del directorio de la aplicación que el instalador recrea significa que la caché arranca fría tras cada actualización, lo que borra en silencio el beneficio justo el día que usted está observando el rendimiento con más lupa
Varios componentes y varios procesos pueden compartir una carpeta local. Los nombres temporales nunca colisionan, el rename final es atómico, y perder una carrera no cuesta nada peor que un fallo o una escritura que deba reintentarse en el siguiente documento. Compartir es el mejor valor predeterminado en una sola máquina, porque cada instancia calienta la caché para las demás. El único efecto secundario visible es que los contadores de cada instancia describen solo su propio tráfico, mientras que CurrentBytes y FileCount describen el de todos, y un tick ocasional de WriteFailureCount puede venir de dos procesos reemplazando la misma entrada a la vez. Una carpeta de solo lectura todavía sirve las entradas existentes; solo no puede refrescar sus marcas de tiempo ni agregar nuevas
Cuándo la caché no es la palanca correcta
La caché elimina trabajo repetido. No hace nada por el primer documento de un lote y poco por cargas de trabajo cuyos conjuntos de glifos nunca se repiten. Si la salida está dominada por una fuente CJK grande usada con texto impredecible, mire qué glifos se jalan hacia cada subconjunto y por qué, cubierto en las notas sobre clausura de subconjuntos de fuentes y glifos conformados. Si el objetivo es el tamaño de archivo y no la CPU, el artículo sobre CompressDocument y subconjuntos de fuentes compactos explica qué cambia el modo compacto en la fuente incrustada. Cuando un lote resulta lento por razones ajenas a las fuentes, el recorrido de salida de informes con fuentes e imágenes muestra a dónde suele ir el resto del tiempo, y el escrito sobre ordenamiento de subconjuntos de fuentes en EndDoc cubre cuándo se construyen los subconjuntos durante la finalización del documento
Como la caché de subconjuntos vive dentro del componente, un servidor de informes en Delphi o C++Builder la adopta configurando una ruta de carpeta y un presupuesto, sin ningún servicio extra que desplegar ni monitorear. La incrustación de fuentes, el subconjunto y el resto de las funciones de generación PDF están listadas en la página del componente HotPDF para Delphi