Skip to content

Commit 105839e

Browse files
authored
perf(vscode): pre-spawned rs fmt standby for the active editor (#6)
* feat(vscode): keep a pre-spawned rs fmt standby for the active editor A cold format pays ~130ms of process start-up and config load before any formatting happens. The fmt stack now keeps one pre-spawned rs fmt child parked on stdin for the active editor's file, so the next format of that file writes to a process that has already paid both. The standby is bounded on purpose: one process, one file, one request. It is armed 2s after the active editor settles (immediately at registration and after a format), killed whenever it could have gone stale — a config content edit, a detection change, restart or dispose — and reaped after five idle minutes. Anything it cannot serve falls back to the cold path unchanged, and the whole mechanism is invisible above debug level except for the hot/cold marker on the completion line. CONTEXT.md records the vocabulary the code and logs use, and the fmt gotcha in AGENTS.md now states the exception and its limits. * chore(vscode): quiet the F5 playground's launch notifications The isolated playground profile installs no extensions, so --disable-extensions only contributed its own notification on every launch. Git is a built-in and ignores that flag entirely: it offered to open the parent repository each time, because every fixture lives inside this repo. Disable that one extension instead. * fix(vscode): drop the standby when the active editor cannot be armed An active editor the provider matches but the stack cannot resolve — an undetected folder, a missing or too-old rstack package — logged the skip and returned, leaving the previous file's standby parked for up to five idle minutes. The editor had still moved on, so that standby no longer tracked it and could serve a programmatic request for a file the user had left. Arming already kills a standby it replaces; this path has nothing to arm, so it has to kill on its own. An editor holding nothing formattable at all still leaves the standby alone: a peek at the output panel must not cost the user their warm process. * docs(vscode): correct the standby's active-editor wording The docstring justified keeping a standby across an ineligible editor with a peek at the output panel, which cannot happen: the active editor is the focused one or, when nothing is focused, the most recently changed one, so focusing a panel leaves it pointing at the same file and fires no change. Name the cases that do reach it — no text document at all, or one this stack does not format — and say why neither is worth a kill. CONTEXT.md stated the invariant more strongly than the code holds it; it now states both outcomes. * docs(vscode): correct the standby's active-editor wording, rename reap to expire The docstring justified keeping a standby across an ineligible editor with a peek at the output panel, which cannot happen: the active editor is the focused one or, when nothing is focused, the most recently changed one, so focusing a panel leaves it pointing at the same file and fires no change. Name the cases that do reach it — no text document at all, or one this stack does not format — and say why neither is worth a kill. CONTEXT.md stated the invariant more strongly than the code holds it; it now states both outcomes. 'Reap' reads as jargon for what the idle timer does, so the term is now 'expire' in the glossary, the log reason and the tests. * fix(vscode): invalidate the standby on every rstack config event The config watcher listened for content changes only, on the reasoning that a create or a delete moves the detection signature and arrives as a detection change instead. That holds for a real create or delete, not for the delete/create pair an editor emits when it saves by atomically replacing the file: the config path is gone and back within one scan, the signature records paths rather than content, and no detection change fires. Both halves were also ignored here, so nothing invalidated the standby and the parked process kept serving the config it had loaded at spawn — the same file then formatted one way hot and another way cold. Watch all three events. A redundant kill costs nothing: it is idempotent and the active editor re-arms through the usual debounce.
1 parent 048ba8c commit 105839e

10 files changed

Lines changed: 1069 additions & 165 deletions

File tree

.vscode/launch.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@
1212
"runtimeExecutable": "${execPath}",
1313
"args": [
1414
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode",
15-
"--disable-extensions",
16-
// An isolated, auto-created profile, so personal user settings
17-
// (formatters, format-on-save, keybindings) cannot leak into the
18-
// playground. `--user-data-dir` would be the stronger isolation, but
19-
// the extension-host debugger strips it; `--profile` is the supported
20-
// mechanism for debug launches.
15+
// An isolated, auto-created profile, so neither personal user settings
16+
// (formatters, format-on-save, keybindings) nor personal extensions
17+
// can leak into the playground — the profile starts with none
18+
// installed, which is why `--disable-extensions` is absent: it would
19+
// only add its "all installed extensions are temporarily disabled"
20+
// notification to every launch. `--user-data-dir` would be the
21+
// stronger isolation, but the extension-host debugger strips it;
22+
// `--profile` is the supported mechanism for debug launches.
2123
"--profile=rstack-playground",
24+
// Built-in extensions ignore the empty profile. Git is the one that
25+
// speaks up: every fixture sits under this repo, so it offers to open
26+
// the parent repository on each launch.
27+
"--disable-extension=vscode.git",
2228
// A fresh profile would otherwise prompt for workspace trust and greet
2329
// with the welcome tour on every first launch.
2430
"--disable-workspace-trust",

CONTEXT.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Ubiquitous language
2+
3+
Glossary of terms used across rstack-editor. Code, docs, commit messages and reviews use these words with exactly these meanings.
4+
5+
## Core
6+
7+
- **Stack** — one tool integration (lint, test, fmt) hosted by the extension shell. A stack registers against the shell and reports status through it; stacks never own UI chrome.
8+
- **Shell** — the always-activating extension core: detection, status bar, output channels, settings migration, stack lifecycle.
9+
- **Detection** — the per-workspace-folder scan deciding which stacks a folder lights up. Detection signals are config files and installed tool binaries, never user settings.
10+
- **Gate** — the per-stack activation condition: detected, workspace trusted, and the enable settings on.
11+
12+
## fmt
13+
14+
- **Cold format** — a format request served by spawning a fresh `rs fmt` process at request time; the request pays the full process start-up cost.
15+
- **Standby** — the single pre-spawned `rs fmt` process held ready for one specific file, so the next format of that file skips the start-up cost. There is at most one standby, and it is only ever armed for the active editor's file ("the standby tracks the active editor"). An editor change that cannot be armed kills it; an editor holding nothing this stack formats leaves it to expire.
16+
- **Arm** — create the standby for a file. Arming happens when the active editor lands on an eligible file and again right after a format consumed the previous standby.
17+
- **Consume** — serve a format request with the armed standby. A standby serves exactly one request; a request the standby cannot serve falls back to a cold format.
18+
- **Hot format** — a format request served by consuming the standby.
19+
- **Expire** — kill an idle standby to reclaim its memory. An expired standby is not an error; the next eligible event simply arms a new one.

packages/vscode/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
3131

3232
- The lint × `rstack.config.*` bridge was built and deliberately removed: a partial editor-side bridge gave wrong results, and a correct one needs upstream work first. `TODO(rstack-bridge)` markers carry the plan. Do not reintroduce a partial bridge.
3333
- The test × `rstack.config.*` bridge stays thin on purpose: it points the upstream machinery at rstack's shipped shim and lets the shim interpret the config inside the worker, same as the CLI. Never re-implement rstack config semantics in the extension.
34-
- The fmt stack is a spawn-per-request `rs fmt --stdin-filepath` MVP. Its cwd is the governing config directory because rs fmt resolves config from cwd only, and formatting errors are log-only by design. The endgame is an upstream LSP, so do not add a warm-process middle tier.
34+
- The fmt stack is a spawn-per-request `rs fmt --stdin-filepath` MVP. Its cwd is the governing config directory because rs fmt resolves config from cwd only, and formatting errors are log-only by design. A single pre-spawned standby that tracks the active editor (see CONTEXT.md) is the accepted, bounded exception to "no warm tier". Do not grow it into a daemon: no long-lived protocol, no process pool, no cross-request state. The endgame is an upstream LSP; the standby retires with it.
3535
- `projectModules.ts` has no cache-invalidation hook and restart must not grow one. Node's ESM registry is keyed by resolved URL and process-lifetime, so clearing the local memo hands back the identical module object (verified); a `?epoch=` query does reload the entry but relative specifiers inside it do not inherit the query, yielding a fresh entry over stale dependencies. In-place reinstalls under an unchanged path need a window reload — say so, don't fake it.
3636
- The VSIX is platform-targeted for exactly one reason: the test stack's AST collection loads a native parser binding. Do not add another native dependency — it multiplies the release matrix.
3737

0 commit comments

Comments
 (0)