Artikel Teknis

Temukan Pemboros Memori PDF: Graph Dependensi Object HotPDF

HotPDF mengekspos profil memori dari PDF yang dimuat sebagai sebuah graph yang bisa Anda query: BuildLoadedObjectDependencyGraph mengembalikan satu node per indirect object dengan estimasi shallow size, retained size berbasis dominator, dan flag apakah object tersebut masih reachable dari Catalog dokumen. Itu mengubah "file ini memakai 800 MB" menjadi "object 4173, sebuah image XObject, secara eksklusif menahan 612 MB", yang merupakan fakta yang bisa Anda tindaklanjuti

Perbedaan antara kedua kalimat itu adalah keseluruhan intinya. Shallow size memberi tahu seberapa besar satu object. Retained size memberi tahu seberapa banyak memori yang benar-benar akan dilepaskan jika object itu hilang, dan itulah angka yang menentukan apakah sebuah perbaikan benar-benar membantu

Mengapa total penggunaan memori bukan fakta yang bisa ditindaklanjuti?

Karena di dalam PDF nyaris tidak ada apa pun yang dimiliki oleh persis satu hal. Satu embedded CID font direferensikan dari resource dictionary setiap halaman yang memakainya. Sebuah stream ICC profile mendukung sebuah colour space yang dibagi oleh sepuluh content stream berbeda. Sebuah Form XObject yang dipakai sebagai stamp muncul di seluruh 400 halaman. Jika Anda secara naif menjumlahkan ukuran object per halaman, Anda menghitung font itu 400 kali dan menyimpulkan setiap halaman sangat besar; jika Anda membaginya dengan 400, Anda menyimpulkan tidak ada yang mahal dan memorinya berasal dari tempat lain

Analisis dominator menyelesaikan ambiguitas itu dengan satu-satunya cara yang bertahan saat berhadapan dengan dokumen nyata. Sebuah object X diatribusikan ke object terdekat yang secara eksklusif menahannya, artinya setiap jalur referensi dari Catalog ke X melewati dominator tersebut. Font yang dibagi oleh semua halaman tidak diatribusikan ke halaman mana pun; ia diatribusikan ke node terdekat yang dilewati semua jalur itu, yang biasanya adalah Catalog itu sendiri. Font yang dipakai persis oleh satu halaman diatribusikan ke halaman itu. Hasilnya adalah EstimatedRetainedBytes dijumlahkan secara benar, bukannya dihitung ganda, dan object di puncak daftar adalah object yang penghapusannya benar-benar akan membebaskan memori

Apa yang sebenarnya dikandung graph itu

Setiap THPDFObjectDependencyNode membawa identitas object sebagai ObjectNumber dan GenerationNumber, ditambah ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, identitas immediate dominator-nya, dan ReachableFromCatalog. Setiap THPDFObjectDependencyEdge mencatat source, target, Path dictionary tempat referensi itu ditemukan, dan apakah Resolved

Field Path itu adalah yang paling jarang dimanfaatkan orang. Itulah perbedaan antara sekadar tahu bahwa object 91 menunjuk ke object 4173 dengan tahu bahwa ia melakukannya lewat /Resources/XObject/Im3, yang langsung memberi tahu Anda apakah Anda sedang melihat konten halaman, appearance stream annotation, atau optional-content group yang tidak pernah dirender siapa pun

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;

Kedua batas itu adalah parameter eksplisit dengan default 250.000 object dan 2.000.000 edge. Saat sebuah dokumen melampaui salah satunya, LimitExceeded diatur dan graph yang dikembalikan adalah prefix yang terpotong, bukan sebuah kebohongan: hasil parsial dengan flag, bukan total yang salah secara diam-diam. Naikkan batasnya secara sengaja untuk sebuah run forensik, dan ingat bahwa analisis ini menyusuri seluruh object graph, sehingga tempatnya ada di jalur diagnostik, bukan di render loop Anda

Apa yang diberitahukan sebuah object yang unreachable?

