HotPDF expone el perfil de memoria de un PDF cargado como un grafo que usted puede consultar: BuildLoadedObjectDependencyGraph devuelve un nodo por cada 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 archivo usa 800 MB" en "el objeto 4173, un XObject de imagen, retiene exclusivamente 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 qué tan grande es un objeto. El tamaño retenido le dice cuánta memoria realmente se liberaría 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 sobre el que se pueda actuar?
Porque en un PDF casi nada es propiedad exclusiva de una sola cosa. Una única 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 diez content streams distintos comparten. Un Form XObject 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 la divide entre 400, concluye que nada es costoso y que la memoria vino de otro lado
El análisis de dominadores resuelve la ambigüedad de la única forma que sobrevive al contacto con documentos reales. Un objeto X se atribuye al objeto más cercano que lo retiene exclusivamente, lo que significa que cada 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 exactamente una página se atribuye a esa página. El resultado es que EstimatedRetainedBytes suma correctamente en lugar de contar dos veces, y los objetos en la parte superior de la lista son objetos cuya eliminación realmente 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 cual se encontró la referencia, y si Resolved
Ese campo Path es el que la gente subutiliza. 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 stream de apariencia de anotación, o un grupo de contenido opcional que nadie renderiza jamás
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 predeterminados de 250,000 objetos y 2,000,000 aristas. Cuando un documento supera cualquiera de los dos, se activa LimitExceeded y el grafo devuelto es un prefijo truncado en lugar de una mentira: resultados parciales con un indicador, no totales silenciosamente incorrectos. Suba los límites de forma deliberada para una ejecución forense, y recuerde que el análisis recorre todo el grafo de objetos, 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 en False es memoria que el documento lleva consigo pero que el visor nunca mostrará. En la práctica proviene de tres lugares: actualizaciones incrementales que reemplazaron una versión anterior de un objeto y dejaron el original atrás, un productor que escribió objetos que después no logró enlazar, o un archivo dañado cuya tabla de referencias cruzadas fue reconstruida y recogió 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 object streams. El segundo y el tercero valen la pena investigarse. Cuando una fracción grande de bytes retenidos está en nodos inalcanzables, usted ha encontrado un argumento concreto para reescribir el archivo en lugar de anexarle datos, y tiene el conteo de bytes para justificar el tiempo de procesamiento adicional ante quien pregunte
Dos contadores del asignador que vale la pena revisar primero
Antes de concluir que un documento es inherentemente grande, verifique si el propio analizador es el costo. HotPDF expone dos contadores que describen cómo se comportó la carga, no lo que contiene el documento
GetLastParserArenaStatistics reporta la arena de bloques retenidos usada para 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 la arena en lugar de a través de un objeto de nombre temporal, que es la asignación que solía dominar el análisis de archivos con muchos diccionarios
GetDocumentStringInternStatistics reporta el pool de interning con alcance de documento que deduplica nombres PDF repetidos, operadores de content stream y 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 adicionales 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 PDF reales, y el pool está ganándose su lugar. Un BypassCount que se acerca a RequestCount significa algo inusual: o un documento genuinamente enorme, o uno que genera nombres únicos a propósito, lo cual es una señal leve que vale la pena registrar en una ruta de ingesta no confiable
Un orden de triaje que funciona
Comience con CatalogRetainedBytes de la información del grafo. Si ese número está cerca del 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 propios cachés, en los bitmaps renderizados, o en el analizador, y los contadores de la arena le dirán cuál
Después tome los diez nodos principales por EstimatedRetainedBytes y observe su ObjectType. Los XObjects de imagen en la cima significan que el archivo está cargado de escaneos, y el downsampling es la solución. Los descriptores de fuente en la cima significan que se incrustaron caras completas donde bastarían subconjuntos. Los content streams en la cima suelen significar gráficos vectoriales generados, a menudo exportaciones de mapas o CAD. Solo después de eso vale la pena mirar los objetos inalcanzables y el comportamiento del asignador. Trabajando en ese orden, usted por lo general encontrará la respuesta en los primeros dos pasos, y para documentos muy grandes el enfoque de streaming descrito en el flujo de trabajo de la API de archivo directo suele ser la corrección estructural, más que cualquier optimización por objeto
Todos estos diagnósticos son simples llamadas Pascal que devuelven registros simples, así que se insertan directamente en un pipeline existente de registro o telemetría. 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 del componente HotPDF Delphi PDF