Skip to content

Conversation

necto
Copy link
Contributor

@necto necto commented Oct 6, 2025

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

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
@necto necto requested review from NagyDonat and steakhal October 6, 2025 13:48
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer labels Oct 6, 2025
@llvmbot
Copy link
Member

llvmbot commented Oct 6, 2025

@llvm/pr-subscribers-clang-static-analyzer-1

Author: Arseniy Zaostrovnykh (necto)

Changes

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


Full diff: https://github.com/llvm/llvm-project/pull/162089.diff

5 Files Affected:

  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/EntryPointStats.h (+2-2)
  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/FunctionSummary.h (+8)
  • (modified) clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp (+47-15)
  • (modified) clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp (+47-1)
  • (modified) clang/test/Analysis/analyzer-stats/entry-point-stats.cpp (+6-4)
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@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@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

@llvmbot
Copy link
Member

llvmbot commented Oct 6, 2025

@llvm/pr-subscribers-clang

Author: Arseniy Zaostrovnykh (necto)

Changes

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


Full diff: https://github.com/llvm/llvm-project/pull/162089.diff

5 Files Affected:

  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/EntryPointStats.h (+2-2)
  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/FunctionSummary.h (+8)
  • (modified) clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp (+47-15)
  • (modified) clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp (+47-1)
  • (modified) clang/test/Analysis/analyzer-stats/entry-point-stats.cpp (+6-4)
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@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@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

@necto necto changed the title [clang][analyzer] Register path- and syntax-analysis times / entry point [clang][analyzer] Save path- and syntax-analysis times per entry point Oct 6, 2025
Comment on lines -36 to +38
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this change (where you modify the behavior of the boolean statistics) related to the rest of this commit (which handles unsigned statistics)? Does this influence anything (I don't see a corresponding change in the test)?

(By the way, does upstream clang have any boolean statistics at all?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I will break it down into a few PRs. here is the first one: #162817

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@necto
Copy link
Contributor Author

necto commented Oct 14, 2025

Superceded by #162817, #162839, and #163341

@necto necto closed this Oct 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:static analyzer clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants