Display rolling-window throughput while a side is actively transferring · Entire
Display rolling-window throughput while a side is actively transferring
The displayed rate was averaged over the side's whole active window (stats start → last byte), which dilutes the streaming portion with the auth + ref-listing setup time before bytes start flowing. For a sync that spends ~3s on setup then streams a pack at 44 MB/s for 10s, the headline reads ~33 MB/s and only slowly drifts upward — much lower than what the user's network monitor reports.
Take a sample on every render tick and keep the last sampleCapacity (10 → ~2s at the default 200ms interval) per side. Compute the displayed rate from the oldest-vs-newest delta within that window, so the number tracks recent wire throughput instead of a session-wide average. Once a side goes idle (no bytes for >idleThreshold) or on the final frame, fall back to the active-window average — a stable post-transfer headline that won't jump around as the ring drains.
The end-of-run "throughput:" line still uses the active-window average (formatSide called with instantRate=0), which is the right summary for a completed sync.
Sessions
Changes
3
internal/syncer
Mprogress.go+106/-17
Mprogress_test.go+97/-3
Msyncer.go+4/-1
// cursor-up + erase-to-end-of-screen to overwrite the whole region in
// place, so '\r'-terminated sideband updates from go-git read as a
// single updating row instead of scrolling line by line.
//
// On every render we also push the current per-side byte total into a
// short ring buffer (samples) so the displayed rate reflects recent
// throughput rather than a session-wide average — the latter
// undercounts the actual transfer rate because the divisor includes
// auth and ref-listing time when no pack data is flowing.
type progressReporter struct {
out io.Writer
stats *statsCollector
done chan struct{}
mu sync.Mutex
rowsDrawn int // rows currently occupying the live region
lastLine string // last progress line, kept so setTransient can redraw without re-sampling
transient string // current sideband-progress line, "" when none
samples map[string]*sampleRing // per-side sliding window of recent (time, bytes) snapshots
}
func newProgressReporter(out io.Writer, stats *statsCollector, interval time.Duration) *progressReporter { return &progressReporter{ out: out, stats: stats, done: make(chan struct{}), samples: map[string]*sampleRing{}, } }
// formatSide renders a single side as host + bytes + rate, with a flow // arrow positioned to indicate direction: source on the left of its // counter, target on the right of its counter. Sides with neither label // fall back to "name: bytes @ rate". func formatSide(side SideBytes, fallbackDur time.Duration, instantBytesPerSec float64, forceDone bool) string { name := side.Display if name == "" { name = side.Label } name = truncateHost(name, maxHostnameWidth)
rateDur := fallbackDur if side.ActiveNanos > 0 { rateDur = time.Duration(side.ActiveNanos) } rate := formatBytes(side.Bytes) + " @ " + formatRate(side.Bytes, rateDur)
done := side.Bytes > 0 && (forceDone || time.Duration(side.IdleNanos) >= idleThreshold)
var rateText string if !done && instantBytesPerSec > 0 { rateText = formatBytes(int64(instantBytesPerSec)) + "/s" } else { rateDur := fallbackDur if side.ActiveNanos > 0 { rateDur = time.Duration(side.ActiveNanos) } rateText = formatRate(side.Bytes, rateDur) }
rate := formatBytes(side.Bytes) + " @ " + rateText if done { rate += doneMark }
return rate }