HotPDF zpřístupňuje paměťový profil načteného PDF jako graf, na který se dá dotazovat: BuildLoadedObjectDependencyGraph vrátí jeden uzel na každý nepřímý objekt s odhadovanou vlastní (shallow) velikostí, velikostí drženou (retained) na základě dominátoru a příznakem, zda je objekt stále dosažitelný z Catalogu dokumentu. Tím se z věty „tento soubor zabírá 800 MB" stane věta „objekt 4173, obrázkový XObject, výhradně drží 612 MB", což je fakt, na jehož základě lze jednat
Rozdíl mezi těmito dvěma větami je celá podstata. Shallow size vám řekne, jak velký je jeden objekt. Retained size vám řekne, kolik paměti by se skutečně uvolnilo, kdyby daný objekt zmizel, a právě to je číslo, které rozhoduje o tom, jestli oprava pomůže
Proč není celková spotřeba paměti fakt, na kterém se dá stavět?
Protože v PDF téměř nic nepatří výhradně jedné jediné věci. Na jedno vložené písmo CID odkazuje slovník zdrojů každé stránky, která ho používá. Proud profilu ICC podkládá barevný prostor, který sdílí deset různých obsahových proudů. Form XObject použitý jako razítko se objevuje na všech 400 stránkách. Když naivně sečtete velikosti objektů po stránkách, započítáte to písmo 400krát a usoudíte, že každá stránka je obrovská; pokud to vydělíte 400, usoudíte, že nic není drahé a paměť pochází odjinud
Dominátorová analýza tuto nejednoznačnost řeší jediným způsobem, který přežije setkání se skutečnými dokumenty. Objekt X se přiřadí nejbližšímu objektu, který ho výhradně drží, což znamená, že každá cesta odkazů z Catalogu k X prochází přes tento dominátor. Písmo sdílené všemi stránkami se nepřiřadí žádné stránce; přiřadí se nejbližšímu uzlu, kterým procházejí všechny tyto cesty, což je obvykle samotný Catalog. Písmo použité přesně jednou stránkou se přiřadí té stránce. Výsledkem je, že EstimatedRetainedBytes se sčítá správně, bez dvojího počítání, a objekty na vrcholu seznamu jsou objekty, jejichž odstranění by skutečně uvolnilo paměť
Co graf ve skutečnosti obsahuje
Každý THPDFObjectDependencyNode nese identitu objektu jako ObjectNumber a GenerationNumber, plus ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, identitu svého bezprostředního dominátoru a ReachableFromCatalog. Každá THPDFObjectDependencyEdge zaznamenává zdroj, cíl, slovníkovou Path, pod kterou byl odkaz nalezen, a zda byla Resolved
Pole Path je právě to, které lidé nedoceňují. Je to rozdíl mezi vědomím, že objekt 91 ukazuje na objekt 4173, a vědomím, že to dělá přes /Resources/XObject/Im3, což vám okamžitě řekne, jestli se díváte na obsah stránky, proud vzhledu anotace, nebo skupinu volitelného obsahu, kterou nikdo nikdy nevykresluje
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;
Obě omezení jsou explicitní parametry s výchozími hodnotami 250 000 objektů a 2 000 000 hran. Když dokument některý z nich překročí, nastaví se LimitExceeded a vrácený graf je oříznutý prefix, ne lež: dílčí výsledky s příznakem, ne tiše špatné součty. Limity zvyšte záměrně pro forenzní běh a pamatujte, že analýza prochází celým grafem objektů, takže patří do diagnostické cesty, ne do vaší vykreslovací smyčky
Co vám prozradí nedosažitelný objekt?
Objekt s ReachableFromCatalog nastaveným na False je paměť, kterou dokument nese s sebou, ale prohlížečka ji nikdy nezobrazí. V praxi pochází ze tří míst: z přírůstkových aktualizací, které nahradily starší verzi objektu a ponechaly originál za sebou, z producenta, který zapsal objekty a pak je nepropojil, nebo z poškozeného souboru, jehož tabulka křížových odkazů byla přestavěna a převzala definice, na které nic neodkazuje
První případ je normální a očekávaný a je to přesně to, k čemu jsou navržené přírůstkové aktualizace a proudy objektů. Druhý a třetí případ stojí za prošetření. Když velká část drženého objemu sedí v nedosažitelných uzlech, máte konkrétní argument pro přepsání souboru namísto připojování k němu, a máte i počet bajtů, kterým to zdůvodníte komukoli, kdo se zeptá, proč to trvá déle
Dva čítače alokátoru, které stojí za přečtení jako první
Než usoudíte, že dokument je svou podstatou velký, ověřte, jestli náklad nepředstavuje samotný parser. HotPDF zpřístupňuje dva čítače, které popisují, jak proběhlo načítání, ne co dokument obsahuje
GetLastParserArenaStatistics hlásí arénu držených bloků použitou pro krátkodobé tokeny a buffery parseru: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount a TemporaryObjectElisionCount. Toto poslední pole počítá klíče slovníku parsované přímo do arény místo přes dočasný objekt jména, což byla alokace, která dřív dominovala parsování souborů bohatých na slovníky
GetDocumentStringInternStatistics hlásí interní pool platný pro daný dokument, který deduplikuje opakující se názvy PDF, operátory obsahových proudů a neměnné řetězce do 64 bajtů. Dává vám RequestCount, HitCount, MissCount, BypassCount, RetainedBytes a ReusedBytes, spolu s právě platnými stropy. Pool je záměrně omezený na 65 536 položek a 4 MiB, takže nepřátelský dokument, který vygeneruje milion unikátních názvů, nemůže proměnit optimalizaci paměti ve zesilovač spotřeby paměti; jakmile je strop dosažen, další řetězce pool obcházejí a BypassCount roste
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;
Vysoká hodnota ReusedBytes spolu s nízkým BypassCount znamená, že dokument má opakující se slovník, jaký mívá většina reálných PDF, a pool se vyplácí. BypassCount blížící se RequestCount znamená něco neobvyklého: buď opravdu obrovský dokument, nebo dokument záměrně generující unikátní názvy, což je mírný signál, který se vyplatí logovat na nedůvěryhodné vstupní cestě
Pořadí třídění, které funguje
Začněte s CatalogRetainedBytes z informací o grafu. Pokud je toto číslo blízké růstu vašeho procesu, paměť je v dokumentu a graf vám ukáže kde. Pokud je hluboko pod ním, paměť je ve vašich vlastních cache, ve vykreslených bitmapách nebo v parseru, a čítače arény vám řeknou, kde přesně
Pak vezměte prvních deset uzlů podle EstimatedRetainedBytes a podívejte se na jejich ObjectType. Obrázkové XObjecty na vrcholu znamenají, že soubor je nabitý skeny a oprava je zmenšení rozlišení. Popisovače písem na vrcholu znamenají, že se vložila celá písma tam, kde by stačily podmnožiny. Obsahové proudy na vrcholu obvykle znamenají generovanou vektorovou grafiku, často mapy nebo exporty z CAD. Teprve poté se vyplatí podívat se na nedosažitelné objekty a chování alokátoru. Pokud budete postupovat v tomto pořadí, odpověď obvykle najdete už v prvních dvou krocích, a u velmi rozsáhlých dokumentů je streamovací přístup popsaný v článku o workflow přímého souborového API často strukturální opravou spíš než jakoukoli optimalizací po jednotlivých objektech
Všechny tyto diagnostiky jsou obyčejná volání v Pascalu, která vrací obyčejné záznamy, takže zapadnou přímo do existující logovací nebo telemetrické cesty. HotPDF je nativní komponenta VCL pro PDF pro Delphi a C++Builder s kompletním zdrojovým kódem; referenční dokumentace API i zkušební verze jsou na stránce HotPDF Delphi PDF component