Stop paying for the same devpod answer twice - #78
Merged
Merged
Conversation
Reviewer's GuideIntroduces a memoized cache for Sequence diagram for updated attach_workspace behaviorsequenceDiagram
actor User
participant dl_main as main/_run_cli
participant attach_workspace
participant setup_hostname
participant workspace_ssh
User->>dl_main: invoke dl with workspace spec
dl_main->>attach_workspace: attach_workspace(workspace_id, shell_command)
alt [interactive attach (shell_command is None)]
attach_workspace->>setup_hostname: setup_hostname(workspace_id)
setup_hostname-->>attach_workspace: hostname set
attach_workspace->>workspace_ssh: workspace_ssh(workspace_id, None)
workspace_ssh-->>attach_workspace: return code
else [one_shot command (shell_command is not None)]
attach_workspace->>workspace_ssh: workspace_ssh(workspace_id, shell_command)
workspace_ssh-->>attach_workspace: return code
end
attach_workspace-->>dl_main: return code
dl_main-->>User: exit status (hostname only set for interactive)
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
devpod is where dl's runtime goes. On this machine `devpod list --output json` takes 0.44s and `devpod status <ws> --output json` 0.56s, against 0.09s for the whole Python interpreter dl runs in. Removing one round-trip is worth five times whatever the language costs, so the only thing that makes dl faster is asking devpod fewer questions. `dl --purge` asked the same question twice: once to print the workspace count it wants confirmed, once again inside purge_all_data. list_workspaces now memoizes for the length of one command, so both reads share a snapshot -- and the count the user answered can no longer disagree with the set that actually gets deleted. Six call sites read that list; whichever of them runs first now pays for it and the rest are free. Memoizing a value that describes mutable state needs a story for staleness, and dl's is that it is the mutator. Every path that changes what devpod would list -- up, stop, delete, and purge's delete loop -- drops the snapshot, main() drops it at the start of each command so nothing driving main() twice inherits the first command's view, and refresh=True bypasses it for a caller that needs post-mutation truth regardless. A read that failed is never remembered: only an answer devpod actually gave is cached, so a transient failure cannot be served again as "this machine has no workspaces", and a missing devpod still raises out of the first read. The attach chain was audited call by call and almost all of it is load-bearing. `devpod list` is what validates the spec and decides whether the workspace already exists; `devpod status` is what makes fast-attach possible at all, since skipping it would mean running the far slower `up` on an already-running workspace. Both stay. The hostname round-trip -- a whole extra `devpod ssh` in front of every attach -- cannot be folded into the attach either: bash reads the hostname once when the shell starts, so it has to be set before the session dl hands over, and `devpod ssh` offers no hook inside that session. It is now skipped for a one-shot `dl <ws> -- cmd`, which renders no prompt for a hostname to appear in, taking that path from four devpod spawns to three. Its value and derivation are untouched. devpod spawns per command, before -> after: `dl --ls` 1 -> 1, `dl <ws>` on a running workspace 4 -> 4, `dl <ws> -- cmd` 4 -> 3, `dl --purge -y` with two workspaces 4 -> 3. Measured at the subprocess boundary, and again end to end against a devpod shim on PATH that charges 0.45s a call: purge 1.94s -> 1.46s, one-shot command 1.92s -> 1.46s. The counts are pinned by tests that assert the exact sequence of devpod invocations, so a change that reintroduces a redundant round-trip fails a test rather than quietly costing half a second. Closes #60
blooop
force-pushed
the
fix/60-cut-devpod-shellouts
branch
from
August 7, 2026 21:48
6f5a28a to
22afd52
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #78 +/- ##
==========================================
+ Coverage 88.64% 88.81% +0.17%
==========================================
Files 11 11
Lines 1559 1574 +15
==========================================
+ Hits 1382 1398 +16
+ Misses 177 176 -1
🚀 New features to boost your workflow:
|
This was referenced Aug 7, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #60 on map #51.
devpod list --output jsoncosts 0.440s on this machine anddevpod 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.Spawn counts — counted twice, two ways
Patched
subprocess.run(argv[0] ==devpod), then independently re-counted end-to-end with a loggingdevpodshim onPATH. Both methods agree.dl --lslistdl <ws>existing + runninglist,status,sshhostname,sshdl <ws> -- cmdlist,status,ssh --commanddl --purge -y(2 workspaces)list,delete,deletedl <ws>existing + stoppedlist,status,context options,up,sshhostname,sshWall time through a shim charging the measured 0.45s per call, in an
env -isandbox:dl <ws> -- echo hi1.924s -> 1.463s,dl --purge -y1.938s -> 1.463s.What changed
list_workspaces()is memoized per command, not per process —main()invalidates at the start of every command, andworkspace_up,workspace_stop,workspace_deleteand purge's delete loop each invalidate after mutating.refresh=Trueis the documented bypass.All seven call sites were audited; none needs a fresh read today, because no site reads the list after a mutation in the same process. 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, not 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 —
dl --lswith no devpod still exits 127 with one stderr line (#57).The attach chain: one call dropped, four kept
devpod ssh --command sudo hostnameis dropped for one-shot commands, kept for interactive. Keeping it interactively is load-bearing: bash resolves\hfrom the hostname read at shell startup, so it must be set before dl hands over the session, anddevpod ssh(v0.26.1) exposes no hook inside that session. 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.devpod list,status,context options,upand the finalsshare all kept with reasons. Droppingstatusin particular would mean runningdevpod up(full devcontainer.json re-parse, seconds) on an already-running workspace — exactly what fast-attach exists to avoid.Suspected but left alone
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 would need ~25 existing tests updated, in files that #58 and #64 are editing right now. Cheapest available follow-up.The durable artifact
New
test/test_devpod_spawn_counts.py(20 tests) asserts the exact ordered sequence of devpod invocations per command, so a future change that reintroduces a redundant call fails a test rather than quietly costing 450ms. A conftest fixture stops the snapshot leaking between tests.Verification
534 passed, 8 deselected after rebasing onto current main.
ruff,ruff format,pylint10.00/10,tyclean. Red-before-green verified with 13 revert-one-thing checks — one of which initially reported a false green from stale bytecode (two reverts leftdl.pythe same size in the same second), so the harness now clears__pycache__.Only user-visible change:
dl <ws> -- cmdno longer sets the container hostname, since nothing in a one-shot command displays it.Incidental finding, not acted on: 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.Summary by Sourcery
Memoize devpod workspace listing per command and adjust attach behaviour to avoid redundant devpod calls, improving performance and preserving correctness of workspace operations.
Enhancements:
Documentation:
Tests: