Skip to content

Cut Windows wrapper overhead for read-only and small commands#809

Open
svarlamov wants to merge 15 commits intomainfrom
codex/windows-readonly-wrapper-fast-path
Open

Cut Windows wrapper overhead for read-only and small commands#809
svarlamov wants to merge 15 commits intomainfrom
codex/windows-readonly-wrapper-fast-path

Conversation

@svarlamov
Copy link
Copy Markdown
Member

@svarlamov svarlamov commented Mar 26, 2026

Summary

  • Fast-path unconditionally read-only commands (status, log, diff, show, rev-parse, ls-files, blame, grep, help, version, etc.) and help/version invocations so they skip all hooks and daemon state
  • Resolve git aliases before the wrapper's passthrough/read-only decision so aliases like st = status --short use the fast path and aliases to mutating commands do not
  • Alias resolution now happens once, early, before the async/sync branch point — this also fixes post-commit stats not showing when committing via alias (e.g. alias.cm = commit), subsuming Fix post-commit stats not showing with git aliases #801
  • Switch common-case wrapper repository discovery from extra git rev-parse subprocesses to the existing filesystem-based fast path
  • Suppress trace2 for git-ai's own internal helper git subprocesses so global trace2 config does not add Windows pipe overhead to wrapper internals
  • Preserve the original global git args whenever rebasing them to the repo root could change semantics, such as relative --git-dir / --work-tree
  • init passthroughs unconditionally (no post-hooks); clone passthroughs only in async mode so post_clone_hook still runs in non-async mode
  • The read-only classification is intentionally narrow: only commands in is_definitely_read_only_command (unconditionally read-only regardless of args) get the fast path. Mixed-mode commands like branch, tag, stash, remote, config are NOT fast-pathed — they always go through the full wrapper. A broader is_read_only_invocation classifier exists in command_classification.rs with per-command arg inspection but is not wired into the critical dispatch path to keep risk low.

Root cause

The Windows slowdown was mostly wrapper preflight overhead, not the proxied git command itself.

Before these changes, a wrapper invocation could do several expensive things before the real git command even ran:

  • spawn multiple internal git subprocesses for repository discovery
  • spawn additional git subprocesses for repo-policy checks
  • emit trace2 from those internal helper git subprocesses when trace2 was globally configured
  • pay extra wrapper startup work even for simple passthrough-style invocations
  • miss read-only fast paths for aliases because alias expansion happened later than the passthrough decision

That architecture is especially expensive on Windows, where each extra child process has noticeable startup cost.

What changed

This PR attacks the problem in three narrow ways:

  1. Read-only passthrough classification — commands that are unconditionally read-only (the is_definitely_read_only_command list), plus --help and no-command invocations, skip all hooks and daemon state. The list only includes commands that are read-only regardless of their arguments. Mixed-mode commands (branch, tag, stash, remote, config, etc.) are not included.

  2. Alias-aware passthrough — wrapper alias expansion now happens once before the async/sync branch point and the passthrough/read-only decision. Read-only aliases (e.g. alias.st = status --short) get the fast path; mutating aliases still go through the full wrapper path. This also fixes the bug where commit aliases (e.g. alias.cm = commit) didn't trigger post-commit stats display in async mode.

  3. Shared wrapper preflightfind_repository() now uses the existing filesystem-based discovery fast path for common global-arg shapes instead of immediately shelling out to git rev-parse. Falls back to git-exec discovery when GIT_DIR, GIT_WORK_TREE, or GIT_CEILING_DIRECTORIES env vars are set. Internal helper git subprocesses now force all trace2 streams off. Repo-root normalization preserves original args when they contain flags that aren't safe to rebase (e.g. relative --git-dir/--work-tree).

Benchmarks

All numbers below are from the same Windows machine/repo style, using stopwatch timing around the wrapper binary.

git status (forced sync wrapper path)

  • pre-fix wrapper: 1043ms, 1556ms, 1090ms, 1123ms, 1049ms (avg 1172ms)
  • patched wrapper: 244ms, 161ms, 146ms, 151ms, 135ms (avg 167ms)

git branch --show-current

