Műszaki cikk

PDF memóriazabálók keresése: HotPDF objektumfüggőségi gráf

A HotPDF egy lekérdezhető gráfként teszi elérhetővé egy betöltött PDF memóriaprofilját: a BuildLoadedObjectDependencyGraph minden indirekt objektumhoz egy csomópontot ad vissza, egy becsült sekély mérettel, egy dominátor alapú megtartott mérettel, és egy jelzővel arra, hogy az objektum még elérhető-e a dokumentum Catalogjából. Ez a „ez a fájl 800 MB-ot használ” állítást „a 4173-as objektum, egy image XObject, kizárólagosan 612 MB-ot tart meg” tényévé alakítja, amelyre cselekedni tudsz

A két mondat közötti különbség az egész lényeg. A sekély méret megmondja, mekkora egy objektum. A megtartott méret megmondja, mennyi memória szabadulna fel ténylegesen, ha az az objektum eltűnne, és ez az a szám, amely eldönti, hogy egy javítás segít-e

Miért nem cselekvésre alkalmas tény a teljes memóriahasználat?

Mert egy PDF-ben szinte semmi sincs pontosan egyetlen dolog tulajdonában. Egy beágyazott CID betűtípusra minden olyan oldal erőforrás-szótárából hivatkoznak, amely használja. Egy ICC profil stream támogat egy színteret, amelyet tíz különböző tartalmi stream oszt meg. Egy bélyegzőként használt Form XObject mind a 400 oldalon megjelenik. Ha naivan összeadod az objektumméreteket oldalanként, azt a betűtípust 400-szor számolod, és arra a következtetésre jutsz, hogy minden oldal hatalmas; ha elosztod 400-zal, arra a következtetésre jutsz, hogy semmi sem drága, és a memória máshonnan jött

A dominátor-elemzés az egyetlen olyan módon oldja fel a kétértelműséget, amely túléli a találkozást a valódi dokumentumokkal. Egy X objektum ahhoz a legközelebbi objektumhoz van hozzárendelve, amely kizárólagosan megtartja, ami azt jelenti, hogy minden hivatkozási útvonal a Catalogtól X-ig áthalad azon a dominátoron. Egy minden oldal által megosztott betűtípus nincs hozzárendelve egyetlen oldalhoz sem; ahhoz a legközelebbi csomóponthoz van hozzárendelve, amelyen mindegyik útvonal áthalad, ami általában maga a Catalog. Egy pontosan egy oldal által használt betűtípus ahhoz az oldalhoz van hozzárendelve. Az eredmény az, hogy az EstimatedRetainedBytes helyesen összegződik, nem duplán számolva, és a lista tetején lévő objektumok azok, amelyek eltávolítása valóban memóriát szabadítana fel

Mit tartalmaz valójában a gráf

Minden THPDFObjectDependencyNode hordozza az objektumazonosítót ObjectNumber és GenerationNumber formájában, plusz az ObjectType, a LifecycleState, az EstimatedShallowBytes, az EstimatedRetainedBytes, az IncomingReferenceCount, az OutgoingReferenceCount, a közvetlen dominátorának azonosítóját, és a ReachableFromCatalog értéket. Minden THPDFObjectDependencyEdge rögzíti a forrást, a célt, a szótár Path értékét, amely alatt a hivatkozást megtalálták, és hogy Resolved-e

Ez a Path mező az, amelyet az emberek alulhasználnak. Ez a különbség aközött, hogy tudod, a 91-es objektum a 4173-as objektumra mutat, és aközött, hogy tudod, ezt a /Resources/XObject/Im3 útvonalon keresztül teszi, ami azonnal megmondja, hogy oldaltartalmat, egy annotáció megjelenési streamjét, vagy egy olyan opcionális tartalomcsoportot nézel-e, amelyet soha senki nem renderel

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;

Mindkét korlát explicit paraméter, alapértelmezésben 250 000 objektummal és 2 000 000 éllel. Amikor egy dokumentum bármelyiket túllépi, a LimitExceeded beáll, és a visszaadott gráf egy csonkolt előtag, nem egy hazugság: részleges eredmények egy jelzővel, nem csendben rossz összegek. Emeld meg a korlátokat tudatosan egy törvényszéki futtatáshoz, és ne feledd, hogy az elemzés bejárja a teljes objektumgráfot, így egy diagnosztikai útvonalba tartozik, nem a render ciklusodba

Mit mond el egy elérhetetlen objektum?

