O HotPDF expõe o perfil de memória de um PDF carregado como um grafo que pode consultar: BuildLoadedObjectDependencyGraph devolve um nó por objeto indireto com um tamanho superficial estimado, um tamanho retido baseado em dominadores, e uma flag que indica se o objeto ainda é alcançável a partir do Catalog do documento. Isso transforma "este ficheiro utiliza 800 MB" em "o objeto 4173, um XObject de imagem, retém exclusivamente 612 MB", que é um facto sobre o qual pode agir
A distinção entre estas duas frases é todo o objetivo. O tamanho superficial diz-lhe quão grande é um objeto. O tamanho retido diz-lhe quanta memória seria efetivamente libertada se esse objeto desaparecesse, que é o número que decide se uma correção resolve algo
Porque não é o consumo total de memória um facto acionável?
Porque, num PDF, quase nada pertence exatamente a uma única coisa. Um único tipo de letra CID incorporado é referenciado a partir do dicionário de recursos de cada página que o utiliza. Um perfil ICC serve de base a um espaço de cor partilhado por dez fluxos de conteúdo diferentes. Um XObject de formulário usado como carimbo aparece em 400 páginas. Se somar ingenuamente os tamanhos dos objetos por página, conta esse tipo de letra 400 vezes e conclui que cada página é enorme; se o dividir por 400, conclui que nada é dispendioso e a memória veio de outro lado
A análise de dominadores resolve a ambiguidade da única forma que sobrevive ao contacto com documentos reais. Um objeto X é atribuído ao objeto mais próximo que o retém exclusivamente, o que significa que todos os caminhos de referência do Catalog até X passam por esse dominador. Um tipo de letra partilhado por todas as páginas não é atribuído a nenhuma página; é atribuído ao nó mais próximo por onde todos esses caminhos passam, que costuma ser o próprio Catalog. Um tipo de letra usado por exatamente uma página é atribuído a essa página. O resultado é que EstimatedRetainedBytes soma corretamente em vez de contar em duplicado, e os objetos no topo da lista são objetos cuja remoção liberta efetivamente memória
O que o grafo realmente contém
Cada THPDFObjectDependencyNode transporta a identidade do objeto como ObjectNumber e GenerationNumber, mais ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, a identidade do seu dominador imediato, e ReachableFromCatalog. Cada THPDFObjectDependencyEdge regista a origem, o destino, o Path do dicionário sob o qual a referência foi encontrada, e se Resolved
Esse campo Path é o mais subaproveitado. É a diferença entre saber que o objeto 91 aponta para o objeto 4173 e saber que o faz através de /Resources/XObject/Im3, o que lhe diz imediatamente se está a observar conteúdo de página, um fluxo de aparência de anotação, ou um grupo de conteúdo opcional que ninguém jamais 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;
Ambos os limites são parâmetros explícitos com predefinições de 250 000 objetos e 2 000 000 arestas. Quando um documento excede qualquer um deles, LimitExceeded é definido e o grafo devolvido é um prefixo truncado e não uma mentira: resultados parciais com uma flag, não totais silenciosamente errados. Aumente os limites deliberadamente para uma análise forense, e lembre-se de que a análise percorre todo o grafo de objetos, pelo que pertence a um caminho de diagnóstico e não ao seu ciclo de renderização
O que lhe diz um objeto inalcançável?
Um objeto com ReachableFromCatalog definido como False é memória que o documento transporta mas que o visualizador nunca mostrará. Na prática, provém de três origens: atualizações incrementais que substituíram uma versão anterior de um objeto e deixaram o original para trás, um produtor que escreveu objetos que depois não conseguiu ligar, ou um ficheiro danificado cuja tabela de referência cruzada foi reconstruída e recolheu definições que nada referencia
O primeiro caso é normal e esperado, e é exatamente para isso que as atualizações incrementais e fluxos de objetos foram concebidas. O segundo e o terceiro caso merecem investigação. Quando uma grande fração dos bytes retidos se encontra em nós inalcançáveis, encontrou um argumento concreto para reescrever o ficheiro em vez de lhe acrescentar dados, e tem a contagem de bytes para justificar o tempo de processamento extra a quem o questionar
Dois contadores do alocador que valem a pena ler primeiro
Antes de concluir que um documento é inerentemente grande, verifique se o próprio analisador é o custo. O HotPDF expõe dois contadores que descrevem como decorreu o carregamento, e não o que o documento contém
GetLastParserArenaStatistics reporta a arena de blocos retidos usada para tokens e buffers de curta duração do analisador: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount e TemporaryObjectElisionCount. Este último campo conta as chaves de dicionário analisadas diretamente para a arena em vez de através de um objeto de nome temporário, que era a alocação que costumava dominar a análise de ficheiros com muitos dicionários
GetDocumentStringInternStatistics reporta a reserva de strings ao nível do documento que deduplica nomes PDF repetidos, operadores de fluxo de conteúdo e strings imutáveis até 64 bytes. Fornece-lhe RequestCount, HitCount, MissCount, BypassCount, RetainedBytes e ReusedBytes, juntamente com os limites em vigor. A reserva está deliberadamente limitada a 65 536 entradas e 4 MiB, pelo que um documento hostil que gere um milhão de nomes únicos não consegue transformar uma otimização de memória numa amplificação de memória; assim que o limite é atingido, as strings seguintes contornam a reserva 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 elevado de ReusedBytes com um BypassCount baixo significa que o documento tem o vocabulário repetitivo típico da maioria dos PDFs reais e a reserva está a compensar o investimento. Um BypassCount próximo de RequestCount significa algo fora do comum: ou um documento genuinamente enorme, ou um que gera nomes únicos de propósito, o que é um sinal ligeiro que vale a pena registar numa via de admissão não fiável
Uma ordem de triagem que funciona
Comece por CatalogRetainedBytes na informação do grafo. Se esse número estiver próximo do crescimento do seu processo, a memória está no documento e o grafo mostrar-lhe-á onde. Se estiver muito abaixo, a memória está nas suas próprias caches, em bitmaps renderizados, ou no analisador, e os contadores da arena dir-lhe-ão qual
Depois, pegue nos dez nós principais por EstimatedRetainedBytes e observe o respetivo ObjectType. XObjects de imagem no topo significam que o ficheiro é pesado em digitalizações e a redução de resolução é a solução. Descritores de tipo de letra no topo significam que foram incorporados tipos de letra completos onde bastariam subconjuntos. Fluxos de conteúdo no topo costumam significar gráficos vetoriais gerados, muitas vezes mapas ou exportações CAD. Só depois disso é que compensa observar objetos inalcançáveis e o comportamento do alocador. Trabalhando por essa ordem, normalmente encontrará a resposta nos dois primeiros passos, e para documentos muito grandes a abordagem em fluxo descrita em o fluxo de trabalho da API de ficheiro direto é frequentemente a correção estrutural em vez de qualquer otimização por objeto
Todos estes diagnósticos são simples chamadas Pascal que devolvem simples registos, pelo que se integram diretamente num caminho de registo ou telemetria já existente. O HotPDF é um componente PDF VCL nativo para Delphi e C++Builder com código-fonte completo; a referência da API e uma versão de avaliação estão disponíveis na página do componente HotPDF para Delphi