Pre-broader-fix wrapper warm runs:

  • 407ms, 402ms, 386ms, 313ms

Patched wrapper warm runs:

  • 110ms, 112ms, 149ms, 103ms, 133ms, 141ms, 140ms, 120ms, 231ms, 166ms

Plain git reference:

  • 103ms, 190ms, 133ms, 279ms, 103ms

git branch <name> HEAD

Pre-broader-fix wrapper:

  • 346ms, 368ms, 411ms, 415ms, 387ms

Patched wrapper warm runs:

  • 206ms, 276ms, 186ms, 194ms, 136ms, 166ms, 189ms, 163ms, 165ms, 138ms

Plain git reference:

  • 196ms, 140ms, 115ms, 113ms, 167ms

Additional passthrough cases

Warm wrapper runs after the latest startup/pass-through changes:

  • git --version: 116ms, 163ms, 153ms, 126ms
  • git remote -v: 150ms, 167ms, 170ms, 201ms, 229ms
  • git config --list --show-origin: 216ms, 376ms, 275ms, 232ms, 253ms

Plain git reference:

  • git --version: 82ms, 102ms, 94ms, 115ms
  • git remote -v: 123ms, 165ms, 175ms, 165ms
  • git config --list --show-origin: 166ms, 244ms, 95ms, 136ms, 147ms

Notes

This does not produce a literal 10x win for every command. After these changes, the remaining floor is mostly actual git work plus Windows process startup for launching the wrapper binary itself. Hitting another step-function improvement across all commands will likely require a bigger architectural move, like a thinner launcher or persistent proxy process.

Updates since last revision

  • Reverted Clap removal from main.rs — the startup trim wasn't worth changing help/usage behavior
  • Ported alias commit test from Fix post-commit stats not showing with git aliases #801 (async_mode_post_commit_shows_stats_with_commit_alias) — this PR's early alias resolution subsumes Fix post-commit stats not showing with git aliases #801 entirely
  • Fixed clippy match_like_matches_macro warning in is_read_only_notes_invocation
  • Simplified wrapper dispatch — inlined should_passthrough_read_only_command() and resolve_wrapper_invocation() which were single-line wrappers; alias resolution and read-only checks are now inline in handle_git()
  • Consolidated normalize_global_args_for_repo_root — merged the separate global_args_are_safe_to_rebase() guard into the normalization loop itself (single pass: normalize -C args and bail if an unsafe flag is encountered). DRY'd resolve_command_base_dir to reuse apply_global_c_arg.
  • Fixed clone passthrough bypassing post_clone_hook — the clone/init early-exit was unconditional, which made post_clone_hook unreachable in non-async mode. Now init passthroughs unconditionally (it has no post-hooks) and clone passthroughs only inside the async_mode block so the non-async clone handler with post_clone_hook is still reached.
  • Narrowed read-only fast path to is_definitely_read_only_command only — removed is_read_only_invocation (the broader arg-inspecting classifier) from the wrapper dispatch path. The fast path now only catches commands that are unconditionally read-only by name. Mixed-mode commands (branch, tag, config, stash, etc.) always go through the full wrapper. This trades a small amount of fast-path coverage for significantly less risk and complexity in the critical dispatch path.

Validation

  • cargo test --package git-ai passthrough_read_only_command -- --nocapture
  • cargo test --package git-ai wrapper_resolves_ -- --nocapture
  • cargo test --package git-ai resolve_fast_discovery_base_dir -- --nocapture
  • cargo test --package git-ai normalize_global_args_for_repo_root -- --nocapture
  • cargo test --package git-ai find_repository_preserves_relative_git_dir_and_work_tree_for_internal_commands -- --nocapture
  • cargo test --package git-ai find_repository_in_path_supports_bare_repositories -- --nocapture
  • cargo test --package git-ai find_repository_in_path_worktree_uses_common_dir_for_isolated_storage -- --nocapture
  • cargo test --package git-ai test_empty_allowlist_allows_everything -- --nocapture
  • cargo test --package git-ai --test async_mode async_mode_post_commit_shows_stats_with_commit_alias -- --nocapture
  • cargo test --package git-ai --test notes_sync_regression notes_sync_clone_fetches_authorship_notes_from_origin -- --nocapture
  • cargo clippy --package git-ai -- -D warnings
  • cargo build --release