Egy objektum, amelynek ReachableFromCatalog értéke False, olyan memória, amelyet a dokumentum hordoz, de a megjelenítő soha nem fog megjeleníteni. A gyakorlatban három helyről származik: inkrementális frissítésekből, amelyek egy objektum korábbi verzióját lecserélték, és az eredetit hátrahagyták, egy előállítóból, amely objektumokat írt, majd nem sikerült őket összekapcsolnia, vagy egy sérült fájlból, amelynek kereszthivatkozás-tábláját újraépítették, és olyan definíciókat vett fel, amelyekre semmi sem hivatkozik

Az első eset normális és várt, és pontosan ez az, amire az inkrementális frissítések és objektumstreamek tervezve vannak. A második és a harmadik érdemes megvizsgálni. Amikor a megtartott byte-ok nagy hányada elérhetetlen csomópontokban ül, konkrét érvet találtál a fájl újraírására a hozzáfűzés helyett, és megvan a byte-számod, amellyel igazolhatod a plusz feldolgozási időt bárkinek, aki megkérdezi

Két allokátorszámláló, amelyet érdemes elsőként elolvasni

Mielőtt arra a következtetésre jutnál, hogy egy dokumentum eleve nagy, ellenőrizd, hogy maga az elemző-e a költség. A HotPDF két számlálót tesz elérhetővé, amelyek azt írják le, hogyan viselkedett a betöltés, nem azt, hogy a dokumentum mit tartalmaz

A GetLastParserArenaStatistics a rövid életű elemzőtokenekhez és pufferekhez használt megtartott blokkarénáról ad jelentést: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount és TemporaryObjectElisionCount. Ez az utolsó mező azokat a szótárkulcsokat számolja, amelyeket közvetlenül az arénába elemeztek egy ideiglenes névobjektum helyett, ez az az allokáció, amely korábban dominálta a szótár-nehéz fájlok elemzését

A GetDocumentStringInternStatistics a dokumentumhatókörű intern poolról ad jelentést, amely az ismétlődő PDF-neveket, tartalmifolyam-operátorokat és a legfeljebb 64 byte-os változtathatatlan stringeket deduplikálja. Megadja a RequestCount, a HitCount, a MissCount, a BypassCount, a RetainedBytes és a ReusedBytes értékeket, az érvényben lévő korlátokkal együtt. A pool szándékosan 65 536 bejegyzésre és 4 MiB-ra van korlátozva, így egy ellenséges dokumentum, amely egymillió egyedi nevet generál, nem tudja egy memóriaoptimalizálásból memóriaerősítővé alakítani; amint a korlát eléri a maximumot, a további stringek megkerülik a poolt, és a BypassCount emelkedik

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;

Egy magas ReusedBytes érték alacsony BypassCount-tal azt jelenti, hogy a dokumentumnak megvan az az ismétlődő szókincse, amellyel a legtöbb valódi PDF rendelkezik, és a pool megdolgozza magát. Egy RequestCount-hoz közelítő BypassCount valami szokatlant jelent: vagy egy valóban hatalmas dokumentumot, vagy egyet, amely szándékosan egyedi neveket generál, ami egy enyhe jelzés, amelyet érdemes naplózni egy nem megbízható befogadási útvonalon

Egy triázssorrend, amely működik

Kezdd a gráfinfó CatalogRetainedBytes értékével. Ha ez a szám közel van a folyamatod növekedéséhez, a memória a dokumentumban van, és a gráf megmutatja, hol. Ha jóval alatta van, a memória a saját gyorsítótáraidban, renderelt bitmapekben vagy az elemzőben van, és az arénaszámlálók megmondják, melyikben

Ezután vedd a top tíz csomópontot EstimatedRetainedBytes szerint, és nézd meg az ObjectType értéküket. A csúcson lévő Image XObjectek azt jelentik, hogy a fájl szkennelés-nehéz, és a downsampling a javítás. A csúcson lévő betűtípus-leírók azt jelentik, hogy teljes betűkészleteket ágyaztak be ott, ahol subsetek is megtennék. A csúcson lévő tartalmi streamek általában generált vektorgrafikát jelentenek, gyakran térképeket vagy CAD exportokat. Csak ezután éri meg megnézni az elérhetetlen objektumokat és az allokátor viselkedését. Ebben a sorrendben dolgozva általában az első két lépésben megtalálod a választ, és nagyon nagy dokumentumoknál a közvetlen fájl API munkafolyamatban leírt streamelő megközelítés gyakran a strukturális javítás, nem bármilyen objektumonkénti optimalizálás

Ezek a diagnosztikák mind egyszerű Pascal hívások, amelyek egyszerű rekordokat adnak vissza, így közvetlenül beilleszthetők egy meglévő naplózási vagy telemetriai útvonalba. A HotPDF egy natív VCL PDF-komponens Delphihez és C++Builderhez, teljes forráskóddal; az API-referencia és egy próbaverzió a HotPDF Delphi PDF komponens oldalán található