HotPDF exponerar minnesprofilen för en inläst PDF som en graf du kan fråga: BuildLoadedObjectDependencyGraph returnerar en nod per indirekt objekt med en uppskattad ytlig storlek, en dominatorbaserad behållen storlek, och en flagga för om objektet fortfarande är nåbart från dokumentets Catalog. Det förvandlar ”den här filen använder 800 MB” till ”objekt 4173, ett bild-XObject, behåller exklusivt 612 MB”, vilket är ett faktum du kan agera på
Skillnaden mellan de två meningarna är hela poängen. Ytlig storlek talar om för dig hur stort ett objekt är. Behållen storlek talar om hur mycket minne som faktiskt skulle frigöras om det objektet försvann, vilket är siffran som avgör om en åtgärd hjälper
Varför är total minnesanvändning inte ett handlingsbart faktum?
Därför att nästan ingenting i en PDF ägs av exakt en sak. Ett enda inbäddat CID-teckensnitt refereras från resursordboken för varje sida som använder det. En ICC-profilström ligger bakom ett färgrum som tio olika innehållsströmmar delar. Ett Form-XObject som används som en stämpel förekommer på alla 400 sidor. Om du naivt summerar objektstorlekar per sida räknar du det teckensnittet 400 gånger och drar slutsatsen att varje sida är enorm; om du delar med 400 drar du slutsatsen att ingenting är dyrt och att minnet kom någon annanstans ifrån
Dominatoranalys löser upp tvetydigheten på det enda sätt som överlever kontakt med verkliga dokument. Ett objekt X attribueras till det närmaste objekt som exklusivt behåller det, vilket betyder att varje referensväg från Catalog till X passerar genom den dominatorn. Ett teckensnitt som delas av alla sidor attribueras inte till någon sida; det attribueras till den närmaste nod som alla de vägarna passerar genom, vilket vanligtvis är Catalog själv. Ett teckensnitt som används av exakt en sida attribueras till den sidan. Resultatet är att EstimatedRetainedBytes summerar korrekt i stället för att dubbelräkna, och objekten högst upp på listan är objekt vars borttagning verkligen skulle frigöra minne
Vad grafen faktiskt innehåller
Varje THPDFObjectDependencyNode bär objektidentiteten som ObjectNumber och GenerationNumber, plus ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, identiteten för dess omedelbara dominator, och ReachableFromCatalog. Varje THPDFObjectDependencyEdge registrerar källa, mål, ordboks-Path:en referensen hittades under, och om den Resolved
Det Path-fältet är det folk underutnyttjar. Det är skillnaden mellan att veta att objekt 91 pekar på objekt 4173 och att veta att det gör det genom /Resources/XObject/Im3, vilket omedelbart talar om för dig om du tittar på sidinnehåll, en anteckningsutseendeström, eller en valfri-innehåll-grupp ingen någonsin renderar
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;
Båda gränserna är explicita parametrar med standardvärden på 250 000 objekt och 2 000 000 kanter. När ett dokument överskrider någon av dem sätts LimitExceeded och den returnerade grafen är ett trunkerat prefix snarare än en lögn: partiella resultat med en flagga, inte tyst felaktiga totalsummor. Höj gränserna medvetet för en forensisk körning, och kom ihåg att analysen vandrar genom hela objektgrafen, så den hör hemma i en diagnostikväg, inte i din renderingsloop
Vad talar ett onåbart objekt om?
Ett objekt med ReachableFromCatalog satt till False är minne dokumentet bär med sig men som visaren aldrig kommer att visa. I praktiken kommer det från tre platser: inkrementella uppdateringar som ersatte en tidigare version av ett objekt och lämnade originalet kvar, en producent som skrev objekt den sedan inte lyckades länka, eller en skadad fil vars korsreferenstabell byggdes om och plockade upp definitioner som ingenting refererar till
Det första fallet är normalt och förväntat, och det är precis vad inkrementella uppdateringar och objektströmmar är utformade för att göra. Det andra och tredje är värda att undersöka. När en stor andel behållna byte sitter i onåbara noder har du hittat ett konkret argument för att skriva om filen i stället för att lägga till i den, och du har bytantalet för att motivera den extra bearbetningstiden för den som frågar
Två allokeringsräknare värda att läsa först
Innan du drar slutsatsen att ett dokument i sig är stort, kontrollera om det är tolkaren själv som kostar. HotPDF exponerar två räknare som beskriver hur inläsningen betedde sig snarare än vad dokumentet innehåller
GetLastParserArenaStatistics rapporterar den behållna blockarenan som används för kortlivade tolkningstoken och buffertar: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount och TemporaryObjectElisionCount. Det sista fältet räknar ordboksnycklar som tolkades direkt in i arenan i stället för via ett temporärt namnobjekt, vilket var allokeringen som brukade dominera tolkningen av ordboksintensiva filer
GetDocumentStringInternStatistics rapporterar den dokumentbundna internpoolen som avdubblerar upprepade PDF-namn, innehållsströmsoperatorer och oföränderliga strängar upp till 64 byte. Den ger dig RequestCount, HitCount, MissCount, BypassCount, RetainedBytes och ReusedBytes, tillsammans med taken som gäller. Poolen är medvetet begränsad till 65 536 poster och 4 MiB, så ett fientligt dokument som genererar en miljon unika namn kan inte förvandla en minnesoptimering till en minnesförstärkare; när taket nås förbigår ytterligare strängar poolen och BypassCount stiger
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;
Ett högt ReusedBytes-värde med ett lågt BypassCount betyder att dokumentet har det upprepande ordförråd de flesta verkliga PDF:er har och att poolen gör sig förtjänt av sin plats. Ett BypassCount som närmar sig RequestCount betyder något ovanligt: antingen ett genuint enormt dokument, eller ett som genererar unika namn med avsikt, vilket är en mild signal värd att logga på en opålitlig intagsväg
En prioriteringsordning som fungerar
Börja med CatalogRetainedBytes från grafinformationen. Om det talet ligger nära din processtillväxt finns minnet i dokumentet och grafen visar dig var. Om det ligger långt under finns minnet i dina egna cachar, i renderade bitmappar, eller i tolkaren, och arenaräknarna talar om vilket
Ta sedan de tio översta noderna efter EstimatedRetainedBytes och titta på deras ObjectType. Bild-XObjects högst upp betyder att filen är skanningstung och att nedsampling är åtgärden. Teckensnittsbeskrivare högst upp betyder att fullständiga typsnitt bäddades in där subset hade räckt. Innehållsströmmar högst upp betyder vanligtvis genererad vektorgrafik, ofta kartor eller CAD-exporter. Först efter det lönar det sig att titta på onåbara objekt och allokeringsbeteende. Om du arbetar i den ordningen hittar du oftast svaret i de två första stegen, och för mycket stora dokument är det strömmande tillvägagångssättet som beskrivs i direktfil-API-arbetsflödet ofta den strukturella lösningen snarare än någon per-objekt-optimering
Alla dessa diagnostiker är enkla Pascal-anrop som returnerar enkla poster, så de faller rakt in i en befintlig loggnings- eller telemetriväg. HotPDF är en nativ VCL PDF-komponent för Delphi och C++Builder med fullständig källkod; API-referensen och en testversion finns på sidan för HotPDF Delphi PDF-komponent