Skip to content

Commit 3095d3a

Browse files
authored
[lldb] Add count for number of DWO files loaded in statistics (#144424)
## Summary A new `totalLoadedDwoFileCount` and `totalDwoFileCount` counters to available statisctics when calling "statistics dump". 1. `GetDwoFileCounts ` is created, and returns a pair of ints representing the number of loaded DWO files and the total number of DWO files, respectively. An override is implemented for `SymbolFileDWARF` that loops through each compile unit, and adds to a counter if it's a DWO unit, and then uses `GetDwoSymbolFile(false)` to check whether the DWO file was already loaded/parsed. 3. In `Statistics`, use `GetSeparateDebugInfo` to sum up the total number of loaded/parsed DWO files along with the total number of DWO files. This is done by checking whether the DWO file was already successfully `loaded` in the collected DWO data, anding adding to the `totalLoadedDwoFileCount`, and adding to `totalDwoFileCount` for all CU units. ## Expected Behavior - When binaries are compiled with split-dwarf and separate DWO files, `totalLoadedDwoFileCount` would be the number of loaded DWO files and `totalDwoFileCount` would be the total count of DWO files. - When using a DWP file instead of separate DWO files, `totalLoadedDwoFileCount` would be the number of parsed compile units, while `totalDwoFileCount` would be the total number of CUs in the DWP file. This should be similar to the counts we get from loading separate DWO files rather than only counting whether a single DWP file was loaded. - When not using split-dwarf, we expect both `totalDwoFileCount` and `totalLoadedDwoFileCount` to be 0 since no separate debug info is loaded. ## Testing **Manual Testing** On an internal script that has many DWO files, `statistics dump` was called before and after a `type lookup` command. The `totalLoadedDwoFileCount` increased as expected after the `type lookup`. ``` (lldb) statistics dump { ... "totalLoadedDwoFileCount": 29, } (lldb) type lookup folly::Optional<unsigned int>::Storage typedef std::conditional<true, folly::Optional<unsigned int>::StorageTriviallyDestructible, folly::Optional<unsigned int>::StorageNonTriviallyDestructible>::type typedef std::conditional<true, folly::Optional<unsigned int>::StorageTriviallyDestructible, folly::Optional<unsigned int>::StorageNonTriviallyDestructible>::type ... (lldb) statistics dump { ... "totalLoadedDwoFileCount": 2160, } ``` **Unit test** Added three unit tests that build with new "third.cpp" and "baz.cpp" files. For tests with w/ flags `-gsplit-dwarf -gpubnames`, this generates 2 DWO files. Then, the test incrementally adds breakpoints, and does a type lookup, and the count should increase for each of these as new DWO files get loaded to support these. ``` $ bin/lldb-dotest -p TestStats.py ~/llvm-sand/external/llvm-project/lldb/test/API/commands/statistics/basic/ ---------------------------------------------------------------------- Ran 20 tests in 211.738s OK (skipped=3) ```
1 parent 97e8266 commit 3095d3a

File tree

10 files changed

+235
-8
lines changed

10 files changed

+235
-8
lines changed

lldb/include/lldb/Symbol/SymbolFile.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,14 @@ class SymbolFile : public PluginInterface {
472472
return false;
473473
};
474474

475+
/// Get number of loaded/parsed DWO files. This is emitted in "statistics
476+
/// dump"
477+
///
478+
/// \returns
479+
/// A pair containing (loaded_dwo_count, total_dwo_count). If this
480+
/// symbol file doesn't support DWO files, both counts will be 0.
481+
virtual std::pair<uint32_t, uint32_t> GetDwoFileCounts() { return {0, 0}; }
482+
475483
virtual lldb::TypeSP
476484
MakeType(lldb::user_id_t uid, ConstString name,
477485
std::optional<uint64_t> byte_size, SymbolContextScope *context,

lldb/include/lldb/Target/Statistics.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ struct ModuleStats {
153153
bool symtab_stripped = false;
154154
bool debug_info_had_variable_errors = false;
155155
bool debug_info_had_incomplete_types = false;
156+
uint32_t dwo_file_count = 0;
157+
uint32_t loaded_dwo_file_count = 0;
156158
};
157159

158160
struct ConstStringStats {

lldb/packages/Python/lldbsuite/test/builders/builder.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,25 @@ def getLLDBObjRoot(self):
247247
def _getDebugInfoArgs(self, debug_info):
248248
if debug_info is None:
249249
return []
250-
if debug_info == "dwarf":
251-
return ["MAKE_DSYM=NO"]
252-
if debug_info == "dwo":
253-
return ["MAKE_DSYM=NO", "MAKE_DWO=YES"]
254-
if debug_info == "gmodules":
255-
return ["MAKE_DSYM=NO", "MAKE_GMODULES=YES"]
256-
return None
250+
251+
debug_options = debug_info if isinstance(debug_info, list) else [debug_info]
252+
option_flags = {
253+
"dwarf": {"MAKE_DSYM": "NO"},
254+
"dwo": {"MAKE_DSYM": "NO", "MAKE_DWO": "YES"},
255+
"gmodules": {"MAKE_DSYM": "NO", "MAKE_GMODULES": "YES"},
256+
"debug_names": {"MAKE_DEBUG_NAMES": "YES"},
257+
"dwp": {"MAKE_DSYM": "NO", "MAKE_DWP": "YES"},
258+
}
259+
260+
# Collect all flags, with later options overriding earlier ones
261+
flags = {}
262+
263+
for option in debug_options:
264+
if not option or option not in option_flags:
265+
return None # Invalid options
266+
flags.update(option_flags[option])
267+
268+
return [f"{key}={value}" for key, value in flags.items()]
257269

258270
def getBuildCommand(
259271
self,

lldb/packages/Python/lldbsuite/test/make/Makefile.rules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ ifeq "$(MAKE_DWO)" "YES"
276276
CFLAGS += -gsplit-dwarf
277277
endif
278278

279+
ifeq "$(MAKE_DEBUG_NAMES)" "YES"
280+
CFLAGS += -gpubnames
281+
endif
282+
279283
ifeq "$(USE_PRIVATE_MODULE_CACHE)" "YES"
280284
THE_CLANG_MODULE_CACHE_DIR := $(BUILDDIR)/private-module-cache
281285
else

lldb/source/Plugins/SymbolFile/DWARF/SymbolFileDWARF.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4420,3 +4420,32 @@ void SymbolFileDWARF::GetCompileOptions(
44204420
args.insert({comp_unit, Args(flags)});
44214421
}
44224422
}
4423+
4424+
std::pair<uint32_t, uint32_t> SymbolFileDWARF::GetDwoFileCounts() {
4425+
uint32_t total_dwo_count = 0;
4426+
uint32_t loaded_dwo_count = 0;
4427+
4428+
DWARFDebugInfo &info = DebugInfo();
4429+
const size_t num_cus = info.GetNumUnits();
4430+
for (size_t cu_idx = 0; cu_idx < num_cus; cu_idx++) {
4431+
DWARFUnit *dwarf_cu = info.GetUnitAtIndex(cu_idx);
4432+
if (dwarf_cu == nullptr)
4433+
continue;
4434+
4435+
// Check if this is a DWO unit by checking if it has a DWO ID.
4436+
if (!dwarf_cu->GetDWOId().has_value())
4437+
continue;
4438+
4439+
total_dwo_count++;
4440+
4441+
// If we have a DWO symbol file, that means we were able to successfully
4442+
// load it.
4443+
SymbolFile *dwo_symfile =
4444+
dwarf_cu->GetDwoSymbolFile(/*load_all_debug_info=*/false);
4445+
if (dwo_symfile) {
4446+
loaded_dwo_count++;
4447+
}
4448+
}
4449+
4450+
return {loaded_dwo_count, total_dwo_count};
4451+
}

lldb/source/Plugins/SymbolFile/DWARF/SymbolFileDWARF.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,11 @@ class SymbolFileDWARF : public SymbolFileCommon {
282282
bool GetSeparateDebugInfo(StructuredData::Dictionary &d,
283283
bool errors_only) override;
284284

285+
// Gets a pair of loaded and total dwo file counts.
286+
// For split-dwarf files, this reports the counts for successfully loaded DWO
287+
// CUs and total DWO CUs. For non-split-dwarf files, this reports 0 for both.
288+
std::pair<uint32_t, uint32_t> GetDwoFileCounts() override;
289+
285290
DWARFContext &GetDWARFContext() { return m_context; }
286291

287292
const std::shared_ptr<SymbolFileDWARFDwo> &GetDwpSymbolFile();

lldb/source/Target/Statistics.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ json::Value ModuleStats::ToJSON() const {
7373
debug_info_had_incomplete_types);
7474
module.try_emplace("symbolTableStripped", symtab_stripped);
7575
module.try_emplace("symbolTableSymbolCount", symtab_symbol_count);
76+
module.try_emplace("dwoFileCount", dwo_file_count);
77+
module.try_emplace("loadedDwoFileCount", loaded_dwo_file_count);
7678

7779
if (!symbol_locator_time.map.empty()) {
7880
json::Object obj;
@@ -86,7 +88,7 @@ json::Value ModuleStats::ToJSON() const {
8688

8789
if (!symfile_modules.empty()) {
8890
json::Array symfile_ids;
89-
for (const auto symfile_id: symfile_modules)
91+
for (const auto symfile_id : symfile_modules)
9092
symfile_ids.emplace_back(symfile_id);
9193
module.try_emplace("symbolFileModuleIdentifiers", std::move(symfile_ids));
9294
}
@@ -322,6 +324,8 @@ llvm::json::Value DebuggerStats::ReportStatistics(
322324
uint32_t num_modules_with_incomplete_types = 0;
323325
uint32_t num_stripped_modules = 0;
324326
uint32_t symtab_symbol_count = 0;
327+
uint32_t total_loaded_dwo_file_count = 0;
328+
uint32_t total_dwo_file_count = 0;
325329
for (size_t image_idx = 0; image_idx < num_modules; ++image_idx) {
326330
Module *module = target != nullptr
327331
? target->GetImages().GetModuleAtIndex(image_idx).get()
@@ -353,6 +357,10 @@ llvm::json::Value DebuggerStats::ReportStatistics(
353357
for (const auto &symbol_module : symbol_modules.Modules())
354358
module_stat.symfile_modules.push_back((intptr_t)symbol_module.get());
355359
}
360+
std::tie(module_stat.loaded_dwo_file_count, module_stat.dwo_file_count) =
361+
sym_file->GetDwoFileCounts();
362+
total_dwo_file_count += module_stat.dwo_file_count;
363+
total_loaded_dwo_file_count += module_stat.loaded_dwo_file_count;
356364
module_stat.debug_info_index_loaded_from_cache =
357365
sym_file->GetDebugInfoIndexWasLoadedFromCache();
358366
if (module_stat.debug_info_index_loaded_from_cache)
@@ -427,6 +435,8 @@ llvm::json::Value DebuggerStats::ReportStatistics(
427435
{"totalDebugInfoEnabled", num_debug_info_enabled_modules},
428436
{"totalSymbolTableStripped", num_stripped_modules},
429437
{"totalSymbolTableSymbolCount", symtab_symbol_count},
438+
{"totalLoadedDwoFileCount", total_loaded_dwo_file_count},
439+
{"totalDwoFileCount", total_dwo_file_count},
430440
};
431441

432442
if (include_targets) {

lldb/test/API/commands/statistics/basic/TestStats.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ def test_default_no_run(self):
177177
"totalDebugInfoIndexLoadedFromCache",
178178
"totalDebugInfoIndexSavedToCache",
179179
"totalDebugInfoParseTime",
180+
"totalDwoFileCount",
181+
"totalLoadedDwoFileCount",
180182
]
181183
self.verify_keys(debug_stats, '"debug_stats"', debug_stat_keys, None)
182184
if self.getPlatform() != "windows":
@@ -287,6 +289,8 @@ def test_default_with_run(self):
287289
"totalDebugInfoIndexLoadedFromCache",
288290
"totalDebugInfoIndexSavedToCache",
289291
"totalDebugInfoParseTime",
292+
"totalDwoFileCount",
293+
"totalLoadedDwoFileCount",
290294
]
291295
self.verify_keys(debug_stats, '"debug_stats"', debug_stat_keys, None)
292296
stats = debug_stats["targets"][0]
@@ -325,6 +329,8 @@ def test_memory(self):
325329
"totalDebugInfoIndexLoadedFromCache",
326330
"totalDebugInfoIndexSavedToCache",
327331
"totalDebugInfoByteSize",
332+
"totalDwoFileCount",
333+
"totalLoadedDwoFileCount",
328334
]
329335
self.verify_keys(debug_stats, '"debug_stats"', debug_stat_keys, None)
330336

@@ -377,6 +383,8 @@ def test_modules(self):
377383
"totalDebugInfoIndexLoadedFromCache",
378384
"totalDebugInfoIndexSavedToCache",
379385
"totalDebugInfoByteSize",
386+
"totalDwoFileCount",
387+
"totalLoadedDwoFileCount",
380388
]
381389
self.verify_keys(debug_stats, '"debug_stats"', debug_stat_keys, None)
382390
stats = debug_stats["targets"][0]
@@ -397,6 +405,8 @@ def test_modules(self):
397405
"symbolTableLoadedFromCache",
398406
"symbolTableParseTime",
399407
"symbolTableSavedToCache",
408+
"dwoFileCount",
409+
"loadedDwoFileCount",
400410
"triple",
401411
"uuid",
402412
]
@@ -485,6 +495,8 @@ def test_breakpoints(self):
485495
"totalDebugInfoIndexLoadedFromCache",
486496
"totalDebugInfoIndexSavedToCache",
487497
"totalDebugInfoByteSize",
498+
"totalDwoFileCount",
499+
"totalLoadedDwoFileCount",
488500
]
489501
self.verify_keys(debug_stats, '"debug_stats"', debug_stat_keys, None)
490502
target_stats = debug_stats["targets"][0]
@@ -512,6 +524,132 @@ def test_breakpoints(self):
512524
self.verify_keys(
513525
breakpoint, 'target_stats["breakpoints"]', bp_keys_exist, None
514526
)
527+
def test_non_split_dwarf_has_no_dwo_files(self):
528+
"""
529+
Test "statistics dump" and the dwo file count.
530+
Builds a binary without split-dwarf mode, and then
531+
verifies the dwo file count is zero after running "statistics dump"
532+
"""
533+
da = {"CXX_SOURCES": "third.cpp baz.cpp", "EXE": self.getBuildArtifact("a.out")}
534+
self.build(dictionary=da, debug_info=["debug_names"])
535+
self.addTearDownCleanup(dictionary=da)
536+
exe = self.getBuildArtifact("a.out")
537+
target = self.createTestTarget(file_path=exe)
538+
debug_stats = self.get_stats()
539+
self.assertIn("totalDwoFileCount", debug_stats)
540+
self.assertIn("totalLoadedDwoFileCount", debug_stats)
541+
542+
# Verify that the dwo file count is zero
543+
self.assertEqual(debug_stats["totalDwoFileCount"], 0)
544+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 0)
545+
546+
def test_no_debug_names_eager_loads_dwo_files(self):
547+
"""
548+
Test the eager loading behavior of DWO files when debug_names is absent by
549+
building a split-dwarf binary without debug_names and then running "statistics dump".
550+
DWO file loading behavior:
551+
- With debug_names: DebugNamesDWARFIndex allows for lazy loading.
552+
DWO files are loaded on-demand when symbols are actually looked up
553+
- Without debug_names: ManualDWARFIndex uses eager loading.
554+
All DWO files are loaded upfront during the first symbol lookup to build a manual index.
555+
"""
556+
da = {"CXX_SOURCES": "third.cpp baz.cpp", "EXE": self.getBuildArtifact("a.out")}
557+
self.build(dictionary=da, debug_info=["dwo"])
558+
self.addTearDownCleanup(dictionary=da)
559+
exe = self.getBuildArtifact("a.out")
560+
target = self.createTestTarget(file_path=exe)
561+
debug_stats = self.get_stats()
562+
self.assertIn("totalDwoFileCount", debug_stats)
563+
self.assertIn("totalLoadedDwoFileCount", debug_stats)
564+
565+
# Verify that all DWO files are loaded
566+
self.assertEqual(debug_stats["totalDwoFileCount"], 2)
567+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 2)
568+
569+
def test_split_dwarf_dwo_file_count(self):
570+
"""
571+
Test "statistics dump" and the dwo file count.
572+
Builds a binary w/ separate .dwo files and debug_names, and then
573+
verifies the loaded dwo file count is the expected count after running
574+
various commands
575+
"""
576+
da = {"CXX_SOURCES": "third.cpp baz.cpp", "EXE": self.getBuildArtifact("a.out")}
577+
# -gsplit-dwarf creates separate .dwo files,
578+
# -gpubnames enables the debug_names accelerator tables for faster symbol lookup
579+
# and lazy loading of DWO files
580+
# Expected output: third.dwo (contains main) and baz.dwo (contains Baz struct/function)
581+
self.build(dictionary=da, debug_info=["dwo", "debug_names"])
582+
self.addTearDownCleanup(dictionary=da)
583+
exe = self.getBuildArtifact("a.out")
584+
target = self.createTestTarget(file_path=exe)
585+
debug_stats = self.get_stats()
586+
587+
# 1) 2 DWO files available but none loaded yet
588+
self.assertEqual(len(debug_stats["modules"]), 1)
589+
self.assertIn("totalLoadedDwoFileCount", debug_stats)
590+
self.assertIn("totalDwoFileCount", debug_stats)
591+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 0)
592+
self.assertEqual(debug_stats["totalDwoFileCount"], 2)
593+
594+
# Since there's only one module, module stats should have the same counts as total counts
595+
self.assertIn("dwoFileCount", debug_stats["modules"][0])
596+
self.assertIn("loadedDwoFileCount", debug_stats["modules"][0])
597+
self.assertEqual(debug_stats["modules"][0]["loadedDwoFileCount"], 0)
598+
self.assertEqual(debug_stats["modules"][0]["dwoFileCount"], 2)
599+
600+
# 2) Setting breakpoint in main triggers loading of third.dwo (contains main function)
601+
self.runCmd("b main")
602+
debug_stats = self.get_stats()
603+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 1)
604+
self.assertEqual(debug_stats["totalDwoFileCount"], 2)
605+
606+
self.assertEqual(debug_stats["modules"][0]["loadedDwoFileCount"], 1)
607+
self.assertEqual(debug_stats["modules"][0]["dwoFileCount"], 2)
608+
609+
# 3) Type lookup forces loading of baz.dwo (contains struct Baz definition)
610+
self.runCmd("type lookup Baz")
611+
debug_stats = self.get_stats()
612+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 2)
613+
self.assertEqual(debug_stats["totalDwoFileCount"], 2)
614+
615+
self.assertEqual(debug_stats["modules"][0]["loadedDwoFileCount"], 2)
616+
self.assertEqual(debug_stats["modules"][0]["dwoFileCount"], 2)
617+
618+
def test_dwp_dwo_file_count(self):
619+
"""
620+
Test "statistics dump" and the loaded dwo file count.
621+
Builds a binary w/ a separate .dwp file and debug_names, and then
622+
verifies the loaded dwo file count is the expected count after running
623+
various commands.
624+
625+
We expect the DWO file counters to reflect the number of compile units
626+
loaded from the DWP file (each representing what was originally a separate DWO file)
627+
"""
628+
da = {"CXX_SOURCES": "third.cpp baz.cpp", "EXE": self.getBuildArtifact("a.out")}
629+
self.build(dictionary=da, debug_info=["dwp", "debug_names"])
630+
self.addTearDownCleanup(dictionary=da)
631+
exe = self.getBuildArtifact("a.out")
632+
target = self.createTestTarget(file_path=exe)
633+
debug_stats = self.get_stats()
634+
635+
# Initially: 2 DWO files available but none loaded yet
636+
self.assertIn("totalLoadedDwoFileCount", debug_stats)
637+
self.assertIn("totalDwoFileCount", debug_stats)
638+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 0)
639+
self.assertEqual(debug_stats["totalDwoFileCount"], 2)
640+
641+
# Setting breakpoint in main triggers parsing of the CU within a.dwp corresponding to third.dwo (contains main function)
642+
self.runCmd("b main")
643+
debug_stats = self.get_stats()
644+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 1)
645+
self.assertEqual(debug_stats["totalDwoFileCount"], 2)
646+
647+
# Type lookup forces parsing of the CU within a.dwp corresponding to baz.dwo (contains struct Baz definition)
648+
self.runCmd("type lookup Baz")
649+
debug_stats = self.get_stats()
650+
self.assertEqual(debug_stats["totalDwoFileCount"], 2)
651+
self.assertEqual(debug_stats["totalLoadedDwoFileCount"], 2)
652+
515653

516654
@skipUnlessDarwin
517655
@no_debug_info_test
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Helper that the lldb command `statistics dump` works in split-dwarf mode.
2+
3+
struct Baz {
4+
int x;
5+
bool y;
6+
};
7+
8+
void baz() {
9+
Baz b;
10+
b.x = 1;
11+
b.y = true;
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Test that the lldb command `statistics dump` works.
2+
3+
void baz();
4+
int main(void) {
5+
baz();
6+
return 0;
7+
}

0 commit comments

Comments
 (0)