Skip to content

perf: parallelize and reduce jj/git status subprocess calls - #109

Merged
hugoh merged 6 commits into
mainfrom
perf-parallel-status
Aug 3, 2026
Merged

perf: parallelize and reduce jj/git status subprocess calls#109
hugoh merged 6 commits into
mainfrom
perf-parallel-status

Conversation

@hugoh

@hugoh hugoh commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • jj backend: collapse the up-to-10-call ancestor-walk (looking for the nearest described commit) into a single revset query.
  • jj backend: run independent Status subprocess calls (working-copy/head-bookmark lookups, then bookmark-list/local-ahead counts) concurrently via errgroup instead of sequentially.
  • git backend: merge the separate commit-message and commit-time git log calls into one, split on git's %x00 NUL-byte escape.
  • git backend: run git status and the remote lookup concurrently (adds an injectable runGitFn seam mirroring the jj backend's runJJFn, for deterministic concurrency tests).
  • copier update from go-tools template v1.16.9 → v1.18.0, picking up commit_types.perf.bump_patch = true in cog.toml (feat(template): treat perf commits as patch-bump-worthy go-tools#62) so this perf-only PR actually triggers a patch release instead of landing on main unreleased. Also brings unrelated upstream template drift (pinned workflow SHA bumps, a renovate config refactor).
  • internal/backend: bound RunCommand's output-pipe drain with cmd.WaitDelay. A subprocess that backgrounds a grandchild (e.g. a shell running cmd &) can exit itself while the grandchild keeps the inherited stdout/stderr pipe open; cmd.Wait() then blocks draining that pipe for the grandchild's full lifetime regardless of context cancellation — a documented os/exec limitation. This is what made CI intermittently take ~60s in TestRunCommand_NonInteractiveTimesOut instead of 50ms.

Test plan

  • go test ./... -race
  • hk check --all
  • New/updated tests assert correctness is preserved and, where feasible, that calls are actually parallelized (timing-based tests against mocked runJJFn/runGitFn, avoiding real-subprocess timing flakiness)
  • New regression test reproduces the orphaned-grandchild pipe hang directly (sh -c "sleep 3 &") and asserts RunCommand returns well under the orphan's lifetime
  • cog bump --auto --dry-run locally confirms a patch bump (v1.2.2) is now proposed for this commit range

🤖 Generated with Claude Code

hugoh added 6 commits August 3, 2026 18:09
fillCommitMsgFromAncestors used to spawn up to 10 sequential jj log
subprocesses, one per generation, looking for the nearest ancestor
with a description. heads(ancestors(@, 11) & description(glob:"?*"))
finds the same commit in one call.
Status previously ran the working-copy query, head-bookmark lookup,
bookmark-list query, and local-ahead count strictly sequentially even
though only the second pair depends on the first. Split into
fetchWorkingCopyAndHead and fillBookmarkTracking, each running its
independent jj calls concurrently via errgroup — cuts the common-case
critical path from 4 sequential subprocess round trips to 2.
Status ran `git show-branch --no-name HEAD` and `git log -1
--format=%cd` as two separate subprocesses for two fields of the same
commit. `git log -1 --format=%s%x00%cd` gets both in one call, split
on git's own %x00 NUL-byte escape so a subject line can't collide
with the separator.
git status and git remote don't depend on each other's output but ran
sequentially. Backend gains an injectable runGitFn seam (mirroring
the jj backend's runJJFn) so Status can dispatch both via errgroup
and tests can verify concurrency without real subprocess timing
noise.
Pulls in commit_types.perf.bump_patch = true in cog.toml so perf-only
PRs (like the preceding backend perf work) trigger an automatic patch
release instead of silently landing on main unreleased. Also picks up
unrelated upstream template drift: pinned-SHA bumps in
.github/workflows/ci.yml and a renovate config refactor
(.renovaterc.json now extends go-tools' copier-owned-files preset
instead of inlining the same two packageRules).
RunCommand's non-interactive timeout only kills the direct child on
ctx cancellation. A subprocess that backgrounds a grandchild (e.g. a
shell running "cmd &") can exit itself while that grandchild keeps
the inherited stdout/stderr pipe open — cmd.Wait() then blocks
draining the pipe for the grandchild's full lifetime regardless of
ctx cancellation, a documented os/exec limitation. This is what made
CI's TestRunCommand_NonInteractiveTimesOut occasionally take ~60s
instead of 50ms. Setting cmd.WaitDelay to the same timeout bounds
that drain the same way ctx bounds the process itself.
@hugoh
hugoh merged commit 37dd6d7 into main Aug 3, 2026
5 checks passed
@hugoh
hugoh deleted the perf-parallel-status branch August 3, 2026 23: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.

1 participant