From 11bdafe280d67007eb6e15a72399d10a04839a66 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 5 Nov 2026 05:00:53 +0000 Subject: [PATCH 1/3] test(resource): pin which names ruleMatchesFunction compares a rule with --- test/unit/analyzer_module_unit_tests.cpp | 91 ++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/unit/analyzer_module_unit_tests.cpp b/test/unit/analyzer_module_unit_tests.cpp index 698b961..5464401 100644 --- a/test/unit/analyzer_module_unit_tests.cpp +++ b/test/unit/analyzer_module_unit_tests.cpp @@ -1218,6 +1218,96 @@ namespace "ResourceModel: an unknown qualifier is an error naming the line"); return report.failures == 0; } + // Pins which names a rule's pattern is compared with: the mangled name, the + // demangled name and the demangled name up to its first '(', exactly or as a glob. + bool testResourceRuleMatchesFunction(TestReport& report) + { + using namespace ctrace::stack::analysis; + + struct Case + { + const char* pattern; + const char* function; + bool expected; + }; + // _ZN2ns6Handle4openEPKc -> ns::Handle::open(char const*) + // _ZN2ns6Handle5closeEv -> ns::Handle::close() + // _Z11acquire_bufPPv -> acquire_buf(void**) + // fopen, _Zbogus -> left unchanged by the demangler + // f -> float (the demangler reads it as a type) + const Case cases[] = { + // A C name the demangler leaves unchanged. + {"fopen", "fopen", true}, + {"fope", "fopen", false}, + {"FOPEN", "fopen", false}, + {"fop*", "fopen", true}, + {"f?pen", "fopen", true}, + {"*open", "fopen", true}, + {"fopen*", "fopen", true}, + {"?open", "fopen", true}, + {"??open", "fopen", false}, + {"_Zbogus", "_Zbogus", true}, + {"_Zbog*", "_Zbogus", true}, + // Exact patterns against each of the three names. + {"_ZN2ns6Handle4openEPKc", "_ZN2ns6Handle4openEPKc", true}, + {"ns::Handle::open(char const*)", "_ZN2ns6Handle4openEPKc", true}, + {"ns::Handle::open", "_ZN2ns6Handle4openEPKc", true}, + {"ns::Handle::open", "_ZN2ns6Handle5closeEv", false}, + {"open", "_ZN2ns6Handle4openEPKc", false}, + {"Handle::open", "_ZN2ns6Handle4openEPKc", false}, + {"ns::Handle::open(", "_ZN2ns6Handle4openEPKc", false}, + {"acquire_buf", "_Z11acquire_bufPPv", true}, + {"acquire_buf(void**)", "_Z11acquire_bufPPv", true}, + {"float", "f", true}, + {"f", "f", true}, + // Globs against each of the three names. + {"_ZN2ns*", "_ZN2ns6Handle4openEPKc", true}, + {"*4openEPKc", "_ZN2ns6Handle4openEPKc", true}, + {"ns::Handle::*", "_ZN2ns6Handle4openEPKc", true}, + {"ns::Handle::*", "_ZN2ns6Handle5closeEv", true}, + {"*(char const*)", "_ZN2ns6Handle4openEPKc", true}, + {"*(char const*)", "_ZN2ns6Handle5closeEv", false}, + {"ns::*::open", "_ZN2ns6Handle4openEPKc", true}, + {"ns::*::open", "_ZN2ns6Handle5closeEv", false}, + {"*open", "_ZN2ns6Handle4openEPKc", true}, + {"*::clos?", "_ZN2ns6Handle5closeEv", true}, + {"acquire_*", "_Z11acquire_bufPPv", true}, + {"release_*", "_Z11acquire_bufPPv", false}, + {"flo*", "f", true}, + // '[' selects glob matching but is compared literally. + {"a[b", "a[b", true}, + {"a[b", "ab", false}, + {"a[b*", "a[bc", true}, + }; + + const auto check = [&](const char* round) + { + llvm::LLVMContext context; + llvm::Module module("rule-match", context); + llvm::FunctionType* type = + llvm::FunctionType::get(llvm::Type::getVoidTy(context), false); + for (const Case& c : cases) + { + llvm::Function* callee = module.getFunction(c.function); + if (callee == nullptr) + { + callee = llvm::Function::Create(type, llvm::Function::ExternalLinkage, + c.function, &module); + } + ResourceRule rule; + rule.functionPattern = c.pattern; + report.expect(ruleMatchesFunction(rule, *callee) == c.expected, + std::string("ResourceModel: ") + round + " pattern '" + c.pattern + + (c.expected ? "' matches '" : "' does not match '") + c.function + + "'"); + } + }; + // Twice, each time in a fresh module: results do not depend on what was matched + // before nor on which llvm::Function carries the name. + check("first round,"); + check("second round,"); + return report.failures == 0; + } bool testOwnershipFactCollector(const std::filesystem::path& repoRoot, TestReport& report) { using namespace ctrace::stack::analysis; @@ -2924,6 +3014,7 @@ int main(int argc, char** argv) (void)testStackBufferPhiCycle(repoRoot, report); (void)testStackBufferPointerRing(report); (void)testResourceModelConditions(report); + (void)testResourceRuleMatchesFunction(report); (void)testOwnershipFactCollector(repoRoot, report); (void)testOwnershipCollectorExceptions(repoRoot, report); (void)testOwnershipSummaryIndex(repoRoot, report); From 7cd302e03069a52327c58e4d3e5088f07466e256 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 5 Nov 2026 05:03:08 +0000 Subject: [PATCH 2/3] perf(resource): demangle each callee once instead of once per rule ruleMatchesFunction demangled the callee's name on every call, so once per rule of the model, at every fixpoint iteration and every pass. It now reads the demangled name and its base from a per-thread cache keyed on the mangled name, of which both are a pure function: no synchronization is needed, and an entry stays valid after the module that held the callee is freed. The cache is cleared when it holds 65536 names, which only bounds its memory. The names a pattern is compared with, and how, are unchanged. --- src/analysis/ResourceLifetimeAnalysis.cpp | 47 ++++++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/analysis/ResourceLifetimeAnalysis.cpp b/src/analysis/ResourceLifetimeAnalysis.cpp index 8751790..089db92 100644 --- a/src/analysis/ResourceLifetimeAnalysis.cpp +++ b/src/analysis/ResourceLifetimeAnalysis.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -1966,20 +1967,54 @@ namespace ctrace::stack::analysis using namespace lifetime_detail; + namespace + { + struct DemangledNames + { + std::string demangled; + std::string demangledBase; + }; + + // A rule is matched against a callee once per rule of the model, at every + // iteration: demangle each name once instead. The entry is keyed on the mangled + // name, of which it is a pure function, so it stays valid after the module that + // held the callee is freed. One cache per thread: the analysis threads share + // nothing. Cleared when full, which only bounds its memory. + const DemangledNames& demangledNamesOf(llvm::StringRef mangled) + { + constexpr std::size_t kMaxCachedNames = 1u << 16; + static thread_local llvm::StringMap cache; + + const auto it = cache.find(mangled); + if (it != cache.end()) + return it->second; + if (cache.size() >= kMaxCachedNames) + cache.clear(); + + const std::string mangledName = mangled.str(); + DemangledNames names; + names.demangled = ctrace_tools::demangle(mangledName.c_str()); + names.demangledBase = names.demangled; + if (const std::size_t pos = names.demangledBase.find('('); pos != std::string::npos) + names.demangledBase = names.demangledBase.substr(0, pos); + + return cache.try_emplace(mangled, std::move(names)).first->second; + } + } // namespace + bool ruleMatchesFunction(const ResourceRule& rule, const llvm::Function& callee) { - const std::string calleeName = callee.getName().str(); - const std::string demangled = ctrace_tools::demangle(calleeName.c_str()); - std::string demangledBase = demangled; - if (const std::size_t pos = demangledBase.find('('); pos != std::string::npos) - demangledBase = demangledBase.substr(0, pos); + const llvm::StringRef calleeName = callee.getName(); + const DemangledNames& names = demangledNamesOf(calleeName); + const std::string& demangled = names.demangled; + const std::string& demangledBase = names.demangledBase; const llvm::StringRef pattern(rule.functionPattern); const bool hasGlob = pattern.contains('*') || pattern.contains('?') || pattern.contains('['); if (!hasGlob) { - return rule.functionPattern == calleeName || rule.functionPattern == demangled || + return pattern == calleeName || rule.functionPattern == demangled || rule.functionPattern == demangledBase; } From ca191e5424025dedaeef7be431c31ab98de210ca Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 5 Nov 2026 23:05:14 +0100 Subject: [PATCH 3/3] test(resource): match a full demangled signature that holds no glob character The two full-signature patterns of testResourceRuleMatchesFunction, "ns::Handle::open(char const*)" and "acquire_buf(void**)", both hold a '*', so they take the glob path: the exact comparison with the full demangled name was never exercised, and dropping it left the test green. "ns::Handle::close()" holds no glob character and only equals the full demangled name. It passes on the code before and after the demangling cache, and fails, in both rounds, once that comparison is dropped. --- test/unit/analyzer_module_unit_tests.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/analyzer_module_unit_tests.cpp b/test/unit/analyzer_module_unit_tests.cpp index 5464401..594281e 100644 --- a/test/unit/analyzer_module_unit_tests.cpp +++ b/test/unit/analyzer_module_unit_tests.cpp @@ -1258,6 +1258,7 @@ namespace {"ns::Handle::open(", "_ZN2ns6Handle4openEPKc", false}, {"acquire_buf", "_Z11acquire_bufPPv", true}, {"acquire_buf(void**)", "_Z11acquire_bufPPv", true}, + {"ns::Handle::close()", "_ZN2ns6Handle5closeEv", true}, {"float", "f", true}, {"f", "f", true}, // Globs against each of the three names.