Teknisk artikkel

Finn PDF-minnesluker: HotPDF-objektavhengighetsgrafer

HotPDF eksponerer minneprofilen til en innlastet PDF som en graf du kan spørre: BuildLoadedObjectDependencyGraph returnerer én node per indirekte objekt med en estimert grunn størrelse, en dominatorbasert holdt størrelse, og et flagg for om objektet fortsatt er nåbart fra dokumentkatalogen. Det gjør «denne filen bruker 800 MB» om til «objekt 4173, et bilde-XObject, holder eksklusivt på 612 MB», som er et faktum du kan handle på

Forskjellen mellom de to setningene er hele poenget. Grunn størrelse forteller deg hvor stort ett objekt er. Holdt størrelse forteller deg hvor mye minne som faktisk ville blitt frigjort dersom det objektet forsvant, og det er tallet som avgjør om en fiks hjelper

Hvorfor er total minnebruk ikke et handlingsrettet faktum?

Fordi nesten ingenting i en PDF eies av bare én ting. En enkelt innebygd CID-skrift refereres fra ressursordboken til hver side som bruker den. En ICC-profilstrøm ligger bak en fargeromsdefinisjon som ti forskjellige innholdsstrømmer deler. Et Form XObject brukt som et stempel dukker opp på alle 400 sidene. Hvis du naivt legger sammen objektstørrelser per side, teller du den skriften 400 ganger og konkluderer med at hver side er enorm; hvis du deler på 400, konkluderer du med at ingenting er kostbart og at minnet kom fra et annet sted

Dominatoranalyse løser opp tvetydigheten på den eneste måten som overlever kontakt med reelle dokumenter. Et objekt X tilskrives det nærmeste objektet som eksklusivt holder på det, som betyr at hver referansesti fra katalogen til X går gjennom den dominatoren. En skrift delt av alle sider tilskrives ingen enkelt side; den tilskrives den nærmeste noden alle disse stiene går gjennom, som vanligvis er selve katalogen. En skrift brukt av nøyaktig én side tilskrives den siden. Resultatet er at EstimatedRetainedBytes summeres korrekt i stedet for å dobbelttelles, og objektene øverst på listen er objekter der fjerning genuint ville frigjort minne

Hva grafen faktisk inneholder

Hver THPDFObjectDependencyNode bærer objektidentiteten som ObjectNumber og GenerationNumber, pluss ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, identiteten til sin umiddelbare dominator, og ReachableFromCatalog. Hver THPDFObjectDependencyEdge registrerer kilde, mål, ordbokstien Path referansen ble funnet under, og om den Resolved

Feltet Path er det folk underbruker. Det er forskjellen mellom å vite at objekt 91 peker på objekt 4173, og å vite at det gjør det gjennom /Resources/XObject/Im3, som umiddelbart forteller deg om du ser på sideinnhold, en annotasjonsutseendestrøm, eller en valgfri innholdsgruppe ingen noensinne rendrer

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;

Begge grensene er eksplisitte parametere med standardverdier på 250 000 objekter og 2 000 000 kanter. Når et dokument overskrider en av dem, settes LimitExceeded, og den returnerte grafen er et avkuttet prefiks fremfor en løgn: delvise resultater med et flagg, ikke stille feilaktige totaler. Hev grensene bevisst for en rettsmedisinsk kjøring, og husk at analysen går gjennom hele objektgrafen, så den hører hjemme i en diagnostisk vei, ikke i rendersløyfen din

Hva forteller et uoppnåelig objekt deg?

Et objekt med ReachableFromCatalog satt til False er minne dokumentet bærer, men som viseren aldri vil vise. I praksis kommer det fra tre steder: inkrementelle oppdateringer som erstattet en tidligere versjon av et objekt og lot originalen bli liggende igjen, en produsent som skrev objekter den deretter ikke klarte å lenke, eller en skadet fil hvis kryssreferansetabell ble gjenoppbygd og fanget opp definisjoner som ingenting refererer til

Det første tilfellet er normalt og forventet, og det er nøyaktig det inkrementelle oppdateringer og objektstrømmer er designet for å gjøre. Det andre og tredje er verdt å undersøke. Når en stor andel av de holdte bytene ligger i uoppnåelige noder, har du funnet et konkret argument for å skrive om filen i stedet for å legge til den, og du har byteantallet som trengs for å rettferdiggjøre den ekstra prosesseringstiden overfor den som spør

To allokatortellere verdt å lese først

Før du konkluderer med at et dokument iboende er stort, sjekk om selve parseren er kostnaden. HotPDF eksponerer to tellere som beskriver hvordan innlastingen oppførte seg, fremfor hva dokumentet inneholder

GetLastParserArenaStatistics rapporterer den holdte blokkarenaen brukt for korttidsparser-tokener og buffere: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount og TemporaryObjectElisionCount. Det siste feltet teller ordboknøkler analysert direkte inn i arenaen i stedet for gjennom et midlertidig navneobjekt, som er allokeringen som tidligere dominerte analysen av ordbok-tunge filer

GetDocumentStringInternStatistics rapporterer den dokumentbundne interneringspoolen som deduplikerer gjentatte PDF-navn, operatorer i innholdsstrømmer og uforanderlige strenger opp til 64 byte. Den gir deg RequestCount, HitCount, MissCount, BypassCount, RetainedBytes og ReusedBytes, sammen med de gjeldende takene. Poolen er bevisst begrenset til 65 536 oppføringer og 4 MiB, slik at et fiendtlig dokument som genererer en million unike navn, ikke kan gjøre en minneoptimalisering om til en minneforsterker; når taket er nådd, går videre strenger utenom poolen og 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;

En høy ReusedBytes-verdi med en lav BypassCount betyr at dokumentet har det repetitive vokabularet de fleste reelle PDF-er har, og at poolen gjør nytte for seg. En BypassCount som nærmer seg RequestCount, betyr noe uvanlig: enten et genuint enormt dokument, eller ett som genererer unike navn med hensikt, noe som er et mildt signal verdt å logge på en utiltrodd mottaksvei

En prioriteringsrekkefølge som virker

Start med CatalogRetainedBytes fra grafinfoen. Hvis det tallet er nær prosessveksten din, ligger minnet i dokumentet, og grafen vil vise deg hvor. Hvis det er langt under, ligger minnet i dine egne cacher, i rendrede bitmaps, eller i parseren, og arenatellerne vil fortelle hvilket

Ta deretter de ti øverste nodene etter EstimatedRetainedBytes og se på ObjectType deres. Bilde-XObjects øverst betyr at filen er skann-tung og at nedskalering er fiksen. Skriftbeskrivelser øverst betyr at fulle skrifter ble bygget inn der subsett hadde holdt. Innholdsstrømmer øverst betyr vanligvis genererte vektorgrafikker, ofte kart eller CAD-eksporter. Først etter det lønner det seg å se på uoppnåelige objekter og allokatoratferd. Ved å jobbe i den rekkefølgen finner du som regel svaret i de to første trinnene, og for svært store dokumenter er strømmetilnærmingen beskrevet i den direkte fil-API-arbeidsflyten ofte den strukturelle fiksen fremfor en per-objekt-optimalisering

Alle disse diagnostikkene er vanlige Pascal-kall som returnerer vanlige records, så de faller rett inn i en eksisterende logg- eller telemetrivei. HotPDF er en native VCL PDF-komponent for Delphi og C++Builder med full kildekode; API-referansen og en prøvebygg finnes på HotPDF sin side for Delphi PDF-komponent