[llvm-branch-commits] [llvm] [BOLT] Create and release .dwo DWARF contexts incrementally (PR #214900)

Rafael Auler via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Fri Aug 7 17:46:48 PDT 2026


https://github.com/rafaelauler created https://github.com/llvm/llvm-project/pull/214900

BOLT opened a DWARFContext for every .dwo during
readDebugInfo and kept them all alive until teardown. On large split-DWARF targets that is tens of GiB held resident through emission, the point of peak RSS.

Make the DWOCUs map a lazily-populated cache instead:

 * Use the newly added DWARFUnit::clearDWO()/hasDWO() to directly manage DWARFUnit's DIE caching mechanism.
 * BinaryContext::getDWOCU() opens a context on demand (keyed off a stable DWOId -> skeleton CU map).
 * Release contexts as soon as they are done with: all of them at the end of readDebugInfo, and per-bucket at the DWARF rewrite merge point.
 * Remove DWOCUs map, which became redundant and whose purpose can now be served by the new id-to-skeleton map, and then fetching the split CU from the skeleton via getNonSkeletonUnitDIE().

Also drop DWARFRewriter::writeDWOFiles()'s use of getDWOContext(), which dereferenced an arbitrary DWOCUs entry -- with contexts now being freed concurrently that would be a use-after-free. The CU's own context answers the .dwp question identically.

>From 76e43e06c6136d968fa28075834ade01b1f389e8 Mon Sep 17 00:00:00 2001
From: Rafael Auler <rafaelauler at fb.com>
Date: Thu, 30 Jul 2026 18:45:01 -0700
Subject: [PATCH] [BOLT] Create and release .dwo DWARF contexts incrementally

BOLT opened a DWARFContext for every .dwo during
readDebugInfo and kept them all alive until teardown. On large
split-DWARF targets that is tens of GiB held resident through
emission, the point of peak RSS.

Make the DWOCUs map a lazily-populated cache instead:

 * Use the newly added DWARFUnit::clearDWO()/hasDWO() to directly
   manage DWARFUnit's DIE caching mechanism.
 * BinaryContext::getDWOCU() opens a context on demand (keyed off a
   stable DWOId -> skeleton CU map).
 * Release contexts as soon as they are done with: all of them at the
   end of readDebugInfo, and per-bucket at the DWARF rewrite merge
   point.
 * Remove DWOCUs map, which became redundant and whose purpose can
   now be served by the new id-to-skeleton map, and then fetching
   the split CU from the skeleton via getNonSkeletonUnitDIE().

Also drop DWARFRewriter::writeDWOFiles()'s use of getDWOContext(),
which dereferenced an arbitrary DWOCUs entry -- with contexts now
being freed concurrently that would be a use-after-free. The CU's own
context answers the .dwp question identically.
---
 bolt/include/bolt/Core/BinaryContext.h |  41 +++++--
 bolt/lib/Core/BinaryContext.cpp        | 160 +++++++++++++++++--------
 bolt/lib/Rewrite/DWARFRewriter.cpp     |  23 ++--
 bolt/lib/Rewrite/RewriteInstance.cpp   |   2 +
 4 files changed, 151 insertions(+), 75 deletions(-)

diff --git a/bolt/include/bolt/Core/BinaryContext.h b/bolt/include/bolt/Core/BinaryContext.h
index 240e5a75d1de5..9c4f21a335cfb 100644
--- a/bolt/include/bolt/Core/BinaryContext.h
+++ b/bolt/include/bolt/Core/BinaryContext.h
@@ -261,15 +261,18 @@ class BinaryContext {
   /// The runtime library.
   std::unique_ptr<RuntimeLibrary> RtLibrary;
 
-  /// DWP Context.
-  std::shared_ptr<DWARFContext> DWPContext;
-
   /// Decoded pseudo probes.
   std::shared_ptr<MCPseudoProbeDecoder> PseudoProbeDecoder;
 
-  /// A map of DWO Ids to CUs.
+  /// Populated once in preprocessDWODebugInfo() and immutable thereafter; it
+  /// lets getDWOCU() (re-)open a DWO context on demand. The context is then
+  /// owned and cached by the skeleton unit itself (DWARFUnit::hasDWO()).
   using DWOIdToCUMapType = std::unordered_map<uint64_t, DWARFUnit *>;
-  DWOIdToCUMapType DWOCUs;
+  DWOIdToCUMapType DWOIdToSkeletonCU;
+
+  /// With a package every split CU shares one DWARFContext, which changes how
+  /// it may be released -- see releaseDWOCU().
+  bool UsesDWP{false};
 
   bool ContainsDwarf5{false};
   bool ContainsDwarfLegacy{false};
@@ -326,14 +329,32 @@ class BinaryContext {
 
   void clearFragmentsToSkip() { FragmentsToSkip.clear(); }
 
-  /// Given DWOId returns CU if it exists in DWOCUs.
+  /// True if split-dwarf files being processed come from a package (as
+  /// opposed to dwo files scattered on disk).
+  bool usesDWP() const { return UsesDWP; }
+
+  /// Given a DWOId, return the corresponding split CU, lazily opening its DWO
+  /// context if needed. Returns std::nullopt if the DWO could not be loaded.
+  ///
+  /// There is no shared mutable state to guard here: DWOIdToSkeletonCU is
+  /// immutable after preprocessing and the context is cached by the skeleton
+  /// unit. Callers must, however, keep a given DWOId to a single thread at a
+  /// time -- during parallel rewriting each DWOId belongs to exactly one
+  /// bucket -- because opening and releasing both mutate the skeleton unit's
+  /// DWO pointer.
   std::optional<DWARFUnit *> getDWOCU(uint64_t DWOId);
 
-  /// Returns DWOContext if it exists.
-  DWARFContext *getDWOContext() const;
+  /// Release the DWO context previously opened for \p DWOId (if any), freeing
+  /// its DWARFContext and parsed unit vector. Same threading contract as
+  /// getDWOCU().
+  void releaseDWOCU(uint64_t DWOId);
+
+  /// Release all currently-open DWO contexts. Used after preprocessing, so that
+  /// contexts are re-opened lazily and during rewriting. Not thread-safe.
+  void releaseAllDWOContexts();
 
-  /// Get Number of DWOCUs in a map.
-  uint32_t getNumDWOCUs() { return DWOCUs.size(); }
+  /// Get the number of split-DWARF CUs in the binary.
+  uint32_t getNumDWOCUs() { return DWOIdToSkeletonCU.size(); }
 
   /// Returns true if DWARF5 is used.
   bool isDWARF5Used() const { return ContainsDwarf5; }
diff --git a/bolt/lib/Core/BinaryContext.cpp b/bolt/lib/Core/BinaryContext.cpp
index 13d7e4bc1a5d6..ef3238edfc635 100644
--- a/bolt/lib/Core/BinaryContext.cpp
+++ b/bolt/lib/Core/BinaryContext.cpp
@@ -1738,18 +1738,78 @@ BinaryFunctionListType BinaryContext::getAllBinaryFunctions() {
   return AllFunctions;
 }
 
+/// Compute the absolute path of the .dwo/.dwp file backing \p SkeletonCU,
+/// honoring --comp-dir-override and relative-path fallbacks. If
+/// \p FellBackToRelative is non-null, it is set to true when the compilation
+/// directory was missing and the relative path was used instead. If
+/// \p DWONameOut is non-null, it receives the raw DW_AT_dwo_name.
+static SmallString<128> getDWOAbsolutePath(DWARFUnit &SkeletonCU,
+                                           bool *FellBackToRelative = nullptr,
+                                           std::string *DWONameOut = nullptr) {
+  std::string DWOName =
+      dwarf::toString(SkeletonCU.getUnitDIE().find(
+                          {dwarf::DW_AT_dwo_name, dwarf::DW_AT_GNU_dwo_name}),
+                      "");
+  SmallString<128> AbsolutePath(DWOName);
+
+  std::string DWOCompDir;
+  if (!opts::CompDirOverride.empty()) {
+    DWOCompDir = opts::CompDirOverride;
+  } else {
+    DWOCompDir = SkeletonCU.getCompilationDir();
+    if (!sys::fs::exists(DWOCompDir) && sys::fs::exists(DWOName)) {
+      DWOCompDir = ".";
+      if (FellBackToRelative)
+        *FellBackToRelative = true;
+    }
+  }
+  // Prevent failures when DWOName is already an absolute path.
+  sys::path::make_absolute(DWOCompDir, AbsolutePath);
+  if (DWONameOut)
+    *DWONameOut = std::move(DWOName);
+  return AbsolutePath;
+}
+
 std::optional<DWARFUnit *> BinaryContext::getDWOCU(uint64_t DWOId) {
-  auto Iter = DWOCUs.find(DWOId);
-  if (Iter == DWOCUs.end())
+  auto Iter = DWOIdToSkeletonCU.find(DWOId);
+  if (Iter == DWOIdToSkeletonCU.end())
+    return std::nullopt;
+  DWARFUnit &SkeletonCU = *Iter->second;
+
+  // The skeleton unit owns and caches its DWO context, so it is the cache:
+  // parseDWO() is a no-op once the context is open, and only then do we need
+  // the .dwo path -- computing it stats the filesystem, so skip that on the
+  // hot path.
+  SmallString<128> AbsolutePath;
+  if (!SkeletonCU.getDWO())
+    AbsolutePath = getDWOAbsolutePath(SkeletonCU);
+
+  DWARFUnit *DWOCU =
+      SkeletonCU
+          .getNonSkeletonUnitDIE(/*ExtractUnitDIEOnly=*/true, AbsolutePath)
+          .getDwarfUnit();
+  // On failure getNonSkeletonUnitDIE() falls back to the skeleton's own DIE.
+  if (!DWOCU || !DWOCU->isDWOUnit())
     return std::nullopt;
+  return DWOCU;
+}
 
-  return Iter->second;
+void BinaryContext::releaseDWOCU(uint64_t DWOId) {
+  // With a .dwp package every split CU aliases one shared DWARFContext
+  if (UsesDWP)
+    return;
+  auto Iter = DWOIdToSkeletonCU.find(DWOId);
+  if (Iter != DWOIdToSkeletonCU.end())
+    Iter->second->clearDWO();
 }
 
-DWARFContext *BinaryContext::getDWOContext() const {
-  if (DWOCUs.empty())
-    return nullptr;
-  return &DWOCUs.begin()->second->getContext();
+void BinaryContext::releaseAllDWOContexts() {
+  // Release via the skeleton map so we free every DWO context regardless of how
+  // it was opened (preprocessDWODebugInfo, the line-table name loop, or
+  // collectDebugScopeBoundaries, which opens CUs directly via
+  // getNonSkeletonUnitDIE).
+  for (auto &KV : DWOIdToSkeletonCU)
+    KV.second->clearDWO();
 }
 
 bool BinaryContext::isValidDwarfUnit(DWARFUnit &DU) const {
@@ -1772,47 +1832,45 @@ void BinaryContext::preprocessDWODebugInfo() {
     DWARFUnit *const DwarfUnit = CU.get();
     if (!isValidDwarfUnit(*DwarfUnit))
       continue;
-    if (std::optional<uint64_t> DWOId = DwarfUnit->getDWOId()) {
-      std::string DWOName = dwarf::toString(
-          DwarfUnit->getUnitDIE().find(
-              {dwarf::DW_AT_dwo_name, dwarf::DW_AT_GNU_dwo_name}),
-          "");
-      SmallString<16> AbsolutePath(DWOName);
-      std::string DWOCompDir = DwarfUnit->getCompilationDir();
-      if (!opts::CompDirOverride.empty()) {
-        DWOCompDir = opts::CompDirOverride;
-      } else if (!sys::fs::exists(DWOCompDir) && sys::fs::exists(DWOName)) {
-        DWOCompDir = ".";
-        this->outs()
-            << "BOLT-WARNING: Debug Fission: Debug Compilation Directory of "
-            << DWOName
-            << " does not exist. Relative path will be used to process .dwo "
-               "files.\n";
-      }
-      // Prevent failures when DWOName is already an absolute path.
-      sys::path::make_absolute(DWOCompDir, AbsolutePath);
-      // Extract only the .dwo CU DIE here: we just need the DWO unit pointer
-      // (for DWOCUs) and the isDWOUnit()/DWOId checks. The full DIE vector is
-      // never read off this cached array -- every consumer streams the DIEs on
-      // demand with DWARFDebugInfoEntry::extractFast (DIEBuilder::
-      // constructFromUnit / collectReferencedTypeSignatures).
-      DWARFUnit *DWOCU =
-          DwarfUnit
-              ->getNonSkeletonUnitDIE(/*ExtractUnitDIEOnly=*/true, AbsolutePath)
-              .getDwarfUnit();
-      if (!DWOCU->isDWOUnit()) {
-        this->outs()
-            << "BOLT-WARNING: Debug Fission: DWO debug information for "
-            << DWOName
-            << " was not retrieved and won't be updated. Please check "
-               "relative path or use '--comp-dir-override' to specify the base "
-               "location.\n";
-        continue;
-      }
-      DWOCUs[*DWOId] = DWOCU;
+    std::optional<uint64_t> DWOId = DwarfUnit->getDWOId();
+    if (!DWOId)
+      continue;
+    std::string DWOName;
+    bool FellBackToRelative = false;
+    SmallString<128> AbsolutePath =
+        getDWOAbsolutePath(*DwarfUnit, &FellBackToRelative, &DWOName);
+    if (FellBackToRelative)
+      this->outs()
+          << "BOLT-WARNING: Debug Fission: Debug Compilation Directory of "
+          << DWOName
+          << " does not exist. Relative path will be used to process .dwo "
+             "files.\n";
+    // Extract only the .dwo CU DIE here: we just need the isDWOUnit()/DWOId
+    // checks. The full DIE vector is never read off this cached array -- every
+    // consumer streams the DIEs on demand with
+    // DWARFDebugInfoEntry::extractFast (DIEBuilder::constructFromUnit /
+    // collectReferencedTypeSignatures).
+    DWARFUnit *DWOCU =
+        DwarfUnit
+            ->getNonSkeletonUnitDIE(/*ExtractUnitDIEOnly=*/true, AbsolutePath)
+            .getDwarfUnit();
+    if (!DWOCU->isDWOUnit()) {
+      this->outs()
+          << "BOLT-WARNING: Debug Fission: DWO debug information for "
+          << DWOName
+          << " was not retrieved and won't be updated. Please check "
+             "relative path or use '--comp-dir-override' to specify the base "
+             "location.\n";
+      continue;
     }
-  }
-  if (!DWOCUs.empty())
+    // Detect a .dwp package on the first split CU we manage to open.
+    if (DWOIdToSkeletonCU.empty())
+      UsesDWP = !DWOCU->getContext().getCUIndex().getRows().empty();
+    // Remember the skeleton CU so its DWO context can be (re-)opened lazily by
+    // getDWOCU() after it is released (see releaseAllDWOContexts).
+    DWOIdToSkeletonCU[*DWOId] = DwarfUnit;
+  }
+  if (!DWOIdToSkeletonCU.empty())
     this->outs() << "BOLT-INFO: processing split DWARF\n";
 }
 
@@ -1925,8 +1983,8 @@ void BinaryContext::preprocessDebugInfo() {
       const char *Name =
           dwarf::toString(CU->getUnitDIE().find(dwarf::DW_AT_name), nullptr);
       if (std::optional<uint64_t> DWOID = CU->getDWOId()) {
-        auto Iter = DWOCUs.find(*DWOID);
-        if (Iter == DWOCUs.end()) {
+        std::optional<DWARFUnit *> DWOCU = getDWOCU(*DWOID);
+        if (!DWOCU) {
           const char *DWOName =
               dwarf::toString(CU->getUnitDIE().find(dwarf::DW_AT_dwo_name),
                               "<missing DW_AT_dwo_name>");
@@ -1935,8 +1993,8 @@ void BinaryContext::preprocessDebugInfo() {
           NumMissingDWOs++;
           continue;
         }
-        Name = dwarf::toString(
-            Iter->second->getUnitDIE().find(dwarf::DW_AT_name), nullptr);
+        Name = dwarf::toString((*DWOCU)->getUnitDIE().find(dwarf::DW_AT_name),
+                               nullptr);
       }
       BinaryLineTable.setRootFile(CU->getCompilationDir(), Name, Checksum,
                                   std::nullopt);
diff --git a/bolt/lib/Rewrite/DWARFRewriter.cpp b/bolt/lib/Rewrite/DWARFRewriter.cpp
index 1c02fc2ffd38e..e416b79f6717f 100644
--- a/bolt/lib/Rewrite/DWARFRewriter.cpp
+++ b/bolt/lib/Rewrite/DWARFRewriter.cpp
@@ -1061,6 +1061,9 @@ void DWARFRewriter::updateDebugInfo() {
     BucketDIEBlders[Idx].reset();
     LocalWriters[Idx].RngListsWriter.reset();
     LocalWriters[Idx].LegacyRangesWriter.reset();
+    for (DWARFUnit *CU : SortedCUs)
+      if (std::optional<uint64_t> DWOId = CU->getDWOId())
+        BC.releaseDWOCU(*DWOId);
   };
 
   for (size_t I = 0; I < TotalTasks; ++I) {
@@ -2250,15 +2253,7 @@ void DWARFRewriter::writeDWOFiles(
     const std::string &DWOName, DebugLocWriter &LocWriter,
     DebugStrOffsetsWriter &StrOffstsWriter, DebugStrWriter &StrWriter,
     DebugRangesSectionWriter &TempRangesSectionWriter) {
-  // Setup DWP code once.
-  DWARFContext *DWOCtx = BC.getDWOContext();
   const uint64_t DWOId = *CU.getDWOId();
-  const DWARFUnitIndex *CUIndex = nullptr;
-  bool IsDWP = false;
-  if (DWOCtx) {
-    CUIndex = &DWOCtx->getCUIndex();
-    IsDWP = !CUIndex->getRows().empty();
-  }
 
   // Skipping CUs that we failed to load.
   std::optional<DWARFUnit *> DWOCU = BC.getDWOCU(DWOId);
@@ -2284,9 +2279,9 @@ void DWARFRewriter::writeDWOFiles(
   std::unique_ptr<ToolOutputFile> TempOut =
       std::make_unique<ToolOutputFile>(AbsolutePath, EC, sys::fs::OF_None);
 
-  const DWARFUnitIndex::Entry *CUDWOEntry = nullptr;
-  if (IsDWP)
-    CUDWOEntry = CUIndex->getFromHash(DWOId);
+  const DWARFUnitIndex::Entry *CUDWOEntry =
+      !BC.usesDWP() ? nullptr
+                    : (*DWOCU)->getContext().getCUIndex().getFromHash(DWOId);
 
   const object::ObjectFile *File =
       (*DWOCU)->getContext().getDWARFObj().getFile();
@@ -2324,7 +2319,7 @@ void DWARFRewriter::writeDWOFiles(
       continue;
     Expected<StringRef> ContentsExp = Section.getContents();
     assert(ContentsExp && "Invalid contents.");
-    if (IsDWP && SectionName == "debug_str.dwo") {
+    if (BC.usesDWP() && SectionName == "debug_str.dwo") {
       if (StrWriter.isInitialized())
         StrDWOContent = StrWriter.getBufferStr();
       else
@@ -2335,7 +2330,7 @@ void DWARFRewriter::writeDWOFiles(
             (*DWOCU)->getContext(), SectionName, *ContentsExp, KnownSections,
             *Streamer, *this, CUDWOEntry, DWOId, OutputData, RangeListssWriter,
             LocWriter, StrOffstsWriter, StrWriter, OverridenSections)) {
-      if (IsDWP && SectionName == "debug_str_offsets.dwo") {
+      if (BC.usesDWP() && SectionName == "debug_str_offsets.dwo") {
         StrOffsetsContent = *OutData;
         continue;
       }
@@ -2343,7 +2338,7 @@ void DWARFRewriter::writeDWOFiles(
     }
   }
 
-  if (IsDWP) {
+  if (BC.usesDWP()) {
     // Handling both .debug_str.dwo and .debug_str_offsets.dwo concurrently. In
     // the original DWP, .debug_str is a deduplicated global table, and the
     // .debug_str.dwo slice for a single CU needs to be extracted according to
diff --git a/bolt/lib/Rewrite/RewriteInstance.cpp b/bolt/lib/Rewrite/RewriteInstance.cpp
index c122614a5ee0f..f7212cfeb97df 100644
--- a/bolt/lib/Rewrite/RewriteInstance.cpp
+++ b/bolt/lib/Rewrite/RewriteInstance.cpp
@@ -3779,6 +3779,8 @@ void RewriteInstance::readDebugInfo() {
                        TimerGroupDesc, opts::TimeRewrite);
     BC->collectDebugScopeBoundaries();
   }
+
+  BC->releaseAllDWOContexts();
 }
 
 void RewriteInstance::preprocessProfileData() {



More information about the llvm-branch-commits mailing list