Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
07bcb96
[clang][analyzer] Record never-set statistics as empty CSV cells
necto Oct 10, 2025
ab54bbd
Fix path running time per entry point recording
necto Oct 10, 2025
f83589b
Add a test case demonstrating the use of explicitly set unsigned stats
necto Oct 10, 2025
22ce0fb
[NFC] Fix code formatting
necto Oct 10, 2025
8e7f252
Fix the entry-pint-stats.cpp for Windows
necto Oct 10, 2025
8f6b909
Update clang/lib/StaticAnalyzer/Checkers/UnsignedStatTesterChecker.cpp
necto Oct 13, 2025
5ae6a1f
Update clang/lib/StaticAnalyzer/Checkers/UnsignedStatTesterChecker.cpp
necto Oct 13, 2025
c4513fa
Remove unused variant
necto Oct 13, 2025
bfb7426
[NFC] Add an explicit else case, clarify comment
necto Oct 13, 2025
6b21117
Update clang/lib/StaticAnalyzer/Checkers/UnsignedStatTesterChecker.cpp
necto Oct 13, 2025
bfd6037
Update clang/lib/StaticAnalyzer/Checkers/UnsignedStatTesterChecker.cpp
necto Oct 13, 2025
b796401
Move UnsignedStatTesterChecker to unit tests
necto Oct 13, 2025
01b7361
[NFC] Remove unnecessary functionality from parseCSVColumnMapping
necto Oct 13, 2025
245a196
[NFC] Simplify range for loop
necto Oct 13, 2025
2a12966
Avoid displaying the timers if they are used only for per-EP stats
necto Oct 13, 2025
4d62304
Fix nullptr dereference
necto Oct 13, 2025
197103e
[NFC] Avoid constructing a string for a comparison's sake
necto Oct 13, 2025
79705dc
[NFC] use std::find instead of a manual enumeration of col headers
necto Oct 13, 2025
a00774d
[NFC] Use upper-camel-case names for lambdas
necto Oct 13, 2025
7eee805
Clear statistics snapshots between unit tests
necto Oct 13, 2025
ccea7fe
[NFC] Fix formatting
necto Oct 13, 2025
4bd7357
Merge branch 'main' into az/empty-metrics
necto Oct 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clang/docs/analyzer/developer-docs/Statistics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ However, note that with ``LLVM_ENABLE_STATS`` disabled, only storage of the valu
If you want to define a statistic only for entry point, EntryPointStats.h has four classes at your disposal:


- ``UnsignedEPStat`` - an unsigned value assigned at most once per entry point. For example: "the number of source characters in an entry-point body".
- ``UnsignedEPStat`` - an unsigned value assigned at most once per entry point. For example: "the number of source characters in an entry-point body". If no value is assigned during analysis of an entry point, the corresponding CSV cell will be empty.
- ``CounterEPStat`` - an additive statistic. It starts with 0 and you can add to it as many times as needed. For example: "the number of bugs discovered".
- ``UnsignedMaxEPStat`` - a maximizing statistic. It starts with 0 and when you join it with a value, it picks the maximum of the previous value and the new one. For example, "the longest execution path of a bug".

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#ifndef CLANG_INCLUDE_CLANG_STATICANALYZER_CORE_PATHSENSITIVE_ENTRYPOINTSTATS_H
#define CLANG_INCLUDE_CLANG_STATICANALYZER_CORE_PATHSENSITIVE_ENTRYPOINTSTATS_H

#include "clang/AST/ASTContext.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/ADT/StringRef.h"

Expand All @@ -25,7 +26,7 @@ class EntryPointStat {
public:
llvm::StringLiteral name() const { return Name; }

static void lockRegistry(llvm::StringRef CPPFileName);
static void lockRegistry(llvm::StringRef CPPFileName, ASTContext &Ctx);

static void takeSnapshot(const Decl *EntryPoint);
static void dumpStatsAsCSV(llvm::raw_ostream &OS);
Expand Down Expand Up @@ -85,7 +86,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());
Expand Down
78 changes: 57 additions & 21 deletions clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@ using namespace ento;

