Skip to content

perf(pwsh/psmux): cut per-pane cold-start cost in splits#78

Merged
Gerrrt merged 4 commits into
mainfrom
perf/psmux-warm-initcache-splits
Jul 2, 2026
Merged

perf(pwsh/psmux): cut per-pane cold-start cost in splits#78
Gerrrt merged 4 commits into
mainfrom
perf/psmux-warm-initcache-splits

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What & why

Investigation of "every tool init costs ~200ms" found Get-InitCache was not missing or falling back — zero cache misses on a full profile load. The real cost splits into:

  1. Dot-sourcing the cached init (~350ms starship / ~210ms atuin / ~195ms mise) — cold-process JIT, not the cache. Unavoidable per fresh pane.
  2. ~60ms/tool of validation-on-hit (Get-Command exe lookup + mtime reads) — this can be skipped in splits.

(An earlier commit added a hidden DOTFILES_INITCACHE_DEBUG=1 instrumentation to reach these findings; it has since been removed now that the cache is confirmed healthy.)

Changes

Get-InitCache (core/10-tools.ps1)

  • psmux fast-path: in a split, if the cache file exists, return it unvalidated — splits inherit the parent env, so the exe/generator can't have drifted. Skips the ~60ms/tool validation. Uses Test-InMux (added to the fragment's requires contract).

