O HotPDF expõe o perfil de memória de um PDF carregado como um grafo que você pode consultar: BuildLoadedObjectDependencyGraph retorna um nó por objeto indireto, com um tamanho raso estimado, um tamanho retido baseado em dominador, e um flag indicando se o objeto ainda é alcançável a partir do Catalog do documento. Isso transforma "esse arquivo usa 800 MB" em "o objeto 4173, um XObject de imagem, retém exclusivamente 612 MB", que é um fato sobre o qual você pode agir
A distinção entre essas duas frases é o ponto central. O tamanho raso diz quão grande um objeto é. O tamanho retido diz quanta memória seria de fato liberada se aquele objeto deixasse de existir, que é o número que decide se uma correção realmente ajuda
Por que o uso total de memória não é um fato acionável?
Porque em um PDF quase nada pertence exatamente a uma única coisa. Uma única fonte CID incorporada é referenciada a partir do dicionário de recursos de toda página que a usa. Um stream de perfil ICC dá suporte a um espaço de cor que dez content streams diferentes compartilham. Um Form XObject usado como carimbo aparece nas 400 páginas. Se você somar ingenuamente os tamanhos de objeto por página, conta aquela fonte 400 vezes e conclui que toda página é enorme; se você dividir por 400, conclui que nada é caro e que a memória veio de outro lugar
A análise de dominadores resolve a ambiguidade da única forma que sobrevive ao contato com documentos reais. Um objeto X é atribuído ao objeto mais próximo que o retém exclusivamente, ou seja, todo caminho de referência do Catalog até X passa por esse dominador. Uma fonte compartilhada por todas as páginas não é atribuída a nenhuma página específica; ela é atribuída ao nó mais próximo pelo qual todos esses caminhos passam, que geralmente é o próprio Catalog. Uma fonte usada por exatamente uma página é atribuída àquela página. O resultado é que EstimatedRetainedBytes soma corretamente, em vez de contar em dobro, e os objetos no topo da lista são objetos cuja remoção realmente liberaria memória
O que o grafo realmente contém
Cada THPDFObjectDependencyNode carrega a identidade do objeto como ObjectNumber e GenerationNumber, além de ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, a identidade do seu dominador imediato, e ReachableFromCatalog. Cada THPDFObjectDependencyEdge registra origem, destino, o Path de dicionário sob o qual a referência foi encontrada, e se ela foi Resolved
Esse campo Path é o que as pessoas subutilizam. É a diferença entre saber que o objeto 91 aponta para o objeto 4173 e saber que ele faz isso por meio de /Resources/XObject/Im3, o que diz imediatamente se você está olhando para conteúdo de página, um appearance stream de anotação, ou um grupo de conteúdo opcional que ninguém nunca renderiza
var
Pdf: THotPDF;
Nodes: THPDFObjectDependencyNodeArray;
Edges: THPDFObjectDependencyEdgeArray;
Info: THPDFObjectDependencyGraphInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('report-800mb.pdf') <> 1 then Exit;
if not Pdf.BuildLoadedObjectDependencyGraph(Nodes, Edges, Info) then Exit;
if Info.LimitExceeded then
Log('Graph truncated: raise MaxObjects / MaxEdges');
Log(Format('%d objects, %d edges, %d reachable, catalog retains %d bytes',
[Info.ObjectCount, Info.EdgeCount, Info.ReachableObjectCount,
Info.CatalogRetainedBytes]));
SortByRetainedDescending(Nodes);
for I := 0 to Min(9, High(Nodes)) do
Log(Format('%d %d obj: shallow %d, retained %d, dominator %d',
[Nodes[I].ObjectNumber, Nodes[I].GenerationNumber,
Nodes[I].EstimatedShallowBytes, Nodes[I].EstimatedRetainedBytes,
Nodes[I].ImmediateDominatorObjectNumber]));
finally
Pdf.Free;
end;
end;
Os dois limites são parâmetros explícitos, com padrões de 250.000 objetos e 2.000.000 de arestas. Quando um documento excede qualquer um deles, LimitExceeded é definido e o grafo retornado é um prefixo truncado, não uma mentira: resultados parciais com um flag, e não totais silenciosamente errados. Aumente os limites deliberadamente para uma execução forense, e lembre-se de que a análise percorre o grafo de objetos inteiro, então ela pertence a um caminho de diagnóstico, não ao seu loop de renderização
O que um objeto inalcançável diz a você?
Um objeto com ReachableFromCatalog definido como False é memória que o documento carrega, mas que o visualizador nunca vai mostrar. Na prática, isso vem de três lugares: atualizações incrementais que substituíram uma versão anterior de um objeto e deixaram o original para trás, um produtor que gravou objetos que depois deixou de vincular, ou um arquivo danificado cuja tabela de referência cruzada foi reconstruída e acabou pegando definições que nada referencia
O primeiro caso é normal e esperado, e é exatamente o que atualizações incrementais e object streams são projetados para fazer. O segundo e o terceiro valem a pena investigar. Quando uma grande fração dos bytes retidos está em nós inalcançáveis, você encontrou um argumento concreto para reescrever o arquivo em vez de anexar a ele, e você tem a contagem de bytes para justificar o tempo extra de processamento para quem perguntar
Dois contadores do alocador que vale a pena ler primeiro
Antes de concluir que um documento é inerentemente grande, verifique se o custo está no próprio parser. O HotPDF expõe dois contadores que descrevem como o carregamento se comportou, e não o que o documento contém
GetLastParserArenaStatistics relata a arena de blocos retidos usada para tokens e buffers de curta duração do parser: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount e TemporaryObjectElisionCount. Esse último campo conta chaves de dicionário analisadas diretamente na arena, em vez de passar por um objeto de nome temporário, que era a alocação que costumava dominar a análise de arquivos ricos em dicionários
GetDocumentStringInternStatistics relata o pool de intern com escopo de documento, que deduplica nomes de PDF repetidos, operadores de content stream e strings imutáveis de até 64 bytes. Ele fornece RequestCount, HitCount, MissCount, BypassCount, RetainedBytes e ReusedBytes, junto com os limites em vigor. O pool é deliberadamente limitado a 65.536 entradas e 4 MiB, de modo que um documento hostil que gere um milhão de nomes únicos não consegue transformar uma otimização de memória em um amplificador de memória; assim que o limite é atingido, strings adicionais contornam o pool e BypassCount sobe
var
Arena: THPDFParserArenaStatistics;
Intern: THPDFDocumentStringInternStatistics;
begin
if Pdf.GetLastParserArenaStatistics(Arena) then
Log(Format('arena: peak %d, reused %d of %d allocations, %d tokens',
[Arena.PeakUsedBytes, Arena.ReusedAllocationCount,
Arena.AllocationCount, Arena.TokenCount]));
if Pdf.GetDocumentStringInternStatistics(Intern) then
Log(Format('intern: %d/%d hits, %d bypassed, %d bytes reused',
[Intern.HitCount, Intern.RequestCount, Intern.BypassCount,
Intern.ReusedBytes]));
end;
Um valor alto de ReusedBytes com um BypassCount baixo significa que o documento tem o vocabulário repetitivo que a maioria dos PDFs reais tem, e o pool está se justificando. Um BypassCount se aproximando de RequestCount significa algo incomum: ou um documento genuinamente enorme, ou um que gera nomes únicos de propósito, o que é um sinal leve que vale a pena registrar em log em um pipeline de entrada não confiável
Uma ordem de triagem que funciona
Comece com CatalogRetainedBytes das informações do grafo. Se esse número estiver próximo do crescimento do seu processo, a memória está no documento e o grafo vai mostrar onde. Se estiver bem abaixo, a memória está nos seus próprios caches, em bitmaps renderizados, ou no parser, e os contadores da arena vão dizer qual
Depois, pegue os dez principais nós por EstimatedRetainedBytes e observe seu ObjectType. Image XObjects no topo significam que o arquivo é pesado em digitalizações e downsampling é a correção. Descritores de fonte no topo significam que faces completas foram incorporadas onde subsets bastariam. Content streams no topo geralmente significam gráficos vetoriais gerados, com frequência mapas ou exportações de CAD. Só depois disso vale a pena olhar para objetos inalcançáveis e o comportamento do alocador. Trabalhando nessa ordem, você geralmente vai encontrar a resposta nos dois primeiros passos, e para documentos muito grandes, a abordagem de streaming descrita em o workflow da API de arquivo direto é frequentemente a correção estrutural, em vez de qualquer otimização por objeto
Todos esses diagnósticos são chamadas Pascal simples retornando records simples, então se encaixam diretamente em um caminho de logging ou telemetria já existente. O HotPDF é um componente VCL nativo de PDF para Delphi e C++Builder com código-fonte completo; a referência de API e uma versão de teste estão na página do componente HotPDF Delphi para PDF