Artigo Técnico

Cache em Disco de Subconjuntos de Tipos de Letra no HotPDF em Delphi

O HotPDF consegue manter subconjuntos de tipos de letra TrueType e OpenType em disco e reutilizá-los entre documentos e entre execuções do processo, pelo que um lote que renderiza dez mil extratos com os mesmos três tipos de letra faz o subsetting desses tipos uma única vez em vez de dez mil vezes. A cache configura-se com duas propriedades, inspeciona-se com um registo, e é segura deixar ligada: uma falha de cache recua para o subsetting normal em memória e nunca impede um documento de ser produzido

Fluxograma do tratamento de entradas de cache danificadas no HotPDF: validar antes de usar, rejeitar e remover antes de qualquer fluxo PDF, recuar para subconjuntos em memória
as entradas danificadas falham em modo aberto — o documento continua a ser produzido, e a saída PDF/A mantém a cache porque as entradas retêm o remapeamento CID

O subsetting é caro por uma razão. Construir um subconjunto significa percorrer o fecho de glifos, reescrever loca e glyf, reconstruir cmap e hmtx, e emitir um mapeamento CID que o PDF consiga endereçar. Para um documento esse custo desaparece no ruído. Para um servidor de relatórios que produz documentos num ciclo, é frequentemente o maior bloco individual de tempo de CPU na execução

O que torna uma cache hit possível

Quatro coisas têm de corresponder: o conteúdo do tipo de letra, o conjunto de glifos usados, o modo de subsetting, e o esquema da cache. Falhe um só e o HotPDF faz o subsetting de raiz, porque um subconjunto só é reutilizável quando teria sido byte-idêntico de qualquer forma

O conjunto de glifos é a condição que surpreende as pessoas. Duas faturas que diferem num único nome de cliente usam conjuntos de glifos diferentes, e por isso produzem subconjuntos diferentes e entradas de cache diferentes. A cache compensa quando os documentos partilham um repertório de glifos — extratos de um modelo fixo, formulários cujos dados variáveis são numéricos, catálogos retirados de uma base de dados de produtos — e não compensa nada quando cada documento desenha uma fatia diferente de um tipo CJK grande. Meça antes de assumir em que caso está

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, a predefinição é 256
    // ... gere o lote ...
    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;

Como sabe se a cache está a fazer alguma coisa?

GetFontSubsetCacheInfo devolve nove contadores, e o rácio entre os dois primeiros responde à pergunta diretamente. HitCount e MissCount dão a taxa de acertos. WriteCount e EvictionCount mostram se as entradas sobrevivem tempo suficiente para serem reutilizadas ou estão a ser empurradas para fora por um orçamento demasiado pequeno. CurrentBytes e FileCount reportam o que está em disco agora

Os três restantes são aqueles sobre os quais vale a pena alertar. CorruptCount conta entradas que falharam a validação e foram removidas — algumas depois de um encerramento incómodo são normais, um fluxo constante significa que o armazenamento é pouco fiável. RejectedCount conta entradas recusadas antes do uso. WriteFailureCount conta entradas que não puderam ser escritas de todo, o que normalmente significa um problema de permissões na pasta em vez de algo sobre tipos de letra. Nenhum destes três para a geração de documentos, que é exatamente porque tem de olhar para eles: uma cache que silenciosamente nunca escreve parece igual por fora a uma cache que funciona, exceto na fatura de CPU

Diagrama das quatro chaves de cache de subconjuntos de fontes no HotPDF: conteúdo da fonte, conjunto de glifos, modo de subconjunto e versão do esquema, com duas faturas que diferem num nome a aterrar em entradas diferentes
duas faturas que diferem num único nome de cliente são duas entradas de cache — a cache só compensa quando os documentos partilham um repertório de glifos

Remoção, orçamentos e o momento em que se encolhe um

