feat: implement multi-process LSP server architecture (#364)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
159
src/clice.cc
159
src/clice.cc
@@ -1,3 +1,160 @@
|
||||
#include <cstdint>
|
||||
#include <print>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/deco/macro.h"
|
||||
#include "eventide/deco/runtime.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "server/master_server.h"
|
||||
#include "server/stateful_worker.h"
|
||||
#include "server/stateless_worker.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
struct Options {
|
||||
DecoKV(names = {"--mode"};
|
||||
help = "Running mode: pipe, socket, stateless-worker, stateful-worker";
|
||||
required = false;)
|
||||
<std::string> mode;
|
||||
|
||||
DecoKV(names = {"--host"}; help = "Socket mode address"; required = false;)
|
||||
<std::string> host = "127.0.0.1";
|
||||
|
||||
DecoKV(names = {"--port"}; help = "Socket mode port"; required = false;)
|
||||
<int> port = 50051;
|
||||
|
||||
DecoKV(names = {"--stateful-worker-count"}; help = "Number of stateful workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateful_worker_count;
|
||||
|
||||
DecoKV(names = {"--stateless-worker-count"}; help = "Number of stateless workers";
|
||||
required = false;)
|
||||
<std::uint32_t> stateless_worker_count;
|
||||
|
||||
DecoKV(names = {"--worker-memory-limit"}; help = "Memory limit per stateful worker (bytes)";
|
||||
required = false;)
|
||||
<std::uint64_t> worker_memory_limit;
|
||||
|
||||
DecoFlag(names = {"-h", "--help"}; help = "Show help message"; required = false;)
|
||||
help;
|
||||
|
||||
DecoFlag(names = {"-v", "--version"}; help = "Show version"; required = false;)
|
||||
version;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
return 0;
|
||||
auto args = deco::util::argvify(argc, argv);
|
||||
auto result = deco::cli::parse<clice::Options>(args);
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_ERROR("{}", result.error().message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto& opts = result->options;
|
||||
|
||||
if(opts.help.value_or(false)) {
|
||||
auto dispatcher = deco::cli::Dispatcher<clice::Options>("clice [OPTIONS]");
|
||||
std::ostringstream oss;
|
||||
dispatcher.usage(oss, true);
|
||||
std::print("{}", oss.str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(opts.version.value_or(false)) {
|
||||
std::println("clice version 0.1.0");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(!opts.mode.has_value()) {
|
||||
LOG_ERROR("--mode is required");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string self_path = llvm::sys::fs::getMainExecutable(argv[0], (void*)main);
|
||||
if(!clice::fs::init_resource_dir(self_path)) {
|
||||
LOG_ERROR("Cannot find the resource dir: {}", self_path);
|
||||
}
|
||||
|
||||
auto& mode = *opts.mode;
|
||||
|
||||
if(mode == "stateless-worker") {
|
||||
return clice::run_stateless_worker_mode();
|
||||
}
|
||||
|
||||
if(mode == "stateful-worker") {
|
||||
auto mem_limit = opts.worker_memory_limit.value_or(4ULL * 1024 * 1024 * 1024);
|
||||
return clice::run_stateful_worker_mode(mem_limit);
|
||||
}
|
||||
|
||||
if(mode == "pipe") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
namespace et = eventide;
|
||||
et::event_loop loop;
|
||||
|
||||
auto transport = et::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport) {
|
||||
LOG_ERROR("failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
et::ipc::JsonPeer peer(loop, std::move(*transport));
|
||||
clice::MasterServer server(loop, peer, std::move(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
loop.schedule(peer.run());
|
||||
return loop.run();
|
||||
}
|
||||
|
||||
if(mode == "socket") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
namespace et = eventide;
|
||||
et::event_loop loop;
|
||||
|
||||
auto host = opts.host.value_or("127.0.0.1");
|
||||
auto port = opts.port.value_or(50051);
|
||||
|
||||
auto acceptor = et::tcp::listen(host, port, {}, loop);
|
||||
if(!acceptor) {
|
||||
LOG_ERROR("failed to listen on {}:{}", host, port);
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_INFO("Listening on {}:{} ...", host, port);
|
||||
|
||||
auto task = [&]() -> et::task<> {
|
||||
auto client = co_await acceptor->accept();
|
||||
if(!client.has_value()) {
|
||||
LOG_ERROR("failed to accept connection");
|
||||
loop.stop();
|
||||
co_return;
|
||||
}
|
||||
|
||||
LOG_INFO("Client connected");
|
||||
|
||||
auto transport = std::make_unique<et::ipc::StreamTransport>(std::move(client.value()));
|
||||
et::ipc::JsonPeer peer(loop, std::move(transport));
|
||||
clice::MasterServer server(loop, peer, std::string(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
co_await peer.run();
|
||||
peer.close();
|
||||
loop.stop();
|
||||
};
|
||||
|
||||
loop.schedule(task());
|
||||
return loop.run();
|
||||
}
|
||||
|
||||
LOG_ERROR("unknown mode '{}'", mode);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -44,13 +44,17 @@ std::unique_ptr<clang::CompilerInvocation>
|
||||
|
||||
std::unique_ptr<clang::CompilerInvocation> invocation;
|
||||
|
||||
/// Arguments from compilation database are already cc1
|
||||
if(params.arguments_from_database) {
|
||||
/// If the second argument is "-cc1", the arguments are already expanded
|
||||
/// (e.g. from compilation database + query_toolchain). Skip driver and "-cc1"
|
||||
/// and create invocation directly from the cc1 args.
|
||||
bool is_cc1 = params.arguments.size() >= 2 && llvm::StringRef(params.arguments[1]) == "-cc1";
|
||||
if(is_cc1) {
|
||||
invocation = std::make_unique<clang::CompilerInvocation>();
|
||||
if(!clang::CompilerInvocation::CreateFromArgs(*invocation,
|
||||
llvm::ArrayRef(params.arguments).drop_front(),
|
||||
*diagnostic_engine,
|
||||
params.arguments[0])) {
|
||||
if(!clang::CompilerInvocation::CreateFromArgs(
|
||||
*invocation,
|
||||
llvm::ArrayRef(params.arguments).drop_front(2),
|
||||
*diagnostic_engine,
|
||||
params.arguments[0])) {
|
||||
LOG_ERROR_RET(nullptr,
|
||||
" Fail to create invocation, arguments list is: {}",
|
||||
print_argv(params.arguments));
|
||||
|
||||
@@ -75,8 +75,6 @@ struct CompilationParams {
|
||||
|
||||
std::string directory;
|
||||
|
||||
bool arguments_from_database = false;
|
||||
|
||||
/// Responsible for storing the arguments.
|
||||
std::vector<const char*> arguments;
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ std::optional<std::string> DiagnosticID::diagnostic_document_uri() const {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool DiagnosticID::is_deprecated() const {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "compile/toolchain.h"
|
||||
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -383,6 +382,7 @@ std::vector<const char*> query_toolchain(const QueryParams& params) {
|
||||
query_driver(params_copy.arguments,
|
||||
[&](const char* driver, llvm::ArrayRef<const char*> cc1_args) {
|
||||
result.emplace_back(params.callback(driver));
|
||||
result.emplace_back(params.callback("-cc1"));
|
||||
for(auto arg: cc1_args) {
|
||||
result.emplace_back(params.callback(arg));
|
||||
}
|
||||
@@ -390,6 +390,8 @@ std::vector<const char*> query_toolchain(const QueryParams& params) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<const char*> query_gcc_toolchain(const QueryParams& params) {
|
||||
@@ -417,8 +419,13 @@ std::vector<const char*> query_gcc_toolchain(const QueryParams& params) {
|
||||
}
|
||||
}
|
||||
|
||||
target = std::format("--target={}", target);
|
||||
install_path = std::format("--gcc-install-dir={}", install_path);
|
||||
llvm::SmallString<64> formatted_target("--target=");
|
||||
formatted_target += target;
|
||||
target = formatted_target;
|
||||
|
||||
llvm::SmallString<64> formatted_install_path("--gcc-install-dir=");
|
||||
formatted_install_path += install_path;
|
||||
install_path = formatted_install_path;
|
||||
|
||||
query_arguments.clear();
|
||||
query_arguments.emplace_back(arguments.consume_front());
|
||||
@@ -429,6 +436,7 @@ std::vector<const char*> query_gcc_toolchain(const QueryParams& params) {
|
||||
std::vector<const char*> result;
|
||||
query_driver(query_arguments, [&](const char* driver, llvm::ArrayRef<const char*> cc1_args) {
|
||||
result.emplace_back(params.callback(driver));
|
||||
result.emplace_back(params.callback("-cc1"));
|
||||
for(auto arg: cc1_args) {
|
||||
result.emplace_back(params.callback(arg));
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
struct CompletionPrefix {
|
||||
LocalSourceRange range;
|
||||
llvm::StringRef spelling;
|
||||
|
||||
@@ -2,36 +2,29 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/language/uri.h"
|
||||
#include "eventide/ipc/lsp/uri.h"
|
||||
#include "feature/feature.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
|
||||
auto to_uri(llvm::StringRef file) -> std::string {
|
||||
const auto file_view = std::string_view(file.data(), file.size());
|
||||
|
||||
if(auto parsed = eventide::language::URI::parse(file_view)) {
|
||||
if(auto parsed = lsp::URI::parse(file_view)) {
|
||||
return parsed->str();
|
||||
}
|
||||
|
||||
if(auto uri = eventide::language::URI::from_file_path(file_view)) {
|
||||
if(auto uri = lsp::URI::from_file_path(file_view)) {
|
||||
return uri->str();
|
||||
}
|
||||
|
||||
return file.str();
|
||||
}
|
||||
|
||||
auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
|
||||
return protocol::Range{
|
||||
.start = converter.to_position(range.begin),
|
||||
.end = converter.to_position(range.end),
|
||||
};
|
||||
}
|
||||
|
||||
void add_tag(protocol::Diagnostic& diagnostic, DiagnosticID id) {
|
||||
if(id.is_deprecated()) {
|
||||
if(!diagnostic.tags.has_value()) {
|
||||
|
||||
@@ -7,18 +7,7 @@
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
|
||||
return protocol::Range{
|
||||
.start = converter.to_position(range.begin),
|
||||
.end = converter.to_position(range.end),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
namespace {} // namespace
|
||||
|
||||
auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::DocumentLink> {
|
||||
|
||||
@@ -18,15 +18,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
|
||||
return protocol::Range{
|
||||
.start = converter.to_position(range.begin),
|
||||
.end = converter.to_position(range.end),
|
||||
};
|
||||
}
|
||||
|
||||
auto to_protocol_symbol_kind(SymbolKind kind) -> protocol::SymbolKind {
|
||||
using enum protocol::SymbolKind;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "compile/compilation_unit.h"
|
||||
#include "eventide/language/position.h"
|
||||
#include "eventide/language/protocol.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
|
||||
namespace clang {
|
||||
|
||||
@@ -18,11 +18,18 @@ class NamedDecl;
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
|
||||
using eventide::language::PositionEncoding;
|
||||
using eventide::language::PositionMapper;
|
||||
using eventide::language::parse_position_encoding;
|
||||
using eventide::ipc::lsp::PositionEncoding;
|
||||
using eventide::ipc::lsp::PositionMapper;
|
||||
using eventide::ipc::lsp::parse_position_encoding;
|
||||
|
||||
inline auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
|
||||
return protocol::Range{
|
||||
.start = converter.to_position(range.begin),
|
||||
.end = converter.to_position(range.end),
|
||||
};
|
||||
}
|
||||
|
||||
struct CodeCompletionOptions {
|
||||
bool enable_keyword_snippet = false;
|
||||
|
||||
@@ -15,8 +15,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
enum class FoldingKind : std::uint8_t {
|
||||
Namespace,
|
||||
Class,
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
namespace tooling = clang::tooling;
|
||||
|
||||
auto format_content(llvm::StringRef file, llvm::StringRef content, tooling::Range range)
|
||||
|
||||
@@ -14,8 +14,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
auto symbol_name(SymbolKind kind) -> llvm::StringRef {
|
||||
switch(kind) {
|
||||
case SymbolKind::Module: return "module";
|
||||
|
||||
@@ -20,8 +20,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
using llvm::dyn_cast;
|
||||
using llvm::dyn_cast_or_null;
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
struct RawToken {
|
||||
LocalSourceRange range;
|
||||
SymbolKind kind = SymbolKind::Invalid;
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace clice::feature {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::language::protocol;
|
||||
|
||||
class SignatureCollector final : public clang::CodeCompleteConsumer {
|
||||
public:
|
||||
SignatureCollector(protocol::SignatureHelp& help, clang::CodeCompleteOptions complete_options) :
|
||||
|
||||
@@ -397,6 +397,8 @@ public:
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
return lookup_result();
|
||||
}
|
||||
|
||||
/// Look up the name in the bases of the given class. Keep stack unchanged.
|
||||
|
||||
89
src/server/config.cpp
Normal file
89
src/server/config.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "server/config.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
#include "eventide/serde/toml.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Replace all occurrences of ${workspace} with the workspace root.
|
||||
static void substitute_workspace(std::string& value, const std::string& workspace_root) {
|
||||
constexpr std::string_view placeholder = "${workspace}";
|
||||
std::string::size_type pos = 0;
|
||||
while((pos = value.find(placeholder, pos)) != std::string::npos) {
|
||||
value.replace(pos, placeholder.size(), workspace_root);
|
||||
pos += workspace_root.size();
|
||||
}
|
||||
}
|
||||
|
||||
void CliceConfig::apply_defaults(const std::string& workspace_root) {
|
||||
auto cpu_count = std::thread::hardware_concurrency();
|
||||
if(cpu_count == 0)
|
||||
cpu_count = 4;
|
||||
|
||||
if(stateful_worker_count == 0) {
|
||||
stateful_worker_count = std::max(1u, cpu_count / 4);
|
||||
}
|
||||
if(stateless_worker_count == 0) {
|
||||
stateless_worker_count = std::max(1u, cpu_count / 4);
|
||||
}
|
||||
if(worker_memory_limit == 0) {
|
||||
worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
|
||||
}
|
||||
if(cache_dir.empty() && !workspace_root.empty()) {
|
||||
cache_dir = path::join(workspace_root, ".clice");
|
||||
}
|
||||
|
||||
// Apply variable substitution to string fields
|
||||
substitute_workspace(compile_commands_path, workspace_root);
|
||||
substitute_workspace(cache_dir, workspace_root);
|
||||
}
|
||||
|
||||
std::optional<CliceConfig> CliceConfig::load(const std::string& path,
|
||||
const std::string& workspace_root) {
|
||||
auto content = fs::read(path);
|
||||
if(!content) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto result = eventide::serde::toml::parse<CliceConfig>(*content);
|
||||
if(!result) {
|
||||
LOG_WARN("Failed to parse config file {}", path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto config = std::move(*result);
|
||||
config.apply_defaults(workspace_root);
|
||||
|
||||
LOG_INFO("Loaded config from {}", path);
|
||||
return config;
|
||||
}
|
||||
|
||||
CliceConfig CliceConfig::load_from_workspace(const std::string& workspace_root) {
|
||||
if(!workspace_root.empty()) {
|
||||
// Try standard config file locations
|
||||
for(auto* name: {"clice.toml", ".clice/config.toml"}) {
|
||||
auto config_path = path::join(workspace_root, name);
|
||||
if(llvm::sys::fs::exists(config_path)) {
|
||||
auto config = load(config_path, workspace_root);
|
||||
if(config)
|
||||
return std::move(*config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No config file found; use defaults
|
||||
CliceConfig config;
|
||||
config.apply_defaults(workspace_root);
|
||||
LOG_INFO(
|
||||
"No clice.toml found, using default configuration " "(stateful={}, stateless={}, memory_limit={}MB)",
|
||||
config.stateful_worker_count,
|
||||
config.stateless_worker_count,
|
||||
config.worker_memory_limit / (1024 * 1024));
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
43
src/server/config.h
Normal file
43
src/server/config.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Configuration for the clice LSP server, loadable from clice.toml.
|
||||
struct CliceConfig {
|
||||
// Worker configuration (0 = auto-detect from system resources)
|
||||
std::uint32_t stateful_worker_count = 0;
|
||||
std::uint32_t stateless_worker_count = 0;
|
||||
std::uint64_t worker_memory_limit = 0; // bytes; 0 = auto
|
||||
|
||||
// Compilation database path (empty = auto-detect)
|
||||
std::string compile_commands_path;
|
||||
|
||||
// Cache directory (empty = default: <workspace>/.clice/)
|
||||
std::string cache_dir;
|
||||
|
||||
// Debounce interval for re-compilation after edits (milliseconds)
|
||||
int debounce_ms = 200;
|
||||
|
||||
// Background indexing
|
||||
bool enable_indexing = true;
|
||||
int idle_timeout_ms = 3000;
|
||||
|
||||
/// Compute default values for any field left at its zero/empty sentinel.
|
||||
void apply_defaults(const std::string& workspace_root);
|
||||
|
||||
/// Try to load configuration from a TOML file.
|
||||
/// Performs ${workspace} variable substitution in string fields.
|
||||
/// Returns std::nullopt if the file does not exist or cannot be parsed.
|
||||
static std::optional<CliceConfig> load(const std::string& path,
|
||||
const std::string& workspace_root);
|
||||
|
||||
/// Load config from the workspace, trying standard locations.
|
||||
/// Returns a default config (with apply_defaults) if no file is found.
|
||||
static CliceConfig load_from_workspace(const std::string& workspace_root);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
586
src/server/master_server.cpp
Normal file
586
src/server/master_server.cpp
Normal file
@@ -0,0 +1,586 @@
|
||||
#include "server/master_server.h"
|
||||
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/uri.h"
|
||||
#include "eventide/reflection/enum.h"
|
||||
#include "eventide/serde/json/json.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "semantic/symbol_kind.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/filesystem.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
namespace refl = eventide::refl;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::JsonPeer::RequestContext;
|
||||
|
||||
MasterServer::MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path) :
|
||||
loop(loop), peer(peer), pool(loop), self_path(std::move(self_path)) {}
|
||||
|
||||
std::string MasterServer::uri_to_path(const std::string& uri) {
|
||||
auto parsed = lsp::URI::parse(uri);
|
||||
if(parsed.has_value()) {
|
||||
auto path = parsed->file_path();
|
||||
if(path.has_value()) {
|
||||
return std::move(*path);
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
void MasterServer::publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const et::serde::RawValue& diagnostics_json) {
|
||||
std::vector<protocol::Diagnostic> diagnostics;
|
||||
if(!diagnostics_json.empty()) {
|
||||
auto status = et::serde::json::from_json(diagnostics_json.data, diagnostics);
|
||||
if(!status) {
|
||||
LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri);
|
||||
}
|
||||
}
|
||||
protocol::PublishDiagnosticsParams params;
|
||||
params.uri = uri;
|
||||
params.version = version;
|
||||
params.diagnostics = std::move(diagnostics);
|
||||
peer.send_notification(params);
|
||||
}
|
||||
|
||||
void MasterServer::clear_diagnostics(const std::string& uri) {
|
||||
protocol::PublishDiagnosticsParams params;
|
||||
params.uri = uri;
|
||||
params.diagnostics = {};
|
||||
peer.send_notification(params);
|
||||
}
|
||||
|
||||
void MasterServer::schedule_build(std::uint32_t path_id, const std::string& uri) {
|
||||
auto it = documents.find(path_id);
|
||||
if(it == documents.end())
|
||||
return;
|
||||
|
||||
auto& doc = it->second;
|
||||
|
||||
if(doc.build_running) {
|
||||
doc.build_requested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or reset debounce timer
|
||||
auto& timer_ptr = debounce_timers[path_id];
|
||||
if(!timer_ptr) {
|
||||
timer_ptr = std::make_unique<et::timer>(et::timer::create(loop));
|
||||
}
|
||||
timer_ptr->start(std::chrono::milliseconds(config.debounce_ms));
|
||||
|
||||
if(!doc.drain_scheduled) {
|
||||
doc.drain_scheduled = true;
|
||||
loop.schedule(run_build_drain(path_id, uri));
|
||||
}
|
||||
}
|
||||
|
||||
et::task<> MasterServer::run_build_drain(std::uint32_t path_id, std::string uri) {
|
||||
// Wait for debounce timer
|
||||
auto timer_it = debounce_timers.find(path_id);
|
||||
if(timer_it != debounce_timers.end() && timer_it->second) {
|
||||
co_await timer_it->second->wait();
|
||||
}
|
||||
|
||||
while(true) {
|
||||
auto doc_it = documents.find(path_id);
|
||||
if(doc_it == documents.end())
|
||||
co_return;
|
||||
|
||||
doc_it->second.build_running = true;
|
||||
doc_it->second.build_requested = false;
|
||||
auto gen = doc_it->second.generation;
|
||||
|
||||
// Send compile request to stateful worker
|
||||
worker::CompileParams params;
|
||||
params.path = std::string(path_pool.resolve(path_id));
|
||||
params.version = doc_it->second.version;
|
||||
params.text = doc_it->second.text;
|
||||
fill_compile_args(path_pool.resolve(path_id), params.directory, params.arguments);
|
||||
|
||||
LOG_DEBUG("Sending compile: path={}, args={}, gen={}",
|
||||
params.path,
|
||||
params.arguments.size(),
|
||||
gen);
|
||||
|
||||
auto result = co_await pool.send_stateful(path_id, params);
|
||||
|
||||
// Re-lookup document (may have been closed during compile)
|
||||
doc_it = documents.find(path_id);
|
||||
if(doc_it == documents.end())
|
||||
co_return;
|
||||
|
||||
auto& doc2 = doc_it->second;
|
||||
|
||||
if(result.has_value()) {
|
||||
// Only publish diagnostics if the generation hasn't changed
|
||||
if(doc2.generation == gen) {
|
||||
publish_diagnostics(uri, doc2.version, result.value().diagnostics);
|
||||
} else {
|
||||
LOG_DEBUG("Generation mismatch ({} vs {}), dropping diagnostics for {}",
|
||||
doc2.generation,
|
||||
gen,
|
||||
uri);
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Compile failed for {}: {}", uri, result.error().message);
|
||||
// Publish empty diagnostics so stale errors don't linger
|
||||
clear_diagnostics(uri);
|
||||
}
|
||||
|
||||
// Check if more builds were requested while compiling
|
||||
if(!doc2.build_requested) {
|
||||
doc2.build_running = false;
|
||||
doc2.drain_scheduled = false;
|
||||
co_return;
|
||||
}
|
||||
// Loop continues for the next build
|
||||
}
|
||||
}
|
||||
|
||||
et::task<> MasterServer::load_workspace() {
|
||||
if(workspace_root.empty())
|
||||
co_return;
|
||||
|
||||
// Create cache directory if configured
|
||||
if(!config.cache_dir.empty()) {
|
||||
auto ec = llvm::sys::fs::create_directories(config.cache_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create cache directory {}: {}", config.cache_dir, ec.message());
|
||||
} else {
|
||||
LOG_INFO("Cache directory: {}", config.cache_dir);
|
||||
}
|
||||
}
|
||||
|
||||
// Search for compile_commands.json
|
||||
std::string cdb_path;
|
||||
|
||||
// If the config specifies a CDB path, use it
|
||||
if(!config.compile_commands_path.empty()) {
|
||||
if(llvm::sys::fs::exists(config.compile_commands_path)) {
|
||||
cdb_path = config.compile_commands_path;
|
||||
} else {
|
||||
LOG_WARN("Configured compile_commands_path not found: {}",
|
||||
config.compile_commands_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise auto-detect in common locations
|
||||
if(cdb_path.empty()) {
|
||||
for(auto* subdir: {"build", "cmake-build-debug", "cmake-build-release", "out", "."}) {
|
||||
auto candidate = path::join(workspace_root, subdir, "compile_commands.json");
|
||||
if(llvm::sys::fs::exists(candidate)) {
|
||||
cdb_path = std::move(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(cdb_path.empty()) {
|
||||
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto updates = cdb.load_compile_database(cdb_path);
|
||||
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, updates.size());
|
||||
}
|
||||
|
||||
void MasterServer::fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments) {
|
||||
auto ctx = cdb.lookup(path, {.resource_dir = true, .query_toolchain = true});
|
||||
directory = ctx.directory.str();
|
||||
arguments.clear();
|
||||
for(auto* arg: ctx.arguments) {
|
||||
arguments.emplace_back(arg);
|
||||
}
|
||||
}
|
||||
|
||||
et::task<bool> MasterServer::ensure_compiled(std::uint32_t path_id, const std::string& uri) {
|
||||
auto doc_it = documents.find(path_id);
|
||||
if(doc_it == documents.end())
|
||||
co_return false;
|
||||
|
||||
// If the document has never been compiled, schedule a build and wait
|
||||
// For now, just return true - the worker may already have an AST
|
||||
// from a previous compile, or the feature request will return empty results.
|
||||
co_return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Forwarding helpers
|
||||
// =========================================================================
|
||||
|
||||
using serde_raw = et::serde::RawValue;
|
||||
|
||||
template <typename WorkerParams>
|
||||
MasterServer::RawResult MasterServer::forward_stateful(const std::string& uri) {
|
||||
auto path = uri_to_path(uri);
|
||||
auto path_id = path_pool.intern(path);
|
||||
|
||||
if(!co_await ensure_compiled(path_id, uri))
|
||||
co_return serde_raw{"null"};
|
||||
|
||||
WorkerParams wp;
|
||||
wp.path = path;
|
||||
|
||||
auto result = co_await pool.send_stateful(path_id, wp);
|
||||
if(!result.has_value())
|
||||
co_return serde_raw{};
|
||||
co_return std::move(result.value());
|
||||
}
|
||||
|
||||
template <typename WorkerParams>
|
||||
MasterServer::RawResult MasterServer::forward_stateful(const std::string& uri,
|
||||
const protocol::Position& position) {
|
||||
auto path = uri_to_path(uri);
|
||||
auto path_id = path_pool.intern(path);
|
||||
|
||||
if(!co_await ensure_compiled(path_id, uri))
|
||||
co_return serde_raw{"null"};
|
||||
|
||||
WorkerParams wp;
|
||||
wp.path = path;
|
||||
|
||||
auto doc_it = documents.find(path_id);
|
||||
if(doc_it != documents.end()) {
|
||||
lsp::PositionMapper mapper(doc_it->second.text, lsp::PositionEncoding::UTF16);
|
||||
wp.offset = mapper.to_offset(position);
|
||||
}
|
||||
|
||||
auto result = co_await pool.send_stateful(path_id, wp);
|
||||
if(!result.has_value())
|
||||
co_return serde_raw{};
|
||||
co_return std::move(result.value());
|
||||
}
|
||||
|
||||
template <typename WorkerParams>
|
||||
MasterServer::RawResult MasterServer::forward_stateless(const std::string& uri,
|
||||
const protocol::Position& position) {
|
||||
auto path = uri_to_path(uri);
|
||||
auto path_id = path_pool.intern(path);
|
||||
|
||||
auto doc_it = documents.find(path_id);
|
||||
if(doc_it == documents.end())
|
||||
co_return serde_raw{};
|
||||
|
||||
auto& doc = doc_it->second;
|
||||
|
||||
lsp::PositionMapper mapper(doc.text, lsp::PositionEncoding::UTF16);
|
||||
|
||||
WorkerParams wp;
|
||||
wp.path = path;
|
||||
wp.version = doc.version;
|
||||
wp.text = doc.text;
|
||||
fill_compile_args(path, wp.directory, wp.arguments);
|
||||
wp.offset = mapper.to_offset(position);
|
||||
|
||||
auto result = co_await pool.send_stateless(wp);
|
||||
if(!result.has_value())
|
||||
co_return serde_raw{};
|
||||
co_return std::move(result.value());
|
||||
}
|
||||
|
||||
void MasterServer::register_handlers() {
|
||||
// === initialize ===
|
||||
peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
|
||||
-> RequestResult<protocol::InitializeParams> {
|
||||
if(lifecycle != ServerLifecycle::Uninitialized) {
|
||||
co_return et::outcome_error(protocol::Error{"Server already initialized"});
|
||||
}
|
||||
|
||||
// Extract workspace root
|
||||
auto& init = params.lsp__initialize_params;
|
||||
if(init.root_uri.has_value()) {
|
||||
workspace_root = uri_to_path(*init.root_uri);
|
||||
}
|
||||
|
||||
lifecycle = ServerLifecycle::Initialized;
|
||||
|
||||
LOG_INFO("Initialized with workspace: {}", workspace_root);
|
||||
|
||||
// Build capabilities
|
||||
protocol::InitializeResult result;
|
||||
|
||||
// Text document sync: incremental
|
||||
protocol::TextDocumentSyncOptions sync_opts;
|
||||
sync_opts.open_close = true;
|
||||
sync_opts.change = protocol::TextDocumentSyncKind::Incremental;
|
||||
sync_opts.save = protocol::variant<protocol::boolean, protocol::SaveOptions>{true};
|
||||
result.capabilities.text_document_sync = std::move(sync_opts);
|
||||
|
||||
// Feature capabilities
|
||||
result.capabilities.hover_provider = true;
|
||||
result.capabilities.completion_provider = protocol::CompletionOptions{};
|
||||
result.capabilities.signature_help_provider = protocol::SignatureHelpOptions{};
|
||||
result.capabilities.definition_provider = true;
|
||||
result.capabilities.document_symbol_provider = true;
|
||||
result.capabilities.document_link_provider = protocol::DocumentLinkOptions{};
|
||||
result.capabilities.code_action_provider = true;
|
||||
result.capabilities.folding_range_provider = true;
|
||||
result.capabilities.inlay_hint_provider = true;
|
||||
|
||||
// Semantic tokens
|
||||
protocol::SemanticTokensOptions sem_opts;
|
||||
{
|
||||
auto lower_first = [](std::string_view name) -> std::string {
|
||||
std::string s(name);
|
||||
if(!s.empty()) {
|
||||
s[0] = static_cast<char>(std::tolower(static_cast<unsigned char>(s[0])));
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
auto to_names = [&](auto names) {
|
||||
return std::ranges::to<std::vector>(names | std::views::transform(lower_first));
|
||||
};
|
||||
|
||||
sem_opts.legend = protocol::SemanticTokensLegend{
|
||||
to_names(refl::reflection<SymbolKind::Kind>::member_names),
|
||||
to_names(refl::reflection<SymbolModifiers::Kind>::member_names),
|
||||
};
|
||||
}
|
||||
sem_opts.full = true;
|
||||
result.capabilities.semantic_tokens_provider = std::move(sem_opts);
|
||||
|
||||
// Server info
|
||||
protocol::ServerInfo info;
|
||||
info.name = "clice";
|
||||
info.version = "0.1.0";
|
||||
result.server_info = std::move(info);
|
||||
|
||||
co_return result;
|
||||
});
|
||||
|
||||
// === initialized ===
|
||||
peer.on_notification([this](const protocol::InitializedParams& params) {
|
||||
// Load configuration from workspace
|
||||
config = CliceConfig::load_from_workspace(workspace_root);
|
||||
|
||||
LOG_INFO("Server ready (stateful={}, stateless={}, debounce={}ms, idle={}ms)",
|
||||
config.stateful_worker_count,
|
||||
config.stateless_worker_count,
|
||||
config.debounce_ms,
|
||||
config.idle_timeout_ms);
|
||||
|
||||
// Start worker pool
|
||||
WorkerPoolOptions pool_opts;
|
||||
pool_opts.self_path = self_path;
|
||||
pool_opts.stateful_count = config.stateful_worker_count;
|
||||
pool_opts.stateless_count = config.stateless_worker_count;
|
||||
pool_opts.worker_memory_limit = config.worker_memory_limit;
|
||||
if(!pool.start(pool_opts)) {
|
||||
LOG_ERROR("Failed to start worker pool");
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycle = ServerLifecycle::Ready;
|
||||
|
||||
// Load CDB in background
|
||||
loop.schedule(load_workspace());
|
||||
});
|
||||
|
||||
// === shutdown ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const protocol::ShutdownParams& params) -> RequestResult<protocol::ShutdownParams> {
|
||||
lifecycle = ServerLifecycle::ShuttingDown;
|
||||
LOG_INFO("Shutdown requested");
|
||||
co_return nullptr;
|
||||
});
|
||||
|
||||
// === exit ===
|
||||
peer.on_notification([this](const protocol::ExitParams& params) {
|
||||
lifecycle = ServerLifecycle::Exited;
|
||||
LOG_INFO("Exit notification received");
|
||||
|
||||
// Graceful shutdown: cancel compilations, stop workers, then stop loop
|
||||
loop.schedule([this]() -> et::task<> {
|
||||
co_await pool.stop();
|
||||
loop.stop();
|
||||
}());
|
||||
});
|
||||
|
||||
// === textDocument/didOpen ===
|
||||
peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto& td = params.text_document;
|
||||
auto path = uri_to_path(td.uri);
|
||||
auto path_id = path_pool.intern(path);
|
||||
|
||||
auto& doc = documents[path_id];
|
||||
doc.version = td.version;
|
||||
doc.text = td.text;
|
||||
doc.generation++;
|
||||
|
||||
LOG_DEBUG("didOpen: {} (v{})", path, td.version);
|
||||
|
||||
schedule_build(path_id, td.uri);
|
||||
});
|
||||
|
||||
// === textDocument/didChange ===
|
||||
peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = path_pool.intern(path);
|
||||
|
||||
auto it = documents.find(path_id);
|
||||
if(it == documents.end())
|
||||
return;
|
||||
|
||||
auto& doc = it->second;
|
||||
doc.version = params.text_document.version;
|
||||
|
||||
// Apply incremental changes
|
||||
for(auto& change: params.content_changes) {
|
||||
std::visit(
|
||||
[&](auto& c) {
|
||||
using T = std::remove_cvref_t<decltype(c)>;
|
||||
if constexpr(std::is_same_v<T,
|
||||
protocol::TextDocumentContentChangeWholeDocument>) {
|
||||
doc.text = c.text;
|
||||
} else {
|
||||
// Incremental change: replace range
|
||||
auto& range = c.range;
|
||||
|
||||
lsp::PositionMapper mapper(doc.text, lsp::PositionEncoding::UTF16);
|
||||
auto start = mapper.to_offset(range.start);
|
||||
auto end = mapper.to_offset(range.end);
|
||||
if(start <= doc.text.size() && end <= doc.text.size() && start <= end) {
|
||||
doc.text.replace(start, end - start, c.text);
|
||||
}
|
||||
}
|
||||
},
|
||||
change);
|
||||
}
|
||||
|
||||
doc.generation++;
|
||||
|
||||
// Notify the owning stateful worker so it marks the document dirty
|
||||
worker::DocumentUpdateParams update;
|
||||
update.path = path;
|
||||
update.version = doc.version;
|
||||
update.text = doc.text;
|
||||
pool.notify_stateful(path_id, update);
|
||||
|
||||
schedule_build(path_id, params.text_document.uri);
|
||||
});
|
||||
|
||||
// === textDocument/didClose ===
|
||||
peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
auto path = uri_to_path(params.text_document.uri);
|
||||
auto path_id = path_pool.intern(path);
|
||||
|
||||
documents.erase(path_id);
|
||||
debounce_timers.erase(path_id);
|
||||
|
||||
// Clear diagnostics for closed file
|
||||
clear_diagnostics(params.text_document.uri);
|
||||
|
||||
LOG_DEBUG("didClose: {}", path);
|
||||
});
|
||||
|
||||
// === textDocument/didSave ===
|
||||
peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) {
|
||||
if(lifecycle != ServerLifecycle::Ready)
|
||||
return;
|
||||
|
||||
// TODO: Trigger dependent file rebuilds
|
||||
LOG_DEBUG("didSave: {}", params.text_document.uri);
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Feature requests routed to stateful workers (RawValue passthrough)
|
||||
// =========================================================================
|
||||
|
||||
// --- textDocument/hover ---
|
||||
peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::HoverParams>(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
params.text_document_position_params.position);
|
||||
});
|
||||
|
||||
// --- textDocument/semanticTokens/full ---
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::SemanticTokensParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::SemanticTokensParams>(params.text_document.uri);
|
||||
});
|
||||
|
||||
// --- textDocument/inlayHint ---
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::InlayHintsParams>(params.text_document.uri);
|
||||
});
|
||||
|
||||
// --- textDocument/foldingRange ---
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::FoldingRangeParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::FoldingRangeParams>(params.text_document.uri);
|
||||
});
|
||||
|
||||
// --- textDocument/documentSymbol ---
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::DocumentSymbolParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::DocumentSymbolParams>(params.text_document.uri);
|
||||
});
|
||||
|
||||
// --- textDocument/documentLink ---
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::DocumentLinkParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::DocumentLinkParams>(params.text_document.uri);
|
||||
});
|
||||
|
||||
// --- textDocument/codeAction ---
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::CodeActionParams>(params.text_document.uri);
|
||||
});
|
||||
|
||||
// --- textDocument/definition ---
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::DefinitionParams& params) -> RawResult {
|
||||
co_return co_await forward_stateful<worker::GoToDefinitionParams>(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
params.text_document_position_params.position);
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Feature requests routed to stateless workers
|
||||
// =========================================================================
|
||||
|
||||
// --- textDocument/completion ---
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult {
|
||||
co_return co_await forward_stateless<worker::CompletionParams>(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
params.text_document_position_params.position);
|
||||
});
|
||||
|
||||
// --- textDocument/signatureHelp ---
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult {
|
||||
co_return co_await forward_stateless<worker::SignatureHelpParams>(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
params.text_document_position_params.position);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
130
src/server/master_server.h
Normal file
130
src/server/master_server.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "compile/command.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "server/config.h"
|
||||
#include "server/worker_pool.h"
|
||||
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include "llvm/Support/Allocator.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
|
||||
/// Global path interning pool. Maps file paths to uint32_t IDs.
|
||||
struct ServerPathPool {
|
||||
llvm::BumpPtrAllocator allocator;
|
||||
llvm::SmallVector<llvm::StringRef> paths;
|
||||
llvm::StringMap<std::uint32_t> cache;
|
||||
|
||||
std::uint32_t intern(llvm::StringRef path) {
|
||||
auto [it, inserted] = cache.try_emplace(path, paths.size());
|
||||
if(inserted) {
|
||||
auto saved = path.copy(allocator);
|
||||
paths.push_back(saved);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
llvm::StringRef resolve(std::uint32_t id) const {
|
||||
return paths[id];
|
||||
}
|
||||
};
|
||||
|
||||
struct DocumentState {
|
||||
int version = 0;
|
||||
std::string text;
|
||||
std::uint64_t generation = 0;
|
||||
bool build_running = false;
|
||||
bool build_requested = false;
|
||||
bool drain_scheduled = false;
|
||||
};
|
||||
|
||||
enum class ServerLifecycle : std::uint8_t {
|
||||
Uninitialized,
|
||||
Initialized,
|
||||
Ready,
|
||||
ShuttingDown,
|
||||
Exited,
|
||||
};
|
||||
|
||||
class MasterServer {
|
||||
public:
|
||||
MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path);
|
||||
|
||||
void register_handlers();
|
||||
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
et::ipc::JsonPeer& peer;
|
||||
WorkerPool pool;
|
||||
ServerPathPool path_pool;
|
||||
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
|
||||
|
||||
std::string self_path;
|
||||
std::string workspace_root;
|
||||
CliceConfig config;
|
||||
|
||||
CompilationDatabase cdb;
|
||||
|
||||
// Document state: path_id -> DocumentState
|
||||
llvm::DenseMap<std::uint32_t, DocumentState> documents;
|
||||
|
||||
// Per-document debounce timers
|
||||
llvm::DenseMap<std::uint32_t, std::unique_ptr<et::timer>> debounce_timers;
|
||||
|
||||
// Helper: convert URI to file path
|
||||
std::string uri_to_path(const std::string& uri);
|
||||
|
||||
// Publish diagnostics to client
|
||||
void publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const eventide::serde::RawValue& diagnostics_json);
|
||||
void clear_diagnostics(const std::string& uri);
|
||||
|
||||
// Schedule a build after debounce
|
||||
void schedule_build(std::uint32_t path_id, const std::string& uri);
|
||||
|
||||
// Build drain coroutine: waits for debounce, then runs compile loop
|
||||
et::task<> run_build_drain(std::uint32_t path_id, std::string uri);
|
||||
|
||||
// Ensure a file has been compiled before servicing feature requests
|
||||
et::task<bool> ensure_compiled(std::uint32_t path_id, const std::string& uri);
|
||||
|
||||
// Load CDB and build initial include graph
|
||||
et::task<> load_workspace();
|
||||
|
||||
// Helper: fill compile arguments from CDB into worker params
|
||||
void fill_compile_args(llvm::StringRef path,
|
||||
std::string& directory,
|
||||
std::vector<std::string>& arguments);
|
||||
|
||||
// Forwarding helpers for feature requests (RawValue passthrough)
|
||||
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
|
||||
|
||||
/// Forward a simple stateful request (path-only worker params).
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateful(const std::string& uri);
|
||||
|
||||
/// Forward a stateful request with position-to-offset conversion.
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateful(const std::string& uri, const protocol::Position& position);
|
||||
|
||||
/// Forward a stateless request with document content and compile args.
|
||||
template <typename WorkerParams>
|
||||
RawResult forward_stateless(const std::string& uri, const protocol::Position& position);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
258
src/server/protocol.h
Normal file
258
src/server/protocol.h
Normal file
@@ -0,0 +1,258 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
#include "eventide/ipc/protocol.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
|
||||
namespace clice::worker {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
|
||||
// === StatefulWorker Requests ===
|
||||
|
||||
struct CompileParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
};
|
||||
|
||||
struct CompileResult {
|
||||
int version;
|
||||
/// Diagnostics serialized as JSON (RawValue) to avoid bincode/serde annotation conflicts.
|
||||
eventide::serde::RawValue diagnostics;
|
||||
std::size_t memory_usage;
|
||||
};
|
||||
|
||||
struct HoverParams {
|
||||
std::string path;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct SemanticTokensParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct InlayHintsParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct FoldingRangeParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentSymbolParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentLinkParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct CodeActionParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct GoToDefinitionParams {
|
||||
std::string path;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
// === StatelessWorker Requests ===
|
||||
|
||||
struct CompletionParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct SignatureHelpParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
struct BuildPCHParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::string content;
|
||||
};
|
||||
|
||||
struct BuildPCHResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
struct BuildPCMParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::string module_name;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
};
|
||||
|
||||
struct BuildPCMResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
struct IndexParams {
|
||||
std::string file;
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::unordered_map<std::string, std::string> pcms;
|
||||
};
|
||||
|
||||
struct IndexResult {
|
||||
bool success;
|
||||
std::string error;
|
||||
std::string tu_index_data;
|
||||
};
|
||||
|
||||
// === Notifications ===
|
||||
|
||||
struct DocumentUpdateParams {
|
||||
std::string path;
|
||||
int version;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct EvictParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct EvictedParams {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
} // namespace clice::worker
|
||||
|
||||
namespace eventide::ipc::protocol {
|
||||
|
||||
// === Stateful Requests ===
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompileParams> {
|
||||
using Result = clice::worker::CompileResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/compile";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::HoverParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/hover";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::SemanticTokensParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/semanticTokens";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::InlayHintsParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/inlayHints";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::FoldingRangeParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/foldingRange";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::DocumentSymbolParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/documentSymbol";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::DocumentLinkParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/documentLink";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CodeActionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/codeAction";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::GoToDefinitionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/goToDefinition";
|
||||
};
|
||||
|
||||
// === Stateless Requests ===
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompletionParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/completion";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::SignatureHelpParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/signatureHelp";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::BuildPCHParams> {
|
||||
using Result = clice::worker::BuildPCHResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/buildPCH";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::BuildPCMParams> {
|
||||
using Result = clice::worker::BuildPCMResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/buildPCM";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::IndexParams> {
|
||||
using Result = clice::worker::IndexResult;
|
||||
constexpr inline static std::string_view method = "clice/worker/index";
|
||||
};
|
||||
|
||||
// === Notifications ===
|
||||
|
||||
template <>
|
||||
struct NotificationTraits<clice::worker::DocumentUpdateParams> {
|
||||
constexpr inline static std::string_view method = "clice/worker/documentUpdate";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct NotificationTraits<clice::worker::EvictParams> {
|
||||
constexpr inline static std::string_view method = "clice/worker/evict";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct NotificationTraits<clice::worker::EvictedParams> {
|
||||
constexpr inline static std::string_view method = "clice/worker/evicted";
|
||||
};
|
||||
|
||||
} // namespace eventide::ipc::protocol
|
||||
340
src/server/stateful_worker.cpp
Normal file
340
src/server/stateful_worker.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
#include "server/stateful_worker.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "feature/feature.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
|
||||
struct DocumentEntry {
|
||||
int version = 0;
|
||||
std::string text;
|
||||
bool has_ast = false;
|
||||
CompilationUnit unit{nullptr};
|
||||
std::atomic<bool> dirty{false};
|
||||
|
||||
// Signaled when the first compilation completes (has_ast becomes true).
|
||||
// Feature handlers co_await this before accessing the AST.
|
||||
et::event ast_ready{false};
|
||||
|
||||
// Compilation context (from CompileParams)
|
||||
std::string directory;
|
||||
std::vector<std::string> arguments;
|
||||
std::pair<std::string, uint32_t> pch;
|
||||
llvm::StringMap<std::string> pcms;
|
||||
|
||||
// Per-document serialization mutex
|
||||
et::mutex strand;
|
||||
};
|
||||
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
static void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize any value to LSP JSON RawValue.
|
||||
template <typename T>
|
||||
static et::serde::RawValue to_raw(const T& value) {
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
|
||||
return et::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
class StatefulWorker {
|
||||
et::ipc::BincodePeer& peer;
|
||||
std::uint64_t memory_limit;
|
||||
|
||||
llvm::StringMap<std::unique_ptr<DocumentEntry>> documents;
|
||||
|
||||
// LRU tracking — owns keys so they don't dangle after request handler returns
|
||||
std::list<std::string> lru;
|
||||
llvm::StringMap<std::list<std::string>::iterator> lru_index;
|
||||
|
||||
void touch_lru(llvm::StringRef path) {
|
||||
auto it = lru_index.find(path);
|
||||
if(it != lru_index.end()) {
|
||||
lru.erase(it->second);
|
||||
}
|
||||
lru.emplace_front(path.str());
|
||||
lru_index[path] = lru.begin();
|
||||
}
|
||||
|
||||
void shrink_if_over_limit() {
|
||||
// TODO: Implement memory-based eviction using memory_limit.
|
||||
// For now, cap at a fixed number of documents.
|
||||
while(documents.size() > 16 && !lru.empty()) {
|
||||
auto path = lru.back();
|
||||
lru.pop_back();
|
||||
lru_index.erase(path);
|
||||
LOG_DEBUG("Evicting document: {}", path);
|
||||
peer.send_notification(worker::EvictedParams{std::string(path)});
|
||||
documents.erase(path);
|
||||
}
|
||||
}
|
||||
|
||||
DocumentEntry& get_or_create(llvm::StringRef path) {
|
||||
auto [it, inserted] = documents.try_emplace(path, nullptr);
|
||||
if(inserted) {
|
||||
it->second = std::make_unique<DocumentEntry>();
|
||||
LOG_DEBUG("Created new document entry: {}", path.str());
|
||||
}
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
/// Look up document, wait for AST, lock strand, run fn(doc) on thread pool, unlock.
|
||||
/// Returns "null" if document not found or AST not usable.
|
||||
template <typename F>
|
||||
et::task<et::serde::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
auto it = documents.find(path);
|
||||
if(it == documents.end())
|
||||
co_return et::serde::RawValue{"null"};
|
||||
|
||||
auto& doc = *it->second;
|
||||
touch_lru(path);
|
||||
|
||||
co_await doc.ast_ready.wait();
|
||||
co_await doc.strand.lock();
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
if(!doc.has_ast || (!doc.unit.completed() && !doc.unit.fatal_error()))
|
||||
return et::serde::RawValue{"null"};
|
||||
return fn(doc);
|
||||
});
|
||||
|
||||
doc.strand.unlock();
|
||||
co_return result.value();
|
||||
}
|
||||
|
||||
public:
|
||||
StatefulWorker(et::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
|
||||
peer(peer), memory_limit(memory_limit) {}
|
||||
|
||||
void register_handlers();
|
||||
};
|
||||
|
||||
void StatefulWorker::register_handlers() {
|
||||
// === Compile ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::CompileParams& params) -> RequestResult<worker::CompileParams> {
|
||||
LOG_INFO("Compile request: path={}, version={}", params.path, params.version);
|
||||
|
||||
auto& doc = get_or_create(params.path);
|
||||
doc.version = params.version;
|
||||
doc.text = params.text;
|
||||
doc.directory = params.directory;
|
||||
doc.arguments = params.arguments;
|
||||
doc.pch = params.pch;
|
||||
doc.pcms.clear();
|
||||
for(auto& [name, pcm_path]: params.pcms) {
|
||||
doc.pcms.try_emplace(name, pcm_path);
|
||||
}
|
||||
|
||||
touch_lru(params.path);
|
||||
|
||||
co_await doc.strand.lock();
|
||||
|
||||
auto compile_result = co_await et::queue([&]() -> worker::CompileResult {
|
||||
LOG_DEBUG("Compiling: path={}, {} args", params.path, doc.arguments.size());
|
||||
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Content;
|
||||
fill_args(cp, doc.directory, doc.arguments);
|
||||
if(!doc.pch.first.empty()) {
|
||||
cp.pch = doc.pch;
|
||||
}
|
||||
cp.add_remapped_file(params.path, doc.text);
|
||||
for(auto& entry: doc.pcms) {
|
||||
cp.pcms.try_emplace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
doc.unit = compile(cp);
|
||||
doc.has_ast = true;
|
||||
doc.dirty.store(false, std::memory_order_release);
|
||||
|
||||
worker::CompileResult result;
|
||||
result.version = doc.version;
|
||||
if(doc.unit.completed() || doc.unit.fatal_error()) {
|
||||
auto diags = feature::diagnostics(doc.unit);
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(diags);
|
||||
result.diagnostics = et::serde::RawValue{json ? std::move(*json) : "[]"};
|
||||
LOG_INFO("Compile done: path={}, {}ms, {} diags, fatal={}",
|
||||
params.path,
|
||||
timer.ms(),
|
||||
diags.size(),
|
||||
doc.unit.fatal_error());
|
||||
} else {
|
||||
result.diagnostics = et::serde::RawValue{"[]"};
|
||||
LOG_WARN("Compile incomplete: path={}, {}ms", params.path, timer.ms());
|
||||
}
|
||||
result.memory_usage = 0; // TODO: query actual memory
|
||||
return result;
|
||||
});
|
||||
|
||||
doc.strand.unlock();
|
||||
doc.ast_ready.set();
|
||||
shrink_if_over_limit();
|
||||
|
||||
co_return compile_result.value();
|
||||
});
|
||||
|
||||
// === DocumentUpdate ===
|
||||
peer.on_notification([this](const worker::DocumentUpdateParams& params) {
|
||||
LOG_TRACE("DocumentUpdate: path={}, version={}", params.path, params.version);
|
||||
|
||||
auto it = documents.find(params.path);
|
||||
if(it == documents.end()) {
|
||||
LOG_TRACE("DocumentUpdate ignored (not tracked): path={}", params.path);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& doc = *it->second;
|
||||
doc.version = params.version;
|
||||
doc.text = params.text;
|
||||
doc.dirty.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
// === Evict ===
|
||||
peer.on_notification([this](const worker::EvictParams& params) {
|
||||
LOG_DEBUG("Evict notification: path={}", params.path);
|
||||
|
||||
auto it = lru_index.find(params.path);
|
||||
if(it != lru_index.end()) {
|
||||
lru.erase(it->second);
|
||||
lru_index.erase(it);
|
||||
}
|
||||
documents.erase(params.path);
|
||||
});
|
||||
|
||||
// === Hover ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::HoverParams& params) -> RequestResult<worker::HoverParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
auto result = feature::hover(doc.unit, params.offset);
|
||||
return result ? to_raw(*result) : et::serde::RawValue{"null"};
|
||||
});
|
||||
});
|
||||
|
||||
// === SemanticTokens ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::SemanticTokensParams& params)
|
||||
-> RequestResult<worker::SemanticTokensParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === InlayHints ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::InlayHintsParams& params) -> RequestResult<worker::InlayHintsParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
LocalSourceRange range{0, static_cast<uint32_t>(doc.text.size())};
|
||||
return to_raw(feature::inlay_hints(doc.unit, range));
|
||||
});
|
||||
});
|
||||
|
||||
// === FoldingRange ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::FoldingRangeParams& params)
|
||||
-> RequestResult<worker::FoldingRangeParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::folding_ranges(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === DocumentSymbol ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::DocumentSymbolParams& params)
|
||||
-> RequestResult<worker::DocumentSymbolParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_symbols(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === DocumentLink ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::DocumentLinkParams& params)
|
||||
-> RequestResult<worker::DocumentLinkParams> {
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::document_links(doc.unit));
|
||||
});
|
||||
});
|
||||
|
||||
// === CodeAction ===
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx,
|
||||
const worker::CodeActionParams& params) -> RequestResult<worker::CodeActionParams> {
|
||||
LOG_TRACE("CodeAction request: path={}", params.path);
|
||||
// TODO: Implement code actions
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
});
|
||||
|
||||
// === GoToDefinition ===
|
||||
peer.on_request([this](RequestContext& ctx, const worker::GoToDefinitionParams& params)
|
||||
-> RequestResult<worker::GoToDefinitionParams> {
|
||||
LOG_TRACE("GoToDefinition request: path={}, offset={}", params.path, params.offset);
|
||||
// TODO: Implement go-to-definition
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
});
|
||||
}
|
||||
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit) {
|
||||
logging::stderr_logger("stateful-worker", logging::options);
|
||||
|
||||
LOG_INFO("Starting stateful worker, memory_limit={}MB", memory_limit / (1024 * 1024));
|
||||
|
||||
et::event_loop loop;
|
||||
|
||||
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport_result) {
|
||||
LOG_ERROR("Failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
|
||||
StatefulWorker worker(peer, memory_limit);
|
||||
worker.register_handlers();
|
||||
|
||||
LOG_INFO("Stateful worker ready, waiting for requests");
|
||||
loop.schedule(peer.run());
|
||||
auto ret = loop.run();
|
||||
LOG_INFO("Stateful worker exiting with code {}", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
12
src/server/stateful_worker.h
Normal file
12
src/server/stateful_worker.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Run the stateful worker process mode.
|
||||
/// The worker holds compiled ASTs and handles feature requests
|
||||
/// (hover, semantic tokens, etc.) alongside compile requests.
|
||||
int run_stateful_worker_mode(std::uint64_t memory_limit);
|
||||
|
||||
} // namespace clice
|
||||
225
src/server/stateless_worker.cpp
Normal file
225
src/server/stateless_worker.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "server/stateless_worker.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
#include "feature/feature.h"
|
||||
#include "server/protocol.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
|
||||
struct ScopedTimer {
|
||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
|
||||
long long ms() const {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
static void fill_args(CompilationParams& cp,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
cp.directory = directory;
|
||||
for(auto& arg: arguments) {
|
||||
cp.arguments.push_back(arg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static et::serde::RawValue to_raw(const T& value) {
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
|
||||
return et::serde::RawValue{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
int run_stateless_worker_mode() {
|
||||
logging::stderr_logger("stateless-worker", logging::options);
|
||||
|
||||
LOG_INFO("Starting stateless worker");
|
||||
|
||||
et::event_loop loop;
|
||||
|
||||
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport_result) {
|
||||
LOG_ERROR("Failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
|
||||
// === BuildPCH ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::BuildPCHParams& params) -> RequestResult<worker::BuildPCHParams> {
|
||||
LOG_INFO("BuildPCH request: file={}", params.file);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::BuildPCHResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
cp.kind = CompilationKind::Preamble;
|
||||
fill_args(cp, params.directory, params.arguments);
|
||||
cp.add_remapped_file(params.file, params.content);
|
||||
|
||||
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"};
|
||||
}
|
||||
cp.output_file = *tmp;
|
||||
|
||||
PCHInfo pch_info;
|
||||
auto unit = compile(cp, pch_info);
|
||||
|
||||
if(unit.completed()) {
|
||||
LOG_INFO("BuildPCH done: file={}, {}ms", params.file, timer.ms());
|
||||
return {true, ""};
|
||||
} else {
|
||||
LOG_WARN("BuildPCH failed: file={}, {}ms", params.file, timer.ms());
|
||||
return {false, "PCH compilation failed"};
|
||||
}
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === BuildPCM ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::BuildPCMParams& params) -> RequestResult<worker::BuildPCMParams> {
|
||||
LOG_INFO("BuildPCM request: file={}, module={}", params.file, params.module_name);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::BuildPCMResult {
|
||||
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);
|
||||
}
|
||||
|
||||
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"};
|
||||
}
|
||||
cp.output_file = *tmp;
|
||||
|
||||
PCMInfo pcm_info;
|
||||
auto unit = compile(cp, pcm_info);
|
||||
|
||||
if(unit.completed()) {
|
||||
LOG_INFO("BuildPCM done: module={}, {}ms", params.module_name, timer.ms());
|
||||
return {true, ""};
|
||||
} else {
|
||||
LOG_WARN("BuildPCM failed: module={}, {}ms", params.module_name, timer.ms());
|
||||
return {false, "PCM compilation failed"};
|
||||
}
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === Completion ===
|
||||
peer.on_request(
|
||||
[&](RequestContext& ctx,
|
||||
const worker::CompletionParams& params) -> RequestResult<worker::CompletionParams> {
|
||||
LOG_DEBUG("Completion request: path={}, offset={}", params.path, params.offset);
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
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.path, params.text);
|
||||
cp.completion = {params.path, params.offset};
|
||||
|
||||
auto items = feature::code_complete(cp);
|
||||
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
|
||||
return to_raw(items);
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === SignatureHelp ===
|
||||
peer.on_request([&](RequestContext& ctx, const worker::SignatureHelpParams& params)
|
||||
-> RequestResult<worker::SignatureHelpParams> {
|
||||
LOG_DEBUG("SignatureHelp request: path={}, offset={}", params.path, params.offset);
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
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.path, params.text);
|
||||
cp.completion = {params.path, params.offset};
|
||||
|
||||
auto help = feature::signature_help(cp);
|
||||
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
|
||||
return to_raw(help);
|
||||
});
|
||||
co_return result.value();
|
||||
});
|
||||
|
||||
// === Index ===
|
||||
peer.on_request([&](RequestContext& ctx,
|
||||
const worker::IndexParams& params) -> RequestResult<worker::IndexParams> {
|
||||
LOG_INFO("Index request: file={}", params.file);
|
||||
|
||||
auto result = co_await et::queue([&]() -> worker::IndexResult {
|
||||
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", ""};
|
||||
}
|
||||
|
||||
LOG_INFO("Index done: file={}, {}ms", params.file, timer.ms());
|
||||
// TODO: Generate TUIndex from the compilation unit
|
||||
return {true, "", ""};
|
||||
});
|
||||
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
|
||||
11
src/server/stateless_worker.h
Normal file
11
src/server/stateless_worker.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Run the stateless worker process mode.
|
||||
/// The worker receives one-shot compilation tasks (BuildPCH, BuildPCM,
|
||||
/// Completion, SignatureHelp, Index) via stdin/stdout bincode IPC,
|
||||
/// executes them on a thread pool, and returns results.
|
||||
int run_stateless_worker_mode();
|
||||
|
||||
} // namespace clice
|
||||
228
src/server/worker_pool.cpp
Normal file
228
src/server/worker_pool.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#include "server/worker_pool.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <string>
|
||||
|
||||
#include "eventide/ipc/transport.h"
|
||||
#include "support/logging.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace {
|
||||
|
||||
/// Coroutine that reads lines from a worker's stderr pipe and logs them
|
||||
/// with a prefix like [SL-0] or [SF-1].
|
||||
et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
std::string buffer;
|
||||
while(true) {
|
||||
auto result = co_await stderr_pipe.read();
|
||||
if(!result.has_value()) {
|
||||
// EOF or error — worker has exited
|
||||
break;
|
||||
}
|
||||
auto& chunk = result.value();
|
||||
if(chunk.empty())
|
||||
break;
|
||||
|
||||
buffer += chunk;
|
||||
|
||||
// Log complete lines
|
||||
std::size_t pos = 0;
|
||||
while(true) {
|
||||
auto nl = buffer.find('\n', pos);
|
||||
if(nl == std::string::npos)
|
||||
break;
|
||||
auto line = buffer.substr(pos, nl - pos);
|
||||
if(!line.empty()) {
|
||||
LOG_INFO("{} {}", prefix, line);
|
||||
}
|
||||
pos = nl + 1;
|
||||
}
|
||||
buffer.erase(0, pos);
|
||||
}
|
||||
|
||||
// Flush any remaining partial line
|
||||
if(!buffer.empty()) {
|
||||
LOG_INFO("{} {}", prefix, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WorkerPool::spawn_worker(const std::string& self_path,
|
||||
bool stateful,
|
||||
std::uint64_t memory_limit) {
|
||||
et::process::options opts;
|
||||
opts.file = self_path;
|
||||
if(stateful) {
|
||||
opts.args = {self_path,
|
||||
"--mode",
|
||||
"stateful-worker",
|
||||
"--worker-memory-limit",
|
||||
std::to_string(memory_limit)};
|
||||
} else {
|
||||
opts.args = {self_path, "--mode", "stateless-worker"};
|
||||
}
|
||||
opts.streams = {
|
||||
et::process::stdio::pipe(true, false), // stdin: child reads
|
||||
et::process::stdio::pipe(false, true), // stdout: child writes
|
||||
et::process::stdio::pipe(false, true), // stderr: child writes
|
||||
};
|
||||
|
||||
auto result = et::process::spawn(opts, loop);
|
||||
if(!result) {
|
||||
LOG_ERROR("Failed to spawn {} worker: {}",
|
||||
stateful ? "stateful" : "stateless",
|
||||
result.error().message());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& spawn = *result;
|
||||
|
||||
// StreamTransport: input = child's stdout (parent reads), output = child's stdin (parent
|
||||
// writes)
|
||||
auto transport = std::make_unique<et::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
|
||||
std::move(spawn.stdin_pipe));
|
||||
auto peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));
|
||||
|
||||
auto& workers = stateful ? stateful_workers : stateless_workers;
|
||||
auto worker_index = workers.size();
|
||||
|
||||
// Build log prefix: [SF-0] for stateful, [SL-0] for stateless
|
||||
std::string prefix =
|
||||
std::string("[") + (stateful ? "SF-" : "SL-") + std::to_string(worker_index) + "]";
|
||||
|
||||
// Schedule stderr log collection
|
||||
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
|
||||
|
||||
workers.push_back(WorkerProcess{
|
||||
.proc = std::move(spawn.proc),
|
||||
.peer = std::move(peer),
|
||||
.owned_documents = 0,
|
||||
});
|
||||
|
||||
auto& w = workers.back();
|
||||
loop.schedule(w.peer->run());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
for(std::uint32_t i = 0; i < options.stateless_count; ++i) {
|
||||
if(!spawn_worker(options.self_path, false, 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for(std::uint32_t i = 0; i < options.stateful_count; ++i) {
|
||||
if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Register evicted notification handler for each stateful worker
|
||||
for(std::size_t i = 0; i < stateful_workers.size(); ++i) {
|
||||
stateful_workers[i].peer->on_notification([this](const worker::EvictedParams& params) {
|
||||
if(on_evicted) {
|
||||
on_evicted(params.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
LOG_INFO("WorkerPool started: {} stateless, {} stateful workers",
|
||||
stateless_workers.size(),
|
||||
stateful_workers.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
et::task<> WorkerPool::stop() {
|
||||
LOG_INFO("WorkerPool stopping...");
|
||||
|
||||
// Close output pipes to signal workers to exit gracefully
|
||||
for(auto& w: stateless_workers) {
|
||||
w.peer->close_output();
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
w.peer->close_output();
|
||||
}
|
||||
|
||||
// Send SIGTERM to all workers
|
||||
for(auto& w: stateless_workers) {
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
|
||||
// Wait for all worker processes to exit
|
||||
for(auto& w: stateless_workers) {
|
||||
co_await w.proc.wait();
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
co_await w.proc.wait();
|
||||
}
|
||||
|
||||
LOG_INFO("WorkerPool stopped");
|
||||
}
|
||||
|
||||
std::size_t WorkerPool::assign_worker(std::uint32_t path_id) {
|
||||
auto it = owner.find(path_id);
|
||||
if(it != owner.end()) {
|
||||
// Already assigned; touch LRU
|
||||
auto lru_it = owner_lru_index.find(path_id);
|
||||
if(lru_it != owner_lru_index.end()) {
|
||||
owner_lru.erase(lru_it->second);
|
||||
}
|
||||
owner_lru.push_front(path_id);
|
||||
owner_lru_index[path_id] = owner_lru.begin();
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// New assignment: pick the least-loaded worker
|
||||
auto selected = pick_least_loaded();
|
||||
owner[path_id] = selected;
|
||||
stateful_workers[selected].owned_documents++;
|
||||
owner_lru.push_front(path_id);
|
||||
owner_lru_index[path_id] = owner_lru.begin();
|
||||
return selected;
|
||||
}
|
||||
|
||||
std::size_t WorkerPool::pick_least_loaded() {
|
||||
std::size_t best = 0;
|
||||
for(std::size_t i = 1; i < stateful_workers.size(); ++i) {
|
||||
if(stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
void WorkerPool::remove_owner(std::uint32_t path_id) {
|
||||
auto it = owner.find(path_id);
|
||||
if(it == owner.end())
|
||||
return;
|
||||
|
||||
auto worker_idx = it->second;
|
||||
stateful_workers[worker_idx].owned_documents--;
|
||||
owner.erase(it);
|
||||
|
||||
auto lru_it = owner_lru_index.find(path_id);
|
||||
if(lru_it != owner_lru_index.end()) {
|
||||
owner_lru.erase(lru_it->second);
|
||||
owner_lru_index.erase(lru_it);
|
||||
}
|
||||
}
|
||||
|
||||
void WorkerPool::clear_owner(std::size_t worker_index) {
|
||||
llvm::SmallVector<std::uint32_t> to_remove;
|
||||
for(auto& [pid, widx]: owner) {
|
||||
if(widx == worker_index) {
|
||||
to_remove.push_back(pid);
|
||||
}
|
||||
}
|
||||
for(auto pid: to_remove) {
|
||||
remove_owner(pid);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
125
src/server/worker_pool.h
Normal file
125
src/server/worker_pool.h
Normal file
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
#include "eventide/ipc/peer.h"
|
||||
#include "server/protocol.h"
|
||||
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
/// Default timeout for IPC requests to worker processes.
|
||||
constexpr inline auto kWorkerRequestTimeout = std::chrono::milliseconds(30000);
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
|
||||
struct WorkerPoolOptions {
|
||||
std::string self_path;
|
||||
std::uint32_t stateless_count = 2;
|
||||
std::uint32_t stateful_count = 2;
|
||||
std::uint64_t worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
|
||||
};
|
||||
|
||||
class WorkerPool {
|
||||
public:
|
||||
WorkerPool(et::event_loop& loop) : loop(loop) {}
|
||||
|
||||
/// Spawn all worker processes. Returns false on failure.
|
||||
bool start(const WorkerPoolOptions& options);
|
||||
|
||||
/// Gracefully stop all workers.
|
||||
et::task<> stop();
|
||||
|
||||
/// Send a request to a stateful worker with path_id affinity routing.
|
||||
template <typename Params>
|
||||
RequestResult<Params> send_stateful(std::uint32_t path_id,
|
||||
const Params& params,
|
||||
et::ipc::request_options opts = {});
|
||||
|
||||
/// Send a request to a stateless worker with round-robin dispatch.
|
||||
template <typename Params>
|
||||
RequestResult<Params> send_stateless(const Params& params, et::ipc::request_options opts = {});
|
||||
|
||||
/// Send a notification to the stateful worker owning path_id (if any).
|
||||
template <typename Params>
|
||||
void notify_stateful(std::uint32_t path_id, const Params& params);
|
||||
|
||||
/// Remove path_id from ownership tracking (e.g. when the master learns a
|
||||
/// document was evicted).
|
||||
void remove_owner(std::uint32_t path_id);
|
||||
|
||||
/// Callback invoked when a stateful worker sends an EvictedParams notification.
|
||||
/// The master should translate the path to a path_id and call remove_owner().
|
||||
std::function<void(const std::string& path)> on_evicted;
|
||||
|
||||
private:
|
||||
struct WorkerProcess {
|
||||
et::process proc;
|
||||
std::unique_ptr<et::ipc::BincodePeer> peer;
|
||||
std::size_t owned_documents = 0;
|
||||
};
|
||||
|
||||
et::event_loop& loop;
|
||||
llvm::SmallVector<WorkerProcess> stateless_workers;
|
||||
llvm::SmallVector<WorkerProcess> stateful_workers;
|
||||
std::size_t next_stateless = 0;
|
||||
|
||||
// Stateful worker routing: path_id -> worker index with LRU tracking
|
||||
llvm::DenseMap<std::uint32_t, std::size_t> owner;
|
||||
std::list<std::uint32_t> owner_lru;
|
||||
llvm::DenseMap<std::uint32_t, std::list<std::uint32_t>::iterator> owner_lru_index;
|
||||
|
||||
std::size_t assign_worker(std::uint32_t path_id);
|
||||
void clear_owner(std::size_t worker_index);
|
||||
std::size_t pick_least_loaded();
|
||||
|
||||
bool spawn_worker(const std::string& self_path, bool stateful, std::uint64_t memory_limit);
|
||||
};
|
||||
|
||||
// --- Template implementations ---------------------------------------------------
|
||||
|
||||
template <typename Params>
|
||||
RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
|
||||
const Params& params,
|
||||
et::ipc::request_options opts) {
|
||||
if(stateful_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateful workers available"});
|
||||
}
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
}
|
||||
auto idx = assign_worker(path_id);
|
||||
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
|
||||
template <typename Params>
|
||||
RequestResult<Params> WorkerPool::send_stateless(const Params& params,
|
||||
et::ipc::request_options opts) {
|
||||
if(stateless_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
if(!opts.timeout.has_value()) {
|
||||
opts.timeout = kWorkerRequestTimeout;
|
||||
}
|
||||
auto idx = next_stateless;
|
||||
next_stateless = (next_stateless + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
}
|
||||
|
||||
template <typename Params>
|
||||
void WorkerPool::notify_stateful(std::uint32_t path_id, const Params& params) {
|
||||
auto it = owner.find(path_id);
|
||||
if(it == owner.end())
|
||||
return;
|
||||
stateful_workers[it->second].peer->send_notification(params);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
@@ -221,6 +221,8 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
using DirectiveVec = llvm::SmallVector<clang::dependency_directives_scan::Directive>;
|
||||
|
||||
llvm::ArrayRef<clang::dependency_directives_scan::Directive>
|
||||
get_directives(SharedScanCache::CachedEntry& entry) {
|
||||
if(mode == ScanMode::Precise) {
|
||||
@@ -229,11 +231,13 @@ private:
|
||||
|
||||
// Fuzzy mode: strip #define/#undef and ALL conditional directives,
|
||||
// so every #include is processed unconditionally by the preprocessor.
|
||||
auto& filtered = filtered_directives[&entry];
|
||||
if(!filtered.empty()) {
|
||||
return filtered;
|
||||
auto& slot = filtered_directives[&entry];
|
||||
if(slot && !slot->empty()) {
|
||||
return *slot;
|
||||
}
|
||||
|
||||
slot = std::make_unique<DirectiveVec>();
|
||||
|
||||
using namespace clang::dependency_directives_scan;
|
||||
for(auto& dir: entry.directives) {
|
||||
switch(dir.Kind) {
|
||||
@@ -252,21 +256,20 @@ private:
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
filtered.push_back(dir);
|
||||
slot->push_back(dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
return *slot;
|
||||
}
|
||||
|
||||
ScanMode mode;
|
||||
SharedScanCache* cache;
|
||||
clang::FileManager* file_mgr;
|
||||
std::deque<SharedScanCache::CachedEntry> local_entries;
|
||||
llvm::DenseMap<SharedScanCache::CachedEntry*,
|
||||
llvm::SmallVector<clang::dependency_directives_scan::Directive>>
|
||||
llvm::DenseMap<SharedScanCache::CachedEntry*, std::unique_ptr<DirectiveVec>>
|
||||
filtered_directives;
|
||||
};
|
||||
|
||||
@@ -433,7 +436,6 @@ private:
|
||||
std::unique_ptr<clang::CompilerInstance>
|
||||
create_scan_instance(llvm::ArrayRef<const char*> arguments,
|
||||
llvm::StringRef directory,
|
||||
bool arguments_from_database,
|
||||
llvm::StringRef content,
|
||||
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs) {
|
||||
clang::DiagnosticOptions diag_opts;
|
||||
@@ -444,10 +446,11 @@ std::unique_ptr<clang::CompilerInstance>
|
||||
|
||||
std::unique_ptr<clang::CompilerInvocation> invocation;
|
||||
|
||||
if(arguments_from_database) {
|
||||
bool is_cc1 = arguments.size() >= 2 && llvm::StringRef(arguments[1]) == "-cc1";
|
||||
if(is_cc1) {
|
||||
invocation = std::make_unique<clang::CompilerInvocation>();
|
||||
if(!clang::CompilerInvocation::CreateFromArgs(*invocation,
|
||||
llvm::ArrayRef(arguments).drop_front(),
|
||||
llvm::ArrayRef(arguments).drop_front(2),
|
||||
*diag_engine,
|
||||
arguments[0])) {
|
||||
return nullptr;
|
||||
@@ -493,7 +496,6 @@ std::unique_ptr<clang::CompilerInstance>
|
||||
|
||||
llvm::StringMap<ScanResult> scan_fuzzy(llvm::ArrayRef<const char*> arguments,
|
||||
llvm::StringRef directory,
|
||||
bool arguments_from_database,
|
||||
llvm::StringRef content,
|
||||
SharedScanCache* cache,
|
||||
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs) {
|
||||
@@ -503,8 +505,7 @@ llvm::StringMap<ScanResult> scan_fuzzy(llvm::ArrayRef<const char*> arguments,
|
||||
vfs = llvm::vfs::createPhysicalFileSystem();
|
||||
}
|
||||
|
||||
auto instance =
|
||||
create_scan_instance(arguments, directory, arguments_from_database, content, vfs);
|
||||
auto instance = create_scan_instance(arguments, directory, content, vfs);
|
||||
if(!instance) {
|
||||
return results;
|
||||
}
|
||||
@@ -543,7 +544,6 @@ llvm::StringMap<ScanResult> scan_fuzzy(llvm::ArrayRef<const char*> arguments,
|
||||
|
||||
ScanResult scan_precise(llvm::ArrayRef<const char*> arguments,
|
||||
llvm::StringRef directory,
|
||||
bool arguments_from_database,
|
||||
llvm::StringRef content,
|
||||
SharedScanCache* cache,
|
||||
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs) {
|
||||
@@ -553,8 +553,7 @@ ScanResult scan_precise(llvm::ArrayRef<const char*> arguments,
|
||||
vfs = llvm::vfs::createPhysicalFileSystem();
|
||||
}
|
||||
|
||||
auto instance =
|
||||
create_scan_instance(arguments, directory, arguments_from_database, content, vfs);
|
||||
auto instance = create_scan_instance(arguments, directory, content, vfs);
|
||||
if(!instance) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ ScanResult scan(llvm::StringRef content);
|
||||
llvm::StringMap<ScanResult>
|
||||
scan_fuzzy(llvm::ArrayRef<const char*> arguments,
|
||||
llvm::StringRef directory,
|
||||
bool arguments_from_database,
|
||||
llvm::StringRef content = {},
|
||||
SharedScanCache* cache = nullptr,
|
||||
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs = nullptr);
|
||||
@@ -86,7 +85,6 @@ llvm::StringMap<ScanResult>
|
||||
/// and conditionals. Used for lazy module dependency resolution.
|
||||
ScanResult scan_precise(llvm::ArrayRef<const char*> arguments,
|
||||
llvm::StringRef directory,
|
||||
bool arguments_from_database,
|
||||
llvm::StringRef content = {},
|
||||
SharedScanCache* cache = nullptr,
|
||||
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs = nullptr);
|
||||
|
||||
Reference in New Issue
Block a user