mise (core/10-tools.ps1)

  • Use mise activate --shims in psmux splits (distinct mise-shims cache name so it doesn't thrash the top-level cache). A split inherits the parent's resolved env, so shims give correct versions at ~12ms vs ~195ms for the full hook-env init. Warm split mise cost measured 261ms → 16ms.

psmux.conf

  • Flip warm offon (psmux's default) and destroy-unattached onoff so the hidden __warm__ standby server can spawn (verified in an isolated -L namespace). Trade-off: detached sessions now persist and can pile up — manage via psmux ls / psmux kill-session. Full effect lands on the next fresh psmux launch, not the running server.

Verification

  • All 72 LoadContract.Tests.ps1 pass; 10-tools.ps1 parses clean; pre-commit validation passed.
  • Warm-server spawn confirmed in an isolated namespace (wtest____warm__.port).
  • Sub-step timings measured end-to-end (split vs top-level).

Revert

psmux experiment reverts cleanly to destroy-unattached on + warm off (or $env:PSMUX_NO_WARM=1) if the session persistence/pileup isn't worth the speedup.

🤖 Generated with Claude Code

Investigation of "every tool init costs ~200ms" found Get-InitCache was
NOT missing/falling back (zero cache misses on a full profile load). The
cost splits into (1) dot-sourcing the cached init (~350ms starship /
~210ms atuin / ~195ms mise) which is cold-process JIT, not the cache, and
(2) ~60ms/tool of validation-on-hit (Get-Command exe lookup + mtime reads).

Get-InitCache (core/10-tools.ps1):
- psmux fast-path: in a split, if the cache file exists, return it
  unvalidated (splits inherit the parent env, so the exe/generator can't
  have drifted) — skips the ~60ms/tool validation. Uses Test-InMux, added
  to the fragment's requires contract.
- hidden DOTFILES_INITCACHE_DEBUG=1 instrumentation: prints a per-tool
  breakdown (Get-Command vs mtime timings + stale reason) on a cache
  MISS/throw. Scaffolding for the investigation; delete once settled.

mise (core/10-tools.ps1): use `mise activate --shims` in psmux splits
(distinct 'mise-shims' cache name so it doesn't thrash the top-level
cache). A split inherits the parent's resolved env, so shims give correct
versions at ~12ms vs ~195ms for the full hook-env init. Warm split mise
cost measured 261ms -> 16ms.

psmux.conf: flip `warm off`->`on` (psmux's default) and `destroy-unattached
on`->`off` so the hidden __warm__ standby server can spawn (verified in an
isolated -L namespace). Trade-off: detached sessions now persist and can
pile up (manage via psmux ls / kill-session). Full effect lands on the
next fresh psmux launch, not the running server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 04:27
The DOTFILES_INITCACHE_DEBUG instrumentation confirmed the cache is
healthy (zero misses on a full profile load) and located the cost, so
drop it. The psmux fast-path and mise-shims-in-splits changes stay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to reduce per-pane PowerShell cold-start latency when creating psmux splits by avoiding repeated init-cache validation work and by using faster mise “shims” activation in mux panes. It also adjusts psmux defaults to allow warm standby shells, moving unavoidable JIT costs off the split keypress.

Changes:

  • Add a mux-oriented fast-path and debug instrumentation to Get-InitCache in core/10-tools.ps1.
  • Switch mise activation to mise activate --shims in mux panes with a separate cache key (mise-shims) to avoid cache thrash.
  • Enable psmux warm pooling and disable destroy-unattached to allow the hidden __warm__ standby server.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
psmux/psmux.conf Enables warm pooling and changes detached-session behavior to support the warm standby server.
powershell/core/10-tools.ps1 Optimizes init-cache behavior for mux panes and speeds up mise activation in splits via shims.
Comments suppressed due to low confidence (1)

powershell/core/10-tools.ps1:167

  • The mux fast-path returns an existing init-cache file without validating either the tool binary mtime or the generator-hash marker. Because $DotfilesInitCacheDir lives under LOCALAPPDATA and survives across psmux/tmux server restarts, this can serve stale init scripts after a tool upgrade or a repo/profile update (the exact cases the mtime/hash validation is meant to catch).
    #                     the hash, so the stale cache self-busts on the next shell
    #                     instead of silently serving the old init until someone
    #                     remembers `init-cache-clear` (B2).
    $genHash = if (Get-Command Get-DotStringSha256 -ErrorAction SilentlyContinue) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread psmux/psmux.conf Outdated
Comment on lines +84 to +86
# destroy-unattached OFF (flipped 2026-07-01, paired with `warm on` below): `on` made a
# detached session die immediately, which also SUPPRESSES psmux's hidden `__warm__`
# standby server (docs/warm-sessions.md) — so instant new-session never kicked in. Off
Addresses PR review: the split fast-path previously returned the cached
init unvalidated, which silently broke the B2 self-bust-on-flag-change
invariant — and with warm/persistent sessions the LOCALAPPDATA cache can
outlive many profile edits, so a split could serve a stale init after a
generator (init-flag) change.

Keep the CHEAP generator-hash check (SHA of a short string + one first-line
read; touches neither PATH nor the exe) in the fast-path, and skip only the
EXPENSIVE half (Get-Command exe resolution + Get-Item mtime — the AV-scanned
~60ms/tool this optimizes away). The rarer tool-BINARY upgrade mid-session
stays uncaught in a split by design; it self-heals on the next top-level
shell / fresh psmux server, or `init-cache-clear` forces it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Gerrrt

Gerrrt commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @copilot-pull-request-reviewer — good catch on the fast-path skipping validation. Addressed in 500a373.

The two cases you flagged have very different cost/likelihood, so I split them:

  • Generator / profile-flag edit (the more likely case, and the documented "B2" self-bust invariant): caught by the generator-hash marker, which is cheap — a SHA over a short string plus one first-line read, touching neither PATH nor the exe. The fast-path now keeps this check, so a split regenerates when a tool's init flags change. This matters more now precisely because destroy-unattached off + warm pooling let the LOCALAPPDATA cache outlive many edits, as you noted.
  • Tool binary upgrade mid-session: caught only by the exe-mtime check, which needs Get-Command <exe> — the AV-scanned PATH resolution that is the ~60ms/tool this optimization removes. Left uncaught in a split by design; it self-heals on the next top-level shell / fresh psmux server, or init-cache-clear forces it. Documented inline.

Verified: fast-path still hits on a hash match, and now regenerates on a generator change in a pane; 72/72 LoadContract tests pass.

Addresses PR review: the destroy-unattached comment pointed at
`docs/warm-sessions.md` as if it were a file in this repo. It lives in the
upstream psmux project, not here — make that explicit with the repo URL so
the pointer isn't a dangling local reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Gerrrt

Gerrrt commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed in the latest commit — docs/warm-sessions.md now explicitly notes it's the upstream psmux doc (https://github.com/psmux/psmux), not a file in this repo, so the pointer isn't dangling.

@Gerrrt Gerrrt merged commit 84d7439 into main Jul 2, 2026
6 checks passed
@Gerrrt Gerrrt deleted the perf/psmux-warm-initcache-splits branch July 2, 2026 04:41
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.

2 participants