FontSubsetCacheMaxBytes tem a predefinição de 268435456 bytes, ou seja 256 MiB, e pode ser baixado em runtime. Baixá-lo despoleta remoção do menos recentemente usado imediata em vez de esperar pela próxima escrita, pelo que um serviço que reaja a pressão de disco consegue libertar espaço no momento em que decide, não num ponto posterior que não controla

Painel do HotPDF dos nove contadores de GetFontSubsetCacheInfo agrupados em taxa de acertos, sobrevivência e poupanças, com três leituras que nomeiam a definição a alterar
o rácio entre HitCount e MissCount responde se a cache está a fazer alguma coisa, e os outros contadores dizem porque não

Definir FontSubsetCacheFolder como uma cadeia vazia desativa o nível de disco sem limpar nada já armazenado, e sem mudar um único byte da saída de tipos de letra. É a propriedade a procurar quando quer isolar a cache durante a resolução de problemas: desligue-a, execute o mesmo lote, e compare os PDFs produzidos. Devem ser idênticos, porque a cache armazena um resultado, não uma política

O que a cache faz quando uma entrada está danificada

Remove-a e faz o subsetting normalmente. Entradas malformadas ou truncadas são rejeitadas antes que o subconjunto consiga chegar a um stream PDF, que é a parte do desenho que mais importa: uma entrada de cache corrompida que chegasse a um documento produziria um PDF com um programa de tipo de letra partido, e essa falha apareceria longe da sua causa — num visualizador, na máquina de um cliente, semanas depois

As escritas são atómicas, pelo que um leitor nunca observa uma entrada meio escrita, e um bloqueio a meio da escrita deixa a cache consistente em vez de envenenada. As entradas de subconjunto compacto retêm os dados de remapeamento CID que os dicionários de tipo de letra do PDF/A exigem, pelo que um subconjunto em cache continua a ser um subconjunto conforme — a saída de arquivo não tem de contornar a cache para se manter válida

// Reponha o nível de disco após uma atualização de tipo de letra ou uma alteração de esquema
Pdf.ClearFontSubsetCache;

// Ou mova-o para um local gravável e deixe o orçamento aplicar-se imediatamente
Pdf.SetFontSubsetCacheFolder('D:\cache\fonts');

Onde colocar a pasta numa implementação real

Três propriedades decidem isto: a pasta tem de ser gravável pela conta com que o serviço corre, deve sentar-se em armazenamento local em vez de numa partilha de rede, e não deve estar dentro de um diretório que um passo de implantação apague. Uma cache numa partilha transforma cada falha numa ida e volta e cada acerto em duas; uma cache sob uma pasta de aplicação que o instalador recria é uma cache que arranca a frio depois de cada atualização

Para serviços com múltiplas instâncias, dê a cada instância a sua própria pasta a menos que tenha confirmado que o armazenamento lida com substituição atómica concorrente da forma que espera. O custo de uma entrada duplicada é uma passagem de subsetting extra; o custo de depurar uma corrida de cache partilhada é uma tarde

Quando procurar outra coisa

A cache reduz trabalho repetido. Não reduz o trabalho do primeiro documento, e não ajuda uma carga de trabalho cujos conjuntos de glifos nunca se repetem. Se a sua saída é dominada por um tipo CJK enorme usado em texto imprevisível, a alavanca mais eficaz é o próprio fecho de subsetting — que glifos são puxados, e porquê — coberta nas notas sobre fecho de subconjunto de tipos de letra e modelação de glifos. Se o seu lote é lento por razões que afinal não são tipos de letra de todo, o guia de saída de relatórios com tipos de letra e imagens mostra para onde vai normalmente o outro tempo, e o caso de estudo sobre o bug de ordenação de subconjunto de tipos de letra no EndDoc é um lembrete de que correção de subsetting e velocidade de subsetting são problemas separados

O HotPDF é um componente PDF VCL nativo para Delphi e C++Builder, e a cache de subconjuntos faz parte da biblioteca em vez de ser um serviço extra, pelo que um servidor de relatórios obtém-na definindo um único caminho de pasta — veja a página do componente HotPDF para a lista completa de funcionalidades de tipos de letra e desempenho