[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