Skip to content

Build: cut redundant devpod shell-outs on hot paths #60

Closed

Description

@blooop

Question

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.

Activity

  1. blooop commented on Aug 7, 2026

    @blooop
    OwnerAuthor

    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.

  2. blooop commented on Aug 7, 2026

    @blooop
    OwnerAuthor

    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.

  3. reopened this on Aug 7, 2026
  4. added 2 commits that reference this issue on Aug 7, 2026
    6f5a28a
    22afd52
  5. blooop commented on Aug 7, 2026

    @blooop
    OwnerAuthor

    Answer

    Landed in #78. Measured on this machine first: devpod list --output json 0.440s, devpod status 0.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 hi 1.924s -> 1.463s, dl --purge -y 1.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions