Articol tehnic

Grafuri de dependențe HotPDF pentru memoria PDF

HotPDF expune profilul de memorie al unui PDF încărcat ca un graf pe care îl poți interoga: BuildLoadedObjectDependencyGraph returnează câte un nod pentru fiecare obiect indirect, cu o dimensiune shallow estimată, o dimensiune reținută bazată pe dominator, și un flag pentru dacă obiectul este încă accesibil din Catalog-ul documentului. Asta transformă „acest fișier folosește 800 MB” în „obiectul 4173, un XObject de imagine, reține exclusiv 612 MB”, ceea ce este un fapt pe care poți acționa

Distincția dintre acele două propoziții este tot rostul. Dimensiunea shallow îți spune cât de mare este un obiect. Dimensiunea reținută îți spune cât de multă memorie s-ar elibera efectiv dacă acel obiect ar dispărea, ceea ce este numărul care decide dacă o remediere ajută

De ce nu este memoria totală un fapt pe care poți acționa?

Pentru că într-un PDF aproape nimic nu este deținut de exact un singur lucru. Un singur font CID inclus este referențiat din dicționarul de resurse al fiecărei pagini care îl folosește. Un profil ICC susține un spațiu de culoare pe care zece content stream-uri diferite îl împart. Un Form XObject folosit ca ștampilă apare pe toate cele 400 de pagini. Dacă aduni naiv dimensiunile obiectelor per pagină, numeri acel font de 400 de ori și concluzionezi că fiecare pagină este uriașă; dacă îl împarți la 400, concluzionezi că nimic nu este costisitor și memoria a venit de altundeva

Analiza dominator rezolvă ambiguitatea în singurul mod care supraviețuiește contactului cu documente reale. Un obiect X este atribuit celui mai apropiat obiect care îl reține exclusiv, ceea ce înseamnă că fiecare traseu de referință de la Catalog la X trece prin acel dominator. Un font partajat de toate paginile nu este atribuit niciunei pagini; este atribuit celui mai apropiat nod prin care trec toate acele trasee, care de obicei este chiar Catalog-ul. Un font folosit de exact o pagină este atribuit acelei pagini. Rezultatul este că EstimatedRetainedBytes se însumează corect, în loc să numere de două ori, iar obiectele din vârful listei sunt obiecte a căror eliminare ar elibera cu adevărat memorie

Ce conține de fapt graful

Fiecare THPDFObjectDependencyNode poartă identitatea obiectului ca ObjectNumber și GenerationNumber, plus ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, identitatea dominatorului său imediat, și ReachableFromCatalog. Fiecare THPDFObjectDependencyEdge înregistrează sursa, ținta, Path-ul dicționarului sub care a fost găsită referința, și dacă s-a Resolved

Acel câmp Path este cel pe care oamenii îl subutilizează. Este diferența dintre a ști că obiectul 91 indică spre obiectul 4173 și a ști că o face prin /Resources/XObject/Im3, ceea ce îți spune imediat dacă te uiți la conținutul unei pagini, la un stream de aparență al unei adnotări, sau la un grup de conținut opțional pe care nimeni nu îl randează vreodată

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;

Ambele limite sunt parametri expliciți cu valorile implicite 250.000 de obiecte și 2.000.000 de muchii. Când un document depășește oricare dintre ele, LimitExceeded este setat, iar graful returnat este un prefix trunchiat, nu o minciună: rezultate parțiale cu un flag, nu totaluri greșite silențios. Ridică limitele deliberat pentru o rulare forensică, și ține minte că analiza parcurge întregul graf de obiecte, deci aparține unui traseu de diagnostic, nu buclei tale de randare

Ce îți spune un obiect inaccesibil?

