Files
clice/src/server/stateless_worker.cpp
ykiko 939ab6d0d4 feat(server): concurrent background indexing with priority control (#432)
## Summary

- Rewrite serial background indexing to concurrent dispatch (up to
`stateless_worker_count / 2` parallel tasks)
- Add depth-counted pause/resume mechanism: completion and
signature-help handlers pause new index dispatches to prioritize user
requests
- Report indexing progress via LSP `$/progress` notifications
(percentage + file count)
- Lower thread scheduling priority (`nice +10`) for index tasks in
stateless workers via RAII `ScopedNice` guard

## Test plan

- [x] `pixi run format` — no changes
- [x] `pixi run unit-test Debug` — 551 passed, 9 skipped (pre-existing)
- [x] `pixi run smoke-test Debug` — 2/2 passed
- [x] `pixi run integration-test Debug` — 121 passed, 3 failed (all
pre-existing on main: header_context x2, staleness x1)
- [ ] Manual test: open a large project (e.g. LLVM), verify progress bar
appears and completion remains responsive during indexing

🤖 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**
  * Pause/resume controls for background indexing
* Concurrent, adaptive background indexing with configurable concurrency
* LSP progress reporting (create/begin/report/end) and updated
completion metrics

* **Behavior Change**
* Code completion and signature help temporarily pause indexing for
responsiveness
* Background indexing runs with reduced scheduling priority on
non-Windows and logs "files dispatched" at finish

* **Tests**
* Test client fixture defaults init options and sets workspace cache dir
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 13:28:59 +08:00

322 lines
10 KiB
C++

#include "server/stateless_worker.h"
#include "compile/compilation.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol.h"
#include "server/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
#include "llvm/Support/raw_ostream.h"
namespace clice {
/// RAII guard that lowers the current process's scheduling priority and
/// restores it on destruction.
struct ScopedNice {
int saved;
explicit ScopedNice(int increment = 10) {
auto p = kota::sys::priority();
saved = p ? *p : 0;
kota::sys::set_priority(saved + increment);
}
~ScopedNice() {
kota::sys::set_priority(saved);
}
};
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::BincodePeer::RequestContext;
/// Extract error messages from compilation diagnostics.
static std::string collect_errors(CompilationUnit& unit) {
std::string errors;
for(auto& diag: unit.diagnostics()) {
if(diag.id.level >= DiagnosticLevel::Error) {
if(!errors.empty())
errors += "; ";
errors += diag.message;
}
}
return errors;
}
/// Build a TUIndex, serialize it, and return as a string.
static std::string serialize_tu_index(CompilationUnit& unit, bool interested_only = false) {
auto tu_index = index::TUIndex::build(unit, interested_only);
if(!interested_only) {
tu_index.main_file_index = index::FileIndex();
}
std::string serialized;
llvm::raw_string_ostream os(serialized);
tu_index.serialize(os);
return serialized;
}
/// Write compilation output to disk, handling tmp+rename pattern.
/// Returns the final path on success, or empty string on failure.
static std::string finalize_output(const std::string& output_path,
const std::string& tmp_path,
const std::string& file,
const char* label) {
if(!output_path.empty()) {
auto ec = fs::rename(tmp_path, output_path);
if(ec) {
return output_path;
} else {
LOG_WARN("{}: rename {} -> {} failed: {}",
label,
tmp_path,
output_path,
ec.error().message());
return tmp_path;
}
}
return tmp_path;
}
static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Preamble;
fill_args(cp, params.directory, params.arguments);
cp.add_remapped_file(params.file, params.text, params.preamble_bound);
std::string tmp_path;
bool has_output = !params.output_path.empty();
if(has_output) {
tmp_path = params.output_path + ".tmp";
} else {
auto tmp = fs::createTemporaryFile("clice-pch", "pch");
if(!tmp) {
LOG_ERROR("BuildPCH: failed to create temp file");
return {false, "Failed to create temporary PCH file"};
}
tmp_path = *tmp;
}
cp.output_file = tmp_path;
PCHInfo pch_info;
auto unit = compile(cp, pch_info);
bool success = unit.completed();
std::string errors;
if(!success)
errors = collect_errors(unit);
std::string tu_index_data;
std::string pch_links_json;
if(success) {
tu_index_data = serialize_tu_index(unit);
auto links = feature::document_links(unit);
auto raw = to_raw(links);
pch_links_json = std::move(raw.data);
}
// Destroy CompilationUnit to flush PCH to disk.
unit = CompilationUnit(nullptr);
if(success) {
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCH");
LOG_INFO("BuildPCH done: file={}, output={}, {}ms", params.file, final_path, timer.ms());
worker::BuildResult result;
result.success = true;
result.output_path = std::move(final_path);
result.deps = pch_info.deps;
result.tu_index_data = std::move(tu_index_data);
result.pch_links_json = std::move(pch_links_json);
return result;
} else {
LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", params.file, timer.ms(), errors);
fs::remove(tmp_path);
return {false, errors.empty() ? "PCH compilation failed" : errors};
}
}
static worker::BuildResult handle_build_pcm(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::ModuleInterface;
fill_args(cp, params.directory, params.arguments);
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
std::string tmp_path;
bool has_output = !params.output_path.empty();
if(has_output) {
tmp_path = params.output_path + ".tmp";
} else {
auto tmp = fs::createTemporaryFile("clice-pcm", "pcm");
if(!tmp) {
LOG_ERROR("BuildPCM: failed to create temp file");
return {false, "Failed to create temporary PCM file"};
}
tmp_path = *tmp;
}
cp.output_file = tmp_path;
PCMInfo pcm_info;
auto unit = compile(cp, pcm_info);
bool success = unit.completed();
std::string errors;
if(!success)
errors = collect_errors(unit);
std::string tu_index_data;
if(success)
tu_index_data = serialize_tu_index(unit, true);
unit = CompilationUnit(nullptr);
if(success) {
auto final_path = finalize_output(params.output_path, tmp_path, params.file, "BuildPCM");
LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
worker::BuildResult result;
result.success = true;
result.output_path = std::move(final_path);
result.deps = pcm_info.deps;
result.tu_index_data = std::move(tu_index_data);
return result;
} else {
LOG_WARN("BuildPCM failed: module={}, {}ms, errors=[{}]",
params.module_name,
timer.ms(),
errors);
fs::remove(tmp_path);
return {false, errors.empty() ? "PCM compilation failed" : errors};
}
}
static worker::BuildResult handle_index(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Indexing;
fill_args(cp, params.directory, params.arguments);
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
auto unit = compile(cp);
if(!unit.completed()) {
LOG_WARN("Index failed: file={}, {}ms", params.file, timer.ms());
return {false, "Index compilation failed"};
}
auto tu_index = index::TUIndex::build(unit);
std::string serialized;
llvm::raw_string_ostream os(serialized);
tu_index.serialize(os);
LOG_INFO("Index done: file={}, {} symbols, {}ms",
params.file,
tu_index.symbols.size(),
timer.ms());
worker::BuildResult result;
result.success = true;
result.tu_index_data = std::move(serialized);
return result;
}
static worker::BuildResult handle_completion(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Completion;
fill_args(cp, params.directory, params.arguments);
if(!params.pch.first.empty()) {
cp.pch = params.pch;
}
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
cp.add_remapped_file(params.file, params.text);
cp.completion = {params.file, params.offset};
auto items = feature::code_complete(cp);
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
worker::BuildResult result;
result.result_json = to_raw(items);
return result;
}
static worker::BuildResult handle_signature_help(const worker::BuildParams& params) {
ScopedTimer timer;
CompilationParams cp;
cp.kind = CompilationKind::Completion;
fill_args(cp, params.directory, params.arguments);
if(!params.pch.first.empty()) {
cp.pch = params.pch;
}
for(auto& [name, path]: params.pcms) {
cp.pcms.try_emplace(name, path);
}
cp.add_remapped_file(params.file, params.text);
cp.completion = {params.file, params.offset};
auto help = feature::signature_help(cp);
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
worker::BuildResult result;
result.result_json = to_raw(help);
return result;
}
int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir) {
logging::stderr_logger(worker_name, logging::options);
if(!log_dir.empty()) {
logging::file_logger(worker_name, log_dir, logging::options);
}
LOG_INFO("Starting stateless worker");
kota::event_loop loop;
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
if(!transport_result) {
LOG_ERROR("Failed to open stdio transport");
return 1;
}
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
peer.on_request([&](RequestContext& ctx,
const worker::BuildParams& params) -> RequestResult<worker::BuildParams> {
using K = worker::BuildKind;
auto result = co_await kota::queue([&]() -> worker::BuildResult {
switch(params.kind) {
case K::BuildPCH: return handle_build_pch(params);
case K::BuildPCM: return handle_build_pcm(params);
case K::Index: {
ScopedNice guard;
return handle_index(params);
}
case K::Completion: return handle_completion(params);
case K::SignatureHelp: return handle_signature_help(params);
}
return {false, "Unknown build kind"};
});
co_return result.value();
});
LOG_INFO("Stateless worker ready, waiting for requests");
loop.schedule(peer.run());
auto ret = loop.run();
LOG_INFO("Stateless worker exiting with code {}", ret);
return ret;
}
} // namespace clice