feat: persistent PCH/PCM cache across sessions (#391)

## Summary

PCH and PCM artifacts are now cached to disk at
`.clice/cache/{pch/,pcm/}` with content-addressed filenames, so they
survive server restarts. Dependency metadata is persisted in
`cache.json` (using eventide serde) with a shared path table for
deduplication.

### Key changes

- **protocol.h**: `output_path` field on `BuildPCHParams` /
`BuildPCMParams` so master specifies where workers write
- **stateless_worker.cpp**: Atomic write via `.tmp` + `fs::rename`;
`CompilationUnit` destroyed before rename to flush the file to disk;
fallback to temp file when `output_path` is empty (unit tests)
- **master_server.h**: `PCMState` struct, `pcm_states` map,
`load_cache()` / `save_cache()` / `cleanup_cache()` methods
- **master_server.cpp**: Cache lifecycle — load from `cache.json` on
startup, save after each PCH/PCM build and on exit; deterministic path
computation (`xxh3` preamble hash for PCH, module name + source path
hash for PCM); stale files (>7 days) cleaned on startup; `cache.json`
uses shared path table to avoid redundant storage of header paths across
entries
- **filesystem.h**: `fs::rename()` helper; `ThreadSafeFS` broadened to
match `.pch` extension instead of `preamble-` prefix
- **tests**: 11 new integration tests covering PCH/PCM persistence,
cross-session reuse, staleness detection, shared preamble dedup, and
restart survival; unit tests updated with `output_path`

### Naming scheme

- **PCH**: `.clice/cache/pch/<016x(xxh3(preamble))>.pch`
- **PCM**:
`.clice/cache/pcm/<module_name>-<016x(xxh3(source_path))>.pcm`

## Test plan

- [x] Unit tests — 448 passed
- [x] Integration tests — 92 passed (including 11 new persistent cache
tests)
- [x] Smoke tests — 1 passed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ykiko
2026-04-05 16:29:21 +08:00
committed by GitHub
parent 31d9c609b6
commit 3838bedcbf
9 changed files with 740 additions and 31 deletions

View File

@@ -49,6 +49,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
"--precompile",
iface};
params.module_name = "Hello";
params.output_path = tmp.path("Hello.pcm");
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value());
@@ -134,6 +135,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
"--precompile",
mod_a};
params.module_name = "A";
params.output_path = tmp.path("A.pcm");
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value() && result.value().success);
@@ -152,6 +154,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
"--precompile",
mod_b};
params.module_name = "B";
params.output_path = tmp.path("B.pcm");
params.pcms = {
{"A", pcm_a}
};
@@ -232,6 +235,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) {
"--precompile",
iface};
params.module_name = "Calc";
params.output_path = tmp.path("Calc.pcm");
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value() && result.value().success);

View File

@@ -52,6 +52,7 @@ TEST_CASE(BuildPCHThenCompile) {
dir,
main_file};
params.content = main_text;
params.output_path = tmp.path("preamble.pch");
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value());

View File

@@ -99,6 +99,7 @@ TEST_CASE(BuildPCHRequest) {
params.arguments =
{"clang++", "-resource-dir", std::string(resource_dir()), "-x", "c++-header", hdr};
params.content = "#pragma once\nint pch_global = 42;\n";
params.output_path = tmp.path("test_pch.pch");
auto result = co_await w.peer->send_request(params);
EXPECT_TRUE(result.has_value());
@@ -170,6 +171,7 @@ TEST_CASE(BuildPCMRequest) {
"--precompile",
src};
params.module_name = "test_module";
params.output_path = tmp.path("test_module.pcm");
auto result = co_await w.peer->send_request(params);
EXPECT_TRUE(result.has_value());