Τεχνικό Άρθρο

Εύρεση Μνήμης PDF: Δέντρα Εξάρτησης HotPDF

Το HotPDF εκθέτει το προφίλ μνήμης ενός φορτωμένου PDF ως γράφημα που μπορείτε να ερωτήσετε: η BuildLoadedObjectDependencyGraph επιστρέφει έναν κόμβο ανά έμμεσο αντικείμενο με ένα εκτιμώμενο μέγεθος shallow, ένα retained μέγεθος βάσει dominator, και μια σημαία για το αν το αντικείμενο είναι ακόμη προσβάσιμο από το Catalog του εγγράφου. Αυτό μετατρέπει το "αυτό το αρχείο χρησιμοποιεί 800 MB" σε "το αντικείμενο 4173, ένα image XObject, κρατά αποκλειστικά 612 MB", κάτι πάνω στο οποίο μπορείτε να ενεργήσετε

Η διάκριση ανάμεσα σε αυτές τις δύο προτάσεις είναι όλο το νόημα. Το shallow μέγεθος σας λέει πόσο μεγάλο είναι ένα αντικείμενο. Το retained μέγεθος σας λέει πόση μνήμη θα ελευθερωνόταν πράγματι αν αυτό το αντικείμενο έφευγε, κάτι που είναι ο αριθμός που αποφασίζει αν μια διόρθωση βοηθά

Γιατί η συνολική χρήση μνήμης δεν είναι πρακτικό γεγονός;

Επειδή σε ένα PDF σχεδόν τίποτα δεν ανήκει σε ένα και μόνο πράγμα. Μια ενσωματωμένη γραμματοσειρά CID αναφέρεται από το resource dictionary κάθε σελίδας που τη χρησιμοποιεί. Ένα ICC profile stream υποστηρίζει έναν χρωματικό χώρο που μοιράζονται δέκα διαφορετικά content streams. Ένα Form XObject που χρησιμοποιείται ως σφραγίδα εμφανίζεται σε όλες τις 400 σελίδες. Αν αθροίσετε αφελώς τα μεγέθη αντικειμένων ανά σελίδα, μετράτε αυτή τη γραμματοσειρά 400 φορές και συμπεραίνετε ότι κάθε σελίδα είναι τεράστια· αν το διαιρέσετε με το 400, συμπεραίνετε ότι τίποτα δεν κοστίζει πολύ και η μνήμη ήρθε από κάπου αλλού

Η ανάλυση dominator λύνει την ασάφεια με τον μόνο τρόπο που επιβιώνει σε επαφή με πραγματικά έγγραφα. Ένα αντικείμενο X αποδίδεται στον πλησιέστερο κόμβο που το κρατά αποκλειστικά, εννοώντας ότι κάθε διαδρομή αναφοράς από το Catalog προς το X περνά μέσα από αυτόν τον dominator. Μια γραμματοσειρά που μοιράζονται όλες οι σελίδες δεν αποδίδεται σε καμία σελίδα· αποδίδεται στον πλησιέστερο κόμβο από τον οποίο περνούν όλες αυτές οι διαδρομές, που συνήθως είναι το ίδιο το Catalog. Μια γραμματοσειρά που χρησιμοποιείται από ακριβώς μία σελίδα αποδίδεται σε αυτή τη σελίδα. Το αποτέλεσμα είναι ότι το EstimatedRetainedBytes αθροίζει σωστά αντί να μετρά διπλά, και τα αντικείμενα στην κορυφή της λίστας είναι αντικείμενα των οποίων η αφαίρεση θα ελευθέρωνε πράγματι μνήμη

Τι περιέχει πράγματι το γράφημα

Κάθε THPDFObjectDependencyNode φέρει την ταυτότητα αντικειμένου ως ObjectNumber και GenerationNumber, συν ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, την ταυτότητα του άμεσου dominator του, και ReachableFromCatalog. Κάθε THPDFObjectDependencyEdge καταγράφει πηγή, στόχο, το Path dictionary κάτω από το οποίο βρέθηκε η αναφορά, και αν Resolved

Αυτό το πεδίο Path είναι αυτό που ο κόσμος υποχρησιμοποιεί. Είναι η διαφορά ανάμεσα στο να ξέρετε ότι το αντικείμενο 91 δείχνει στο αντικείμενο 4173 και στο να ξέρετε ότι το κάνει μέσω του /Resources/XObject/Im3, κάτι που σας λέει αμέσως αν κοιτάτε περιεχόμενο σελίδας, ένα appearance stream annotation, ή μια ομάδα optional-content που κανείς δεν αποδίδει ποτέ

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;

Και τα δύο όρια είναι ρητές παράμετροι με προεπιλογές 250.000 αντικείμενα και 2.000.000 ακμές. Όταν ένα έγγραφο υπερβαίνει οποιοδήποτε από τα δύο, το LimitExceeded ορίζεται και το επιστρεφόμενο γράφημα είναι ένα περικομμένο πρόθεμα και όχι ψέμα: μερικά αποτελέσματα με μια σημαία, όχι σιωπηλά λανθασμένα σύνολα. Ανεβάστε τα όρια σκόπιμα για μια forensic εκτέλεση, και θυμηθείτε ότι η ανάλυση διατρέχει ολόκληρο το γράφημα αντικειμένων, οπότε ανήκει σε μια διαγνωστική διαδρομή, όχι στον βρόχο απόδοσής σας

Τι σας λέει ένα μη προσβάσιμο αντικείμενο;

