Odborný článok

Nájdite žrútov pamäte PDF: grafy závislostí HotPDF

HotPDF sprístupňuje pamäťový profil načítaného PDF ako graf, ktorý môžete dopytovať: BuildLoadedObjectDependencyGraph vráti jeden uzol na každý nepriamy objekt s odhadovanou plytkou veľkosťou, veľkosťou zadržania založenou na dominátoroch a príznakom, či je objekt stále dosiahnuteľný z Catalogu dokumentu. To mení „tento súbor používa 800 MB“ na „objekt 4173, obrázkový XObject, výhradne zadržiava 612 MB“ — čo je fakt, na základe ktorého môžete konať

Rozdiel medzi týmito dvoma vetami je celá podstata veci. Plytká veľkosť vám povie, aký veľký je jeden objekt. Zadržaná veľkosť vám povie, koľko pamäte by sa naozaj uvoľnilo, keby tento objekt zmizol — a práve toto číslo rozhoduje o tom, či oprava pomôže

Prečo celková spotreba pamäte nie je fakt, na základe ktorého sa dá konať?

Pretože v PDF takmer nič nepatrí presne jednej veci. Na jedno vložené CID písmo odkazuje slovník zdrojov každej stránky, ktorá ho používa. Tok profilu ICC podkladá farebný priestor, ktorý zdieľa desať rôznych obsahových tokov. Form XObject použitý ako pečiatka sa objaví na všetkých 400 stránkach. Ak naivne spočítate veľkosti objektov po stránkach, započítate to písmo 400-krát a usúdite, že každá stránka je obrovská; ak ho vydelíte 400, usúdite, že nič nie je nákladné a pamäť pochádza odniekiaľ inde

Analýza dominátorov rieši túto nejednoznačnosť jediným spôsobom, ktorý prežije stretnutie so skutočnými dokumentmi. Objekt X sa priradí najbližšiemu objektu, ktorý ho výhradne zadržiava — teda takému, cez ktorý prechádza každá referenčná cesta z Catalogu k X. Písmo zdieľané všetkými stránkami sa nepriradí žiadnej stránke; priradí sa najbližšiemu uzlu, cez ktorý prechádzajú všetky tie cesty, čo je zvyčajne samotný Catalog. Písmo použité presne jednou stránkou sa priradí tejto stránke. Výsledkom je, že EstimatedRetainedBytes sa sčítava správne namiesto dvojitého počítania, a objekty na vrchole zoznamu sú objekty, ktorých odstránenie by naozaj uvoľnilo pamäť

Čo graf naozaj obsahuje

Každý THPDFObjectDependencyNode nesie identitu objektu ako ObjectNumber a GenerationNumber, plus ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, identitu svojho bezprostredného dominátora a ReachableFromCatalog. Každá THPDFObjectDependencyEdge zaznamenáva zdroj, cieľ, slovníkovú Path, pod ktorou bola referencia nájdená, a či bola Resolved

Pole Path je práve to, ktoré ľudia nedostatočne využívajú. Je to rozdiel medzi tým, že viete, že objekt 91 ukazuje na objekt 4173, a tým, že viete, že to robí cez /Resources/XObject/Im3 — čo vám okamžite povie, či sa pozeráte na obsah stránky, tok vzhľadu anotácie, alebo skupinu voliteľného obsahu, ktorú nikto nikdy nevykreslí

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;

Obe hranice sú explicitné parametre s predvolenými hodnotami 250 000 objektov a 2 000 000 hrán. Keď dokument prekročí ktorúkoľvek z nich, nastaví sa LimitExceeded a vrátený graf je skrátená predpona, nie klamstvo: čiastočné výsledky s príznakom, nie ticho nesprávne súčty. Limity zvyšujte zámerne pre forenzný beh a pamätajte, že analýza prechádza celým grafom objektov, takže patrí do diagnostickej cesty, nie do vašej vykresľovacej slučky

Čo vám povie nedosiahnuteľný objekt?

Objekt, ktorého ReachableFromCatalog je False, je pamäť, ktorú dokument nesie, no čítačka ju nikdy nezobrazí. V praxi pochádza z troch zdrojov: inkrementálnych aktualizácií, ktoré nahradili skoršiu verziu objektu a pôvodnú ponechali za sebou, producenta, ktorý zapísal objekty a potom sa mu ich nepodarilo prepojiť, alebo poškodeného súboru, ktorého tabuľka krížových odkazov bola prestavaná a prevzala definície, na ktoré nič neodkazuje