Review & Testing Checklist for Human

This PR touches the wrapper's main dispatch path — every git invocation goes through it. 4 items to verify:

  • Clone passthrough split correctness: init exits unconditionally, clone exits only in async mode. Verify git clone <url> in non-async mode still runs post_clone_hook (fetches authorship notes from origin). Verify async-mode clone still skips daemon telemetry. The non-async clone handler also checks skip_hooks, which the async early-exit does not — confirm this difference is acceptable.
  • Alias resolution for commit in async mode: Run git config alias.cm commit, then git cm -m "test" with AI content and verify post-commit stats appear. The ported test covers this but manual verification on a real repo is worthwhile.
  • Windows performance: Re-run benchmarks on a Windows machine to confirm the numbers hold with the latest simplification changes (Clap revert, narrower read-only classification).
  • normalize_global_args_for_repo_root single-pass safety: The function normalizes and bails mid-loop if an unsafe flag is encountered, returning the original args. Verify that bailing mid-loop is equivalent to the previous two-pass behavior in all edge cases — particularly when -C args appear before and after an unsafe flag.

Notes

  • This PR subsumes Fix post-commit stats not showing with git aliases #801 — that PR has been closed.
  • proxy_to_git still re-parses args for trace2 suppression (minor redundancy, not a correctness issue).
  • The broader is_read_only_invocation classifier and its per-command sub-classifiers (is_read_only_branch_invocation, is_read_only_tag_invocation, etc.) remain in command_classification.rs as tested library code but are not used in the critical wrapper dispatch path. They can be wired in later if desired after more validation.

Link to Devin session: https://app.devin.ai/sessions/7bcaf7c2296b4a4699df61a164ed5986
Requested by: @svarlamov

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

@svarlamov svarlamov changed the title Speed up read-only wrapper commands on Windows Cut Windows wrapper overhead for read-only and small commands Mar 26, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

svarlamov added a commit that referenced this pull request Mar 27, 2026
1. try_find_repository_no_git_exec: catch construction errors from
   repository_from_discovered_paths and return Ok(None) so the git-exec
   fallback in find_repository still runs. Previously, errors like
   canonicalize failures or bad core.worktree config would propagate
   and silently skip hooks.

2. is_read_only_branch_invocation: split list-mode triggers from
   list-output modifiers (-v, --verbose, --no-color, --no-column,
   --ignore-case). Only triggers should classify as read-only when
   positional args are present. `git branch -v feature` creates a
   branch — it is NOT read-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

svarlamov added a commit that referenced this pull request Mar 27, 2026
When GIT_DIR, GIT_WORK_TREE, or GIT_CEILING_DIRECTORIES environment
variables are set, the filesystem-based fast discovery path may find a
different repository than git's own discovery logic. Fall back to the
git-exec path (git rev-parse) which honours these env vars.

Addresses Devin review feedback on PR #809.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
svarlamov added a commit that referenced this pull request Mar 27, 2026
When GIT_DIR, GIT_WORK_TREE, or GIT_CEILING_DIRECTORIES environment
variables are set, the filesystem-based fast discovery path may find a
different repository than git's own discovery logic. Fall back to the
git-exec path (git rev-parse) which honours these env vars.

Addresses Devin review feedback on PR #809.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
svarlamov and others added 7 commits March 27, 2026 16:01
1. try_find_repository_no_git_exec: catch construction errors from
   repository_from_discovered_paths and return Ok(None) so the git-exec
   fallback in find_repository still runs. Previously, errors like
   canonicalize failures or bad core.worktree config would propagate
   and silently skip hooks.

2. is_read_only_branch_invocation: split list-mode triggers from
   list-output modifiers (-v, --verbose, --no-color, --no-column,
   --ignore-case). Only triggers should classify as read-only when
   positional args are present. `git branch -v feature` creates a
   branch — it is NOT read-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
