feat: integrate PCH into MasterServer build drain (#381)
## Summary - Add `ensure_pch()` helper to MasterServer that builds/reuses precompiled headers via stateless workers, with preamble hash-based staleness detection (xxh3_64bits) - Fix `BuildPCHParams` to carry `preamble_bound` so the stateless worker truncates content at the preamble boundary (fixes redefinition errors when PCH included full file) - Wire PCH into both `run_build_drain` (stateful compile path) and `forward_stateless` (completion/signatureHelp path) - Add PCH state cleanup on `didClose` and hash invalidation on `didSave` ## Test plan - [x] 398 unit tests pass (including 6 new PCH tests: PreambleHash x3, PCHWorker x2, BuildPCHRequest assertion) - [x] 5 new integration tests pass (`test_pch.py`: diagnostics on open, body edit recompile, no-include file, hover with PCH, completion with PCH) - [x] 21 existing integration tests pass unchanged - [x] Build succeeds with 0 errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Precompiled header (PCH) caching to speed compilations and reduce edit latency * Automatic attachment of cached PCH to compile requests, improving hover and completion responsiveness * Module-aware completions expanded to include available module artifacts from other files * **Bug Fixes** * PCH cache cleared on file close; saving now triggers broader PCH invalidation to prevent stale PCH use * **Tests** * Added unit and integration tests exercising PCH build, reuse, and editor interactions <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
#include "support/filesystem.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
#include "llvm/Support/xxhash.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
@@ -274,6 +276,64 @@ int bar() { return 3; }
|
||||
|
||||
}; // TEST_SUITE(Compiler)
|
||||
|
||||
TEST_SUITE(PreambleHash) {
|
||||
|
||||
TEST_CASE(StableForBodyChanges) {
|
||||
// Same preamble (#include lines) but different body → same hash → PCH reusable.
|
||||
llvm::StringRef v1 = R"cpp(
|
||||
#include "a.h"
|
||||
#include "b.h"
|
||||
int x = 1;
|
||||
)cpp";
|
||||
llvm::StringRef v2 = R"cpp(
|
||||
#include "a.h"
|
||||
#include "b.h"
|
||||
int x = 2;
|
||||
void foo() {}
|
||||
)cpp";
|
||||
|
||||
auto bound1 = compute_preamble_bound(v1);
|
||||
auto bound2 = compute_preamble_bound(v2);
|
||||
EXPECT_EQ(bound1, bound2);
|
||||
|
||||
auto hash1 = llvm::xxh3_64bits(v1.substr(0, bound1));
|
||||
auto hash2 = llvm::xxh3_64bits(v2.substr(0, bound2));
|
||||
EXPECT_EQ(hash1, hash2);
|
||||
}
|
||||
|
||||
TEST_CASE(ChangesForNewInclude) {
|
||||
// Different preamble (#include added) → different hash → PCH must rebuild.
|
||||
llvm::StringRef v1 = R"cpp(
|
||||
#include "a.h"
|
||||
int x = 1;
|
||||
)cpp";
|
||||
llvm::StringRef v2 = R"cpp(
|
||||
#include "a.h"
|
||||
#include "b.h"
|
||||
int x = 1;
|
||||
)cpp";
|
||||
|
||||
auto bound1 = compute_preamble_bound(v1);
|
||||
auto bound2 = compute_preamble_bound(v2);
|
||||
EXPECT_NE(bound1, bound2);
|
||||
|
||||
auto hash1 = llvm::xxh3_64bits(v1.substr(0, bound1));
|
||||
auto hash2 = llvm::xxh3_64bits(v2.substr(0, bound2));
|
||||
EXPECT_NE(hash1, hash2);
|
||||
}
|
||||
|
||||
TEST_CASE(ZeroBoundNoPCH) {
|
||||
// No preprocessor directives → bound is 0 → PCH should be skipped.
|
||||
llvm::StringRef code = R"cpp(
|
||||
int main() { return 0; }
|
||||
)cpp";
|
||||
|
||||
auto bound = compute_preamble_bound(code);
|
||||
EXPECT_EQ(bound, 0u);
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(PreambleHash)
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace clice::testing
|
||||
|
||||
153
tests/unit/server/pch_worker_tests.cpp
Normal file
153
tests/unit/server/pch_worker_tests.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "test/test.h"
|
||||
#include "server/protocol.h"
|
||||
#include "server/worker_test_helpers.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
// ============================================================================
|
||||
// End-to-end PCH compilation through real workers:
|
||||
// 1. Stateless worker builds PCH for preamble headers
|
||||
// 2. Stateful worker compiles a file using the PCH
|
||||
// ============================================================================
|
||||
|
||||
TEST_SUITE(PCHWorker) {
|
||||
|
||||
TEST_CASE(BuildPCHThenCompile) {
|
||||
TempDir tmp;
|
||||
|
||||
tmp.touch("common.h", R"cpp(struct Point { int x, y; };)cpp" "\n");
|
||||
auto header = tmp.path("common.h");
|
||||
|
||||
std::string main_text = "#include \"common.h\"\nPoint p{1,2};\n";
|
||||
tmp.touch("main.cpp", main_text);
|
||||
auto main_file = tmp.path("main.cpp");
|
||||
|
||||
auto dir = std::string(tmp.root);
|
||||
|
||||
// --- Phase 1: Build PCH via stateless worker ---
|
||||
WorkerHandle sl;
|
||||
ASSERT_TRUE(sl.spawn("stateless-worker"));
|
||||
|
||||
std::string pch_path;
|
||||
bool phase1_done = false;
|
||||
|
||||
sl.run([&]() -> et::task<> {
|
||||
worker::BuildPCHParams params;
|
||||
params.file = main_file;
|
||||
params.directory = dir;
|
||||
params.arguments = {"clang++",
|
||||
"-resource-dir",
|
||||
std::string(resource_dir()),
|
||||
"-x",
|
||||
"c++-header",
|
||||
"-I",
|
||||
dir,
|
||||
main_file};
|
||||
params.content = main_text;
|
||||
|
||||
auto result = co_await sl.peer->send_request(params);
|
||||
CO_ASSERT_TRUE(result.has_value());
|
||||
CO_ASSERT_TRUE(result.value().success);
|
||||
pch_path = result.value().pch_path;
|
||||
EXPECT_FALSE(pch_path.empty());
|
||||
|
||||
phase1_done = true;
|
||||
sl.peer->close_output();
|
||||
});
|
||||
|
||||
ASSERT_TRUE(phase1_done);
|
||||
ASSERT_FALSE(pch_path.empty());
|
||||
|
||||
// Verify the PCH file exists on disk.
|
||||
ASSERT_TRUE(llvm::sys::fs::exists(pch_path));
|
||||
|
||||
// --- Phase 2: Compile with PCH via stateful worker ---
|
||||
WorkerHandle sf;
|
||||
ASSERT_TRUE(sf.spawn("stateful-worker"));
|
||||
|
||||
bool phase2_done = false;
|
||||
|
||||
auto preamble_bound = compute_preamble_bound(main_text);
|
||||
|
||||
sf.run([&]() -> et::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = main_file;
|
||||
params.version = 1;
|
||||
params.text = main_text;
|
||||
params.directory = dir;
|
||||
params.arguments = {"clang++",
|
||||
"-resource-dir",
|
||||
std::string(resource_dir()),
|
||||
"-fsyntax-only",
|
||||
"-I",
|
||||
dir,
|
||||
main_file};
|
||||
params.pch = {pch_path, preamble_bound};
|
||||
|
||||
auto result = co_await sf.peer->send_request(params);
|
||||
CO_ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value().version, 1);
|
||||
|
||||
phase2_done = true;
|
||||
sf.peer->close_output();
|
||||
});
|
||||
|
||||
ASSERT_TRUE(phase2_done);
|
||||
|
||||
// Cleanup PCH temp file.
|
||||
std::remove(pch_path.c_str());
|
||||
}
|
||||
|
||||
TEST_CASE(CompileWithoutPCHStillWorks) {
|
||||
TempDir tmp;
|
||||
|
||||
tmp.touch("common.h", R"cpp(struct Point { int x, y; };)cpp" "\n");
|
||||
std::string main_text = "#include \"common.h\"\nPoint p{1,2};\n";
|
||||
tmp.touch("main.cpp", main_text);
|
||||
auto main_file = tmp.path("main.cpp");
|
||||
|
||||
auto dir = std::string(tmp.root);
|
||||
|
||||
WorkerHandle sf;
|
||||
ASSERT_TRUE(sf.spawn("stateful-worker"));
|
||||
|
||||
bool compile_done = false;
|
||||
|
||||
sf.run([&]() -> et::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = main_file;
|
||||
params.version = 1;
|
||||
params.text = main_text;
|
||||
params.directory = dir;
|
||||
params.arguments = {"clang++",
|
||||
"-resource-dir",
|
||||
std::string(resource_dir()),
|
||||
"-fsyntax-only",
|
||||
"-I",
|
||||
dir,
|
||||
main_file};
|
||||
// pch left as default (empty path, 0 bound).
|
||||
|
||||
auto result = co_await sf.peer->send_request(params);
|
||||
CO_ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value().version, 1);
|
||||
|
||||
compile_done = true;
|
||||
sf.peer->close_output();
|
||||
});
|
||||
|
||||
ASSERT_TRUE(compile_done);
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(PCHWorker)
|
||||
|
||||
} // namespace
|
||||
} // namespace clice::testing
|
||||
@@ -102,6 +102,12 @@ TEST_CASE(BuildPCHRequest) {
|
||||
|
||||
auto result = co_await w.peer->send_request(params);
|
||||
EXPECT_TRUE(result.has_value());
|
||||
if(!result.has_value()) {
|
||||
w.peer->close_output();
|
||||
co_return;
|
||||
}
|
||||
EXPECT_TRUE(result.value().success);
|
||||
EXPECT_FALSE(result.value().pch_path.empty());
|
||||
test_done = true;
|
||||
w.peer->close_output();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user