[clang-tools-extra] [clangd] Report preamble and AST build stats in --check (PR #213349)
via cfe-commits
cfe-commits at lists.llvm.org
Fri Jul 31 13:54:20 PDT 2026
https://github.com/StefanoD updated https://github.com/llvm/llvm-project/pull/213349
>From 7ff9a040d3cb6c2a5e35fa8e873f5133028c770b Mon Sep 17 00:00:00 2001
From: Stefano Di Martino <stefano.d at posteo.de>
Date: Fri, 31 Jul 2026 21:10:21 +0200
Subject: [PATCH] [clangd] Report preamble and AST build stats in --check
clangd --check is the tool users are pointed at when clangd feels slow, but
it reported nothing about where the time actually went. Profiling clangd
opening large translation units in llvm-project shows preamble construction
is 64-78% of the cold-open CPU cost (and PCH serialization roughly a fifth of
that), yet none of it was observable from --check.
There was also a real asymmetry: --check called buildPreamble() without a
PreambleBuildStats, and buildPreamble only installs the TimerFS wrapper when
Stats is non-null (Preamble.cpp). So unlike the LSP path (TUScheduler.cpp),
--check could not report filesystem time at all, and was measuring a slightly
different code path from the one the server runs.
Pass a PreambleBuildStats and report it, and time ParsedAST::build. Example
on clang/lib/AST/ASTContext.cpp:
Preamble stats: 5.92s total, of which 0.03s (1%) in the filesystem;
178455475 bytes memory at peak, 62794336 bytes serialized
AST stats: 2.64s to build
The filesystem ratio is directly useful: it distinguishes "clangd is slow
because your headers are on a network filesystem" from "clangd is slow
because this TU is genuinely expensive to parse".
Installing TimerFS on the --check path costs nothing measurable. A/B of the
unpatched vs patched binary, same build, both pinned to one core with the
performance governor and boost disabled (AMD Ryzen 7 1700, 21 runs each,
llvm/lib/Transforms/Scalar/GVN.cpp, --check-locations=0):
Benchmark 1 (21 runs): clangd-baseline ... --check=GVN.cpp
measurement mean +- s min ... max delta
wall_time 4.36s +- 27.8ms 4.31s ... 4.41s 0%
peak_rss 241MB +- 214KB 241MB ... 242MB 0%
cpu_cycles 12.1G +- 77.0M 12.0G ... 12.2G 0%
instructions 12.1G +- 7.53M 12.1G ... 12.2G 0%
cache_references 1.40G +- 5.21M 1.39G ... 1.41G 0%
cache_misses 426M +- 2.33M 421M ... 429M 0%
branch_misses 117M +- 718K 116M ... 119M 0%
Benchmark 2 (21 runs): clangd ... --check=GVN.cpp
measurement mean +- s min ... max delta
wall_time 4.34s +- 40.6ms 4.30s ... 4.51s - 0.4% +- 0.5%
peak_rss 241MB +- 198KB 241MB ... 242MB + 0.0% +- 0.1%
cpu_cycles 12.0G +- 115M 11.9G ... 12.5G - 0.4% +- 0.5%
instructions 12.1G +- 6.09M 12.1G ... 12.2G + 0.0% +- 0.0%
cache_references 1.40G +- 4.74M 1.39G ... 1.41G - 0.0% +- 0.2%
cache_misses 424M +- 2.48M 420M ... 428M - 0.3% +- 0.4%
branch_misses 117M +- 618K 116M ... 119M - 0.2% +- 0.4%
No delta is statistically significant (poop reported no significance markers
on any counter); instructions are unchanged at +0.0% +- 0.0%.
Test plan: check.test extended to cover both new lines; passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply at anthropic.com>
---
clang-tools-extra/clangd/test/GH75115.test | 2 ++
clang-tools-extra/clangd/test/check.test | 2 ++
clang-tools-extra/clangd/tool/Check.cpp | 16 +++++++++++++++-
3 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/clangd/test/GH75115.test b/clang-tools-extra/clangd/test/GH75115.test
index dc86f5b9a6cee..2d4ffc25178c2 100644
--- a/clang-tools-extra/clangd/test/GH75115.test
+++ b/clang-tools-extra/clangd/test/GH75115.test
@@ -5,7 +5,9 @@
// CHECK: Building preamble...
// CHECK-NEXT: Built preamble
// CHECK-NEXT: Indexing headers...
+// CHECK-NEXT: Preamble stats:
// CHECK-NEXT: Building AST...
+// CHECK-NEXT: AST stats:
// CHECK-NEXT: Indexing AST...
// CHECK-NEXT: Building inlay hints
// CHECK-NEXT: semantic highlighting
diff --git a/clang-tools-extra/clangd/test/check.test b/clang-tools-extra/clangd/test/check.test
index 60a542fba8bea..9f5cda1ac332e 100644
--- a/clang-tools-extra/clangd/test/check.test
+++ b/clang-tools-extra/clangd/test/check.test
@@ -5,7 +5,9 @@
// CHECK: internal (cc1) args are: -cc1
// CHECK: Building preamble...
// CHECK: Built preamble
+// CHECK: Preamble stats: {{.*}}s total, of which {{.*}}s ({{.*}}) in the filesystem; {{[0-9]+}} bytes memory at peak, {{[0-9]+}} bytes serialized
// CHECK: Building AST...
+// CHECK: AST stats: {{.*}}s to build
// CHECK: Testing features at each token
// CHECK-DAG: tweak: ExpandDeducedType
// CHECK-DAG: hover: true
diff --git a/clang-tools-extra/clangd/tool/Check.cpp b/clang-tools-extra/clangd/tool/Check.cpp
index 03c4f58a49c9c..f1b63afcaac38 100644
--- a/clang-tools-extra/clangd/tool/Check.cpp
+++ b/clang-tools-extra/clangd/tool/Check.cpp
@@ -245,6 +245,7 @@ class Checker {
// Build preamble and AST, and index them.
bool buildAST() {
log("Building preamble...");
+ PreambleBuildStats PreambleStats;
Preamble = buildPreamble(
File, *Invocation, Inputs, /*StoreInMemory=*/true,
[&](CapturedASTCtx Ctx,
@@ -254,16 +255,29 @@ class Checker {
log("Indexing headers...");
Index.updatePreamble(File, /*Version=*/"null", Ctx.getASTContext(),
Ctx.getPreprocessor(), *PI);
- });
+ },
+ &PreambleStats);
if (!Preamble) {
elog("Failed to build preamble");
return false;
}
+ log("Preamble stats: {0:F2}s total, of which {1:F2}s ({2:P0}) in the "
+ "filesystem; {3} bytes memory at peak, {4} bytes serialized",
+ PreambleStats.TotalBuildTime, PreambleStats.FileSystemTime,
+ PreambleStats.TotalBuildTime > 0
+ ? PreambleStats.FileSystemTime / PreambleStats.TotalBuildTime
+ : 0.0,
+ PreambleStats.BuildSize, PreambleStats.SerializedSize);
ErrCount += showErrors(Preamble->Diags);
log("Building AST...");
+ auto ASTStart = std::chrono::steady_clock::now();
AST = ParsedAST::build(File, Inputs, std::move(Invocation),
/*InvocationDiags=*/std::vector<Diag>{}, Preamble);
+ log("AST stats: {0:F2}s to build",
+ std::chrono::duration<double>(std::chrono::steady_clock::now() -
+ ASTStart)
+ .count());
if (!AST) {
elog("Failed to build AST");
return false;
More information about the cfe-commits
mailing list