Skip to content

Commit c1d5be8

Browse files
authored
[analyzer] Add time-trace scopes for high-level analyzer steps (llvm#125508)
Specifically, add a scope for - each work-list step, - each entry point, - each checker run within a step, and - bug-suppression phase at the end of the analysis of an entry-point. These scopes add no perceptible run-time overhead when time-tracing is disabled. You can enable it and generate a time trace using the `-ftime-trace=file.json` option. See also the RFC: https://discourse.llvm.org/t/analyzer-rfc-ftime-trace-time-scopes-for-steps-and-entry-points/84343 -- CPP-6065
1 parent 925d347 commit c1d5be8

File tree

11 files changed

+406
-11
lines changed

11 files changed

+406
-11
lines changed

clang/docs/analyzer/developer-docs.rst

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Contents:
1111
developer-docs/InitializerLists
1212
developer-docs/nullability
1313
developer-docs/RegionStore
14+
developer-docs/PerformanceInvestigation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
=========================
2+
Performance Investigation
3+
=========================
4+
5+
Multiple factors contribute to the time it takes to analyze a file with Clang Static Analyzer.
6+
A translation unit contains multiple entry points, each of which take multiple steps to analyze.
7+
8+
You can add the ``-ftime-trace=file.json`` option to break down the analysis time into individual entry points and steps within each entry point.
9+
You can explore the generated JSON file in a Chromium browser using the ``chrome://tracing`` URL,
10+
or using `speedscope <https://speedscope.app>`_.
11+
Once you narrow down to specific analysis steps you are interested in, you can more effectively employ heavier profilers,
12+
such as `Perf <https://perfwiki.github.io/main/>`_ and `Callgrind <https://valgrind.org/docs/manual/cl-manual.html>`_.
13+
14+
Each analysis step has a time scope in the trace, corresponds to processing of an exploded node, and is designated with a ``ProgramPoint``.
15+
If the ``ProgramPoint`` is associated with a location, you can see it on the scope metadata label.
16+
17+
Here is an example of a time trace produced with
18+
19+
.. code-block:: bash
20+
:caption: Clang Static Analyzer invocation to generate a time trace of string.c analysis.
21+
22+
clang -cc1 -nostdsysteminc -analyze -analyzer-constraints=range \
23+
-setup-static-analyzer -analyzer-checker=core,unix,alpha.unix.cstring,debug.ExprInspection \
24+
-verify ./clang/test/Analysis/string.c \
25+
-ftime-trace=trace.json -ftime-trace-granularity=1
26+
27+
.. image:: ../images/speedscope.png
28+
29+
On the speedscope screenshot above, under the first time ruler is the bird's-eye view of the entire trace that spans a little over 60 milliseconds.
30+
Under the second ruler (focused on the 18.09-18.13ms time point) you can see a narrowed-down portion.
31+
The second box ("HandleCode memset...") that spans entire screen (and actually extends beyond it) corresponds to the analysis of ``memset16_region_cast()`` entry point that is defined in the "string.c" test file on line 1627.
32+
Below it, you can find multiple sub-scopes each corresponding to processing of a single exploded node.
33+
34+
- First: a ``PostStmt`` for some statement on line 1634. This scope has a selected subscope "CheckerManager::runCheckersForCallEvent (Pre)" that takes 5 microseconds.
35+
- Four other nodes, too small to be discernible at this zoom level
36+
- Last on this screenshot: another ``PostStmt`` for a statement on line 1635.
37+
38+
In addition to the ``-ftime-trace`` option, you can use ``-ftime-trace-granularity`` to fine-tune the time trace.
39+
40+
- ``-ftime-trace-granularity=NN`` dumps only time scopes that are longer than NN microseconds.
41+
- ``-ftime-trace-verbose`` enables some additional dumps in the frontend related to template instantiations.
42+
At the moment, it has no effect on the traces from the static analyzer.
43+
44+
Note: Both Chrome-tracing and speedscope tools might struggle with time traces above 100 MB in size.
45+
Luckily, in most cases the default max-steps boundary of 225 000 produces the traces of approximately that size
46+
for a single entry point.
47+
You can use ``-analyze-function=get_global_options`` together with ``-ftime-trace`` to narrow down analysis to a specific entry point.
53.5 KB
Loading

clang/include/clang/Analysis/ProgramPoint.h

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ class ProgramPoint {
8585
LoopExitKind,
8686
EpsilonKind};
8787

88+
static StringRef getProgramPointKindName(Kind K);
89+
std::optional<SourceLocation> getSourceLocation() const;
90+
8891
private:
8992
const void *Data1;
9093
llvm::PointerIntPair<const void *, 2, unsigned> Data2;

clang/lib/Analysis/ProgramPoint.cpp

+115
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,121 @@ LLVM_DUMP_METHOD void ProgramPoint::dump() const {
4949
return printJson(llvm::errs());
5050
}
5151

52+
StringRef ProgramPoint::getProgramPointKindName(Kind K) {
53+
switch (K) {
54+
case BlockEdgeKind:
55+
return "BlockEdge";
56+
case BlockEntranceKind:
57+
return "BlockEntrance";
58+
case BlockExitKind:
59+
return "BlockExit";
60+
case PreStmtKind:
61+
return "PreStmt";
62+
case PreStmtPurgeDeadSymbolsKind:
63+
return "PreStmtPurgeDeadSymbols";
64+
case PostStmtPurgeDeadSymbolsKind:
65+
return "PostStmtPurgeDeadSymbols";
66+
case PostStmtKind:
67+
return "PostStmt";
68+
case PreLoadKind:
69+
return "PreLoad";
70+
case PostLoadKind:
71+
return "PostLoad";
72+
case PreStoreKind:
73+
return "PreStore";
74+
case PostStoreKind:
75+
return "PostStore";
76+
case PostConditionKind:
77+
return "PostCondition";
78+
case PostLValueKind:
79+
return "PostLValue";
80+
case PostAllocatorCallKind:
81+
return "PostAllocatorCall";
82+
case PostInitializerKind:
83+
return "PostInitializer";
84+
case CallEnterKind:
85+
return "CallEnter";
86+
case CallExitBeginKind:
87+
return "CallExitBegin";
88+
case CallExitEndKind:
89+
return "CallExitEnd";
90+
case FunctionExitKind:
91+
return "FunctionExit";
92+
case PreImplicitCallKind:
93+
return "PreImplicitCall";
94+
case PostImplicitCallKind:
95+
return "PostImplicitCall";
96+
case LoopExitKind:
97+
return "LoopExit";
98+
case EpsilonKind:
99+
return "Epsilon";
100+
}
101+
llvm_unreachable("Unknown ProgramPoint kind");
102+
}
103+
104+
std::optional<SourceLocation> ProgramPoint::getSourceLocation() const {
105+
switch (getKind()) {
106+
case BlockEdgeKind:
107+
// If needed, the source and or destination beginning can be used to get
108+
// source location.
109+
return std::nullopt;
110+
case BlockEntranceKind:
111+
// If needed, first statement of the block can be used.
112+
return std::nullopt;
113+
case BlockExitKind:
114+
if (const auto *B = castAs<BlockExit>().getBlock()) {
115+
if (const auto *T = B->getTerminatorStmt()) {
116+
return T->getBeginLoc();
117+
}
118+
}
119+
return std::nullopt;
120+
case PreStmtKind:
121+
case PreStmtPurgeDeadSymbolsKind:
122+
case PostStmtPurgeDeadSymbolsKind:
123+
case PostStmtKind:
124+
case PreLoadKind:
125+
case PostLoadKind:
126+
case PreStoreKind:
127+
case PostStoreKind:
128+
case PostConditionKind:
129+
case PostLValueKind:
130+
case PostAllocatorCallKind:
131+
if (const Stmt *S = castAs<StmtPoint>().getStmt())
132+
return S->getBeginLoc();
133+
return std::nullopt;
134+
case PostInitializerKind:
135+
if (const auto *Init = castAs<PostInitializer>().getInitializer())
136+
return Init->getSourceLocation();
137+
return std::nullopt;
138+
case CallEnterKind:
139+
if (const Stmt *S = castAs<CallEnter>().getCallExpr())
140+
return S->getBeginLoc();
141+
return std::nullopt;
142+
case CallExitBeginKind:
143+
if (const Stmt *S = castAs<CallExitBegin>().getReturnStmt())
144+
return S->getBeginLoc();
145+
return std::nullopt;
146+
case CallExitEndKind:
147+
return std::nullopt;
148+
case FunctionExitKind:
149+
if (const auto *B = castAs<FunctionExitPoint>().getBlock();
150+
B && B->getTerminatorStmt())
151+
return B->getTerminatorStmt()->getBeginLoc();
152+
return std::nullopt;
153+
case PreImplicitCallKind:
154+
return castAs<ImplicitCallPoint>().getLocation();
155+
case PostImplicitCallKind:
156+
return castAs<ImplicitCallPoint>().getLocation();
157+
case LoopExitKind:
158+
if (const Stmt *S = castAs<LoopExit>().getLoopStmt())
159+
return S->getBeginLoc();
160+
return std::nullopt;
161+
case EpsilonKind:
162+
return std::nullopt;
163+
}
164+
llvm_unreachable("Unknown ProgramPoint kind");
165+
}
166+
52167
void ProgramPoint::printJson(llvm::raw_ostream &Out, const char *NL) const {
53168
const ASTContext &Context =
54169
getLocationContext()->getAnalysisDeclContext()->getASTContext();

clang/lib/StaticAnalyzer/Core/BugReporter.cpp

+33-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
#include "llvm/Support/Compiler.h"
6363
#include "llvm/Support/ErrorHandling.h"
6464
#include "llvm/Support/MemoryBuffer.h"
65+
#include "llvm/Support/TimeProfiler.h"
6566
#include "llvm/Support/raw_ostream.h"
6667
#include <algorithm>
6768
#include <cassert>
@@ -287,6 +288,34 @@ class PathDiagnosticBuilder : public BugReporterContext {
287288
const PathSensitiveBugReport *getBugReport() const { return R; }
288289
};
289290

291+
std::string timeTraceName(const BugReportEquivClass &EQ) {
292+
if (!llvm::timeTraceProfilerEnabled())
293+
return "";
294+
const auto &BugReports = EQ.getReports();
295+
if (BugReports.empty())
296+
return "Empty Equivalence Class";
297+
const BugReport *R = BugReports.front().get();
298+
const auto &BT = R->getBugType();
299+
return ("Flushing EQC " + BT.getDescription()).str();
300+
}
301+
302+
llvm::TimeTraceMetadata timeTraceMetadata(const BugReportEquivClass &EQ) {
303+
// Must be called only when constructing non-bogus TimeTraceScope
304+
assert(llvm::timeTraceProfilerEnabled());
305+
306+
const auto &BugReports = EQ.getReports();
307+
if (BugReports.empty())
308+
return {};
309+
const BugReport *R = BugReports.front().get();
310+
const auto &BT = R->getBugType();
311+
auto Loc = R->getLocation().asLocation();
312+
std::string File = "";
313+
if (const auto *Entry = Loc.getFileEntry())
314+
File = Entry->tryGetRealPathName().str();
315+
return {BT.getCheckerName().str(), std::move(File),
316+
static_cast<int>(Loc.getLineNumber())};
317+
}
318+
290319
} // namespace
291320

292321
//===----------------------------------------------------------------------===//
@@ -2892,6 +2921,7 @@ std::optional<PathDiagnosticBuilder> PathDiagnosticBuilder::findValidReport(
28922921

28932922
if (R->isValid()) {
28942923
if (Reporter.getAnalyzerOptions().ShouldCrosscheckWithZ3) {
2924+
llvm::TimeTraceScope TCS{"Crosscheck with Z3"};
28952925
// If crosscheck is enabled, remove all visitors, add the refutation
28962926
// visitor and check again
28972927
R->clearVisitors();
@@ -3119,7 +3149,9 @@ BugReport *PathSensitiveBugReporter::findReportInEquivalenceClass(
31193149
return exampleReport;
31203150
}
31213151

3122-
void BugReporter::FlushReport(BugReportEquivClass& EQ) {
3152+
void BugReporter::FlushReport(BugReportEquivClass &EQ) {
3153+
llvm::TimeTraceScope TCS{timeTraceName(EQ),
3154+
[&EQ]() { return timeTraceMetadata(EQ); }};
31233155
SmallVector<BugReport*, 10> bugReports;
31243156
BugReport *report = findReportInEquivalenceClass(EQ, bugReports);
31253157
if (!report)

clang/lib/StaticAnalyzer/Core/BugSuppression.cpp

+28
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include "clang/StaticAnalyzer/Core/BugReporter/BugSuppression.h"
1010
#include "clang/AST/DynamicRecursiveASTVisitor.h"
1111
#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
12+
#include "llvm/Support/FormatVariadic.h"
13+
#include "llvm/Support/TimeProfiler.h"
1214

1315
using namespace clang;
1416
using namespace ento;
@@ -119,6 +121,29 @@ class CacheInitializer : public DynamicRecursiveASTVisitor {
119121
Ranges &Result;
120122
};
121123

124+
std::string timeScopeName(const Decl *DeclWithIssue) {
125+
if (!llvm::timeTraceProfilerEnabled())
126+
return "";
127+
return llvm::formatv(
128+
"BugSuppression::isSuppressed init suppressions cache for {0}",
129+
DeclWithIssue->getDeclKindName())
130+
.str();
131+
}
132+
133+
llvm::TimeTraceMetadata getDeclTimeTraceMetadata(const Decl *DeclWithIssue) {
134+
assert(DeclWithIssue);
135+
assert(llvm::timeTraceProfilerEnabled());
136+
std::string Name = "<noname>";
137+
if (const auto *ND = dyn_cast<NamedDecl>(DeclWithIssue)) {
138+
Name = ND->getNameAsString();
139+
}
140+
const auto &SM = DeclWithIssue->getASTContext().getSourceManager();
141+
auto Line = SM.getPresumedLineNumber(DeclWithIssue->getBeginLoc());
142+
auto Fname = SM.getFilename(DeclWithIssue->getBeginLoc());
143+
return llvm::TimeTraceMetadata{std::move(Name), Fname.str(),
144+
static_cast<int>(Line)};
145+
}
146+
122147
} // end anonymous namespace
123148

124149
// TODO: Introduce stable IDs for checkers and check for those here
@@ -177,6 +202,9 @@ bool BugSuppression::isSuppressed(const PathDiagnosticLocation &Location,
177202
std::make_pair(DeclWithIssue, CachedRanges{}));
178203
Ranges &SuppressionRanges = InsertionResult.first->second;
179204
if (InsertionResult.second) {
205+
llvm::TimeTraceScope TimeScope(
206+
timeScopeName(DeclWithIssue),
207+
[DeclWithIssue]() { return getDeclTimeTraceMetadata(DeclWithIssue); });
180208
// We haven't checked this declaration for suppressions yet!
181209
CacheInitializer::initialize(DeclWithIssue, SuppressionRanges);
182210
}

0 commit comments

Comments
 (0)