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 Starting fires but Running never follows, the LSP initialize handshake is hung
  • Detect permanent failures: if Stopped fires and Starting does 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 with observe(), notifyHandshakeStarted(), and dispose()
  • LanguageClientWrapperImpl - wires the monitor into initAndStart(); disposes it via #subscriptions; suppresses the noisy client.start() rejection log (expected during crash/retry cycles)
  • All timers promoted to instance fields so dispose() can clear them cleanly

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/browser or src/common has 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
Edited by Olena Horal-Koretska

Merge request reports

Loading
Loading