Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .memory/unprocessed/daemon-logger-lock-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Daemon logger isolation does not cover the `run` wrapper before exec

The daemon starts its configured server through a child `lsp-cli run` process. That wrapper writes
startup records through the global synchronous system logger before it replaces itself with the
actual LSP server. Consequently, holding the global log lock before initial daemon startup can still
delay server startup even after daemon-owned logging is isolated. The latency playground must acquire
the lock after the daemon reports `READY` when testing coordinator responsiveness.
32 changes: 0 additions & 32 deletions .memory/unprocessed/references-lua-performance.md

This file was deleted.

117 changes: 0 additions & 117 deletions .memory/unprocessed/release-reference-profile.md

This file was deleted.

27 changes: 0 additions & 27 deletions .memory/unprocessed/request-window-implementation.md

This file was deleted.

27 changes: 0 additions & 27 deletions .memory/unprocessed/request-window-validation-difficulties.md

This file was deleted.

11 changes: 8 additions & 3 deletions GOTCHAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

- Some servers send client requests such as `client/registerCapability` immediately after the
`initialize` response and expect those requests to be answered before later client traffic.
lsp-cli therefore drains and replies to queued server requests right after `initialized` and
before sending later requests, instead of assuming request-response traffic is strictly
one-directional.
lsp-cli therefore drains and replies to requests already queued after `initialized` and before
later outgoing operations. Requests that race with the next client request are answered while
that request is outstanding, instead of assuming request-response traffic is strictly
one-directional or waiting for a fixed post-initialization quiet period.


## Diagnostics
Expand All @@ -31,6 +32,10 @@

# Daemon gotchas

- Daemon initialization compares workspace URIs literally. Directory URIs produced by
`path_to_file_uri` end with `/`; raw socket clients and fixtures must use the same trailing
slash in `rootUri` and workspace folder URIs, or initialization is rejected.

- LSP 3.17 assumes one server serves one tool. `lsp-cli daemon` therefore implements a
conservative proxy policy instead of transparent multi-client sharing: only one client may be
connected at a time, downstream `shutdown`/`exit` are handled locally, and a later client with
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ lsp-cli stop-all

The same background daemon is spawned and left idle after `lsp-cli <CMD> --detach` is finished.

A new connection has two seconds to send its first complete message. Silent or
incomplete connections are closed without interrupting the active client. Up to
16 connections can wait to start a session; additional connections are closed,
including `stop` connections while all 16 slots are occupied.


## Configuration Files

Expand Down Expand Up @@ -246,6 +251,8 @@ detect:
daemon:
# Shut down an idle daemon after this much time.
idle-timeout: "60"
# Disconnect an output peer that remains backlogged for this long. Default: 2 seconds.
write-stall-timeout: "2"

lsp:
# Example server preference list for C++.
Expand All @@ -259,6 +266,20 @@ lsp:
- jedi-language-server
```

Each daemon output uses an ordered writer queue. A queue is marked stalled at 64
messages or 8 MiB of framed data and unmarked after both values fall below those
limits. The daemon continues accepting messages during the grace period, so memory
can grow if traffic continues; it disconnects a slow client or stops an unresponsive
server if the queue stays marked for `write-stall-timeout`. A message larger than
8 MiB is allowed and begins writing after earlier messages on that output.
Server restart and shutdown also advance through daemon events. The daemon keeps
accepting control traffic while a process starts or exits; an unresponsive server
is force-stopped after the existing two-second shutdown and exit deadlines.
Daemon traffic, lifecycle, error, and captured-server-stderr logs use a 64-record
worker queue, so formatting, stderr, and the global log file do not block forwarding.
New records are dropped when this queue is full, and the daemon reports the count
after logging resumes. Normal shutdown waits at most 100 milliseconds for logs.

## Language Configs: filetypes/*.yaml

Files in `filetypes/` define how `lsp-cli` recognizes a language.
Expand Down
34 changes: 34 additions & 0 deletions playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,40 @@ Server selection follows the configured preferences and server availability.
The projects reuse a similar domain across languages so symbol names are easy to remember
while trying different LSP servers.

To measure daemon forwarding overhead with a deterministic immediate-reply server:

```sh
cargo build
python3 scripts/daemon_latency.py --binary target/debug/lsp-cli --samples 100 --pipeline 16
```

Run this from the repository root. Add another `--binary /path/to/baseline/lsp-cli`
to compare builds; use `--skip-handshake-checks` for binaries predating independent
handshakes. The script uses `playground/rust` with temporary server
configuration and sockets; it requires Python 3 but no installed language server.
It reports direct and warm-daemon p50/p95/p99 request latency in milliseconds,
with sample counts for sequential requests and batches of 16 pipelined requests.
Initialization is outside the measured interval. It also checks busy rejection,
warm reuse, restart after capability changes or dynamic registration,
initialization-failure recovery, silent/trickling handshake expiration while active
requests continue, and stop behind a silent connection.

The daemon uses one outstanding event per reader or accept worker, keeping event
backlog bounded without a forwarding-loop polling delay. Individual message sizes
are not capped. First-message reads run independently with a two-second absolute
deadline and at most 16 pending handshakes. Excess connections are closed, including
stop connections when all slots are occupied. Writes, logging, and upstream
shutdown use per-output writer workers, so a peer that stops reading does not block
the coordinator. An output is flagged at 64 queued messages or 8 MiB of framed data;
it is unflagged after both values fall below their limits, or disconnected after the
configured `daemon.write-stall-timeout` (two seconds by default). Messages continue
to queue during that grace period, so memory use can temporarily exceed both limits.
Process start, shutdown, exit monitoring, and reaping run through a lifecycle worker;
daemon logging uses a bounded worker and a 100-millisecond shutdown flush budget.
The check holds the global log lock while forwarding and while stopping, and verifies
that overflow is reported after the lock is released. These measurements describe an immediate-reply fixture, not a
latency guarantee for real language servers or stalled peers.

The Lua playground exercises discovery of the local `normalize_timestamp` function,
which may be absent from LuaLS workspace-symbol results:

Expand Down
Loading