Skip to content

Lazy start Quarto LSP to reduce memory usage in non-Quarto workspaces#1059

Open
jmcphers wants to merge 2 commits into
mainfrom
bugfix/lsp-lazy-activation
Open

Lazy start Quarto LSP to reduce memory usage in non-Quarto workspaces#1059
jmcphers wants to merge 2 commits into
mainfrom
bugfix/lsp-lazy-activation

Conversation

@jmcphers

Copy link
Copy Markdown
Collaborator

The Quarto language server is a separate Node process that consumes 50-100mb of RAM in every Positron session.

Until now it was launched the moment the extension activated, which meant every VS Code window paid that memory cost, including windows that never touch a Quarto document. The most common example is an R project (or any other workspace) that happens to have the Quarto extension installed but contains no .qmd/.rmd files at all. Those sessions were carrying ~98 MB of steady-state overhead for a server they would never use.

The goal of this change is to reduce steady-state memory usage, and especially to bring it to zero for workspaces that have no Quarto content.

What changed

The language server is now started lazily, on first actual need, rather than eagerly at activation. Concretely, the extension still creates the language client during activation (so everything is wired up and ready), but it defers spawning the server process until one of the following happens:

  • A document the server handles (a Quarto document, or a relevant YAML config file) is opened — this covers both documents already open when the window loads and any opened later.
  • The workspace is found to already contain Quarto content, so that project-wide features like workspace symbol search and cross-file navigation still work before any file is opened.
  • A feature issues a request to the server (for example the visual editor or Zotero integration).

If none of these ever occur, such as the "R project with no Quarto files" case, the server is never spawned and the ~98 MB is never allocated. For real Quarto projects, behavior is effectively unchanged; the server comes up as soon as it's needed.

How it's wired up

Activation now returns a small handle instead of the raw language client. The handle exposes a lazy request transport, an onReady hook for work that should run "if and when" the server starts (used for pushing the color theme and Zotero configuration), an idempotent ensureStarted, and a way to ask for the client only if it's already running.

This let a couple of consumers get simpler and better-behaved:

  • Zotero config sync no longer force-starts the server. It pushes configuration whenever the server becomes ready and re-pushes on config changes only if the server is already running. This also replaced the old fixed-delay/retry-with-sleep approach with a clean readiness callback.
  • The visual editor now receives the lazy request transport directly rather than the language client.

deactivate was updated to be a no-op when the server was never started.

Tests

Because the server no longer starts eagerly, the workspace-symbol tests could race against startup and indexing. Added a shared waitForWorkspaceSymbol helper (built on a general waitForCondition poller) and a suiteSetup in the affected suites that waits for the server to be running and to have indexed the workspace before asserting.

@jmcphers
jmcphers requested a review from juliasilge July 17, 2026 20:41
@posit-snyk-bot

posit-snyk-bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@jmcphers jmcphers changed the title lazy start Quarto LSP Lazy start Quarto LSP to reduce memory usage in non-Quarto workspaces Jul 17, 2026

@juliasilge juliasilge left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working well for me! 🙌

  • Can you add an entry to CHANGELOG? I think might be a good change to make visible for folks, or for us to be able to easily track down later.
  • I have one question about Zotero, and whether we should await the library config.

// sync and re-syncs after any config change made while the server was down).
// We deliberately do NOT start the server just to push config; if it isn't
// running the config will be picked up the next time it starts.
context.subscriptions.push(lsp.onReady(() => { void pushLibraryConfig(); }));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this race with the first Zotero RPC after lazy startup? pushLibraryConfig() is fire-and-forget and uses the same lazy lspRequest, so a user Zotero request that triggered startup may reach the server before setLibraryConfig. Since the backend returns empty results when no Zotero source is configured, maybe Zotero RPCs should await an initial config-sync promise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Addressed in 29d77bc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants