From edfd376f5b041fef94a13a3f3fdf44eef87c56fb Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 23 Mar 2026 22:57:34 +0900 Subject: [PATCH 01/13] docs(json): document diagnosticsSummary counters and parity semantics --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 05159f9..c2fbf06 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,10 @@ Ready-to-adapt workflow examples: --dump-filter prints filter decisions (stderr) ``` +JSON reports include a root-level `diagnosticsSummary` object: +`{"info": , "warning": , "error": }`. +These counters are computed from emitted diagnostics (post-filter), matching human summary totals. + To generate `compile_commands.json` with CMake, configure with `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` and point to the resulting file (often under `build/`). From 1105debbd6b531056fdc9231925452c9212b54ea Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 23 Mar 2026 22:57:59 +0900 Subject: [PATCH 02/13] feat(api): add public DiagnosticSummary helpers for emitted diagnostics --- include/StackUsageAnalyzer.hpp | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/include/StackUsageAnalyzer.hpp b/include/StackUsageAnalyzer.hpp index 4cfcf5a..a2393d3 100644 --- a/include/StackUsageAnalyzer.hpp +++ b/include/StackUsageAnalyzer.hpp @@ -1,8 +1,10 @@ // StackUsageAnalyzer.hpp #pragma once +#include #include #include +#include #include #include @@ -239,6 +241,35 @@ namespace ctrace::stack std::string message; }; + struct DiagnosticSummary + { + std::size_t info = 0; + std::size_t warning = 0; + std::size_t error = 0; + }; + + [[nodiscard]] constexpr DiagnosticSummary + summarizeDiagnostics(std::span diagnostics) noexcept + { + DiagnosticSummary summary; + for (const Diagnostic& diagnostic : diagnostics) + { + switch (diagnostic.severity) + { + case DiagnosticSeverity::Info: + ++summary.info; + break; + case DiagnosticSeverity::Warning: + ++summary.warning; + break; + case DiagnosticSeverity::Error: + ++summary.error; + break; + } + } + return summary; + } + // Global result for a module struct AnalysisResult { @@ -250,6 +281,13 @@ namespace ctrace::stack std::vector diagnostics; }; + [[nodiscard]] constexpr DiagnosticSummary + summarizeDiagnostics(const AnalysisResult& result) noexcept + { + return summarizeDiagnostics( + std::span(result.diagnostics.data(), result.diagnostics.size())); + } + // Serialize an AnalysisResult to a simple JSON format (for CI / GitHub Actions). // `inputFile`: path of the analyzed file (the one you pass to analyzeFile). std::string toJson(const AnalysisResult& result, const std::string& inputFile); From 3b4fd18e7d3cdb51c87d86ee41569f2178f2b57e Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 23 Mar 2026 22:58:34 +0900 Subject: [PATCH 03/13] fix(cross-tu): stabilize resource summary cache seeding for deterministic warnings --- src/app/AnalyzerApp.cpp | 429 +++++++++++++++++++++------------------- 1 file changed, 228 insertions(+), 201 deletions(-) diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp index 2c2ea01..0d4c4a3 100644 --- a/src/app/AnalyzerApp.cpp +++ b/src/app/AnalyzerApp.cpp @@ -154,6 +154,7 @@ struct ModuleTarjan std::vector stack; std::vector> sccs; int nextIndex = 0; + std::byte padding1[64 - sizeof(int)]{}; // cache line isolation void run(std::size_t N, const std::vector>& edges) { @@ -668,15 +669,7 @@ static std::shared_ptr& loadedModules, const AnalysisConfig& cfg); -struct DiagnosticSummary -{ - std::size_t info = 0; - std::size_t warning = 0; - std::size_t error = 0; -}; - static void accumulateSummary(DiagnosticSummary& total, const DiagnosticSummary& add); -static DiagnosticSummary summarizeDiagnostics(const AnalysisResult& result); static void stampResultFilePaths(AnalysisResult& result, const std::string& inputFilename) { @@ -1512,7 +1505,7 @@ static bool writeSummaryCacheFile(const std::filesystem::path& cacheFile, effectObj["action"] = encodeSummaryActionName(effect.action); effectObj["argIndex"] = static_cast(effect.argIndex); effectObj["offset"] = static_cast(effect.offset); - effectObj["viaPointerSlot"] = effect.viaPointerSlot; + effectObj["viaPointerSlot"] = static_cast(effect.viaPointerSlot); effectObj["resourceKind"] = effect.resourceKind; effectArray.push_back(std::move(effectObj)); } @@ -1524,7 +1517,7 @@ static bool writeSummaryCacheFile(const std::filesystem::path& cacheFile, } llvm::json::Object root; - root["schema"] = "resource-summary-cache-v1"; + root["schema"] = "resource-summary-cache-v2"; root["functions"] = std::move(functionArray); std::ofstream out(cacheFile, std::ios::out | std::ios::trunc | std::ios::binary); @@ -1555,7 +1548,7 @@ readSummaryCacheFile(const std::filesystem::path& cacheFile) if (!obj) return std::nullopt; auto schema = obj->getString("schema"); - if (!schema || *schema != "resource-summary-cache-v1") + if (!schema || *schema != "resource-summary-cache-v2") return std::nullopt; const auto* functions = obj->getArray("functions"); @@ -1633,6 +1626,7 @@ buildCrossTUSummaryIndex(const std::vector& loadedModules, !cfg.resourceSummaryMemoryOnly && !cfg.resourceSummaryCacheDir.empty(); const unsigned maxJobs = resolveConfiguredJobs(cfg); std::unordered_map memoryCache; + std::unordered_map finalCacheWrites; std::vector moduleIRHashes; std::vector moduleCompileArgsHashes; const std::string filterHash = computeFunctionFilterSignature(cfg); @@ -1756,224 +1750,274 @@ buildCrossTUSummaryIndex(const std::vector& loadedModules, std::vector moduleSummaries(N); std::size_t totalModuleAnalyses = 0; - for (unsigned level = 0; level <= maxLevel; ++level) + constexpr unsigned kCrossTUGlobalMaxIterations = 12; + bool globalConverged = false; + for (unsigned globalIter = 0; globalIter < kCrossTUGlobalMaxIterations; ++globalIter) { - const auto& group = levelGroups[level]; - if (group.empty()) - continue; - - const auto levelStart = Clock::now(); - const std::string externalHash = hashSummaryIndex(globalIndex); - - auto buildModuleSummary = - [&](std::size_t moduleIndex) -> ctrace::stack::analysis::ResourceSummaryIndex + const std::string beforePassHash = hashSummaryIndex(globalIndex); + for (unsigned level = 0; level <= maxLevel; ++level) { - const analyzer::ScopedHotspot hotspot(cfg.timing, - "app.cross_tu.resource_summary.build_module"); - const LoadedInputModule& loaded = loadedModules[moduleIndex]; - analysis::FunctionFilter filter = analysis::buildFunctionFilter(*loaded.module, cfg); - auto shouldAnalyze = [&](const llvm::Function& F) -> bool - { return filter.shouldAnalyze(F); }; - return analysis::buildResourceLifetimeSummaryIndex(*loaded.module, shouldAnalyze, - cfg.resourceModelPath, &globalIndex); - }; + const auto& group = levelGroups[level]; + if (group.empty()) + continue; - // Try cache for each module at this level, collect modules that need building. - auto tryCacheForModule = [&](std::size_t moduleIndex) -> bool - { - const std::string cacheKeyPayload = std::string(kCacheSchema) + "|" + modelHash + "|" + - externalHash + "|" + filterHash + "|" + - moduleCompileArgsHashes[moduleIndex] + "|" + - moduleIRHashes[moduleIndex]; - const std::string cacheKey = md5Hex(cacheKeyPayload); + const auto levelStart = Clock::now(); + const std::string externalHash = hashSummaryIndex(globalIndex); - if (const auto memIt = memoryCache.find(cacheKey); memIt != memoryCache.end()) + auto buildModuleSummary = + [&](std::size_t moduleIndex) -> ctrace::stack::analysis::ResourceSummaryIndex { - moduleSummaries[moduleIndex] = memIt->second; - return true; - } - if (allowDiskCache) + const analyzer::ScopedHotspot hotspot(cfg.timing, + "app.cross_tu.resource_summary.build_module"); + const LoadedInputModule& loaded = loadedModules[moduleIndex]; + analysis::FunctionFilter filter = + analysis::buildFunctionFilter(*loaded.module, cfg); + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return filter.shouldAnalyze(F); }; + return analysis::buildResourceLifetimeSummaryIndex( + *loaded.module, shouldAnalyze, cfg.resourceModelPath, &globalIndex); + }; + + // Try cache for each module at this level, collect modules that need building. + auto tryCacheForModule = [&](std::size_t moduleIndex) -> bool { - const std::filesystem::path cacheFile = - std::filesystem::path(cfg.resourceSummaryCacheDir) / (cacheKey + ".json"); - auto cached = readSummaryCacheFile(cacheFile); - if (cached) + const std::string cacheKeyPayload = std::string(kCacheSchema) + "|" + modelHash + + "|" + externalHash + "|" + filterHash + "|" + + moduleCompileArgsHashes[moduleIndex] + "|" + + moduleIRHashes[moduleIndex]; + const std::string cacheKey = md5Hex(cacheKeyPayload); + + if (const auto memIt = memoryCache.find(cacheKey); memIt != memoryCache.end()) { - moduleSummaries[moduleIndex] = std::move(*cached); - memoryCache.emplace(cacheKey, moduleSummaries[moduleIndex]); + moduleSummaries[moduleIndex] = memIt->second; return true; } - } - return false; - }; + if (allowDiskCache) + { + const std::filesystem::path cacheFile = + std::filesystem::path(cfg.resourceSummaryCacheDir) / (cacheKey + ".json"); + auto cached = readSummaryCacheFile(cacheFile); + if (cached) + { + moduleSummaries[moduleIndex] = std::move(*cached); + memoryCache.insert_or_assign(cacheKey, moduleSummaries[moduleIndex]); + return true; + } + } + return false; + }; - auto cacheAndStoreModule = [&](std::size_t moduleIndex) - { - const std::string cacheKeyPayload = std::string(kCacheSchema) + "|" + modelHash + "|" + - externalHash + "|" + filterHash + "|" + - moduleCompileArgsHashes[moduleIndex] + "|" + - moduleIRHashes[moduleIndex]; - const std::string cacheKey = md5Hex(cacheKeyPayload); - memoryCache.emplace(cacheKey, moduleSummaries[moduleIndex]); - if (allowDiskCache) + auto cacheAndStoreModule = [&](std::size_t moduleIndex) { - const std::filesystem::path cacheFile = - std::filesystem::path(cfg.resourceSummaryCacheDir) / (cacheKey + ".json"); - (void)writeSummaryCacheFile(cacheFile, moduleSummaries[moduleIndex]); + const std::string cacheKeyPayload = std::string(kCacheSchema) + "|" + modelHash + + "|" + externalHash + "|" + filterHash + "|" + + moduleCompileArgsHashes[moduleIndex] + "|" + + moduleIRHashes[moduleIndex]; + const std::string cacheKey = md5Hex(cacheKeyPayload); + memoryCache.insert_or_assign(cacheKey, moduleSummaries[moduleIndex]); + finalCacheWrites.insert_or_assign(cacheKey, moduleSummaries[moduleIndex]); + }; + + // Collect trivial and cyclic SCCs. + std::vector trivialModules; + std::vector cyclicSCCIndices; + + for (std::size_t sccIdx : group) + { + const auto& scc = sccOrder[sccIdx]; + const bool isTrivial = scc.size() == 1 && !filteredEdges[scc[0]].count(scc[0]); + if (isTrivial) + trivialModules.push_back(scc[0]); + else + cyclicSCCIndices.push_back(sccIdx); } - }; - - // Collect trivial and cyclic SCCs. - std::vector trivialModules; - std::vector cyclicSCCIndices; - - for (std::size_t sccIdx : group) - { - const auto& scc = sccOrder[sccIdx]; - const bool isTrivial = scc.size() == 1 && !filteredEdges[scc[0]].count(scc[0]); - if (isTrivial) - trivialModules.push_back(scc[0]); - else - cyclicSCCIndices.push_back(sccIdx); - } - // Process trivial SCCs: try cache, then build missing ones in parallel. - std::vector missingTrivial; - for (std::size_t moduleIndex : trivialModules) - { - if (!tryCacheForModule(moduleIndex)) - missingTrivial.push_back(moduleIndex); - } + // Sort cyclic SCCs by minimum module index for deterministic processing order. + // Cyclic SCCs at the same level are processed sequentially, and each SCC sees + // the globalIndex effects of previously processed SCCs. Non-deterministic ordering + // (from Tarjan DFS over unordered_set) causes different summaries between runs. + // SCC members are already sorted, so sccOrder[sccIdx][0] is the minimum index. + std::sort(cyclicSCCIndices.begin(), cyclicSCCIndices.end(), + [&](std::size_t a, std::size_t b) + { return sccOrder[a][0] < sccOrder[b][0]; }); + + // Process trivial SCCs: try cache, then build missing ones in parallel. + std::vector missingTrivial; + for (std::size_t moduleIndex : trivialModules) + { + if (!tryCacheForModule(moduleIndex)) + missingTrivial.push_back(moduleIndex); + } - if (!missingTrivial.empty()) - { - if (maxJobs <= 1 || missingTrivial.size() <= 1) + if (!missingTrivial.empty()) { + if (maxJobs <= 1 || missingTrivial.size() <= 1) + { + for (std::size_t moduleIndex : missingTrivial) + moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); + } + else + { + runParallelWork(missingTrivial.size(), maxJobs, + [&](std::size_t slot) + { + const std::size_t moduleIndex = missingTrivial[slot]; + moduleSummaries[moduleIndex] = + buildModuleSummary(moduleIndex); + }); + } for (std::size_t moduleIndex : missingTrivial) - moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); + cacheAndStoreModule(moduleIndex); } - else + totalModuleAnalyses += missingTrivial.size(); + + // Merge trivial SCCs into globalIndex. + for (std::size_t moduleIndex : trivialModules) { - runParallelWork(missingTrivial.size(), maxJobs, - [&](std::size_t slot) - { - const std::size_t moduleIndex = missingTrivial[slot]; - moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); - }); + (void)analysis::mergeResourceSummaryIndex(globalIndex, + moduleSummaries[moduleIndex]); } - for (std::size_t moduleIndex : missingTrivial) - cacheAndStoreModule(moduleIndex); - } - totalModuleAnalyses += missingTrivial.size(); - - // Merge trivial SCCs into globalIndex. - for (std::size_t moduleIndex : trivialModules) - (void)analysis::mergeResourceSummaryIndex(globalIndex, moduleSummaries[moduleIndex]); - // Process cyclic SCCs with internal iteration. - for (std::size_t sccIdx : cyclicSCCIndices) - { - const auto& scc = sccOrder[sccIdx]; - std::vector sccPrevSummaries(N); - std::unordered_set sccChangedNames; - bool sccConverged = false; - - for (unsigned sccIter = 0; sccIter < kCrossTUMaxIterations; ++sccIter) + // Process cyclic SCCs with internal iteration. + for (std::size_t sccIdx : cyclicSCCIndices) { - std::vector dirtyInSCC; - if (sccIter == 0) - { - dirtyInSCC = std::vector(scc.begin(), scc.end()); - } - else + const auto& scc = sccOrder[sccIdx]; + std::vector sccPrevSummaries(N); + std::unordered_set sccChangedNames; + bool sccConverged = false; + + for (unsigned sccIter = 0; sccIter < kCrossTUMaxIterations; ++sccIter) { - for (std::size_t m : scc) + std::vector dirtyInSCC; + if (sccIter == 0) { - bool isDirty = false; - for (const std::string& callee : filteredResourceCalleeNames[m]) + dirtyInSCC = std::vector(scc.begin(), scc.end()); + } + else + { + for (std::size_t m : scc) { - if (sccChangedNames.count(callee)) + bool isDirty = false; + for (const std::string& callee : filteredResourceCalleeNames[m]) { - isDirty = true; - break; + if (sccChangedNames.count(callee)) + { + isDirty = true; + break; + } } + if (isDirty) + dirtyInSCC.push_back(m); + else + moduleSummaries[m] = sccPrevSummaries[m]; } - if (isDirty) - dirtyInSCC.push_back(m); - else - moduleSummaries[m] = sccPrevSummaries[m]; } - } - for (std::size_t m : dirtyInSCC) - moduleSummaries[m] = buildModuleSummary(m); - totalModuleAnalyses += dirtyInSCC.size(); + for (std::size_t m : dirtyInSCC) + moduleSummaries[m] = buildModuleSummary(m); + totalModuleAnalyses += dirtyInSCC.size(); - analysis::ResourceSummaryIndex sccMerged; - for (std::size_t m : scc) - (void)analysis::mergeResourceSummaryIndex(sccMerged, moduleSummaries[m]); + analysis::ResourceSummaryIndex sccMerged; + for (std::size_t m : scc) + (void)analysis::mergeResourceSummaryIndex(sccMerged, moduleSummaries[m]); - analysis::ResourceSummaryIndex prevSccMerged; - for (std::size_t m : scc) - (void)analysis::mergeResourceSummaryIndex(prevSccMerged, sccPrevSummaries[m]); - const bool iterConverged = - analysis::resourceSummaryIndexEquals(sccMerged, prevSccMerged); + analysis::ResourceSummaryIndex prevSccMerged; + for (std::size_t m : scc) + { + (void)analysis::mergeResourceSummaryIndex(prevSccMerged, + sccPrevSummaries[m]); + } + const bool iterConverged = + analysis::resourceSummaryIndexEquals(sccMerged, prevSccMerged); - sccChangedNames = - analysis::computeChangedResourceFunctionNames(prevSccMerged, sccMerged); + sccChangedNames = + analysis::computeChangedResourceFunctionNames(prevSccMerged, sccMerged); - for (std::size_t m : scc) - sccPrevSummaries[m] = moduleSummaries[m]; + for (std::size_t m : scc) + sccPrevSummaries[m] = moduleSummaries[m]; - if (cfg.timing) - { - coretrace::log(coretrace::Level::Info, - " Resource cyclic SCC (size={}) iteration {}{} (dirty={})\n", - scc.size(), sccIter + 1, iterConverged ? " converged" : "", - dirtyInSCC.size()); + if (cfg.timing) + { + coretrace::log( + coretrace::Level::Info, + " Resource cyclic SCC (size={}) iteration {}{} (dirty={})\n", + scc.size(), sccIter + 1, iterConverged ? " converged" : "", + dirtyInSCC.size()); + } + + if (iterConverged) + { + sccConverged = true; + break; + } + + for (std::size_t m : scc) + (void)analysis::mergeResourceSummaryIndex(globalIndex, moduleSummaries[m]); } - if (iterConverged) + if (!sccConverged) { - sccConverged = true; - break; + coretrace::log(coretrace::Level::Warn, + "Resource cross-TU: cyclic SCC (size={}) reached " + "iteration cap ({})\n", + scc.size(), kCrossTUMaxIterations); } for (std::size_t m : scc) (void)analysis::mergeResourceSummaryIndex(globalIndex, moduleSummaries[m]); } - if (!sccConverged) + if (cfg.timing) { - coretrace::log(coretrace::Level::Warn, - "Resource cross-TU: cyclic SCC (size={}) reached " - "iteration cap ({})\n", - scc.size(), kCrossTUMaxIterations); + const auto levelEnd = Clock::now(); + const auto ms = + std::chrono::duration_cast(levelEnd - levelStart) + .count(); + coretrace::log( + coretrace::Level::Info, + " Resource level {}: {} trivial, {} cyclic ({} modules) in {} ms\n", level, + trivialModules.size(), cyclicSCCIndices.size(), + trivialModules.size() + + [&]() + { + std::size_t n = 0; + for (std::size_t s : cyclicSCCIndices) + n += sccOrder[s].size(); + return n; + }(), + ms); } - - for (std::size_t m : scc) - (void)analysis::mergeResourceSummaryIndex(globalIndex, moduleSummaries[m]); } + const std::string afterPassHash = hashSummaryIndex(globalIndex); if (cfg.timing) { - const auto levelEnd = Clock::now(); - const auto ms = - std::chrono::duration_cast(levelEnd - levelStart) - .count(); - coretrace::log( - coretrace::Level::Info, - " Resource level {}: {} trivial, {} cyclic ({} modules) in {} ms\n", level, - trivialModules.size(), cyclicSCCIndices.size(), - trivialModules.size() + - [&]() - { - std::size_t n = 0; - for (std::size_t s : cyclicSCCIndices) - n += sccOrder[s].size(); - return n; - }(), - ms); + coretrace::log(coretrace::Level::Info, + "Resource global convergence pass {}{} (summary size: {})\n", + globalIter + 1, (afterPassHash == beforePassHash) ? " converged" : "", + globalIndex.functions.size()); + } + if (afterPassHash == beforePassHash) + { + globalConverged = true; + break; + } + } + + if (!globalConverged && cfg.timing) + { + coretrace::log(coretrace::Level::Warn, + "Resource cross-TU: global convergence reached iteration cap ({})\n", + kCrossTUGlobalMaxIterations); + } + + if (allowDiskCache) + { + for (const auto& entry : finalCacheWrites) + { + const std::filesystem::path cacheFile = + std::filesystem::path(cfg.resourceSummaryCacheDir) / (entry.first + ".json"); + (void)writeSummaryCacheFile(cacheFile, entry.second); } } @@ -2446,6 +2490,10 @@ buildCrossTUUninitializedSummaryIndex(const std::vector& load cyclicSCCIndices.push_back(sccIdx); } + // Sort cyclic SCCs by minimum module index for deterministic processing order. + std::sort(cyclicSCCIndices.begin(), cyclicSCCIndices.end(), + [&](std::size_t a, std::size_t b) { return sccOrder[a][0] < sccOrder[b][0]; }); + // Process trivial SCCs in parallel. if (!trivialModules.empty()) { @@ -2626,27 +2674,6 @@ static void accumulateSummary(DiagnosticSummary& total, const DiagnosticSummary& total.error += add.error; } -static DiagnosticSummary summarizeDiagnostics(const AnalysisResult& result) -{ - DiagnosticSummary summary; - for (const auto& d : result.diagnostics) - { - switch (d.severity) - { - case DiagnosticSeverity::Info: - ++summary.info; - break; - case DiagnosticSeverity::Warning: - ++summary.warning; - break; - case DiagnosticSeverity::Error: - ++summary.error; - break; - } - } - return summary; -} - struct RunPlan { AnalysisConfig cfg; From 81ba6881bf3ae559bf827d2d5b996d65d101626e Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 23 Mar 2026 22:59:01 +0900 Subject: [PATCH 04/13] feat(report): expose diagnosticsSummary at JSON root --- src/report/ReportSerialization.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/report/ReportSerialization.cpp b/src/report/ReportSerialization.cpp index f4d7080..9cfbc0e 100644 --- a/src/report/ReportSerialization.cpp +++ b/src/report/ReportSerialization.cpp @@ -132,6 +132,7 @@ namespace ctrace::stack static std::string toJsonImpl(const AnalysisResult& result, const std::string* inputFile, const std::vector* inputFiles) { + const DiagnosticSummary diagnosticsSummary = summarizeDiagnostics(result); std::ostringstream os; os << "{\n"; os << " \"meta\": {\n"; @@ -281,7 +282,12 @@ namespace ctrace::stack os << ","; os << "\n"; } - os << " ]\n"; + os << " ],\n"; + os << " \"diagnosticsSummary\": {\n"; + os << " \"info\": " << diagnosticsSummary.info << ",\n"; + os << " \"warning\": " << diagnosticsSummary.warning << ",\n"; + os << " \"error\": " << diagnosticsSummary.error << "\n"; + os << " }\n"; os << "}\n"; return os.str(); } From 125c99e91417a46a08ce8e30a8c8f0ba9f03fde5 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 24 Mar 2026 16:29:28 +0900 Subject: [PATCH 05/13] perf(ci): restore resource lifetime cache before linux self-analysis --- .github/workflows/ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d506c4..73321a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,6 +117,18 @@ jobs: python3 -u run_test.py --jobs="${TEST_JOBS}" # Self-analysis (Linux only) + + # Resource summary cache — stale entries are harmless (cache key includes + # schema + model + IR hash, so mismatches simply trigger a rebuild). + - name: Restore resource summary cache + if: runner.os == 'Linux' + uses: actions/cache@v4 + with: + path: .cache/resource-lifetime + key: resource-cache-${{ runner.os }}-${{ hashFiles('models/resource-lifetime/**') }} + restore-keys: | + resource-cache-${{ runner.os }}- + - name: Self-analysis (analyze own source code) if: runner.os == 'Linux' run: | @@ -130,7 +142,6 @@ jobs: --json-out artifacts/self-analysis.json \ --fail-on error \ --analyzer-arg=--analysis-profile=fast \ - --analyzer-arg=--resource-summary-cache-memory-only \ --analyzer-arg=--resource-model=models/resource-lifetime/generic.txt - name: Upload SARIF to Code Scanning From ddd436454ddd9fc5408e9671614468d4169cf7d3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 24 Mar 2026 16:29:49 +0900 Subject: [PATCH 06/13] perf(ci): run analyzer once and export SARIF via --sarif-out --- scripts/ci/run_code_analysis.py | 52 +++++++++++++-------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/scripts/ci/run_code_analysis.py b/scripts/ci/run_code_analysis.py index c9ae35d..60bca94 100755 --- a/scripts/ci/run_code_analysis.py +++ b/scripts/ci/run_code_analysis.py @@ -300,12 +300,15 @@ def analyzer_cmd( compdb_path: Path | None, base_dir: str | None, extra_args: list[str], + sarif_out: str | None = None, ) -> list[str]: cmd = [str(analyzer), *[str(x) for x in inputs], f"--format={fmt}"] if compdb_path: cmd.append(f"--compdb={compdb_path}") if base_dir: cmd.append(f"--base-dir={base_dir}") + if sarif_out: + cmd.append(f"--sarif-out={sarif_out}") cmd.extend(extra_args) return cmd @@ -371,25 +374,31 @@ def main() -> int: print("No input files selected.", file=sys.stderr) return 2 - print(f"Running analyzer (JSON) on {len(selected_inputs)} file(s).") - json_cmd = analyzer_cmd( + sarif_out_path: str | None = None + if args.sarif_out: + ensure_parent(Path(args.sarif_out)) + sarif_out_path = str(Path(args.sarif_out).resolve()) + + print(f"Running analyzer on {len(selected_inputs)} file(s).") + cmd = analyzer_cmd( analyzer=analyzer, inputs=selected_inputs, fmt="json", compdb_path=compdb_path, base_dir=args.base_dir, extra_args=args.analyzer_arg, + sarif_out=sarif_out_path, ) - json_run = subprocess.run(json_cmd, check=False, capture_output=True, text=True) - if json_run.returncode != 0: - if json_run.stdout: - sys.stdout.write(json_run.stdout) - if json_run.stderr: - sys.stderr.write(json_run.stderr) - return json_run.returncode + run = subprocess.run(cmd, check=False, capture_output=True, text=True) + if run.returncode != 0: + if run.stdout: + sys.stdout.write(run.stdout) + if run.stderr: + sys.stderr.write(run.stderr) + return run.returncode try: - payload = json.loads(json_run.stdout) + payload = json.loads(run.stdout) except json.JSONDecodeError as exc: print(f"Analyzer returned invalid JSON: {exc}", file=sys.stderr) return 2 @@ -397,7 +406,7 @@ def main() -> int: if args.json_out: json_output_path = Path(args.json_out) ensure_parent(json_output_path) - json_output_path.write_text(json_run.stdout, encoding="utf-8") + json_output_path.write_text(run.stdout, encoding="utf-8") diags = payload.get("diagnostics", []) if not isinstance(diags, list): @@ -410,27 +419,6 @@ def main() -> int: print_diags(diags, args.print_diagnostics) - if args.sarif_out: - print("Running analyzer (SARIF export).") - sarif_cmd = analyzer_cmd( - analyzer=analyzer, - inputs=selected_inputs, - fmt="sarif", - compdb_path=compdb_path, - base_dir=args.base_dir, - extra_args=args.analyzer_arg, - ) - sarif_run = subprocess.run(sarif_cmd, check=False, capture_output=True, text=True) - if sarif_run.returncode != 0: - if sarif_run.stdout: - sys.stdout.write(sarif_run.stdout) - if sarif_run.stderr: - sys.stderr.write(sarif_run.stderr) - return sarif_run.returncode - sarif_output_path = Path(args.sarif_out) - ensure_parent(sarif_output_path) - sarif_output_path.write_text(sarif_run.stdout, encoding="utf-8") - failed = (args.fail_on == "error" and errors > 0) or ( args.fail_on == "warning" and (errors > 0 or warnings > 0) ) From 25cb48ebb9589a3bceb9d1afc29088f6cc61c8b3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 24 Mar 2026 16:30:08 +0900 Subject: [PATCH 07/13] feat(cli): add sarifOutPath to parsed arguments --- include/cli/ArgParser.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/cli/ArgParser.hpp b/include/cli/ArgParser.hpp index c14215b..f9a7ffa 100644 --- a/include/cli/ArgParser.hpp +++ b/include/cli/ArgParser.hpp @@ -26,6 +26,7 @@ namespace ctrace::stack::cli std::vector inputFilenames; std::string sarifBaseDir; + std::string sarifOutPath; std::string configPath; std::string compileCommandsPath; From c76547612bec0b2ee7646b2b54aa10a4256c84ff Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 24 Mar 2026 16:30:31 +0900 Subject: [PATCH 08/13] feat(output): write SARIF report to file without a second analyzer run --- src/app/AnalyzerApp.cpp | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp index 0d4c4a3..f2925f5 100644 --- a/src/app/AnalyzerApp.cpp +++ b/src/app/AnalyzerApp.cpp @@ -1231,6 +1231,44 @@ static int emitSarifOutput(const std::vector& results, const Anal return 0; } +static bool writeSarifToFile(const std::vector& results, const AnalysisConfig& cfg, + const std::vector& inputFilenames, + const std::string& sarifBaseDir, + const NormalizedPathFilters& normalizedFilters, + const std::string& outPath) +{ + const bool applyFilter = + cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty(); + std::string sarifContent; + if (results.size() == 1) + { + AnalysisResult filtered = applyFilter + ? filterResult(results[0].second, cfg, normalizedFilters) + : results[0].second; + filtered = filterWarningsOnly(filtered, cfg); + sarifContent = ctrace::stack::toSarif(filtered, results[0].first, + "coretrace-stack-analyzer", "0.1.0", sarifBaseDir); + } + else + { + AnalysisResult merged = mergeAnalysisResults(results, cfg); + AnalysisResult filtered = + applyFilter ? filterResult(merged, cfg, normalizedFilters) : merged; + filtered = filterWarningsOnly(filtered, cfg); + sarifContent = ctrace::stack::toSarif(filtered, inputFilenames.front(), + "coretrace-stack-analyzer", "0.1.0", sarifBaseDir); + } + + std::ofstream ofs(outPath, std::ios::binary); + if (!ofs) + { + coretrace::log(coretrace::Level::Error, "Failed to open SARIF output file: {}\n", outPath); + return false; + } + ofs << sarifContent; + return ofs.good(); +} + static int emitHumanOutput(const std::vector& results, const AnalysisConfig& cfg, const NormalizedPathFilters& normalizedFilters) { @@ -2680,6 +2718,7 @@ struct RunPlan std::vector inputFilenames; NormalizedPathFilters normalizedFilters; std::string sarifBaseDir; + std::string sarifOutPath; ctrace::stack::cli::OutputFormat outputFormat = ctrace::stack::cli::OutputFormat::Human; std::uint64_t hasFilter : 1 = false; std::uint64_t needsCrossTUResourceSummaries : 1 = false; @@ -2704,6 +2743,7 @@ class RunPlanBuilder plan.inputFilenames = std::move(parsedArgs_.inputFilenames); plan.outputFormat = parsedArgs_.outputFormat; plan.sarifBaseDir = std::move(parsedArgs_.sarifBaseDir); + plan.sarifOutPath = std::move(parsedArgs_.sarifOutPath); if (parsedArgs_.compileCommandsExplicit) { @@ -2870,6 +2910,17 @@ class AnalyzerApp std::unique_ptr outputStrategy = makeOutputStrategy(plan.outputFormat); const int exitCode = outputStrategy->emit(plan, results); + + if (!plan.sarifOutPath.empty()) + { + if (!writeSarifToFile(results, plan.cfg, plan.inputFilenames, plan.sarifBaseDir, + plan.normalizedFilters, plan.sarifOutPath)) + { + return AppResult::failure( + "Failed to write SARIF output to: " + plan.sarifOutPath); + } + } + analyzer::dumpHotspotSummary(std::cerr, plan.cfg.timing); return AppResult::success(exitCode); } From 7c7de52503d81975a41bd3f537cdbbc8fdb0e175 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 24 Mar 2026 16:31:32 +0900 Subject: [PATCH 09/13] feat(cli): support --sarif-out option parsing --- src/cli/ArgParser.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cli/ArgParser.cpp b/src/cli/ArgParser.cpp index 14ac583..1971080 100644 --- a/src/cli/ArgParser.cpp +++ b/src/cli/ArgParser.cpp @@ -1579,6 +1579,17 @@ namespace ctrace::stack::cli continue; } } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--sarif-out", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + parsed.sarifOutPath = std::move(value); + continue; + } + } if (std::strncmp(arg, "--mode=", 7) == 0) { const char* modeStr = arg + 7; From 3ef4a3763483b3f961903f161c98d99ba6562712 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 24 Mar 2026 16:33:07 +0900 Subject: [PATCH 10/13] chore(style): format code with clang-format --- src/app/AnalyzerApp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp index f2925f5..2ed644b 100644 --- a/src/app/AnalyzerApp.cpp +++ b/src/app/AnalyzerApp.cpp @@ -2916,8 +2916,8 @@ class AnalyzerApp if (!writeSarifToFile(results, plan.cfg, plan.inputFilenames, plan.sarifBaseDir, plan.normalizedFilters, plan.sarifOutPath)) { - return AppResult::failure( - "Failed to write SARIF output to: " + plan.sarifOutPath); + return AppResult::failure("Failed to write SARIF output to: " + + plan.sarifOutPath); } } From 66b20220286c9466c951a76ed39b5cc2c702fe46 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Mar 2026 23:14:54 +0900 Subject: [PATCH 11/13] build(cmake): log build options during configuration --- CMakeLists.txt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6a2643..cffc4f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,7 +49,10 @@ FetchContent_MakeAvailable(coretrace-logger) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") -# Options de build +# =========================== +# Build Options +# =========================== + option(BUILD_CLI "Build stack_usage_analyzer CLI tool" ON) option(BUILD_SHARED_LIB "Build shared library variant" ON) option(ENABLE_STACK_USAGE "Emit per-function stack usage (.su) files" ON) @@ -57,6 +60,14 @@ option(ENABLE_WARN_PADDED "Enable -Wpadded warnings" ON) option(ENABLE_WARN_REORDER_INIT_LIST "Enable -Wreorder-init-list when supported" ON) option(ENABLE_Z3_BACKEND "Enable optional Z3 SMT backend if Z3 is available" ON) +message("Building CLI Tool = ${BUILD_CLI}") +message("Building Shared Library = ${BUILD_SHARED_LIB}") +message("Emitting Stack Usage (.su) Files = ${ENABLE_STACK_USAGE}") +message("Enabling -Wpadded Warnings = ${ENABLE_WARN_PADDED}") +message("Enabling -Wreorder-init-list = ${ENABLE_WARN_REORDER_INIT_LIST}") +message("Enabling Z3 SMT Backend = ${ENABLE_Z3_BACKEND}") + + if(ENABLE_WARN_REORDER_INIT_LIST) check_cxx_compiler_flag("-Wreorder-init-list" CTRACE_STACK_HAS_WREORDER_INIT_LIST) endif() From d1e81620cdddb2c6e09ed862cf917684696c4202 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Mar 2026 23:30:00 +0900 Subject: [PATCH 12/13] chore(cmake); refactor cmake --- CMakeLists.txt | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cffc4f4..535b1e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,33 +21,16 @@ set(LLVM_LINK_LLVM_DYLIB ON) find_package(LLVM REQUIRED CONFIG) -include(FetchContent) +message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") +message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") -# Optional ASAN enablement at the top-level to match the cc dependency. -option(ENABLE_DEBUG_ASAN "Enable debug symbols and AddressSanitizer" OFF) -if(DEFINED DEBUG_ASAN) - set(ENABLE_DEBUG_ASAN ${DEBUG_ASAN} CACHE BOOL - "Enable debug symbols and AddressSanitizer" FORCE) -endif() +# =========================== +# INCLUDE .CMAKE +# =========================== -FetchContent_Declare( - cc - GIT_REPOSITORY https://github.com/CoreTrace/coretrace-compiler.git - GIT_TAG main -) -FetchContent_MakeAvailable(cc) - -set(CORETRACE_LOGGER_BUILD_EXAMPLES OFF CACHE BOOL "Disable logger examples" FORCE) -set(CORETRACE_LOGGER_BUILD_TESTS OFF CACHE BOOL "Disable logger tests" FORCE) -include(FetchContent) -FetchContent_Declare(coretrace-logger - GIT_REPOSITORY https://github.com/CoreTrace/coretrace-log.git - GIT_TAG main -) -FetchContent_MakeAvailable(coretrace-logger) +include(${CMAKE_SOURCE_DIR}/cmake/compiler/coretrace-compiler.cmake) -message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") -message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") +include(${CMAKE_SOURCE_DIR}/cmake/logger/coretraceLog.cmake) # =========================== # Build Options From c6919c1af0ac65bc50f0dc8c4bfb4c227a4ffade Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Mar 2026 23:30:13 +0900 Subject: [PATCH 13/13] chore(cmake); refactor cmake --- cmake/compiler/coretrace-compiler.cmake | 15 +++++++++++++++ cmake/logger/coretraceLog.cmake | 10 ++++++++++ 2 files changed, 25 insertions(+) create mode 100644 cmake/compiler/coretrace-compiler.cmake create mode 100644 cmake/logger/coretraceLog.cmake diff --git a/cmake/compiler/coretrace-compiler.cmake b/cmake/compiler/coretrace-compiler.cmake new file mode 100644 index 0000000..17a80fc --- /dev/null +++ b/cmake/compiler/coretrace-compiler.cmake @@ -0,0 +1,15 @@ +include(FetchContent) + +# Optional ASAN enablement at the top-level to match the cc dependency. +option(ENABLE_DEBUG_ASAN "Enable debug symbols and AddressSanitizer" OFF) +if(DEFINED DEBUG_ASAN) + set(ENABLE_DEBUG_ASAN ${DEBUG_ASAN} CACHE BOOL + "Enable debug symbols and AddressSanitizer" FORCE) +endif() + +FetchContent_Declare( + cc + GIT_REPOSITORY https://github.com/CoreTrace/coretrace-compiler.git + GIT_TAG main +) +FetchContent_MakeAvailable(cc) diff --git a/cmake/logger/coretraceLog.cmake b/cmake/logger/coretraceLog.cmake new file mode 100644 index 0000000..eb115cd --- /dev/null +++ b/cmake/logger/coretraceLog.cmake @@ -0,0 +1,10 @@ +set(CORETRACE_LOGGER_BUILD_EXAMPLES OFF CACHE BOOL "Disable logger examples" OFF) +set(CORETRACE_LOGGER_BUILD_TESTS OFF CACHE BOOL "Disable logger tests" OFF) + +include(FetchContent) + +FetchContent_Declare(coretrace-logger + GIT_REPOSITORY https://github.com/CoreTrace/coretrace-log.git + GIT_TAG main +) +FetchContent_MakeAvailable(coretrace-logger)