|
| 1 | +# Server Architecture |
| 2 | + |
| 3 | +clice uses a **multi-process architecture** where a single **Master Server** coordinates multiple **Worker** processes. This design isolates Clang AST operations (which are memory-heavy and may crash) from the main LSP event loop. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +``` |
| 8 | +┌──────────────┐ JSON/LSP ┌────────────────┐ Bincode/IPC ┌──────────────────┐ |
| 9 | +│ LSP Client │ ◄──────────► │ Master Server │ ◄─────────────► │ Stateful Workers │ |
| 10 | +│ (Editor) │ (stdio) │ │ (stdio) │ (AST cache) │ |
| 11 | +└──────────────┘ │ - Lifecycle │ └──────────────────┘ |
| 12 | + │ - Documents │ |
| 13 | + │ - CDB │ Bincode/IPC ┌──────────────────┐ |
| 14 | + │ - Build drain │ ◄─────────────► │ Stateless Workers│ |
| 15 | + │ - Indexing │ (stdio) │ (one-shot tasks)│ |
| 16 | + └────────────────┘ └──────────────────┘ |
| 17 | +``` |
| 18 | + |
| 19 | +## Master Server |
| 20 | + |
| 21 | +The master server (`src/server/master_server.cpp`) is the central coordinator. It runs a single-threaded async event loop and never touches Clang directly. Its responsibilities: |
| 22 | + |
| 23 | +### LSP Lifecycle |
| 24 | + |
| 25 | +The server progresses through these states: |
| 26 | + |
| 27 | +1. **Uninitialized** — waiting for `initialize` request |
| 28 | +2. **Initialized** — capabilities exchanged, waiting for `initialized` notification |
| 29 | +3. **Ready** — workers spawned, workspace loaded, accepting requests |
| 30 | +4. **ShuttingDown** — `shutdown` received, draining work |
| 31 | +5. **Exited** — `exit` received, stopping the event loop |
| 32 | + |
| 33 | +On `initialized`, the master: |
| 34 | + |
| 35 | +- Loads configuration from `clice.toml` (or uses defaults) |
| 36 | +- Starts the worker pool (spawns stateful + stateless processes) |
| 37 | +- Loads `compile_commands.json` and builds an include graph |
| 38 | +- Starts the background indexer coroutine (if enabled) |
| 39 | + |
| 40 | +### Document Management |
| 41 | + |
| 42 | +Each open document is tracked in a `DocumentState` with: |
| 43 | + |
| 44 | +- Current `version` and `text` (kept in sync via `didOpen`/`didChange`) |
| 45 | +- A `generation` counter to detect stale compile results |
| 46 | +- Build state flags (`build_running`, `build_requested`, `drain_scheduled`) |
| 47 | + |
| 48 | +When a document is opened or changed: |
| 49 | + |
| 50 | +1. The include graph is re-scanned (via dependency directives) |
| 51 | +2. The compile unit is registered/updated in the `CompileGraph` |
| 52 | +3. A debounced build is scheduled |
| 53 | + |
| 54 | +### Build Drain |
| 55 | + |
| 56 | +The `run_build_drain` coroutine implements debounced compilation: |
| 57 | + |
| 58 | +1. Wait for the debounce timer (default 200ms) to expire |
| 59 | +2. Ensure PCH/PCM dependencies are ready via `CompileGraph` |
| 60 | +3. Send a `compile` request to the assigned stateful worker |
| 61 | +4. Publish diagnostics from the result (or clear them on failure) |
| 62 | +5. If more edits arrived during compilation (`build_requested`), loop back to step 2 |
| 63 | + |
| 64 | +This ensures rapid typing doesn't trigger a compile per keystroke. |
| 65 | + |
| 66 | +### Request Routing |
| 67 | + |
| 68 | +Feature requests are split between two worker types: |
| 69 | + |
| 70 | +**Stateful workers** (affinity-routed by file path): |
| 71 | + |
| 72 | +- `textDocument/hover` |
| 73 | +- `textDocument/semanticTokens/full` |
| 74 | +- `textDocument/inlayHint` |
| 75 | +- `textDocument/foldingRange` |
| 76 | +- `textDocument/documentSymbol` |
| 77 | +- `textDocument/documentLink` |
| 78 | +- `textDocument/codeAction` |
| 79 | +- `textDocument/definition` |
| 80 | + |
| 81 | +**Stateless workers** (round-robin): |
| 82 | + |
| 83 | +- `textDocument/completion` |
| 84 | +- `textDocument/signatureHelp` |
| 85 | + |
| 86 | +All feature responses use `RawValue` passthrough — the worker serializes the LSP result to JSON, and the master forwards the raw JSON bytes to the client without deserializing. This avoids bincode↔JSON conversion overhead and serde annotation conflicts. |
| 87 | + |
| 88 | +## Worker Pool |
| 89 | + |
| 90 | +The worker pool (`src/server/worker_pool.cpp`) manages spawning and communicating with worker processes. Each worker is a child process of the same `clice` binary, launched with `--mode stateful-worker` or `--mode stateless-worker`. |
| 91 | + |
| 92 | +### Communication |
| 93 | + |
| 94 | +Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `eventide::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol. |
| 95 | + |
| 96 | +### Stateful Worker Routing |
| 97 | + |
| 98 | +Stateful workers use **affinity routing**: each file is consistently assigned to the same worker so that the worker retains the cached AST. Assignment uses a **least-loaded** strategy for new files, with **LRU tracking** to manage ownership. |
| 99 | + |
| 100 | +When a worker exceeds its document capacity (currently hardcoded at 16 documents), it evicts the least-recently-used document and notifies the master via an `evicted` notification. |
| 101 | + |
| 102 | +### Stateless Worker Routing |
| 103 | + |
| 104 | +Stateless workers use simple **round-robin** dispatch. Each request includes the full source text and compilation arguments, so any worker can handle it independently. |
| 105 | + |
| 106 | +## Stateful Worker |
| 107 | + |
| 108 | +The stateful worker (`src/server/stateful_worker.cpp`) caches compiled ASTs in memory. Key behavior: |
| 109 | + |
| 110 | +- **Compile**: Parses source code into a `CompilationUnit`, caches the AST, and returns diagnostics as a `RawValue` (JSON bytes) |
| 111 | +- **Feature queries**: Look up the cached AST and invoke the corresponding `feature::*` function (hover, semantic tokens, etc.), serializing the result to JSON |
| 112 | +- **Document updates**: Received as notifications — the worker updates the stored text and marks the document as `dirty`, causing feature queries to return `null` until recompilation |
| 113 | +- **Eviction**: LRU-based; evicts the oldest document when capacity is exceeded, notifying the master |
| 114 | +- **Concurrency**: Each document has a per-document `et::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `et::queue`. |
| 115 | + |
| 116 | +## Stateless Worker |
| 117 | + |
| 118 | +The stateless worker (`src/server/stateless_worker.cpp`) handles one-shot requests that don't benefit from cached ASTs: |
| 119 | + |
| 120 | +- **Completion**: Creates a fresh compilation with `CompilationKind::Completion` and invokes `feature::code_complete` |
| 121 | +- **Signature help**: Similar to completion, using `feature::signature_help` |
| 122 | +- **Build PCH**: Compiles a precompiled header to a temporary file |
| 123 | +- **Build PCM**: Compiles a C++20 module interface to a temporary file |
| 124 | +- **Index**: Compiles a file for indexing (TUIndex generation — currently a stub) |
| 125 | + |
| 126 | +All requests are dispatched to a thread pool via `et::queue`. |
| 127 | + |
| 128 | +## Compile Graph |
| 129 | + |
| 130 | +The compile graph (`src/server/compile_graph.cpp`) tracks compilation unit dependencies as a DAG. It handles: |
| 131 | + |
| 132 | +- **Registration**: Each file registers its included dependencies |
| 133 | +- **Cascade invalidation**: When a file changes, all transitive dependents are marked dirty and their ongoing compilations are cancelled |
| 134 | +- **Dependency compilation**: Before compiling a file, `compile_deps` ensures all dependencies (PCH, PCMs) are built first |
| 135 | +- **Cancellation**: Uses `et::cancellation_source` to abort in-flight compilations when files are invalidated |
| 136 | + |
| 137 | +## Configuration |
| 138 | + |
| 139 | +The server reads configuration from `clice.toml` (or `.clice/config.toml`) in the workspace root. If no config file exists, sensible defaults are computed from system resources: |
| 140 | + |
| 141 | +| Setting | Default | Description | |
| 142 | +| ------------------------ | --------------------- | ------------------------------------------- | |
| 143 | +| `stateful_worker_count` | CPU cores / 4 | Number of stateful worker processes | |
| 144 | +| `stateless_worker_count` | CPU cores / 4 | Number of stateless worker processes | |
| 145 | +| `worker_memory_limit` | 4 GB | Memory limit per stateful worker | |
| 146 | +| `compile_commands_path` | auto-detect | Path to `compile_commands.json` | |
| 147 | +| `cache_dir` | `<workspace>/.clice/` | Cache directory for PCH/PCM files | |
| 148 | +| `debounce_ms` | 200 | Debounce interval for recompilation | |
| 149 | +| `enable_indexing` | true | Enable background indexing | |
| 150 | +| `idle_timeout_ms` | 3000 | Idle time before background indexing starts | |
| 151 | + |
| 152 | +String values support `${workspace}` substitution. |
| 153 | + |
| 154 | +## IPC Protocol |
| 155 | + |
| 156 | +The master and workers communicate using custom RPC messages defined in `src/server/protocol.h`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type. |
| 157 | + |
| 158 | +### Stateful Worker Messages |
| 159 | + |
| 160 | +| Method | Direction | Purpose | |
| 161 | +| ----------------------------- | ------------ | ------------------------------------- | |
| 162 | +| `clice/worker/compile` | Request | Compile source and return diagnostics | |
| 163 | +| `clice/worker/hover` | Request | Get hover info at position | |
| 164 | +| `clice/worker/semanticTokens` | Request | Get semantic tokens for file | |
| 165 | +| `clice/worker/inlayHints` | Request | Get inlay hints for range | |
| 166 | +| `clice/worker/foldingRange` | Request | Get folding ranges | |
| 167 | +| `clice/worker/documentSymbol` | Request | Get document symbols | |
| 168 | +| `clice/worker/documentLink` | Request | Get document links | |
| 169 | +| `clice/worker/codeAction` | Request | Get code actions for range | |
| 170 | +| `clice/worker/goToDefinition` | Request | Go to definition at position | |
| 171 | +| `clice/worker/documentUpdate` | Notification | Update document text (marks dirty) | |
| 172 | +| `clice/worker/evict` | Notification | Master → Worker: evict a document | |
| 173 | +| `clice/worker/evicted` | Notification | Worker → Master: document was evicted | |
| 174 | + |
| 175 | +### Stateless Worker Messages |
| 176 | + |
| 177 | +| Method | Direction | Purpose | |
| 178 | +| ---------------------------- | --------- | ---------------------------- | |
| 179 | +| `clice/worker/completion` | Request | Code completion at position | |
| 180 | +| `clice/worker/signatureHelp` | Request | Signature help at position | |
| 181 | +| `clice/worker/buildPCH` | Request | Build precompiled header | |
| 182 | +| `clice/worker/buildPCM` | Request | Build C++20 module interface | |
| 183 | +| `clice/worker/index` | Request | Index a translation unit | |
0 commit comments