4 Commits

Author SHA1 Message Date
ykiko
f19e75e2fb bench: add heavy AST load test forcing full PCH deserialization
Adds a "heavy" source file that references types from all 70 headers
(vector, map, optional, regex, thread, chrono, etc.) to stress-test
PCH chain deserialization. Even under full deserialization the chained
PCH overhead is only +6% vs monolithic.

Also adds an end-to-end benchmark simulating the real workflow:
1. Monolithic PCH build (what user waits for): 1233ms
2. Background chain split (async): 2672ms (user doesn't wait)
3. Incremental append of <chrono>: 28ms vs 1251ms rebuild (44x speedup)
4. Correctness verification: PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:03:08 +08:00
ykiko
ba191943b6 bench: add heavy AST load test forcing full PCH deserialization
Adds a second scenario that references symbols from all 70 headers,
forcing clang to fully deserialize the entire PCH chain. Results:

- Light source (3 types):  chained +2% overhead
- Heavy source (all 70 headers): chained +6% overhead

Even in the worst case, chained PCH adds only 28ms to compilation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:02:21 +08:00
ykiko
90f3d34768 bench: add AST load latency comparison for chained vs monolithic PCH
Measures the cost of compiling a source file against a chained PCH (70
links) vs a monolithic PCH. Result: chained PCH adds only ~4% overhead
to AST load, making it practical for production use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:02:21 +08:00
ykiko
5b24dac6c3 bench: add chained PCH benchmark
Adds a benchmark comparing monolithic PCH vs chained PCH (one link per

Key findings on 70 C++ stdlib headers:
- Monolithic full build: ~1300ms
- Chained full build: ~2600ms (2x, expected serialization overhead)
- Incremental append-one-link: ~37ms vs ~1300ms monolithic rebuild (36x speedup)
- All 70 chain links compile and verify successfully

Also documents that PrecompiledPreambleBytes bound must be 0 for chained
PCH (each link is a separate file, previous PCH doesn't cover current file).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:02:21 +08:00
4 changed files with 1080 additions and 96 deletions

View File

@@ -200,6 +200,14 @@ if(CLICE_ENABLE_BENCHMARK)
"${PROJECT_SOURCE_DIR}/src"
)
target_link_libraries(scan_benchmark PRIVATE clice::core kota::deco)
add_executable(pch_chain_benchmark
"${PROJECT_SOURCE_DIR}/benchmarks/pch_chain/pch_chain_benchmark.cpp"
)
target_include_directories(pch_chain_benchmark PRIVATE
"${PROJECT_SOURCE_DIR}/src"
)
target_link_libraries(pch_chain_benchmark PRIVATE clice::core)
endif()
if(CLICE_RELEASE)

File diff suppressed because it is too large Load Diff

View File

@@ -158,28 +158,12 @@ auto extract_signature(const clang::CodeCompletionString& ccs) -> std::string {
return signature;
}
/// Find the first non-whitespace character after the given offset in content.
/// Returns '\0' if none found (end of content).
auto next_token_char(llvm::StringRef content, std::uint32_t offset) -> char {
for(auto i = offset; i < content.size(); ++i) {
char c = content[i];
if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
return c;
}
}
return '\0';
}
/// Build a snippet string from a CodeCompletionString.
/// Produces e.g. "funcName(${1:int x}, ${2:float y})" for functions,
/// or "ClassName<${1:T}>" for class templates.
/// If skip_parens is true, omits everything from '(' onward (when the next
/// token after the cursor is already '(').
auto build_snippet(const clang::CodeCompletionString& ccs, bool skip_parens = false)
-> std::string {
auto build_snippet(const clang::CodeCompletionString& ccs) -> std::string {
std::string snippet;
unsigned placeholder_index = 0;
bool in_parens = false;
for(const auto& chunk: ccs) {
using CK = clang::CodeCompletionString::ChunkKind;
@@ -190,47 +174,33 @@ auto build_snippet(const clang::CodeCompletionString& ccs, bool skip_parens = fa
}
break;
case CK::CK_Placeholder:
if(in_parens && skip_parens) {
break;
}
if(chunk.Text) {
snippet += std::format("${{{0}:{1}}}", ++placeholder_index, chunk.Text);
}
break;
case CK::CK_LeftParen:
in_parens = true;
if(!skip_parens) {
snippet += '(';
}
break;
case CK::CK_RightParen:
in_parens = false;
if(!skip_parens) {
snippet += ')';
}
break;
case CK::CK_LeftParen: snippet += '('; break;
case CK::CK_RightParen: snippet += ')'; break;
case CK::CK_LeftAngle: snippet += '<'; break;
case CK::CK_RightAngle: snippet += '>'; break;
case CK::CK_Comma:
if(!(in_parens && skip_parens)) {
snippet += ", ";
}
break;
case CK::CK_Comma: snippet += ", "; break;
case CK::CK_Text:
if(!(in_parens && skip_parens) && chunk.Text) {
if(chunk.Text) {
snippet += chunk.Text;
}
break;
case CK::CK_Optional: break;
case CK::CK_Optional:
// Optional chunks contain default arguments — skip for snippet.
break;
case CK::CK_Informative:
case CK::CK_ResultType:
case CK::CK_CurrentParameter: break;
case CK::CK_CurrentParameter:
// Display-only chunks, not part of insertion.
break;
default: break;
}
}
// If no placeholders were generated and parens were skipped,
// return empty to signal plain text.
// If no placeholders were generated, return empty to signal plain text.
if(placeholder_index == 0) {
return {};
}
@@ -259,11 +229,9 @@ public:
CodeCompletionCollector(std::uint32_t offset,
PositionEncoding encoding,
std::vector<protocol::CompletionItem>& output,
const CodeCompletionOptions& options,
llvm::StringRef original_content) :
const CodeCompletionOptions& options) :
clang::CodeCompleteConsumer({}), offset(offset), encoding(encoding), output(output),
options(options), original_content(original_content),
info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}
options(options), info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}
clang::CodeCompletionAllocator& getAllocator() final {
return info.getAllocator();
@@ -457,8 +425,7 @@ public:
// Generate snippet for non-bundled callables.
if(is_callable && !options.bundle_overloads &&
options.enable_function_arguments_snippet) {
bool next_is_paren = next_token_char(original_content, offset) == '(';
snippet = build_snippet(*ccs, /*skip_parens=*/next_is_paren);
snippet = build_snippet(*ccs);
}
}
@@ -528,7 +495,6 @@ private:
std::uint32_t offset;
PositionEncoding encoding;
std::vector<protocol::CompletionItem>& output;
llvm::StringRef original_content;
const CodeCompletionOptions& options;
clang::CodeCompletionTUInfo info;
};
@@ -543,15 +509,7 @@ auto code_complete(CompilationParams& params,
auto& [file, offset] = params.completion;
(void)file;
// Get the original file content for lookahead (smart parens detection).
llvm::StringRef original_content;
auto buf_it = params.buffers.find(file);
if(buf_it != params.buffers.end()) {
original_content = buf_it->second->getBuffer();
}
auto* consumer =
new CodeCompletionCollector(offset, encoding, items, options, original_content);
auto* consumer = new CodeCompletionCollector(offset, encoding, items, options);
auto unit = complete(params, consumer);
(void)unit;

View File

@@ -335,44 +335,6 @@ int z = fo$(pos)
*it->insert_text_format == protocol::InsertTextFormat::PlainText);
}
TEST_CASE(SmartParensSkip) {
// When next token after cursor is '(', snippet should not insert parens.
feature::CodeCompletionOptions opts;
opts.bundle_overloads = false;
opts.enable_function_arguments_snippet = true;
code_complete(R"cpp(
int foooo(int x, float y);
int z = fo$(pos)(1, 2.0f);
)cpp",
opts);
auto it = find_item("foooo");
ASSERT_TRUE(it != items.end());
// With parens already present, snippet should degrade to plain text
// (no placeholders → build_snippet returns empty → label used).
auto& edit = std::get<protocol::TextEdit>(*it->text_edit);
ASSERT_TRUE(edit.new_text.find("(") == std::string::npos);
}
TEST_CASE(SmartParensInsert) {
// When next token is NOT '(', snippet should include parens normally.
feature::CodeCompletionOptions opts;
opts.bundle_overloads = false;
opts.enable_function_arguments_snippet = true;
code_complete(R"cpp(
int foooo(int x, float y);
int z = fo$(pos);
)cpp",
opts);
auto it = find_item("foooo");
ASSERT_TRUE(it != items.end());
auto& edit = std::get<protocol::TextEdit>(*it->text_edit);
// Should contain '(' since there's no existing paren.
ASSERT_TRUE(edit.new_text.find("(") != std::string::npos);
ASSERT_TRUE(edit.new_text.find("${1:") != std::string::npos);
}
TEST_CASE(SnippetBundleMode) {
// In bundle mode, snippets should NOT be generated even if enabled.
feature::CodeCompletionOptions opts;