Object dengan ReachableFromCatalog bernilai False adalah memori yang dibawa dokumen tapi tidak akan pernah ditampilkan oleh viewer. Dalam praktiknya itu berasal dari tiga tempat: incremental update yang menggantikan versi lama sebuah object dan meninggalkan yang asli, producer yang menulis object lalu gagal menautkannya, atau file yang rusak yang cross-reference table-nya dibangun ulang dan mengambil definisi yang tidak direferensikan apa pun

Kasus pertama normal dan memang diharapkan, dan itu persis apa yang dirancang untuk dilakukan oleh incremental update dan object stream. Kasus kedua dan ketiga layak diselidiki. Saat sebagian besar retained bytes berada di node yang unreachable, Anda sudah menemukan argumen konkret untuk menulis ulang file itu daripada menambahkannya, dan Anda punya jumlah byte-nya untuk membenarkan waktu pemrosesan ekstra kepada siapa pun yang bertanya

Dua counter allocator yang layak dibaca lebih dulu

Sebelum Anda menyimpulkan bahwa sebuah dokumen memang secara inheren besar, periksa apakah parser itu sendiri yang menjadi biayanya. HotPDF mengekspos dua counter yang mendeskripsikan bagaimana proses loading berperilaku, bukan apa yang dikandung dokumen itu

GetLastParserArenaStatistics melaporkan retained block arena yang dipakai untuk token dan buffer parser yang berumur pendek: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount, dan TemporaryObjectElisionCount. Field terakhir itu menghitung dictionary key yang diparsing langsung ke dalam arena alih-alih lewat temporary name object, yaitu alokasi yang dulu mendominasi parsing file yang sarat dictionary

GetDocumentStringInternStatistics melaporkan intern pool ber-scope dokumen yang mendeduplikasi nama PDF, operator content-stream, dan string immutable hingga 64 byte yang berulang. Ia memberi Anda RequestCount, HitCount, MissCount, BypassCount, RetainedBytes, dan ReusedBytes, beserta batas yang berlaku. Pool ini sengaja dibatasi pada 65.536 entri dan 4 MiB, sehingga dokumen jahat yang menghasilkan sejuta nama unik tidak bisa mengubah optimisasi memori menjadi amplifier memori; begitu batas tercapai, string berikutnya melewati pool dan BypassCount naik

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;

Nilai ReusedBytes yang tinggi dengan BypassCount yang rendah berarti dokumen itu memiliki kosakata repetitif seperti kebanyakan PDF nyata, dan pool-nya memang berguna. BypassCount yang mendekati RequestCount berarti ada sesuatu yang tidak biasa: entah dokumen yang sungguh sangat besar, atau dokumen yang sengaja menghasilkan nama unik, yang merupakan sinyal ringan yang layak dicatat pada intake path yang tidak tepercaya

Urutan triase yang berhasil

Mulai dengan CatalogRetainedBytes dari graph info. Jika angka itu mendekati pertumbuhan process Anda, memorinya ada di dalam dokumen dan graph itu akan menunjukkan di mana. Jika jauh lebih rendah, memorinya ada di cache Anda sendiri, di bitmap yang dirender, atau di parser, dan counter arena akan memberi tahu yang mana

Lalu ambil sepuluh node teratas berdasarkan EstimatedRetainedBytes dan lihat ObjectType-nya. Image XObject di posisi teratas berarti file itu sarat scan dan downsampling adalah perbaikannya. Font descriptor di posisi teratas berarti full face di-embed padahal subset saja sudah cukup. Content stream di posisi teratas biasanya berarti vector graphics yang dihasilkan, sering kali peta atau hasil ekspor CAD. Baru setelah itu ada gunanya melihat object yang unreachable dan perilaku allocator. Bekerja dalam urutan itu, Anda biasanya akan menemukan jawaban di dua langkah pertama, dan untuk dokumen yang sangat besar, pendekatan streaming yang dijelaskan di direct file API workflow sering kali menjadi perbaikan struktural, bukan sekadar optimisasi per-object

Semua diagnostik ini adalah pemanggilan Pascal biasa yang mengembalikan record biasa, sehingga langsung bisa masuk ke jalur logging atau telemetry yang sudah ada. HotPDF adalah komponen PDF VCL native untuk Delphi dan C++Builder dengan source lengkap; referensi API dan build trial ada di halaman komponen PDF Delphi HotPDF