svarlamov added a commit that referenced this pull request Mar 27, 2026
When GIT_DIR, GIT_WORK_TREE, or GIT_CEILING_DIRECTORIES environment
variables are set, the filesystem-based fast discovery path may find a
different repository than git's own discovery logic. Fall back to the
git-exec path (git rev-parse) which honours these env vars.

Addresses Devin review feedback on PR #809.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@svarlamov svarlamov force-pushed the codex/windows-readonly-wrapper-fast-path branch from c04e950 to d6ae99e Compare March 27, 2026 16:05
svarlamov and others added 2 commits March 27, 2026 16:11
Add high-volume read-only commands that IDEs (VS Code, JetBrains),
git clients (GitLens, Graphite CLI), and other tools call frequently:

Unconditionally read-only (added to is_definitely_read_only_command):
  ls-remote, show-ref, cherry, show-branch, whatchanged, verify-pack,
  check-ref-format, fsck, column, fmt-merge-msg, get-tar-commit-id,
  patch-id, stripspace

Subcommand classifiers (added to is_read_only_invocation):
  symbolic-ref: read when 1 positional, write when 2+, reject -d/--delete
  reflog: deny-list (only expire/delete mutate); catches implicit show
    with refs/flags like `git reflog HEAD`, `git reflog --all`
  notes: allow-list (list/show/get-ref); must be conservative because
    --ref flag can precede the subcommand

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When GIT_DIR, GIT_WORK_TREE, or GIT_CEILING_DIRECTORIES environment
variables are set, the filesystem-based fast discovery path may find a
different repository than git's own discovery logic. Fall back to the
git-exec path (git rev-parse) which honours these env vars.

Addresses Devin review feedback on PR #809.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@svarlamov svarlamov force-pushed the codex/windows-readonly-wrapper-fast-path branch from d6ae99e to 5d8e67a Compare March 27, 2026 16:12
- Revert Clap removal from main.rs to preserve help/usage behavior
- Restore is_repo_creating early-exit for clone/init in async mode that
  was dropped during the refactor, preventing the daemon from receiving
  misleading wrapper state for repo-creating commands
- Port async_mode_post_commit_shows_stats_with_commit_alias test from
  PR #801 which is now subsumed by this PR's alias resolution changes
- Fix clippy match_like_matches_macro warning in is_read_only_notes_invocation

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ svarlamov
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration bot and others added 3 commits March 27, 2026 22:12
…fe_to_rebase into normalize_global_args_for_repo_root, DRY resolve_command_base_dir

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
… non-async mode

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 21 additional findings in Devin Review.

Open in Devin Review

Comment on lines +147 to +150
command_args_contain_any(&parsed.command_args, &list_mode_triggers)
|| parsed.pos_command(0).is_none()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 pos_command ignores -- separator, causing false read-only classification for dash-prefixed branch/tag names

The is_read_only_branch_invocation and is_read_only_tag_invocation classifiers use parsed.pos_command(0).is_none() to distinguish list mode (read-only) from creation mode (mutating). However, pos_command at src/git/cli_parser.rs:60-96 does not handle the -- end-of-options separator — it treats -- and any subsequent dash-prefixed arguments as flags to skip rather than positional arguments. This means git branch -- -v (which creates a branch named "-v") is incorrectly classified as read-only and bypasses all git-ai hooks, losing attribution tracking for that commit.

Example trace for git branch -- -v

command_args = ["--", "-v"]

  • command_args_contain_any(mutating_flags): no match
  • command_args_contain_any(list_mode_triggers): no match
  • pos_command(0): "--" starts with '-' → skip; "-v" starts with '-' → skip; returns None
  • Result: read-only (WRONG — this creates branch "-v")
Prompt for agents
The pos_command() method in src/git/cli_parser.rs (lines 60-96) needs to handle the -- end-of-options separator. After encountering --, all remaining arguments should be treated as positional regardless of whether they start with -. Add a saw_separator boolean that flips to true when -- is encountered, and when true, treat every subsequent argument as positional. This will fix the false read-only classification in is_read_only_branch_invocation and is_read_only_tag_invocation for commands like git branch -- -v or git tag -- -v1.0.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…_invocation in wrapper dispatch

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
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