diff --git a/private_Library/private_Application Support/private_Code/User/settings.json.tmpl b/private_Library/private_Application Support/private_Code/User/settings.json.tmpl index 107c69f7..0537f8ac 100644 --- a/private_Library/private_Application Support/private_Code/User/settings.json.tmpl +++ b/private_Library/private_Application Support/private_Code/User/settings.json.tmpl @@ -89,8 +89,88 @@ }, // Homebrew/mise symlink farms make plain (non-excluded) symlink traversal expensive. "search.followSymlinks": false, - // Cap the TypeScript server's heap instead of leaving it unbounded. - "typescript.tsserver.maxTsServerMemory": 2048, + // Cap the TypeScript server's heap instead of leaving it unbounded (default 3072 MiB). + // `js/ts.tsserver.maxMemory` replaces the deprecated `typescript.tsserver.maxTsServerMemory` + // (still honored as a fallback -- `readMaxTsServerMemory()` in the installed extension's + // compiled dist/extension.js reads `tsserver.maxMemory` with the old key as + // `fallbackSubSectionNameOverride` -- but this stops relying on that and on the Settings UI + // deprecation nag). That same function clamps whatever it reads with `Math.max(n, 128)`, so + // 128 MiB is the floor no value here can go below. + // + // Scope, and why this file: both keys are `scope: "window"` in the extension's own + // package.json (verified against the copy installed under the remote server's + // extensions/typescript-language-features). The remote Machine settings file parses only + // [MACHINE, MACHINE_OVERRIDABLE] and silently drops everything else, so this file -- the + // client-side User settings -- is the only place it can live. The consequence is worth + // stating because it constrains the value: this applies to *every* VS Code window, local + // macOS projects included, not just the remote workspace. + // + // Value. There is exactly one process class in the remote tree with a heap knob, and this is + // it. The extension host, file watcher, pty host and the JSON/Markdown language servers expose + // nothing at any value (see remote.extensionKind below, which is the only lever that shrinks + // the extension host). The TOML server is the one exception to that, and it is named here + // because the exception turns out to be useless rather than absent: tamasfe.even-better-toml + // does expose levers -- `evenBetterToml.taplo.environment` injects arbitrary env into the + // forked bundled server, and `taplo.bundled: false` + `taplo.path` substitutes the binary + // outright -- but nothing reachable through them bounds what actually grows there. Measured + // on a live workspace, that process's 274 MiB PSS is ~99 MiB of native malloc `[heap]` plus + // ~14 MiB of rwxp JIT code -- 41% of the process that no V8 flag can reach at any value -- + // with the ~160 MiB remainder split between V8's managed heap and WASM linear memory in + // proportions smaps cannot separate (taplo is Rust compiled to WASM, whose linear memory is + // an ArrayBuffer backing store outside the old space). Since `--max-old-space-size` bounds + // only the old space, a ceiling there cannot bound this process below its share however low + // it is set: NODE_OPTIONS by that route is inert, not merely leaky. Recorded so this does + // not have to be re-derived from a blanket "no lever exists" claim. + // + // TypeScript spawns *two* processes that each read this setting independently -- a full + // semantic server and a lighter "partialSemantic" one -- so it is a per-process ceiling, not + // a combined one. `js/ts.tsserver.useSyntaxServer: "never"` would collapse the pair into a + // single process and is the only way to make the ceiling combined; deliberately not set, + // because the two rest at 112/141 MiB against a 320 MiB share so there is nothing to buy, + // and the cost is that syntax-only work (folding, outline, in-file rename) then queues behind + // semantic work. + // + // 768 is deliberately NOT low enough to fire before the workspace template's memory watchdog, + // and that ordering is a decision rather than an oversight. The watchdog gives tsserver a + // 320 MiB share of a 2048 MiB envelope, compared in PSS. A heap ceiling bounds V8's old space + // only, while PSS also carries the node binary, native allocations, external ArrayBuffers and + // V8's new/code/large-object spaces -- a remainder that is near-constant per role rather than + // proportional to the heap. So the two relate by addition, not by a ratio: + // + // peak PSS ~= resting PSS + ceiling + // + // and for V8 to fail first the ceiling would have to be <= share - resting, which on measured + // resting PSS of 141/111 MiB is about 179 MiB. Three reasons not to set it there: + // 1. It is within spitting distance of the 128 MiB clamp above -- no room left to be wrong. + // 2. Window scope (above) means it would cap tsserver on real TypeScript projects opened + // locally on the Mac, where 179 MiB is a crash rather than a bound. A ceiling set too + // low turns a working feature into a crash loop; a ceiling set too high is merely inert. + // That asymmetry decides it. + // 3. It would not buy the recoverable failure it appears to. A V8 fatal heap error and an + // external SIGKILL arrive at the same handler in the extension, which shows "The JS/TS + // language service crashed." and, after five crashes in five minutes, "The service will + // not be restarted." The watchdog's ten-minute dwell cannot reach that rate. A binding + // heap ceiling on a project that legitimately needs the memory reaches it immediately. + // + // So the watchdog stays first for tsserver, and this ceiling keeps its original and only job: + // stop a runaway tsserver from claiming the whole pod. 768 sits well above both processes' + // measured resting PSS while bounding the pair at ~1.5 GiB of heap instead of the 6 GiB two + // 3072-defaults sanction. Cost on a large TypeScript project: a project whose server needs + // more than 768 MiB of heap will crash and restart. That is a real cost and it is why this is + // not lower. + // + // Nothing here is load-bearing for the template: the watchdog is coherent with these settings + // absent, which is the state it is actually in most of the time (see below). + // + // Not yet verified in force. Live evidence from a remote session started after dotfiles#773 + // set the old key: both tsserver processes still run with --max-old-space-size=3072, VS Code's + // default. Since the extension does honor the old key as a fallback, that means these settings + // are not reaching the window at all -- most likely simply not applied on the Mac. To check: + // in a reconnected remote window open Settings, search "tsserver.maxMemory", and confirm both + // the value AND its provenance ("Modified elsewhere"/User). Then, in a terminal on the remote, + // `pgrep -af tsserver.js` must show --max-old-space-size=768. If the Settings UI shows 768 but + // the process shows 3072, the window has not reloaded since the change. + "js/ts.tsserver.maxMemory": 768, "typescript.disableAutomaticTypeAcquisition": true, // Reduce the git extension's background repository scanning. "git.autoRepositoryDetection": "openEditors", @@ -114,12 +194,47 @@ "remote.SSH.remotePlatform": { "coder-vscode.coder.{{ (bitwardenSecrets "288eeda0-d57f-4a91-8651-b2090163ecc0" .bwsAccessToken).value }}--{{ .coderUsername }}--{{ .coderUsername }}.main": "linux" }, - // APPLICATION/WINDOW-scoped in VS Code's own configuration registry (never MACHINE or + // APPLICATION-scoped in VS Code's own configuration registry (never MACHINE or // MACHINE_OVERRIDABLE), so it must live in this file -- the remote Machine settings file // parses only [MACHINE, MACHINE_OVERRIDABLE] scopes and silently drops anything else. + // + // Pins extensions that do no workspace-filesystem or child-process work to the client (UI) + // extension host instead of the remote one, so their code never runs on the remote at all -- + // the extension host is the tree's biggest, most volatile process (528 -> 843 MiB PSS over + // three days on one live workspace, the hourly minimum climbing throughout) precisely because + // of what it loads. + // + // This is the "load less" lever, and it is the ONLY lever: unlike tsserver above, the + // extension host has no heap ceiling at any value. Established by reading the server bundle + // on disk rather than assumed -- it registers no heap-sizing setting, `server-main.js --help` + // exposes no memory option, no `remote.*` configuration property exists in the server bundle, + // and this build has no `server-env-setup` support. NODE_OPTIONS in particular does not work + // and would be actively bad: `server-main.js` deletes NODE_OPTIONS from the child environment + // immediately before every fork it performs, so setting it would cap the parent and every + // unrelated mise-node process in the pod while leaving the extension host at its default. + // (The one channel that does propagate is the parent's own `process.execArgv`, which the + // extension host inherits verbatim -- but the only injection point is a shell script inside + // the versioned `cli/servers/Stable-/` directory that VS Code replaces wholesale on + // every server upgrade, and the role rests above any share it could be given anyway.) + // + // Each entry below was checked against the extension actually installed under + // ~/.vscode-server/extensions on a live workspace: no `main`-side use of + // node's `fs`/`child_process`, no language-server client, no commands operating on files the + // user hasn't opened. + // - vscode-icons-team.vscode-icons, bierner.markdown-mermaid: pre-existing, unchanged. + // - hashicorp.hcl, samuelcolvin.jinjahtml: grammar/syntax-highlighting only (`contributes` + // is just `languages`/`grammars`[/`breakpoints` metadata for jinjahtml, not an actual + // debug adapter]); confirmed no LSP client or fs/child_process usage in either's source. + // Considered and left on the remote (default, unset here): davidanson.vscode-markdownlint -- + // it contributes a `markdownlint.lintWorkspace` command that walks the whole workspace tree, + // which is exactly the "real work against the workspace filesystem" case this lever is not + // for. tamasfe.even-better-toml already declares its own `extensionKind: ["workspace"]` for + // the same reason (it validates TOML against the real workspace, e.g. Cargo.lock/uv.lock). "remote.extensionKind": { "vscode-icons-team.vscode-icons": ["ui"], - "bierner.markdown-mermaid": ["ui"] + "bierner.markdown-mermaid": ["ui"], + "hashicorp.hcl": ["ui"], + "samuelcolvin.jinjahtml": ["ui"] }, "remote.SSH.defaultExtensions": [ "bierner.markdown-mermaid",