Un obiect cu ReachableFromCatalog setat pe False este memorie pe care documentul o poartă, dar pe care viewer-ul nu o va arăta niciodată. În practică vine din trei locuri: actualizări incrementale care au înlocuit o versiune anterioară a unui obiect și au lăsat originalul în urmă, un producător care a scris obiecte pe care apoi a eșuat să le lege, sau un fișier deteriorat a cărui tabelă cross-reference a fost reconstruită și a preluat definiții pe care nimic nu le referențiază

Primul caz este normal și așteptat, și este exact ce sunt proiectate să facă actualizările incrementale și object stream-urile. Al doilea și al treilea merită investigate. Când o fracțiune mare din bytes reținuți se află în noduri inaccesibile, ai găsit un argument concret pentru rescrierea fișierului în loc de a-l extinde, și ai numărul de bytes pentru a justifica timpul suplimentar de procesare oricui întreabă

Două contoare de alocator care merită citite primele

Înainte de a concluziona că un document este intrinsec mare, verifică dacă parserul însuși este costul. HotPDF expune două contoare care descriu cum s-a comportat încărcarea, nu ce conține documentul

GetLastParserArenaStatistics raportează arena de blocuri reținute folosită pentru token-uri și buffere de parser de scurtă durată: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount și TemporaryObjectElisionCount. Acel ultim câmp numără cheile de dicționar parsate direct în arenă în loc de printr-un obiect nume temporar, care este alocarea care obișnuia să domine parsarea fișierelor bogate în dicționare

GetDocumentStringInternStatistics raportează pool-ul de interning la nivel de document care deduplică nume PDF repetate, operatori de content stream și string-uri imuabile de până la 64 de bytes. Îți oferă RequestCount, HitCount, MissCount, BypassCount, RetainedBytes și ReusedBytes, împreună cu plafoanele în vigoare. Pool-ul este mărginit deliberat la 65.536 de intrări și 4 MiB, astfel încât un document ostil care generează un milion de nume unice nu poate transforma o optimizare de memorie într-un amplificator de memorie; odată ce plafonul este atins, string-urile ulterioare ocolesc pool-ul, iar BypassCount crește

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;

O valoare mare a ReusedBytes cu un BypassCount mic înseamnă că documentul are vocabularul repetitiv pe care majoritatea PDF-urilor reale îl au, iar pool-ul își câștigă locul. Un BypassCount care se apropie de RequestCount înseamnă ceva neobișnuit: fie un document cu adevărat uriaș, fie unul care generează nume unice intenționat, ceea ce este un semnal ușor care merită logat pe un flux de ingestie nesigur

O ordine de triaj care funcționează

Începe cu CatalogRetainedBytes din informațiile grafului. Dacă acel număr este apropiat de creșterea procesului tău, memoria este în document, iar graful îți va arăta unde. Dacă este mult sub, memoria este în propriile tale cache-uri, în bitmap-uri randate, sau în parser, iar contoarele arenei îți vor spune care

Apoi ia primele zece noduri după EstimatedRetainedBytes și uită-te la ObjectType-ul lor. XObject-uri de imagine în vârf înseamnă că fișierul este încărcat cu scanări, iar downsampling-ul este remedierea. Descriptori de font în vârf înseamnă că fețe complete au fost incluse acolo unde subseturile ar fi fost suficiente. Content stream-uri în vârf de obicei înseamnă grafică vectorială generată, adesea hărți sau exporturi CAD. Doar după asta merită să te uiți la obiectele inaccesibile și comportamentul alocatorului. Lucrând în această ordine, de obicei vei găsi răspunsul în primii doi pași, iar pentru documente foarte mari abordarea prin streaming descrisă în workflow-ul API-ului de fișiere direct este frecvent remedierea structurală, nu vreo optimizare per-obiect

Toate aceste diagnostice sunt apeluri Pascal simple care returnează înregistrări simple, deci se integrează direct într-un traseu existent de logging sau telemetrie. HotPDF este o componentă VCL nativă pentru Delphi și C++Builder cu sursă completă; referința API și o versiune de test sunt pe pagina componentei HotPDF Delphi PDF