[clang] [clang][analyzer] Register path- and syntax-analysis times / entry point (PR #162089)
Arseniy Zaostrovnykh via cfe-commits
cfe-commits at lists.llvm.org
Mon Oct 6 06:48:28 PDT 2025
https://github.com/necto created https://github.com/llvm/llvm-project/pull/162089
Register and dump per-entry-point time taken by the path-sensitive and syntax-only analyses stages in milliseconds (SyntaxRunningTime, PathRunningTime).
Also introduce a difference between unset stats and 0 for statistics categories for which it makes sense. So now if the syntax analysis does not run for a certain entry point, it's SyntaxRunningTime will have an empty value and not 0.
This patch enables the timers if DumpEntryPointStatsToCSV is set, because in most cases you dump these stats to get a detailed view on analyzer performance.
--
CPP-7097
>From c0d2a8d4e2bf557fbc8b8fb94cbde681c6d9f243 Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Mon, 6 Oct 2025 15:37:02 +0200
Subject: [PATCH] [clang][analyzer] Register path- and syntax-analysis times /
entry point
Register and dump per-entry-point time taken by the path-sensitive and
syntax-only analyses stages in milliseconds (SyntaxRunningTime,
PathRunningTime).
Also introduce a difference between unset stats and 0 for statistics
categories for which it makes sense. So now if the syntax analysis does
not run for a certain entry point, it's SyntaxRunningTime will have an
empty value and not 0.
This patch enables the timers if DumpEntryPointStatsToCSV is set,
because in most cases you dump these stats to get a detailed view on
analyzer performance.
--
CPP-7097
---
.../Core/PathSensitive/EntryPointStats.h | 4 +-
.../Core/PathSensitive/FunctionSummary.h | 8 +++
.../StaticAnalyzer/Core/EntryPointStats.cpp | 62 ++++++++++++++-----
.../Frontend/AnalysisConsumer.cpp | 48 +++++++++++++-
.../analyzer-stats/entry-point-stats.cpp | 10 +--
5 files changed, 110 insertions(+), 22 deletions(-)
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/EntryPointStats.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/EntryPointStats.h
index 448e40269ca2d..d02f77194ad8c 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/EntryPointStats.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/EntryPointStats.h
@@ -47,7 +47,7 @@ class BoolEPStat : public EntryPointStat {
public:
explicit BoolEPStat(llvm::StringLiteral Name);
- unsigned value() const { return Value && *Value; }
+ std::optional<bool> value() const { return Value; }
void set(bool V) {
assert(!Value.has_value());
Value = V;
@@ -98,7 +98,7 @@ class UnsignedEPStat : public EntryPointStat {
public:
explicit UnsignedEPStat(llvm::StringLiteral Name);
- unsigned value() const { return Value.value_or(0); }
+ std::optional<unsigned> value() const { return Value; }
void reset() { Value.reset(); }
void set(unsigned V) {
assert(!Value.has_value());
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/FunctionSummary.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/FunctionSummary.h
index 761395260a0cf..db4aec7c84754 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/FunctionSummary.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/FunctionSummary.h
@@ -48,6 +48,9 @@ class FunctionSummariesTy {
/// The number of times the function has been inlined.
unsigned TimesInlined : 32;
+ /// Running time for syntax-based AST analysis in milliseconds.
+ std::optional<unsigned> SyntaxRunningTime = std::nullopt;
+
FunctionSummary()
: TotalBasicBlocks(0), InlineChecked(0), MayInline(0),
TimesInlined(0) {}
@@ -69,6 +72,11 @@ class FunctionSummariesTy {
return I;
}
+ FunctionSummary const *findSummary(const Decl *D) const {
+ auto I = Map.find(D);
+ return I == Map.end() ? nullptr : &I->second;
+ }
+
void markMayInline(const Decl *D) {
MapTy::iterator I = findOrInsertSummary(D);
I->second.InlineChecked = 1;
diff --git a/clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp b/clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp
index 62ae62f2f2154..33c5df8132beb 100644
--- a/clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp
+++ b/clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp
@@ -33,7 +33,15 @@ struct Registry {
struct Snapshot {
const Decl *EntryPoint;
- std::vector<bool> BoolStatValues;
+ // Boolean statistics are always set explicitly. If they are not set, their
+ // value is absent resulting in empty CSV cells
+ std::vector<std::optional<bool>> BoolStatValues;
+ // Explicitly set statistics may not have a value set, so they are separate
+ // from other unsigned statistics
+ std::vector<std::optional<unsigned>> UnsignedExplicitlySetStatValues;
+ // These are counting and maximizing statistics that initialize to 0, which
+ // is meaningful even if they are never updated, so their value is always
+ // present.
std::vector<unsigned> UnsignedStatValues;
void dumpAsCSV(llvm::raw_ostream &OS) const;
@@ -48,10 +56,14 @@ static llvm::ManagedStatic<Registry> StatsRegistry;
namespace {
template <typename Callback> void enumerateStatVectors(const Callback &Fn) {
+ // This order is important, it matches the order of Snapshot 3 fields:
+ // - BoolStatValues
Fn(StatsRegistry->BoolStats);
+ // - UnsignedExplicitlySetStatValues
+ Fn(StatsRegistry->UnsignedStats);
+ // - UnsignedStatValues
Fn(StatsRegistry->CounterStats);
Fn(StatsRegistry->UnsignedMaxStats);
- Fn(StatsRegistry->UnsignedStats);
}
} // namespace
@@ -120,11 +132,21 @@ UnsignedEPStat::UnsignedEPStat(llvm::StringLiteral Name)
StatsRegistry->UnsignedStats.push_back(this);
}
+static std::vector<std::optional<unsigned>>
+consumeUnsignedExplicitlySetStats() {
+ std::vector<std::optional<unsigned>> Result;
+ Result.reserve(StatsRegistry->UnsignedStats.size());
+ for (auto *M : StatsRegistry->UnsignedStats) {
+ Result.push_back(M->value());
+ M->reset();
+ }
+ return Result;
+}
+
static std::vector<unsigned> consumeUnsignedStats() {
std::vector<unsigned> Result;
Result.reserve(StatsRegistry->CounterStats.size() +
- StatsRegistry->UnsignedMaxStats.size() +
- StatsRegistry->UnsignedStats.size());
+ StatsRegistry->UnsignedMaxStats.size());
for (auto *M : StatsRegistry->CounterStats) {
Result.push_back(M->value());
M->reset();
@@ -133,10 +155,6 @@ static std::vector<unsigned> consumeUnsignedStats() {
Result.push_back(M->value());
M->reset();
}
- for (auto *M : StatsRegistry->UnsignedStats) {
- Result.push_back(M->value());
- M->reset();
- }
return Result;
}
@@ -159,6 +177,17 @@ static std::string getUSR(const Decl *D) {
}
void Registry::Snapshot::dumpAsCSV(llvm::raw_ostream &OS) const {
+ auto printAsBoolOpt = [&OS](std::optional<bool> B) {
+ OS << (B.has_value() ? (*B ? "true" : "false") : "");
+ };
+ auto printAsUnsignOpt = [&OS](std::optional<unsigned> U) {
+ OS << (U.has_value() ? std::to_string(*U) : "");
+ };
+ auto commaIfNeeded = [&OS](const auto &Vec1, const auto &Vec2) {
+ if (!Vec1.empty() && !Vec2.empty())
+ OS << ",";
+ };
+
OS << '"';
llvm::printEscapedString(getUSR(EntryPoint), OS);
OS << "\",\"";
@@ -166,14 +195,15 @@ void Registry::Snapshot::dumpAsCSV(llvm::raw_ostream &OS) const {
llvm::printEscapedString(
clang::AnalysisDeclContext::getFunctionName(EntryPoint), OS);
OS << "\",";
- auto PrintAsBool = [&OS](bool B) { OS << (B ? "true" : "false"); };
- llvm::interleave(BoolStatValues, OS, PrintAsBool, ",");
- OS << ((BoolStatValues.empty() || UnsignedStatValues.empty()) ? "" : ",");
+ llvm::interleave(BoolStatValues, OS, printAsBoolOpt, ",");
+ commaIfNeeded(BoolStatValues, UnsignedExplicitlySetStatValues);
+ llvm::interleave(UnsignedExplicitlySetStatValues, OS, printAsUnsignOpt, ",");
+ commaIfNeeded(UnsignedExplicitlySetStatValues, UnsignedStatValues);
llvm::interleave(UnsignedStatValues, OS, [&OS](unsigned U) { OS << U; }, ",");
}
-static std::vector<bool> consumeBoolStats() {
- std::vector<bool> Result;
+static std::vector<std::optional<bool>> consumeBoolStats() {
+ std::vector<std::optional<bool>> Result;
Result.reserve(StatsRegistry->BoolStats.size());
for (auto *M : StatsRegistry->BoolStats) {
Result.push_back(M->value());
@@ -184,9 +214,11 @@ static std::vector<bool> consumeBoolStats() {
void EntryPointStat::takeSnapshot(const Decl *EntryPoint) {
auto BoolValues = consumeBoolStats();
+ auto UnsignedExplicitlySetValues = consumeUnsignedExplicitlySetStats();
auto UnsignedValues = consumeUnsignedStats();
- StatsRegistry->Snapshots.push_back(
- {EntryPoint, std::move(BoolValues), std::move(UnsignedValues)});
+ StatsRegistry->Snapshots.push_back({EntryPoint, std::move(BoolValues),
+ std::move(UnsignedExplicitlySetValues),
+ std::move(UnsignedValues)});
}
void EntryPointStat::dumpStatsAsCSV(llvm::StringRef FileName) {
diff --git a/clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp b/clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp
index cf01e2f37c662..a5efa901b0714 100644
--- a/clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp
+++ b/clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp
@@ -39,6 +39,7 @@
#include "llvm/Support/TimeProfiler.h"
#include "llvm/Support/Timer.h"
#include "llvm/Support/raw_ostream.h"
+#include <cmath>
#include <memory>
#include <utility>
@@ -142,7 +143,7 @@ class AnalysisConsumer : public AnalysisASTConsumer,
DigestAnalyzerOptions();
if (Opts.AnalyzerDisplayProgress || Opts.PrintStats ||
- Opts.ShouldSerializeStats) {
+ Opts.ShouldSerializeStats || !Opts.DumpEntryPointStatsToCSV.empty()) {
AnalyzerTimers = std::make_unique<llvm::TimerGroup>(
"analyzer", "Analyzer timers");
SyntaxCheckTimer = std::make_unique<llvm::Timer>(
@@ -706,6 +707,7 @@ AnalysisConsumer::getModeForDecl(Decl *D, AnalysisMode Mode) {
}
static UnsignedEPStat PathRunningTime("PathRunningTime");
+static UnsignedEPStat SyntaxRunningTime("SyntaxRunningTime");
void AnalysisConsumer::HandleCode(Decl *D, AnalysisMode Mode,
ExprEngine::InliningModes IMode,
@@ -744,6 +746,8 @@ void AnalysisConsumer::HandleCode(Decl *D, AnalysisMode Mode,
SyntaxCheckTimer->stopTimer();
llvm::TimeRecord CheckerEndTime = SyntaxCheckTimer->getTotalTime();
CheckerEndTime -= CheckerStartTime;
+ FunctionSummaries.findOrInsertSummary(D)->second.SyntaxRunningTime =
+ std::lround(CheckerEndTime.getWallTime() * 1000);
DisplayTime(CheckerEndTime);
}
}
@@ -758,6 +762,36 @@ void AnalysisConsumer::HandleCode(Decl *D, AnalysisMode Mode,
}
}
+namespace {
+template <typename DeclT>
+static clang::Decl *preferDefinitionImpl(clang::Decl *D) {
+ if (auto *X = dyn_cast<DeclT>(D))
+ if (auto *Def = X->getDefinition())
+ return Def;
+ return D;
+}
+
+template <> clang::Decl *preferDefinitionImpl<ObjCMethodDecl>(clang::Decl *D) {
+ if (const auto *X = dyn_cast<ObjCMethodDecl>(D)) {
+ for (auto *I : X->redecls())
+ if (I->hasBody())
+ return I;
+ }
+ return D;
+}
+
+static Decl *getDefinitionOrCanonicalDecl(Decl *D) {
+ assert(D);
+ D = D->getCanonicalDecl();
+ D = preferDefinitionImpl<VarDecl>(D);
+ D = preferDefinitionImpl<FunctionDecl>(D);
+ D = preferDefinitionImpl<TagDecl>(D);
+ D = preferDefinitionImpl<ObjCMethodDecl>(D);
+ assert(D);
+ return D;
+}
+} // namespace
+
//===----------------------------------------------------------------------===//
// Path-sensitive checking.
//===----------------------------------------------------------------------===//
@@ -774,6 +808,16 @@ void AnalysisConsumer::RunPathSensitiveChecks(Decl *D,
if (!Mgr->getAnalysisDeclContext(D)->getAnalysis<RelaxedLiveVariables>())
return;
+ const Decl *DefDecl = getDefinitionOrCanonicalDecl(D);
+
+ // Get the SyntaxRunningTime from the function summary, because it is computed
+ // during the AM_Syntax analysis, which is done at a different point in time
+ // and in different order, but always before AM_Path.
+ if (const auto *Summary = FunctionSummaries.findSummary(DefDecl);
+ Summary && Summary->SyntaxRunningTime.has_value()) {
+ SyntaxRunningTime.set(*Summary->SyntaxRunningTime);
+ }
+
ExprEngine Eng(CTU, *Mgr, VisitedCallees, &FunctionSummaries, IMode);
// Execute the worklist algorithm.
@@ -788,6 +832,8 @@ void AnalysisConsumer::RunPathSensitiveChecks(Decl *D,
ExprEngineTimer->stopTimer();
llvm::TimeRecord ExprEngineEndTime = ExprEngineTimer->getTotalTime();
ExprEngineEndTime -= ExprEngineStartTime;
+ PathRunningTime.set(static_cast<unsigned>(
+ std::lround(ExprEngineEndTime.getWallTime() * 1000)));
DisplayTime(ExprEngineEndTime);
}
diff --git a/clang/test/Analysis/analyzer-stats/entry-point-stats.cpp b/clang/test/Analysis/analyzer-stats/entry-point-stats.cpp
index 9cbe04550a8d3..b9099c6248108 100644
--- a/clang/test/Analysis/analyzer-stats/entry-point-stats.cpp
+++ b/clang/test/Analysis/analyzer-stats/entry-point-stats.cpp
@@ -8,6 +8,8 @@
// CHECK-NEXT: "c:@F at fib#i#": {
// CHECK-NEXT: "File": "{{.*}}entry-point-stats.cpp",
// CHECK-NEXT: "DebugName": "fib(unsigned int)",
+// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}",
+// CHECK-NEXT: "SyntaxRunningTime": "{{[0-9]+}}",
// CHECK-NEXT: "NumBlocks": "{{[0-9]+}}",
// CHECK-NEXT: "NumBlocksUnreachable": "{{[0-9]+}}",
// CHECK-NEXT: "NumCTUSteps": "{{[0-9]+}}",
@@ -39,12 +41,13 @@
// CHECK-NEXT: "MaxQueueSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxReachableSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxTimeSpentSolvingZ3Queries": "{{[0-9]+}}",
-// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}",
-// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}"
+// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}"
// CHECK-NEXT: },
// CHECK-NEXT: "c:@F at main#I#**C#": {
// CHECK-NEXT: "File": "{{.*}}entry-point-stats.cpp",
// CHECK-NEXT: "DebugName": "main(int, char **)",
+// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}",
+// CHECK-NEXT: "SyntaxRunningTime": "{{[0-9]+}}",
// CHECK-NEXT: "NumBlocks": "{{[0-9]+}}",
// CHECK-NEXT: "NumBlocksUnreachable": "{{[0-9]+}}",
// CHECK-NEXT: "NumCTUSteps": "{{[0-9]+}}",
@@ -76,8 +79,7 @@
// CHECK-NEXT: "MaxQueueSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxReachableSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxTimeSpentSolvingZ3Queries": "{{[0-9]+}}",
-// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}",
-// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}"
+// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}"
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NOT: non_entry_point
More information about the cfe-commits
mailing list