Skip to content

Commit bb3b020

Browse files
nectoNagyDonat
andauthored
[clang][analyzer] Print empty per-EP metrics as empty CSV cells, fix missing PathRunningTime metric (llvm#162839)
To avoid information loss, introduce a difference between unset stats and 0 for statistics that are supposed to be set once per entry point. Now, if the statistic is not set for an entry point, the corresponding CSV cell will be empty, and not 0. Thanks to this differentiation, I noticed that `PathRunningTime` was actually never set, and fixed that. Additionally, 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. Finally, I added a dedicated debug checker that demonstrates the use of a statistic and tested the set and unset scenarios explicitly. -- CPP-7097 --------- Co-authored-by: Donát Nagy <[email protected]>
1 parent 1db148c commit bb3b020

File tree

7 files changed

+254
-40
lines changed

7 files changed

+254
-40
lines changed

clang/docs/analyzer/developer-docs/Statistics.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ However, note that with ``LLVM_ENABLE_STATS`` disabled, only storage of the valu
2222
If you want to define a statistic only for entry point, EntryPointStats.h has four classes at your disposal:
2323

2424

25-
- ``UnsignedEPStat`` - an unsigned value assigned at most once per entry point. For example: "the number of source characters in an entry-point body".
25+
- ``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.
2626
- ``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".
2727
- ``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".
2828

clang/include/clang/StaticAnalyzer/Core/PathSensitive/EntryPointStats.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#ifndef CLANG_INCLUDE_CLANG_STATICANALYZER_CORE_PATHSENSITIVE_ENTRYPOINTSTATS_H
1010
#define CLANG_INCLUDE_CLANG_STATICANALYZER_CORE_PATHSENSITIVE_ENTRYPOINTSTATS_H
1111

12+
#include "clang/AST/ASTContext.h"
1213
#include "llvm/ADT/Statistic.h"
1314
#include "llvm/ADT/StringRef.h"
1415

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

28-
static void lockRegistry(llvm::StringRef CPPFileName);
29+
static void lockRegistry(llvm::StringRef CPPFileName, ASTContext &Ctx);
2930

3031
static void takeSnapshot(const Decl *EntryPoint);
3132
static void dumpStatsAsCSV(llvm::raw_ostream &OS);
@@ -85,7 +86,7 @@ class UnsignedEPStat : public EntryPointStat {
8586

8687
public:
8788
explicit UnsignedEPStat(llvm::StringLiteral Name);
88-
unsigned value() const { return Value.value_or(0); }
89+
std::optional<unsigned> value() const { return Value; }
8990
void reset() { Value.reset(); }
9091
void set(unsigned V) {
9192
assert(!Value.has_value());

clang/lib/StaticAnalyzer/Core/EntryPointStats.cpp

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,21 @@ using namespace ento;
2424

2525
namespace {
2626
struct Registry {
27+
std::vector<UnsignedEPStat *> ExplicitlySetStats;
28+
std::vector<UnsignedMaxEPStat *> MaxStats;
2729
std::vector<CounterEPStat *> CounterStats;
28-
std::vector<UnsignedMaxEPStat *> UnsignedMaxStats;
29-
std::vector<UnsignedEPStat *> UnsignedStats;
3030

3131
bool IsLocked = false;
3232

3333
struct Snapshot {
3434
const Decl *EntryPoint;
35-
std::vector<unsigned> UnsignedStatValues;
35+
// Explicitly set statistics may not have a value set, so they are separate
36+
// from other unsigned statistics
37+
std::vector<std::optional<unsigned>> ExplicitlySetStatValues;
38+
// These are counting and maximizing statistics that initialize to 0, which
39+
// is meaningful even if they are never updated, so their value is always
40+
// present.
41+
std::vector<unsigned> MaxOrCountStatValues;
3642

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

4753
namespace {
4854
template <typename Callback> void enumerateStatVectors(const Callback &Fn) {
55+
// This order is important, it matches the order of the Snapshot fields:
56+
// - ExplicitlySetStatValues
57+
Fn(StatsRegistry->ExplicitlySetStats);
58+
// - MaxOrCountStatValues
59+
Fn(StatsRegistry->MaxStats);
4960
Fn(StatsRegistry->CounterStats);
50-
Fn(StatsRegistry->UnsignedMaxStats);
51-
Fn(StatsRegistry->UnsignedStats);
5261
}
62+
63+
void clearSnapshots(void *) { StatsRegistry->Snapshots.clear(); }
64+
5365
} // namespace
5466

5567
static void checkStatName(const EntryPointStat *M) {
@@ -69,7 +81,8 @@ static void checkStatName(const EntryPointStat *M) {
6981
}
7082
}
7183

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

85102
[[maybe_unused]] static bool isRegistered(llvm::StringLiteral Name) {
@@ -101,30 +118,36 @@ UnsignedMaxEPStat::UnsignedMaxEPStat(llvm::StringLiteral Name)
101118
: EntryPointStat(Name) {
102119
assert(!StatsRegistry->IsLocked);
103120
assert(!isRegistered(Name));
104-
StatsRegistry->UnsignedMaxStats.push_back(this);
121+
StatsRegistry->MaxStats.push_back(this);
105122
}
106123

107124
UnsignedEPStat::UnsignedEPStat(llvm::StringLiteral Name)
108125
: EntryPointStat(Name) {
109126
assert(!StatsRegistry->IsLocked);
110127
assert(!isRegistered(Name));
111-
StatsRegistry->UnsignedStats.push_back(this);
128+
StatsRegistry->ExplicitlySetStats.push_back(this);
112129
}
113130

114-
static std::vector<unsigned> consumeUnsignedStats() {
115-
std::vector<unsigned> Result;
116-
Result.reserve(StatsRegistry->CounterStats.size() +
117-
StatsRegistry->UnsignedMaxStats.size() +
118-
StatsRegistry->UnsignedStats.size());
119-
for (auto *M : StatsRegistry->CounterStats) {
131+
static std::vector<std::optional<unsigned>> consumeExplicitlySetStats() {
132+
std::vector<std::optional<unsigned>> Result;
133+
Result.reserve(StatsRegistry->ExplicitlySetStats.size());
134+
for (auto *M : StatsRegistry->ExplicitlySetStats) {
120135
Result.push_back(M->value());
121136
M->reset();
122137
}
123-
for (auto *M : StatsRegistry->UnsignedMaxStats) {
138+
return Result;
139+
}
140+
141+
static std::vector<unsigned> consumeMaxAndCounterStats() {
142+
std::vector<unsigned> Result;
143+
Result.reserve(StatsRegistry->CounterStats.size() +
144+
StatsRegistry->MaxStats.size());
145+
// Order is important, it must match the order in enumerateStatVectors
146+
for (auto *M : StatsRegistry->MaxStats) {
124147
Result.push_back(M->value());
125148
M->reset();
126149
}
127-
for (auto *M : StatsRegistry->UnsignedStats) {
150+
for (auto *M : StatsRegistry->CounterStats) {
128151
Result.push_back(M->value());
129152
M->reset();
130153
}
@@ -150,20 +173,33 @@ static std::string getUSR(const Decl *D) {
150173
}
151174

152175
void Registry::Snapshot::dumpAsCSV(llvm::raw_ostream &OS) const {
176+
auto PrintAsUnsignOpt = [&OS](std::optional<unsigned> U) {
177+
OS << (U.has_value() ? std::to_string(*U) : "");
178+
};
179+
auto CommaIfNeeded = [&OS](const auto &Vec1, const auto &Vec2) {
180+
if (!Vec1.empty() && !Vec2.empty())
181+
OS << ",";
182+
};
183+
auto PrintAsUnsigned = [&OS](unsigned U) { OS << U; };
184+
153185
OS << '"';
154186
llvm::printEscapedString(getUSR(EntryPoint), OS);
155187
OS << "\",\"";
156188
OS << StatsRegistry->EscapedCPPFileName << "\",\"";
157189
llvm::printEscapedString(
158190
clang::AnalysisDeclContext::getFunctionName(EntryPoint), OS);
159-
OS << "\"";
160-
OS << (UnsignedStatValues.empty() ? "" : ",");
161-
llvm::interleave(UnsignedStatValues, OS, [&OS](unsigned U) { OS << U; }, ",");
191+
OS << "\",";
192+
llvm::interleave(ExplicitlySetStatValues, OS, PrintAsUnsignOpt, ",");
193+
CommaIfNeeded(ExplicitlySetStatValues, MaxOrCountStatValues);
194+
llvm::interleave(MaxOrCountStatValues, OS, PrintAsUnsigned, ",");
162195
}
163196

164197
void EntryPointStat::takeSnapshot(const Decl *EntryPoint) {
165-
auto UnsignedValues = consumeUnsignedStats();
166-
StatsRegistry->Snapshots.push_back({EntryPoint, std::move(UnsignedValues)});
198+
auto ExplicitlySetValues = consumeExplicitlySetStats();
199+
auto MaxOrCounterValues = consumeMaxAndCounterStats();
200+
StatsRegistry->Snapshots.push_back({EntryPoint,
201+
std::move(ExplicitlySetValues),
202+
std::move(MaxOrCounterValues)});
167203
}
168204

169205
void EntryPointStat::dumpStatsAsCSV(llvm::StringRef FileName) {

clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include "llvm/Support/TimeProfiler.h"
4040
#include "llvm/Support/Timer.h"
4141
#include "llvm/Support/raw_ostream.h"
42+
#include <cmath>
4243
#include <memory>
4344
#include <utility>
4445

@@ -125,6 +126,7 @@ class AnalysisConsumer : public AnalysisASTConsumer,
125126
std::unique_ptr<llvm::Timer> SyntaxCheckTimer;
126127
std::unique_ptr<llvm::Timer> ExprEngineTimer;
127128
std::unique_ptr<llvm::Timer> BugReporterTimer;
129+
bool ShouldClearTimersToPreventDisplayingThem;
128130

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

141-
EntryPointStat::lockRegistry(getMainFileName(CI.getInvocation()));
143+
EntryPointStat::lockRegistry(getMainFileName(CI.getInvocation()),
144+
CI.getASTContext());
142145
DigestAnalyzerOptions();
143146

144147
if (Opts.AnalyzerDisplayProgress || Opts.PrintStats ||
145-
Opts.ShouldSerializeStats) {
148+
Opts.ShouldSerializeStats || !Opts.DumpEntryPointStatsToCSV.empty()) {
146149
AnalyzerTimers = std::make_unique<llvm::TimerGroup>(
147150
"analyzer", "Analyzer timers");
148151
SyntaxCheckTimer = std::make_unique<llvm::Timer>(
@@ -154,6 +157,12 @@ class AnalysisConsumer : public AnalysisASTConsumer,
154157
*AnalyzerTimers);
155158
}
156159

160+
// Avoid displaying the timers created above in case we only want to record
161+
// per-entry-point stats.
162+
ShouldClearTimersToPreventDisplayingThem = !Opts.AnalyzerDisplayProgress &&
163+
!Opts.PrintStats &&
164+
!Opts.ShouldSerializeStats;
165+
157166
if (Opts.PrintStats || Opts.ShouldSerializeStats) {
158167
llvm::EnableStatistics(/* DoPrintOnExit= */ false);
159168
}
@@ -276,6 +285,9 @@ class AnalysisConsumer : public AnalysisASTConsumer,
276285
checkerMgr->runCheckersOnASTDecl(D, *Mgr, *RecVisitorBR);
277286
if (SyntaxCheckTimer)
278287
SyntaxCheckTimer->stopTimer();
288+
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
289+
AnalyzerTimers->clear();
290+
}
279291
}
280292
return true;
281293
}
@@ -569,6 +581,9 @@ void AnalysisConsumer::runAnalysisOnTranslationUnit(ASTContext &C) {
569581
checkerMgr->runCheckersOnASTDecl(TU, *Mgr, BR);
570582
if (SyntaxCheckTimer)
571583
SyntaxCheckTimer->stopTimer();
584+
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
585+
AnalyzerTimers->clear();
586+
}
572587

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

@@ -788,7 +806,12 @@ void AnalysisConsumer::RunPathSensitiveChecks(Decl *D,
788806
ExprEngineTimer->stopTimer();
789807
llvm::TimeRecord ExprEngineEndTime = ExprEngineTimer->getTotalTime();
790808
ExprEngineEndTime -= ExprEngineStartTime;
809+
PathRunningTime.set(static_cast<unsigned>(
810+
std::lround(ExprEngineEndTime.getWallTime() * 1000)));
791811
DisplayTime(ExprEngineEndTime);
812+
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
813+
AnalyzerTimers->clear();
814+
}
792815
}
793816

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

801824
flushReports(BugReporterTimer.get(), Eng.getBugReporter());
825+
if (AnalyzerTimers && ShouldClearTimersToPreventDisplayingThem) {
826+
AnalyzerTimers->clear();
827+
}
802828
}
803829

804830
//===----------------------------------------------------------------------===//

clang/test/Analysis/analyzer-stats/entry-point-stats.cpp

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
// CHECK-NEXT: "c:@F@fib#i#": {
99
// CHECK-NEXT: "File": "{{.*}}entry-point-stats.cpp",
1010
// CHECK-NEXT: "DebugName": "fib(unsigned int)",
11+
// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}",
12+
// CHECK-NEXT: "MaxBugClassSize": "{{[0-9]+}}",
13+
// CHECK-NEXT: "MaxCFGSize": "{{[0-9]+}}",
14+
// CHECK-NEXT: "MaxQueueSize": "{{[0-9]+}}",
15+
// CHECK-NEXT: "MaxReachableSize": "{{[0-9]+}}",
16+
// CHECK-NEXT: "MaxTimeSpentSolvingZ3Queries": "{{[0-9]+}}",
17+
// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}",
1118
// CHECK-NEXT: "NumBlocks": "{{[0-9]+}}",
1219
// CHECK-NEXT: "NumBlocksUnreachable": "{{[0-9]+}}",
1320
// CHECK-NEXT: "NumCTUSteps": "{{[0-9]+}}",
@@ -33,18 +40,18 @@
3340
// CHECK-NEXT: "NumTimesZ3SpendsTooMuchTimeOnASingleEQClass": "{{[0-9]+}}",
3441
// CHECK-NEXT: "NumTimesZ3TimedOut": "{{[0-9]+}}",
3542
// CHECK-NEXT: "NumZ3QueriesDone": "{{[0-9]+}}",
36-
// CHECK-NEXT: "TimeSpentSolvingZ3Queries": "{{[0-9]+}}",
43+
// CHECK-NEXT: "TimeSpentSolvingZ3Queries": "{{[0-9]+}}"
44+
// CHECK-NEXT: },
45+
// CHECK-NEXT: "c:@F@main#I#**C#": {
46+
// CHECK-NEXT: "File": "{{.*}}entry-point-stats.cpp",
47+
// CHECK-NEXT: "DebugName": "main(int, char **)",
48+
// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}",
3749
// CHECK-NEXT: "MaxBugClassSize": "{{[0-9]+}}",
3850
// CHECK-NEXT: "MaxCFGSize": "{{[0-9]+}}",
3951
// CHECK-NEXT: "MaxQueueSize": "{{[0-9]+}}",
4052
// CHECK-NEXT: "MaxReachableSize": "{{[0-9]+}}",
4153
// CHECK-NEXT: "MaxTimeSpentSolvingZ3Queries": "{{[0-9]+}}",
4254
// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}",
43-
// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}"
44-
// CHECK-NEXT: },
45-
// CHECK-NEXT: "c:@F@main#I#**C#": {
46-
// CHECK-NEXT: "File": "{{.*}}entry-point-stats.cpp",
47-
// CHECK-NEXT: "DebugName": "main(int, char **)",
4855
// CHECK-NEXT: "NumBlocks": "{{[0-9]+}}",
4956
// CHECK-NEXT: "NumBlocksUnreachable": "{{[0-9]+}}",
5057
// CHECK-NEXT: "NumCTUSteps": "{{[0-9]+}}",
@@ -70,14 +77,7 @@
7077
// CHECK-NEXT: "NumTimesZ3SpendsTooMuchTimeOnASingleEQClass": "{{[0-9]+}}",
7178
// CHECK-NEXT: "NumTimesZ3TimedOut": "{{[0-9]+}}",
7279
// CHECK-NEXT: "NumZ3QueriesDone": "{{[0-9]+}}",
73-
// CHECK-NEXT: "TimeSpentSolvingZ3Queries": "{{[0-9]+}}",
74-
// CHECK-NEXT: "MaxBugClassSize": "{{[0-9]+}}",
75-
// CHECK-NEXT: "MaxCFGSize": "{{[0-9]+}}",
76-
// CHECK-NEXT: "MaxQueueSize": "{{[0-9]+}}",
77-
// CHECK-NEXT: "MaxReachableSize": "{{[0-9]+}}",
78-
// CHECK-NEXT: "MaxTimeSpentSolvingZ3Queries": "{{[0-9]+}}",
79-
// CHECK-NEXT: "MaxValidBugClassSize": "{{[0-9]+}}",
80-
// CHECK-NEXT: "PathRunningTime": "{{[0-9]+}}"
80+
// CHECK-NEXT: "TimeSpentSolvingZ3Queries": "{{[0-9]+}}"
8181
// CHECK-NEXT: }
8282
// CHECK-NEXT: }
8383
// CHECK-NOT: non_entry_point

clang/unittests/StaticAnalyzer/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ add_clang_unittest(StaticAnalysisTests
2020
SValSimplifyerTest.cpp
2121
SValTest.cpp
2222
TestReturnValueUnderConstruction.cpp
23+
UnsignedStatDemo.cpp
2324
Z3CrosscheckOracleTest.cpp
2425
CLANG_LIBS
2526
clangBasic

0 commit comments

Comments
 (0)