Tehnički članak

PDF memorijski proždrljivci: HotPDF grafovi zavisnosti

HotPDF izlaže memorijski profil učitanog PDF-a kao graf koji možete pretraživati: BuildLoadedObjectDependencyGraph vraća po jedan čvor za svaki indirektni objekat, sa procenjenom plitkom veličinom, zadržanom veličinom zasnovanom na dominatoru, i zastavicom koja pokazuje da li je objekat i dalje dostižan iz Catalog-a dokumenta. To pretvara „ovaj fajl koristi 800 MB” u „objekat 4173, image XObject, isključivo zadržava 612 MB”, a to je činjenica na osnovu koje možete delovati

Razlika između te dve rečenice je čitava poenta. Plitka veličina govori vam koliko je jedan objekat veliki. Zadržana veličina govori vam koliko bi se memorije zaista oslobodilo kada bi taj objekat nestao, a upravo taj broj odlučuje da li popravka zaista pomaže

Zašto ukupna potrošnja memorije nije činjenica na kojoj se može delovati?

Zato što u PDF-u gotovo ništa ne pripada tačno jednoj stvari. Jedan ugrađen CID font se referencira iz rečnika resursa svake stranice koja ga koristi. ICC profil stream stoji iza prostora boja koji deli deset različitih tokova sadržaja. Form XObject korišćen kao pečat pojavljuje se na svih 400 stranica. Ako naivno saberete veličine objekata po stranici, izbrojaćete taj font 400 puta i zaključiti da je svaka stranica ogromna; ako ga podelite sa 400, zaključićete da ništa nije skupo i da je memorija potekla odnekud drugde

Analiza dominatora rešava tu dvosmislenost na jedini način koji preživi susret sa stvarnim dokumentima. Objekat X se pripisuje najbližem objektu koji ga isključivo zadržava, što znači da svaka referentna putanja od Catalog-a do X prolazi kroz taj dominator. Font koji dele sve stranice ne pripisuje se nijednoj stranici; pripisuje se najbližem čvoru kroz koji prolaze sve te putanje, a to je obično sam Catalog. Font korišćen na tačno jednoj stranici pripisuje se toj stranici. Rezultat je da se EstimatedRetainedBytes ispravno sabira umesto da duplo broji, a objekti na vrhu liste su objekti čije bi uklanjanje zaista oslobodilo memoriju

Šta graf zapravo sadrži

Svaki THPDFObjectDependencyNode nosi identitet objekta kao ObjectNumber i GenerationNumber, plus ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, identitet svog neposrednog dominatora, i ReachableFromCatalog. Svaki THPDFObjectDependencyEdge beleži izvor, cilj, Path rečnika pod kojim je referenca pronađena, i da li je Resolved

Baš to polje Path je ono koje ljudi nedovoljno koriste. To je razlika između saznanja da objekat 91 pokazuje na objekat 4173 i saznanja da to čini preko /Resources/XObject/Im3, što vam odmah kaže da li gledate sadržaj stranice, appearance stream anotacije, ili grupu opcionog sadržaja koju niko nikada ne renderuje

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 granice su eksplicitni parametri sa podrazumevanim vrednostima od 250.000 objekata i 2.000.000 grana. Kada dokument premaši bilo koju od njih, postavlja se LimitExceeded, a vraćeni graf je skraćen prefiks, a ne laž: parcijalni rezultati sa zastavicom, a ne tiho pogrešni ukupni brojevi. Podignite ograničenja namerno za forenzičko pokretanje, i imajte na umu da analiza prolazi kroz ceo graf objekata, pa joj je mesto u dijagnostičkoj putanji, a ne u vašoj petlji za renderovanje

Šta vam govori nedostižan objekat?

Objekat kod koga je ReachableFromCatalog postavljen na False je memorija koju dokument nosi, ali koju pregledač nikada neće prikazati. U praksi dolazi sa tri mesta: inkrementalna ažuriranja koja su zamenila raniju verziju objekta i ostavila original iza sebe, proizvođač koji je upisao objekte koje kasnije nije uspeo da poveže, ili oštećen fajl čija je tabela unakrsnih referenci ponovo izgrađena i pokupila definicije na koje ništa ne pokazuje

