feat: Add Language Server startup monitoring
Description
Introduces LanguageServerStartupMonitor - a class that observes the vscode-languageclient state machine to track and time each phase of Language Server startup.
Why state machine listening helps
The vscode-languageclient library manages the LS process lifecycle internally and exposes a onDidChangeState event with three states: Starting, Running, and Stopped. By listening to these transitions we can:
- Know exactly when the OS process spawned (
Starting) and when the LSP handshake completed (Running), giving us precise timing for each phase - Detect stalls: if
Startingfires butRunningnever follows, the LSPinitializehandshake is hung - Detect permanent failures: if
Stoppedfires andStartingdoes not follow within a grace period, the library has given up retrying - Count restart attempts across crash/retry cycles
How each phase is measured
| Phase | Start | End | Timeout |
|---|---|---|---|
| Spawn | observe() called |
State.Starting fires |
10s - rejects the startup promise |
| Handshake | State.Starting fires |
State.Running fires |
30s per attempt - resets on each retry |
| Restart grace | State.Stopped fires |
next State.Starting fires |
5s - logs permanent failure |
On successful startup the State.Running log reports both durations:
[debug]: [LS startup] Language Server is running (spawn took 2.3ms, handshake took 652.4ms, total 654.8ms)notifyHandshakeStarted() is called from initializationOptions (invoked by the library just before sending the LSP initialize request) to record #tHandshakeStart. It is idempotent per spawn attempt and silently ignored if the process has not yet reached spawning phase.
Changes
LanguageServerStartupMonitor- new class withobserve(),notifyHandshakeStarted(), anddispose()LanguageClientWrapperImpl- wires the monitor intoinitAndStart(); disposes it via#subscriptions; suppresses the noisyclient.start()rejection log (expected during crash/retry cycles)- All timers promoted to instance fields so
dispose()can clear them cleanly
Related Issues
References #2269 (closed)
How has this been tested?
Scenario 1 - Happy path
Run npm run build:desktop, run extension in development host.
Expected extension logs:
[debug]: [LS startup] Language Server process spawning (attempt 1)
[debug]: [LS startup] Language Server is running (spawn took 2.3ms, handshake took 652.4ms, total 654.8ms)Scenario 2 - Spawn failure
Remove LS js bundle ind dist-desktop/assets/language-server/main-bundle-node.js
Run extension (without build)
Expected logs (library retries several times, then grace period fires after 5s):
[LS startup] Language Server process spawning (attempt 1)
[LS startup] Language Server process stopped, waiting for restart
[LS startup] Language Server process spawning (attempt 2)
[LS startup] Language Server process stopped, waiting for restart
[LS startup] Language Server process spawning (attempt 3)
[LS startup] Language Server process stopped, waiting for restart
[LS startup] Language Server process spawning (attempt 4)
[LS startup] Language Server process stopped, waiting for restart
[LS startup] Language Server process spawning (attempt 5)
[LS startup] Language Server process stopped, waiting for restart
[LS startup] Language Server failed to restart after 5 attempt(s). Try to restart the GitLab extension.Rebuild after
Scenario 3 - Handshake timeout (process spawns but LSP initialize hangs)
In the LS, delay initialize handler by more than 30seconds. Add:
await new Promise((resolve) => setTimeout(resolve, 35000));Link the updated LS in the extension running bun run watch -- --editor=vscode
Once extension has started wait for Language Server failed to start notification to show up after ~35seconds.
The log will show
[debug] Language Server process started but LSP initialize handshake did not complete within 30s (spawn took 5.2ms). Try to restart the GitLab extension.- If
src/browserorsrc/commonhas been modified, please consider interoperability with the Web IDE. See Running the Extension in WebIDE. - Consider an end-to-end test for significant new features that aren't covered by integration tests.
Screenshots (if appropriate)
What CHANGELOG entry will this MR create?
-
fix:Bug fix fixes - a user-facing issue in production - included in changelog -
feature:New feature - a user-facing change which adds functionality - included in changelog -
BREAKING CHANGE:(fix or feature that would cause existing functionality to change) - should bump major version, mentioned in the changelog - None - other non-user-facing changes