You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Build: cut redundant devpod shell-outs on hot paths #60
Build: cut redundant devpod shell-outs on the hot paths.
list_workspaces() is uncached and called from 6+ sites (dl.py:171, 240, 686, 726, 731, 763, 1075); dl --purge runs devpod list twice back-to-back (:1075 then :240). Memoize per-invocation (single process, short-lived — a call-scoped cache or explicit pass-through is enough).
A single dl <ws> attach chains devpod list + devpod status + devpod context options + devpod up + devpod ssh --command sudo hostname + devpod ssh (dl.py:1093, :915, :788, :410) — the hostname SSH is a full extra round-trip before every attach. Determine which calls are droppable or mergeable without changing behavior; drop what's safe.
Acceptance: count of devpod subprocess spawns for dl --ls, dl <ws> (existing, running), and dl --purge -y before vs after.
Explicitly deferred to the Rust rewrite effort by #53 (go) — the map's destination admits "or explicitly deferred".
This ticket is where devlaunch's performance actually lives, and #53 measured it: devpod list --output json costs 0.44/0.47/0.44s, against 0.09s for the entire Python interpreter startup. One devpod round-trip is 5x the cost the language rewrite eliminates — and devlaunch-core in Rust still pays it, because devpod is a Go binary. So:
dl --purge making the same devpod list call twice (dl.py:1075 then :240) burns ~900ms.
A single dl <ws> attach chains list + status + context options + up + ssh --command sudo hostname + ssh ≈ ~2.7s of round-trips, one of which is a full extra SSH just to read a hostname.
Deferred rather than dropped precisely because it matters: the call-graph design is language-independent and transfers to the core unchanged, so building it against Python first would implement it twice. Carried forward as a requirement on devlaunch-core with this ticket's acceptance signal intact — devpod spawn counts for dl --ls, dl <ws> (existing, running), and dl --purge -y, before vs after.
Deferral reversed — this lands in Python, in a final Python release, before the port.
The deferral reasoning in the comment above was wrong on one point: "implementing it twice is waste" overstated the cost. The port ports whatever is in the tree at cut-over, so landing the fix first means the Rust core is ported from fixed code — strictly better than porting the unfixed call graph and re-fixing it there. The memoization is a few lines; the incremental cost of carrying it through the port is ~zero.
And the win goes to current users now rather than at cut-over: ~900ms off dl --purge (the duplicate devpod list at dl.py:1075 / :240) and up to ~2.7s off a dl <ws> attach. Acceptance signal unchanged — devpod spawn counts for dl --ls, dl <ws> (existing, running), and dl --purge -y, before vs after.
Landed in #78. Measured on this machine first: devpod list --output json0.440s, devpod status0.559s, against 0.09s for all of dl's own work — so every removed round-trip is worth more than the entire Python startup this map once considered rewriting away.
Counted twice, two ways (patched subprocess.run filtering on argv[0], then independently with a logging devpod shim on PATH). Both agree:
command
before
after
dl --ls
1
1
dl <ws> existing + running
4
4
dl <ws> -- cmd
4
3
dl --purge -y (2 workspaces)
4
3
dl <ws> existing + stopped
6
6
Wall time via a shim charging the measured 0.45s per call: dl <ws> -- echo hi1.924s -> 1.463s, dl --purge -y1.938s -> 1.463s.
list_workspaces() is memoized per command, not per process, with every mutating path invalidating it. All seven call sites were audited; the win is dl --purge, which read the same list twice back to back — and memoizing also makes the count the user confirms and the set actually deleted provably the same snapshot, which it previously was not. Two safety properties are tested rather than assumed: a failed or unparsable read is never cached, so a transient failure cannot be re-served as "no workspaces"; and a cached empty list cannot mask a missing binary (#57).
The attach chain: one dropped, four kept with reasons
devpod ssh --command sudo hostname is dropped for one-shot commands and kept for interactive, because bash reads the hostname once at shell startup, so it must be set before dl hands over the session, and devpod ssh (v0.26.1) exposes no hook inside it. Folding it in as --command 'sudo hostname X; exec $SHELL -l' would swap devpod's interactive session path for its command path — different PTY, terminfo and window-size handling — which is not provably safe on the path users hit constantly.
Dropping devpod status was considered and rejected: it would mean running devpod up (full devcontainer.json re-parse, seconds) on an already-running workspace, which is exactly what fast-attach exists to avoid.
The durable artifact
test/test_devpod_spawn_counts.py asserts the exact ordered sequence of devpod invocations per command. A future change that reintroduces a redundant call now fails a test instead of quietly costing 450ms — which matters more than the numbers above, since this is the second time these calls have accumulated.
Fog this sharpens
The devpod list for bare-name specs is genuinely replaceable by the devpod status we make anyway (devpod status <unknown> exits 1 with "workspace not found"), taking a running interactive attach from 4 to 3. Not done here: it reroutes validation for every bare-name spec and needed ~25 existing tests updated in files #58 and #64 were editing concurrently. Cheapest follow-up available on this map.
Also noticed: in this repo's own container (runArgs: --network=host) setup_hostname silently fails and the hostname stays the host's — consistent with it being best-effort, but it means the round-trip we keep for interactive attach buys nothing at all in that configuration.
Activity
blooop commentedon Aug 7, 2026
Explicitly deferred to the Rust rewrite effort by #53 (go) — the map's destination admits "or explicitly deferred".
This ticket is where devlaunch's performance actually lives, and #53 measured it:
devpod list --output jsoncosts 0.44/0.47/0.44s, against 0.09s for the entire Python interpreter startup. One devpod round-trip is 5x the cost the language rewrite eliminates — anddevlaunch-corein Rust still pays it, becausedevpodis a Go binary. So:dl --purgemaking the samedevpod listcall twice (dl.py:1075then:240) burns ~900ms.dl <ws>attach chainslist+status+context options+up+ssh --command sudo hostname+ssh≈ ~2.7s of round-trips, one of which is a full extra SSH just to read a hostname.Deferred rather than dropped precisely because it matters: the call-graph design is language-independent and transfers to the core unchanged, so building it against Python first would implement it twice. Carried forward as a requirement on
devlaunch-corewith this ticket's acceptance signal intact — devpod spawn counts fordl --ls,dl <ws>(existing, running), anddl --purge -y, before vs after.blooop commentedon Aug 7, 2026
Deferral reversed — this lands in Python, in a final Python release, before the port.
The deferral reasoning in the comment above was wrong on one point: "implementing it twice is waste" overstated the cost. The port ports whatever is in the tree at cut-over, so landing the fix first means the Rust core is ported from fixed code — strictly better than porting the unfixed call graph and re-fixing it there. The memoization is a few lines; the incremental cost of carrying it through the port is ~zero.
And the win goes to current users now rather than at cut-over: ~900ms off
dl --purge(the duplicatedevpod listatdl.py:1075/:240) and up to ~2.7s off adl <ws>attach. Acceptance signal unchanged — devpod spawn counts fordl --ls,dl <ws>(existing, running), anddl --purge -y, before vs after.Stop paying for the same devpod answer twice
Stop paying for the same devpod answer twice
blooop commentedon Aug 7, 2026
Answer
Landed in #78. Measured on this machine first:
devpod list --output json0.440s,devpod status0.559s, against 0.09s for all of dl's own work — so every removed round-trip is worth more than the entire Python startup this map once considered rewriting away.Counted twice, two ways (patched
subprocess.runfiltering on argv[0], then independently with a loggingdevpodshim on PATH). Both agree:dl --lsdl <ws>existing + runningdl <ws> -- cmddl --purge -y(2 workspaces)dl <ws>existing + stoppedWall time via a shim charging the measured 0.45s per call:
dl <ws> -- echo hi1.924s -> 1.463s,dl --purge -y1.938s -> 1.463s.list_workspaces()is memoized per command, not per process, with every mutating path invalidating it. All seven call sites were audited; the win isdl --purge, which read the same list twice back to back — and memoizing also makes the count the user confirms and the set actually deleted provably the same snapshot, which it previously was not. Two safety properties are tested rather than assumed: a failed or unparsable read is never cached, so a transient failure cannot be re-served as "no workspaces"; and a cached empty list cannot mask a missing binary (#57).The attach chain: one dropped, four kept with reasons
devpod ssh --command sudo hostnameis dropped for one-shot commands and kept for interactive, because bash reads the hostname once at shell startup, so it must be set before dl hands over the session, anddevpod ssh(v0.26.1) exposes no hook inside it. Folding it in as--command 'sudo hostname X; exec $SHELL -l'would swap devpod's interactive session path for its command path — different PTY, terminfo and window-size handling — which is not provably safe on the path users hit constantly.Dropping
devpod statuswas considered and rejected: it would mean runningdevpod up(full devcontainer.json re-parse, seconds) on an already-running workspace, which is exactly what fast-attach exists to avoid.The durable artifact
test/test_devpod_spawn_counts.pyasserts the exact ordered sequence of devpod invocations per command. A future change that reintroduces a redundant call now fails a test instead of quietly costing 450ms — which matters more than the numbers above, since this is the second time these calls have accumulated.Fog this sharpens
The
devpod listfor bare-name specs is genuinely replaceable by thedevpod statuswe make anyway (devpod status <unknown>exits 1 with "workspace not found"), taking a running interactive attach from 4 to 3. Not done here: it reroutes validation for every bare-name spec and needed ~25 existing tests updated in files #58 and #64 were editing concurrently. Cheapest follow-up available on this map.Also noticed: in this repo's own container (
runArgs: --network=host)setup_hostnamesilently fails and the hostname stays the host's — consistent with it being best-effort, but it means the round-trip we keep for interactive attach buys nothing at all in that configuration.