HotPDF expone el perfil de memoria de un PDF cargado como un grafo que se puede consultar: BuildLoadedObjectDependencyGraph devuelve un nodo por objeto indirecto con un tamaño superficial estimado, un tamaño retenido basado en dominadores y un indicador de si el objeto sigue siendo alcanzable desde el Catalog del documento. Eso convierte "este fichero usa 800 MB" en "el objeto 4173, un XObject de imagen, retiene en exclusiva 612 MB", que es un hecho sobre el que se puede actuar
La distinción entre esas dos frases es todo el punto. El tamaño superficial le dice cuán grande es un objeto. El tamaño retenido le dice cuánta memoria se liberaría realmente si ese objeto desapareciera, que es el número que decide si una corrección ayuda
¿Por qué el uso total de memoria no es un hecho accionable?
Porque en un PDF casi nada pertenece exactamente a una sola cosa. Una fuente CID incrustada se referencia desde el diccionario de recursos de cada página que la usa. Un perfil ICC respalda un espacio de color que comparten diez flujos de contenido distintos. Un XObject de formulario usado como sello aparece en las 400 páginas. Si suma ingenuamente los tamaños de objeto por página, cuenta esa fuente 400 veces y concluye que cada página es enorme; si lo divide entre 400, concluye que nada es costoso y que la memoria vino de otro sitio
El análisis de dominadores resuelve la ambigüedad de la única manera que sobrevive al contacto con documentos reales. Un objeto X se atribuye al objeto más cercano que lo retiene en exclusiva, lo que significa que toda ruta de referencia desde el Catalog hasta X pasa por ese dominador. Una fuente compartida por todas las páginas no se atribuye a ninguna página; se atribuye al nodo más cercano por el que pasan todas esas rutas, que suele ser el propio Catalog. Una fuente usada por una única página se atribuye a esa página. El resultado es que EstimatedRetainedBytes suma correctamente en lugar de contar por duplicado, y los objetos en lo alto de la lista son objetos cuya eliminación de verdad liberaría memoria
Qué contiene realmente el grafo
Cada THPDFObjectDependencyNode lleva la identidad del objeto como ObjectNumber y GenerationNumber, además de ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, la identidad de su dominador inmediato y ReachableFromCatalog. Cada THPDFObjectDependencyEdge registra origen, destino, el Path del diccionario bajo el que se encontró la referencia y si se Resolved
Ese campo Path es el que la gente infrautiliza. Es la diferencia entre saber que el objeto 91 apunta al objeto 4173 y saber que lo hace a través de /Resources/XObject/Im3, lo cual le dice de inmediato si está mirando contenido de página, un flujo de apariencia de anotación o un grupo de contenido opcional que nadie renderiza nunca
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 límites son parámetros explícitos con valores por defecto de 250.000 objetos y 2.000.000 de aristas. Cuando un documento supera cualquiera de los dos, LimitExceeded queda fijado y el grafo devuelto es un prefijo truncado en lugar de una mentira: resultados parciales con un indicador, no totales silenciosamente erróneos. Suba los límites de forma deliberada para una ejecución forense, y recuerde que el análisis recorre el grafo de objetos completo, así que pertenece a una ruta de diagnóstico, no a su bucle de renderizado
¿Qué le dice un objeto inalcanzable?
Un objeto con ReachableFromCatalog a False es memoria que el documento carga pero que el visor nunca mostrará. En la práctica viene de tres sitios: actualizaciones incrementales que sustituyeron una versión anterior de un objeto y dejaron atrás el original, un productor que escribió objetos y luego no los enlazó, o un fichero dañado cuya tabla de referencias cruzadas se reconstruyó y arrastró definiciones que nada referencia
El primer caso es normal y esperado, y es precisamente para lo que están diseñadas las actualizaciones incrementales y los flujos de objetos. El segundo y el tercero merecen investigarse. Cuando una fracción grande de los bytes retenidos está en nodos inalcanzables, ha encontrado un argumento concreto para reescribir el fichero en lugar de seguir añadiéndole datos, y tiene el recuento de bytes para justificar el tiempo de proceso extra ante quien pregunte
Dos contadores del asignador que merece la pena leer primero
Antes de concluir que un documento es inherentemente grande, compruebe si el coste está en el propio analizador. HotPDF expone dos contadores que describen cómo se comportó la carga, no lo que contiene el documento
GetLastParserArenaStatistics informa del área de bloques retenidos usada para los tokens y búferes de vida corta del analizador: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount y TemporaryObjectElisionCount. Ese último campo cuenta las claves de diccionario analizadas directamente en el área en lugar de a través de un objeto de nombre temporal, que era la asignación que solía dominar el análisis de ficheros con muchos diccionarios
GetDocumentStringInternStatistics informa del pool de internamiento a nivel de documento que deduplica los nombres PDF repetidos, los operadores de flujo de contenido y las cadenas inmutables de hasta 64 bytes. Le da RequestCount, HitCount, MissCount, BypassCount, RetainedBytes y ReusedBytes, junto con los topes vigentes. El pool está deliberadamente acotado a 65.536 entradas y 4 MiB, así que un documento hostil que genere un millón de nombres únicos no puede convertir una optimización de memoria en un amplificador de memoria; una vez alcanzado el tope, las cadenas siguientes evitan el pool y BypassCount sube
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;
Un valor alto de ReusedBytes con un BypassCount bajo significa que el documento tiene el vocabulario repetitivo que tienen la mayoría de los PDFs reales y que el pool se está ganando su sitio. Un BypassCount que se acerca a RequestCount significa algo inusual: o bien un documento genuinamente enorme, o bien uno que genera nombres únicos a propósito, lo cual es una señal leve que merece registrarse en una ruta de ingesta no confiable
Un orden de triaje que funciona
Empiece por CatalogRetainedBytes de la información del grafo. Si ese número se acerca al crecimiento de su proceso, la memoria está en el documento y el grafo le mostrará dónde. Si está muy por debajo, la memoria está en sus propias cachés, en mapas de bits renderizados o en el analizador, y los contadores del área le dirán cuál
Después tome los diez nodos principales por EstimatedRetainedBytes y mire su ObjectType. Los XObjects de imagen en lo alto significan que el fichero está cargado de escaneos y que el submuestreo es la solución. Los descriptores de fuente en lo alto significan que se incrustaron tipografías completas donde bastaría un subconjunto. Los flujos de contenido en lo alto suelen significar gráficos vectoriales generados, a menudo mapas o exportaciones de CAD. Solo después de eso merece la pena mirar los objetos inalcanzables y el comportamiento del asignador. Trabajando en ese orden, normalmente encontrará la respuesta en los dos primeros pasos, y para documentos muy grandes el enfoque en flujo descrito en la API directa de ficheros suele ser la solución estructural en lugar de cualquier optimización por objeto
Todos estos diagnósticos son simples llamadas Pascal que devuelven registros simples, así que encajan directamente en una ruta de registro o telemetría ya existente. HotPDF es un componente VCL nativo para Delphi y C++Builder con código fuente completo; la referencia de la API y una compilación de prueba están en la página de HotPDF para Delphi