namespace {
struct Registry {
std::vector<UnsignedEPStat *> ExplicitlySetStats;
std::vector<UnsignedMaxEPStat *> MaxStats;
std::vector<CounterEPStat *> CounterStats;
std::vector<UnsignedMaxEPStat *> UnsignedMaxStats;
std::vector<UnsignedEPStat *> UnsignedStats;

bool IsLocked = false;

struct Snapshot {
const Decl *EntryPoint;
std::vector<unsigned> UnsignedStatValues;
// Explicitly set statistics may not have a value set, so they are separate
// from other unsigned statistics
std::vector<std::optional<unsigned>> ExplicitlySetStatValues;
// 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> MaxOrCountStatValues;

void dumpAsCSV(llvm::raw_ostream &OS) const;
};
Expand All @@ -46,10 +52,16 @@ static llvm::ManagedStatic<Registry> StatsRegistry;

namespace {
template <typename Callback> void enumerateStatVectors(const Callback &Fn) {
// This order is important, it matches the order of the Snapshot fields:
// - ExplicitlySetStatValues
Fn(StatsRegistry->ExplicitlySetStats);
// - MaxOrCountStatValues
Fn(StatsRegistry->MaxStats);
Fn(StatsRegistry->CounterStats);
Fn(StatsRegistry->UnsignedMaxStats);
Fn(StatsRegistry->UnsignedStats);
}

void clearSnapshots(void *) { StatsRegistry->Snapshots.clear(); }

} // namespace

static void checkStatName(const EntryPointStat *M) {
Expand All @@ -69,7 +81,8 @@ static void checkStatName(const EntryPointStat *M) {
}
}

void EntryPointStat::lockRegistry(llvm::StringRef CPPFileName) {
void EntryPointStat::lockRegistry(llvm::StringRef CPPFileName,
ASTContext &Ctx) {
auto CmpByNames = [](const EntryPointStat *L, const EntryPointStat *R) {
return L->name() < R->name();
};
Expand All @@ -80,6 +93,10 @@ void EntryPointStat::lockRegistry(llvm::StringRef CPPFileName) {
StatsRegistry->IsLocked = true;
llvm::raw_string_ostream OS(StatsRegistry->EscapedCPPFileName);
llvm::printEscapedString(CPPFileName, OS);
// Make sure snapshots (that reference function Decl's) do not persist after
// the AST is destroyed. This is especially relevant in the context of unit
// tests that construct and destruct multiple ASTs in the same process.
Ctx.AddDeallocation(clearSnapshots, nullptr);
}

[[maybe_unused]] static bool isRegistered(llvm::StringLiteral Name) {
Expand All @@ -101,30 +118,36 @@ UnsignedMaxEPStat::UnsignedMaxEPStat(llvm::StringLiteral Name)
: EntryPointStat(Name) {
assert(!StatsRegistry->IsLocked);
assert(!isRegistered(Name));
StatsRegistry->UnsignedMaxStats.push_back(this);
StatsRegistry->MaxStats.push_back(this);
}

UnsignedEPStat::UnsignedEPStat(llvm::StringLiteral Name)
: EntryPointStat(Name) {
assert(!StatsRegistry->IsLocked);
assert(!isRegistered(Name));
StatsRegistry->UnsignedStats.push_back(this);
StatsRegistry->ExplicitlySetStats.push_back(this);
}

static std::vector<unsigned> consumeUnsignedStats() {
std::vector<unsigned> Result;
Result.reserve(StatsRegistry->CounterStats.size() +
StatsRegistry->UnsignedMaxStats.size() +
StatsRegistry->UnsignedStats.size());
for (auto *M : StatsRegistry->CounterStats) {
static std::vector<std::optional<unsigned>> consumeExplicitlySetStats() {
std::vector<std::optional<unsigned>> Result;
Result.reserve(StatsRegistry->ExplicitlySetStats.size());
for (auto *M : StatsRegistry->ExplicitlySetStats) {
Result.push_back(M->value());
M->reset();
}
for (auto *M : StatsRegistry->UnsignedMaxStats) {
return Result;
}

static std::vector<unsigned> consumeMaxAndCounterStats() {
std::vector<unsigned> Result;
Result.reserve(StatsRegistry->CounterStats.size() +
StatsRegistry->MaxStats.size());
// Order is important, it must match the order in enumerateStatVectors
for (auto *M : StatsRegistry->MaxStats) {
Result.push_back(M->value());
M->reset();
}
for (auto *M : StatsRegistry->UnsignedStats) {
for (auto *M : StatsRegistry->CounterStats) {
Result.push_back(M->value());
M->reset();
}
Expand All @@ -150,20 +173,33 @@ static std::string getUSR(const Decl *D) {
}

void Registry::Snapshot::dumpAsCSV(llvm::raw_ostream &OS) const {
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 << ",";
};
auto PrintAsUnsigned = [&OS](unsigned U) { OS << U; };

OS << '"';
llvm::printEscapedString(getUSR(EntryPoint), OS);
OS << "\",\"";
OS << StatsRegistry->EscapedCPPFileName << "\",\"";
llvm::printEscapedString(
clang::AnalysisDeclContext::getFunctionName(EntryPoint), OS);
OS << "\"";
OS << (UnsignedStatValues.empty() ? "" : ",");
llvm::interleave(UnsignedStatValues, OS, [&OS](unsigned U) { OS << U; }, ",");
OS << "\",";
llvm::interleave(ExplicitlySetStatValues, OS, PrintAsUnsignOpt, ",");
CommaIfNeeded(ExplicitlySetStatValues, MaxOrCountStatValues);
llvm::interleave(MaxOrCountStatValues, OS, PrintAsUnsigned, ",");
}

void EntryPointStat::takeSnapshot(const Decl *EntryPoint) {
auto UnsignedValues = consumeUnsignedStats();
StatsRegistry->Snapshots.push_back({EntryPoint, std::move(UnsignedValues)});
auto ExplicitlySetValues = consumeExplicitlySetStats();
auto MaxOrCounterValues = consumeMaxAndCounterStats();
StatsRegistry->Snapshots.push_back({EntryPoint,
std::move(ExplicitlySetValues),
std::move(MaxOrCounterValues)});
}

void EntryPointStat::dumpStatsAsCSV(llvm::StringRef FileName) {
Expand Down
30 changes: 28 additions & 2 deletions clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -125,6 +126,7 @@ class AnalysisConsumer : public AnalysisASTConsumer,
std::unique_ptr<llvm::Timer> SyntaxCheckTimer;
std::unique_ptr<llvm::Timer> ExprEngineTimer;
std::unique_ptr<llvm::Timer> BugReporterTimer;
bool ShouldClearTimersToPreventDisplayingThem;

/// The information about analyzed functions shared throughout the
/// translation unit.
Expand All @@ -138,11 +140,12 @@ class AnalysisConsumer : public AnalysisASTConsumer,
Injector(std::move(injector)), CTU(CI),
MacroExpansions(CI.getLangOpts()) {

EntryPointStat::lockRegistry(getMainFileName(CI.getInvocation()));
EntryPointStat::lockRegistry(getMainFileName(CI.getInvocation()),
CI.getASTContext());
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>(
Expand All @@ -154,6 +157,12 @@ class AnalysisConsumer : public AnalysisASTConsumer,
*AnalyzerTimers);
}

// Avoid displaying the timers created above in case we only want to record
// per-entry-point stats.
ShouldClearTimersToPreventDisplayingThem = !Opts.AnalyzerDisplayProgress &&
!Opts.PrintStats &&
!Opts.ShouldSerializeStats;

if (Opts.PrintStats || Opts.ShouldSerializeStats) {
llvm::EnableStatistics(/* DoPrintOnExit= */ false);
}
Expand Down Expand Up @@ -276,6 +285,9 @@ class AnalysisConsumer : public AnalysisASTConsumer,
checkerMgr->runCheckersOnASTDecl(D, *Mgr, *RecVisitorBR);
if (SyntaxCheckTimer)
SyntaxCheckTimer->stopTimer();
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
AnalyzerTimers->clear();
}
}
return true;
}
Expand Down Expand Up @@ -569,6 +581,9 @@ void AnalysisConsumer::runAnalysisOnTranslationUnit(ASTContext &C) {
checkerMgr->runCheckersOnASTDecl(TU, *Mgr, BR);
if (SyntaxCheckTimer)
SyntaxCheckTimer->stopTimer();
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
AnalyzerTimers->clear();
}

// Run the AST-only checks using the order in which functions are defined.
// If inlining is not turned on, use the simplest function order for path
Expand Down Expand Up @@ -745,6 +760,9 @@ void AnalysisConsumer::HandleCode(Decl *D, AnalysisMode Mode,
llvm::TimeRecord CheckerEndTime = SyntaxCheckTimer->getTotalTime();
CheckerEndTime -= CheckerStartTime;
DisplayTime(CheckerEndTime);
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
AnalyzerTimers->clear();
}
}
}

Expand Down Expand Up @@ -788,7 +806,12 @@ 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);
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
AnalyzerTimers->clear();
}
}

if (!Mgr->options.DumpExplodedGraphTo.empty())
Expand All @@ -799,6 +822,9 @@ void AnalysisConsumer::RunPathSensitiveChecks(Decl *D,
Eng.ViewGraph(Mgr->options.TrimGraph);

flushReports(BugReporterTimer.get(), Eng.getBugReporter());
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
AnalyzerTimers->clear();
}
}

//===----------------------------------------------------------------------===//
Expand Down
28 changes: 14 additions & 14 deletions clang/test/Analysis/analyzer-stats/entry-point-stats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
// 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: "MaxBugClassSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxCFGSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxQueueSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxReachableSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxTimeSpentSolvingZ3Queries": "{{[0-9]+}}",
// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}",
// CHECK-NEXT: "NumBlocks": "{{[0-9]+}}",
// CHECK-NEXT: "NumBlocksUnreachable": "{{[0-9]+}}",
// CHECK-NEXT: "NumCTUSteps": "{{[0-9]+}}",
Expand All @@ -33,18 +40,18 @@
// CHECK-NEXT: "NumTimesZ3SpendsTooMuchTimeOnASingleEQClass": "{{[0-9]+}}",
// CHECK-NEXT: "NumTimesZ3TimedOut": "{{[0-9]+}}",
// CHECK-NEXT: "NumZ3QueriesDone": "{{[0-9]+}}",
// CHECK-NEXT: "TimeSpentSolvingZ3Queries": "{{[0-9]+}}",
// CHECK-NEXT: "TimeSpentSolvingZ3Queries": "{{[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: "MaxBugClassSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxCFGSize": "{{[0-9]+}}",
// 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: },
// CHECK-NEXT: "c:@F@main#I#**C#": {
// CHECK-NEXT: "File": "{{.*}}entry-point-stats.cpp",
// CHECK-NEXT: "DebugName": "main(int, char **)",
// CHECK-NEXT: "NumBlocks": "{{[0-9]+}}",
// CHECK-NEXT: "NumBlocksUnreachable": "{{[0-9]+}}",
// CHECK-NEXT: "NumCTUSteps": "{{[0-9]+}}",
Expand All @@ -70,14 +77,7 @@
// CHECK-NEXT: "NumTimesZ3SpendsTooMuchTimeOnASingleEQClass": "{{[0-9]+}}",
// CHECK-NEXT: "NumTimesZ3TimedOut": "{{[0-9]+}}",
// CHECK-NEXT: "NumZ3QueriesDone": "{{[0-9]+}}",
// CHECK-NEXT: "TimeSpentSolvingZ3Queries": "{{[0-9]+}}",
// CHECK-NEXT: "MaxBugClassSize": "{{[0-9]+}}",
// CHECK-NEXT: "MaxCFGSize": "{{[0-9]+}}",
// 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: "TimeSpentSolvingZ3Queries": "{{[0-9]+}}"
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NOT: non_entry_point
Expand Down
1 change: 1 addition & 0 deletions clang/unittests/StaticAnalyzer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ add_clang_unittest(StaticAnalysisTests
SValSimplifyerTest.cpp
SValTest.cpp
TestReturnValueUnderConstruction.cpp
UnsignedStatDemo.cpp
Z3CrosscheckOracleTest.cpp
CLANG_LIBS
clangBasic
Expand Down
Loading