@@ -8,13 +8,17 @@ compile_commands.json
88CompilationDatabaseLoader (JSONCompilationDatabase / loadFromDirectory)
99 |
1010AnalysisDriver + llvm::DefaultThreadPool
11- | one task per TU, each with its own fresh rule instances and
12- | its own ClangTool -- see "Parallel translation-unit analysis"
11+ | one task per TU, each with its own fresh rule instances -- see
12+ | "Parallel translation-unit analysis"
13+ | IncrementalCache hit? -> skip straight to Rule::restoreState()
14+ | + cached diagnostics, no ClangTool at all -- see "Incremental
15+ | analysis cache"
1316 |
1417 +----+-----------------------------+
1518 | |
1619RuleFrontendAction -> ASTConsumer |
17- | per TU (inside one task): |
20+ | per TU (inside one task, |
21+ | only on a cache miss): |
1822 | - MatchFinder dispatches |
1923 | AST-matcher rules |
2024 | (missing-override, |
@@ -87,8 +91,10 @@ default, means "all available hardware threads"). Each task:
87911 . Builds its own fresh set of rule instances via ` RuleRegistry ` (the
8892 same rule ids ` AnalysisDriver ` was constructed with), not the driver's
8993 own instances.
90- 2 . Runs its own ` clang::tooling::ClangTool ` against just that one file,
91- with its own local ` std::vector<Diagnostic> ` sink.
94+ 2 . If an incremental cache is configured, checks it first (see below) --
95+ a hit skips the rest of this list entirely. Otherwise runs its own
96+ ` clang::tooling::ClangTool ` against just that one file, with its own
97+ local ` std::vector<Diagnostic> ` sink.
9298
9399Fresh instances per task, not shared ones, because an AST-matcher rule
94100stashes its current TU's ` TranslationUnitContext ` in a member between
@@ -115,6 +121,65 @@ returning -- output would otherwise depend on however the thread pool
115121happened to interleave tasks, which is a real, if easy to miss,
116122determinism regression once a driver goes from sequential to parallel.
117123
124+ ## Incremental analysis cache
125+
126+ ` IncrementalCache ` (` include/sentinel/core/IncrementalCache.h ` ,
127+ ` lib/core/IncrementalCache.cpp ` ) is deliberately Clang-agnostic, in the
128+ same spirit as ` dataflow::runForwardWorklist ` and ` graph/SCC.h ` : it knows
129+ about a "primary key" string, a list of ` {file, contentHash} `
130+ dependencies, and a ` CacheEntry ` blob; it has no idea what a TU or an AST
131+ is. All the Clang-specific glue lives in ` AnalysisDriver.cpp ` .
132+
133+ Two problems make a single "content hash of the file" not enough:
134+
135+ - A TU's result depends on more than the file's own text -- its compile
136+ flags, the enabled rule set, ` --header-filter ` , and the cpp-sentinel
137+ build itself can all change what gets reported without the file
138+ changing at all. These fold into the ** primary key**
139+ (` IncrementalCache::computeKey() ` ), computable * before* parsing, and
140+ changing any of them invalidates every entry that depended on the old
141+ value (there's no partial invalidation across a key change -- simplest
142+ correct behavior, and a version/flag/rule-set change is rare next to a
143+ source edit).
144+ - The TU's result also depends on every header it transitively includes,
145+ and that set isn't knowable until * after* a real parse. So freshness
146+ for a given primary key is checked lazily: ` AnalysisDriver `
147+ (` collectDependencyFiles() ` in ` RuleFrontendAction.cpp ` , wired through
148+ an optional out-parameter on ` createRuleFrontendAction() ` ) walks the
149+ TU's ` SourceManager::fileinfo_begin()/end() ` after a real analysis and
150+ records every real on-disk file it touched -- the main file plus every
151+ header -- each hashed via the same FNV-1a ` sentinel::hashContent() `
152+ ` Fingerprint.cpp ` already used (extracted to ` ContentHash.h ` so both
153+ agree on one implementation). ` IncrementalCache::lookup() ` re-hashes
154+ every recorded dependency's * current* content on every call; any
155+ mismatch (or a deleted file) is a miss.
156+
157+ The trickiest part is ` lock-order-inversion ` again: its ` finalize() `
158+ needs the full cross-TU picture, but a cache-hit TU is never re-parsed,
159+ so there's no fresh ` LockOrderFact ` list for it that run. `Rule::
160+ serializeState()` / ` restoreState()` close that gap generically -- a rule
161+ can serialize whatever per-TU state its ` finalize() ` needs into an opaque
162+ blob ` IncrementalCache ` stores alongside a TU's diagnostics, and
163+ reconstitute it on a later cache hit into a fresh per-task instance,
164+ which then flows into ` Rule::mergeFrom() ` exactly like a freshly-computed
165+ task's state would (see "Parallel translation-unit analysis" above) --
166+ the cache doesn't need a special code path for this at all, because a
167+ cache hit and a cache miss both end up producing "a rule instance with
168+ some state," merged the same way either way. Default no-op, like
169+ ` mergeFrom() ` ; only ` LockOrderInversionRule ` overrides it, serializing
170+ ` factStore_.facts() ` to a small JSON array.
171+ ` AnalysisDriverCacheTest.CrossFileCycleStillDetectedWithOneCachedAndOneChangedFile `
172+ is the test that actually proves this: two files form a lock-order cycle,
173+ one is touched (forcing a miss) and one isn't (a hit), and the cycle must
174+ still be detected -- exactly the case a cache that only stored
175+ diagnostics (and not this per-rule state) would silently break.
176+
177+ A TU that fails to compile is never cached (` AnalysisDriver.cpp ` ): its
178+ dependency list is likely incomplete (Clang can bail out before touching
179+ every header a correct parse would), so caching it risks a stale entry
180+ surviving even after the real problem is fixed -- simplest and safest to
181+ just retry a compile failure in full on every run.
182+
118183## The flagship rule: ` lock-order-inversion `
119184
120185Three moving pieces, in ` lib/rules/LockOrderInversionRule.cpp ` :
0 commit comments