Prvý prípad je normálny a očakávaný, a presne na to sú navrhnuté inkrementálne aktualizácie a object streamy. Druhý a tretí stoja za preskúmanie. Keď veľká časť zadržaných bajtov sedí v nedosiahnuteľných uzloch, našli ste konkrétny argument na prepísanie súboru namiesto pripájania k nemu, a máte aj počet bajtov, ktorým zdôvodníte dodatočný čas spracovania komukoľvek, kto sa spýta

Dva počítadlá alokátora, ktoré sa oplatí prečítať ako prvé

Skôr než usúdite, že dokument je zo svojej podstaty veľký, skontrolujte, či nákladom nie je samotný parser. HotPDF sprístupňuje dve počítadlá, ktoré opisujú, ako sa správalo načítanie, nie to, čo dokument obsahuje

GetLastParserArenaStatistics hlási zadržanú blokovú arénu použitú na krátko žijúce tokeny a buffery parsera: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount a TemporaryObjectElisionCount. Toto posledné pole počíta kľúče slovníkov naparsované priamo do arény namiesto cez dočasný objekt názvu — čo bola alokácia, ktorá kedysi dominovala pri parsovaní súborov bohatých na slovníky

GetDocumentStringInternStatistics hlási internovací pool v rozsahu dokumentu, ktorý deduplikuje opakujúce sa názvy PDF, operátory obsahových tokov a nemenné reťazce do 64 bajtov. Dáva vám RequestCount, HitCount, MissCount, BypassCount, RetainedBytes a ReusedBytes, spolu s platnými stropmi. Pool je zámerne ohraničený na 65 536 položiek a 4 MiB, takže nepriateľský dokument, ktorý vygeneruje milión unikátnych názvov, nemôže zmeniť pamäťovú optimalizáciu na pamäťový zosilňovač; keď sa dosiahne strop, ďalšie reťazce pool obídu a BypassCount stúpa

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 s nízkym BypassCount znamená, že dokument má opakujúcu sa slovnú zásobu, akú má väčšina reálnych PDF, a pool sa oplatí. BypassCount blížiaci sa k RequestCount znamená niečo nezvyčajné: buď naozaj obrovský dokument, alebo taký, ktorý zámerne generuje unikátne názvy — čo je mierny signál, ktorý sa oplatí zaznamenávať na nedôveryhodnej vstupnej ceste

Poradie triedenia, ktoré funguje

Začnite s CatalogRetainedBytes z informácií o grafe. Ak je toto číslo blízko rastu vášho procesu, pamäť je v dokumente a graf vám ukáže kde. Ak je výrazne nižšie, pamäť je vo vašich vlastných cache, vo vykreslených bitmapách, alebo v parseri, a počítadlá arény vám povedia, kde presne

Potom zoberte prvých desať uzlov podľa EstimatedRetainedBytes a pozrite sa na ich ObjectType. Obrázkové XObjecty na vrchole znamenajú, že súbor je bohatý na skeny a riešením je zníženie rozlíšenia. Deskriptory písiem na vrchole znamenajú, že sa vložili celé rezy tam, kde by stačili subsety. Obsahové toky na vrchole zvyčajne znamenajú generovanú vektorovú grafiku, často mapy alebo exporty z CAD. Až potom sa oplatí pozrieť na nedosiahnuteľné objekty a správanie alokátora. Ak budete postupovať v tomto poradí, odpoveď obyčajne nájdete v prvých dvoch krokoch, a pri veľmi veľkých dokumentoch je streamovaný prístup opísaný v článku workflow priameho súborového API často štrukturálnou opravou namiesto akejkoľvek optimalizácie po objektoch

Všetky tieto diagnostiky sú obyčajné volania Pascalu vracajúce obyčajné záznamy, takže zapadnú priamo do existujúcej cesty logovania či telemetrie. HotPDF je natívny VCL PDF komponent pre Delphi a C++Builder s úplným zdrojovým kódom; referencia API a skúšobná verzia sú na stránke HotPDF Delphi PDF component