Prvi slučaj je normalan i očekivan, i tačno je ono za šta su inkrementalna ažuriranja i tokovi objekata osmišljeni. Drugi i treći slučaj vredi istražiti. Kada veliki deo zadržanih bajtova leži u nedostižnim čvorovima, pronašli ste konkretan argument za ponovno pisanje fajla umesto dodavanja na njega, a broj bajtova imate kao opravdanje za dodatno vreme obrade svakome ko pita

Dva brojača alokatora koje vredi pročitati prvo

Pre nego što zaključite da je dokument suštinski velik, proverite da li je trošak sam parser. HotPDF izlaže dva brojača koja opisuju kako se ponašalo učitavanje, a ne šta dokument sadrži

GetLastParserArenaStatistics prijavljuje zadržanu arenu blokova korišćenu za kratkotrajne tokene i bafere parsera: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount i TemporaryObjectElisionCount. To poslednje polje broji ključeve rečnika parsirane direktno u arenu umesto kroz privremeni objekat imena, što je alokacija koja je nekada dominirala parsiranjem fajlova bogatih rečnicima

GetDocumentStringInternStatistics prijavljuje pool za interniranje na nivou dokumenta koji deduplicira ponovljena PDF imena, operatore toka sadržaja i nepromenljive niske do 64 bajta. Daje vam RequestCount, HitCount, MissCount, BypassCount, RetainedBytes i ReusedBytes, zajedno sa granicama koje su na snazi. Pool je namerno ograničen na 65.536 unosa i 4 MiB, tako da neprijateljski dokument koji generiše milion jedinstvenih imena ne može da pretvori memorijsku optimizaciju u memorijski pojačivač; kada se dostigne granica, dalje niske zaobilaze pool i BypassCount raste

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;

Visoka vrednost ReusedBytes uz nizak BypassCount znači da dokument ima onaj ponavljajući vokabular koji imaju većina stvarnih PDF-ova i da se pool isplati. BypassCount koji se približava RequestCount znači nešto neuobičajeno: ili zaista ogroman dokument, ili dokument koji namerno generiše jedinstvena imena, što je blag signal koji vredi beležiti na putanji nepouzdanog prijema

Redosled trijaže koji funkcioniše

Počnite sa CatalogRetainedBytes iz informacija o grafu. Ako je taj broj blizu rasta vašeg procesa, memorija je u dokumentu, a graf će vam pokazati gde. Ako je znatno niži, memorija je u vašim sopstvenim kešovima, u renderovanim bitmapama, ili u parseru, a brojači arene će reći koje od toga

Zatim uzmite prvih deset čvorova po EstimatedRetainedBytes i pogledajte njihov ObjectType. Image XObject-i na vrhu znače da je fajl pretežno skeniran i da je popravka smanjenje rezolucije (downsampling). Font deskriptori na vrhu znače da su ugrađeni čitavi fontovi tamo gde bi bili dovoljni podskupovi. Tokovi sadržaja na vrhu obično znače generisanu vektorsku grafiku, često mape ili CAD izvoze. Tek posle toga isplati se pogledati nedostižne objekte i ponašanje alokatora. Radeći tim redosledom, odgovor ćete obično naći u prva dva koraka, a za veoma velike dokumente pristup zasnovan na strimovanju opisan u radnom toku direktnog File API-ja često je strukturna popravka, a ne bilo kakva optimizacija po objektu

Sve ove dijagnostike su obični Pascal pozivi koji vraćaju obične zapise (records), tako da se direktno uklapaju u postojeću putanju logovanja ili telemetrije. HotPDF je izvorna VCL PDF komponenta za Delphi i C++Builder sa punim izvornim kodom; API referenca i probna verzija nalaze se na stranici HotPDF Delphi PDF komponente