Ένα αντικείμενο με το ReachableFromCatalog ορισμένο σε False είναι μνήμη που το έγγραφο κουβαλά αλλά ο viewer δεν θα δείξει ποτέ. Στην πράξη προέρχεται από τρία σημεία: incremental updates που αντικατέστησαν μια παλαιότερη έκδοση ενός αντικειμένου και άφησαν πίσω το αρχικό, έναν παραγωγό που έγραψε αντικείμενα και μετά απέτυχε να τα συνδέσει, ή ένα κατεστραμμένο αρχείο του οποίου ο πίνακας cross-reference ανακατασκευάστηκε και πήρε ορισμούς που τίποτα δεν αναφέρει

Η πρώτη περίπτωση είναι φυσιολογική και αναμενόμενη, και είναι ακριβώς αυτό για το οποίο σχεδιάστηκαν τα incremental updates και object streams. Η δεύτερη και η τρίτη αξίζει να διερευνηθούν. Όταν ένα μεγάλο ποσοστό retained bytes βρίσκεται σε μη προσβάσιμους κόμβους, έχετε βρει ένα συγκεκριμένο επιχείρημα για επανεγγραφή του αρχείου αντί για προσάρτηση σε αυτό, και έχετε τον αριθμό bytes για να δικαιολογήσετε τον επιπλέον χρόνο επεξεργασίας σε όποιον ρωτήσει

Δύο μετρητές allocator που αξίζει να διαβαστούν πρώτοι

Πριν συμπεράνετε ότι ένα έγγραφο είναι εγγενώς μεγάλο, ελέγξτε αν το κόστος βρίσκεται στον ίδιο τον parser. Το HotPDF εκθέτει δύο μετρητές που περιγράφουν πώς συμπεριφέρθηκε η φόρτωση αντί για το τι περιέχει το έγγραφο

Η GetLastParserArenaStatistics αναφέρει την retained arena blocks που χρησιμοποιείται για βραχύβια tokens και buffers του parser: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount και TemporaryObjectElisionCount. Αυτό το τελευταίο πεδίο μετρά κλειδιά dictionary που αναλύθηκαν απευθείας στην arena αντί μέσω ενός προσωρινού αντικειμένου ονόματος, το allocation που κάποτε κυριαρχούσε στην ανάλυση αρχείων πλούσιων σε dictionaries

Η GetDocumentStringInternStatistics αναφέρει το pool intern σε επίπεδο εγγράφου που αποεπαναλαμβάνει επαναλαμβανόμενα ονόματα PDF, τελεστές content stream και αμετάβλητα strings έως 64 bytes. Σας δίνει RequestCount, HitCount, MissCount, BypassCount, RetainedBytes και ReusedBytes, μαζί με τα όρια σε ισχύ. Το pool είναι σκόπιμα οριοθετημένο στις 65.536 εγγραφές και 4 MiB, οπότε ένα εχθρικό έγγραφο που παράγει ένα εκατομμύριο μοναδικά ονόματα δεν μπορεί να μετατρέψει μια βελτιστοποίηση μνήμης σε ενισχυτή μνήμης· μόλις φτάσει το όριο, τα επόμενα strings παρακάμπτουν το pool και το BypassCount ανεβαίνει

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;

Μια υψηλή τιμή ReusedBytes με χαμηλό BypassCount σημαίνει ότι το έγγραφο έχει το επαναλαμβανόμενο λεξιλόγιο που έχουν τα περισσότερα πραγματικά PDF και το pool αποδίδει. Ένα BypassCount που πλησιάζει το RequestCount σημαίνει κάτι ασυνήθιστο: είτε ένα γνησίως τεράστιο έγγραφο, είτε ένα που παράγει σκόπιμα μοναδικά ονόματα, κάτι που είναι ένα ήπιο σήμα άξιο καταγραφής σε ένα μη έμπιστο μονοπάτι εισαγωγής

Μια σειρά διαλογής που δουλεύει

Ξεκινήστε με το CatalogRetainedBytes από τις πληροφορίες του γραφήματος. Αν αυτός ο αριθμός είναι κοντά στην αύξηση μνήμης της διεργασίας σας, η μνήμη βρίσκεται στο έγγραφο και το γράφημα θα σας δείξει πού. Αν είναι πολύ χαμηλότερος, η μνήμη βρίσκεται στις δικές σας caches, σε rendered bitmaps, ή στον parser, και οι μετρητές arena θα σας πουν ποιο

Έπειτα πάρτε τους δέκα κορυφαίους κόμβους κατά EstimatedRetainedBytes και κοιτάξτε το ObjectType τους. Image XObjects στην κορυφή σημαίνει ότι το αρχείο είναι βαρύ σε σαρώσεις και το downsampling είναι η λύση. Font descriptors στην κορυφή σημαίνει ότι ενσωματώθηκαν πλήρη faces εκεί όπου θα αρκούσαν subsets. Content streams στην κορυφή συνήθως σημαίνει παραγόμενα διανυσματικά γραφικά, συχνά χάρτες ή εξαγωγές CAD. Μόνο μετά από αυτό αξίζει να κοιτάξετε μη προσβάσιμα αντικείμενα και συμπεριφορά allocator. Δουλεύοντας με αυτή τη σειρά, συνήθως θα βρείτε την απάντηση στα πρώτα δύο βήματα, και για πολύ μεγάλα έγγραφα η προσέγγιση streaming που περιγράφεται στο API απευθείας αρχείου είναι συχνά η δομική λύση αντί για οποιαδήποτε βελτιστοποίηση ανά αντικείμενο

Όλα αυτά τα diagnostics είναι απλές κλήσεις Pascal που επιστρέφουν απλές εγγραφές, οπότε εντάσσονται απευθείας σε ένα υπάρχον μονοπάτι logging ή telemetry. Το HotPDF είναι ένα εγγενές VCL PDF component για Delphi και C++Builder με πλήρη πηγαίο κώδικα· η αναφορά API και μια δοκιμαστική έκδοση βρίσκονται στη σελίδα του HotPDF Delphi PDF component