Files
cleveragents-core/benchmarks/lsp_stub_bench.py
T
CoreRasurae 23803f14ec
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 4m0s
CI / docker (pull_request) Successful in 38s
CI / integration_tests (pull_request) Successful in 4m48s
CI / coverage (pull_request) Successful in 4m31s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 15s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 35s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m27s
CI / integration_tests (push) Successful in 3m3s
CI / docker (push) Successful in 38s
CI / coverage (push) Successful in 4m25s
CI / benchmark-publish (push) Successful in 16m28s
CI / benchmark-regression (pull_request) Successful in 28m52s
feat(lsp): add LSP server stub
Added minimal LSP server entrypoint supporting initialize/shutdown/exit
handshake over JSON-RPC stdin/stdout transport with Content-Length
framing. Unsupported methods return MethodNotFound error with descriptive
message. Wired LSP requests through ACP facade in local mode. Added
agents lsp serve CLI command with --log-level flag, PID output, and
startup banner. Created reference documentation for the stub server.
Includes Behave BDD tests for protocol handshake, Robot smoke test, and
ASV startup latency benchmark.

ISSUES CLOSED: #203
2026-03-06 18:16:47 +00:00

122 lines
4.0 KiB
Python

"""ASV benchmarks for LSP server stub startup latency.
Measures the performance of:
- LspServer construction overhead
- Initialize request handling latency
- Full handshake (initialize + shutdown + exit) latency
- MethodNotFound error response latency
"""
from __future__ import annotations
import importlib
import io
import json
import sys
from pathlib import Path
# Ensure the local *source* tree is importable even when ASV has an
# older build of the package installed.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
# Force-reload so ASV picks up the source tree version. Reload the
# specific submodule that contains the benchmarked code, not just the
# top-level package.
import cleveragents # noqa: E402
import cleveragents.lsp.server # noqa: E402
importlib.reload(cleveragents)
importlib.reload(cleveragents.lsp.server)
from cleveragents.lsp.server import LspServer # noqa: E402
def _encode_message(msg: dict) -> bytes:
"""Encode a JSON-RPC message with Content-Length framing."""
payload = json.dumps(msg).encode("utf-8")
header = f"Content-Length: {len(payload)}\r\n\r\n".encode()
return header + payload
def _make_streams(*messages: dict) -> tuple[io.BytesIO, io.BytesIO]:
"""Build input/output streams from a sequence of JSON-RPC messages."""
raw = b"".join(_encode_message(msg) for msg in messages)
return io.BytesIO(raw), io.BytesIO()
class LspStubStartupSuite:
"""Benchmark LSP server stub startup and request handling."""
timeout = 60
def setup(self) -> None:
"""Pre-build common messages and reusable streams for benchmarks."""
self.init_msg = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {"capabilities": {}},
}
self.shutdown_msg = {
"jsonrpc": "2.0",
"id": 2,
"method": "shutdown",
}
self.exit_msg = {
"jsonrpc": "2.0",
"method": "exit",
}
self.completion_msg = {
"jsonrpc": "2.0",
"id": 3,
"method": "textDocument/completion",
"params": {},
}
# Pre-build the 100-message payload so that list construction
# overhead is excluded from the benchmark measurement.
batch_messages: list[dict] = [self.init_msg]
for i in range(100):
batch_messages.append(
{
"jsonrpc": "2.0",
"id": i + 10,
"method": "textDocument/completion",
"params": {},
}
)
batch_messages.append(self.exit_msg)
self._batch_raw = b"".join(_encode_message(msg) for msg in batch_messages)
def time_server_construction(self) -> None:
"""Measure LspServer construction overhead."""
inp, out = _make_streams(self.exit_msg)
LspServer(input_stream=inp, output_stream=out)
def time_initialize_request(self) -> None:
"""Measure initialize request handling latency."""
inp, out = _make_streams(self.init_msg, self.exit_msg)
server = LspServer(input_stream=inp, output_stream=out)
server.run()
def time_full_handshake(self) -> None:
"""Measure full lifecycle: initialize + shutdown + exit."""
inp, out = _make_streams(self.init_msg, self.shutdown_msg, self.exit_msg)
server = LspServer(input_stream=inp, output_stream=out)
server.run()
def time_method_not_found(self) -> None:
"""Measure MethodNotFound error response latency (after initialize)."""
inp, out = _make_streams(self.init_msg, self.completion_msg, self.exit_msg)
server = LspServer(input_stream=inp, output_stream=out)
server.run()
def time_100_method_not_found(self) -> None:
"""Measure 100 MethodNotFound responses (after initialize)."""
inp = io.BytesIO(self._batch_raw)
out = io.BytesIO()
server = LspServer(input_stream=inp, output_stream=out)
server.run()