jeffparsons / jeffparsons/git-full-send

receive is one opaque duration: split its phases so slow pushes explain themselves

Open
#91 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

`sync` and `update-worktree` both decompose their time now (#75, #76): `update_worktree` reports `read_tree.{load_index,resolve_tree,apply,write_index}_ms` alongside index state and paths written, and `sync` reports its `encode_phases`. `receive` is the exception — the record carries `duration_ms`, `inbound{}` and `outbound{}` and nothing else — and it is now the largest unexplained cost in the Stile `dev` integration.

Two weeks of real usage, client on a MacBook and server an aarch64 EC2 workstation over a Tailscale SSH tunnel (34.6k-file monorepo worktree, 2.5 GB object store, 29,327 refs, `--repo` == `--worktree`), taken from the two `metrics.jsonl` sinks. After #89 collapsed the advertisement and a host-side fix stopped ctime churn, `update-worktree` sits at ~260 ms and the per-`dev`-command cost is ~1.8 s p50 — of which the push connections are the biggest slice, and the only one that can't be attributed.

Steady state, 3–7 KB of pack: `receive` takes 320–350 ms per connection, and the client's `push_ms` is within ~50 ms of it, so the time is on the server. Two pushes per sync (code + extra) makes that ~0.7 s of the ~1.0 s floor. Nothing in the record says what those milliseconds are.

Pushes carrying real payload are worse, and not in proportion to their size:

| pack bytes | `receive` duration | implied MB/s |
| ---: | ---: | ---: |
| 468,135 | 758 ms | 0.62 |
| 1,189,511 | 4,746 ms | 0.25 |
| 1,830,408 | 4,765 ms | 0.38 |
| 2,220,286 | 3,904 ms | 0.57 |
| 14,015,283 | 4,286 ms | 3.27 |

The 14 MB push is the *fastest* per byte, so this isn't link throughput; something about the 1–2 MB shape costs ~4.7 s. Plausible causes are all server-side and all knowable where the number is produced — `unpack-objects` vs `index-pack` either side of `transfer.unpackLimit` (100 objects) and per-loose-object fsync, delta resolution against the large object store, ref update and its fsync — but from the outside they're indistinguishable, which is exactly the situation #75–#78 set out to end. In practice this is the case a developer feels: the first sync after switching branches or rebasing.

Suggested decomposition, mirroring what `update_worktree` already does:

- auth preamble read, `receive-pack` spawn, inbound streaming, wait-for-exit, and the report/ref-update tail as separate durations
- object-level accounting to go with the byte counts: objects received, and whether `receive-pack` kept a pack or exploded it to loose objects

Per ADR-0013's constraint, anything expensive to obtain should sit behind a flag and be documented as such — though most of the above is timing around work that already happens.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at the receive entry point and trace the receive-pack lifecycle, using the existing metrics.jsonl sinks and the decomposition patterns from #75–#78 as references. Read ADR-0013 before deciding which measurements require a flag. Done means receive reports the requested phase timings and object-level accounting, including pack-versus-loose handling, with the expensive measurements documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
git, rust
Domain
devtools, observability
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.