Freeze per-side rate once a transfer goes idle · Entire

Freeze per-side rate once a transfer goes idle

3197193→main· Soph·2mo ago·5 files·+129 added/-18 removed

Previously the rate displayed in the --progress ticker and the end-of-run throughput line was computed as bytes / (now − start), which kept shrinking once bytes stopped flowing — a sync that streamed 11 MB at 367 KB/s would tick down to 366, 365, 364 ... while the post-transfer phases (planning, ref creation) ran. Misleading.

Track per-side lastByteAt on every non-empty Read and surface two new fields on each SideBytes snapshot: ActiveNanos (start → last byte) and IdleNanos (last byte → snapshot time). Renderers compute the rate from ActiveNanos so it freezes at the actual transfer rate, and append a "✓" marker once IdleNanos crosses 750ms (or unconditionally on the final render) so it's visually obvious which sides have finished.

Sessions

8416b4de95ddView transcript

Changes

5

85 unmodified lines

const ( sideSeparator = " │ " flowArrow = " → " doneMark = " ✓" maxHostnameWidth = 30 idleThreshold = 750 * time.Millisecond )

// 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, dur time.Duration) string { // // The displayed rate is computed against the side's active window // (start → last byte) when known, so once a transfer ends the number // freezes at the actual transfer rate instead of decaying as the wall // clock keeps ticking. forceDone (set on the final render) and a // per-side idle gap >idleThreshold append a "✓" marker. }

func formatSide(side SideBytes, fallbackDur time.Duration, forceDone bool) string { name := side.Display if name == "" { name = side.Label } name = truncateHost(name, maxHostnameWidth) rate := formatBytes(side.Bytes) + " @ " + formatRate(side.Bytes, dur)

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) if done { rate += doneMark }

switch side.Label { case "source": return name + flowArrow + rate } }

// Display carries the hostname extracted from the endpoint URL so live // renderers can show "github.com → … → host" without re-parsing the URL. // Empty when the endpoint URL was not http(s) or failed to parse. // // ActiveNanos and IdleNanos let renderers freeze the per-side rate once // a transfer finishes: ActiveNanos spans from stats start to the most // recent byte read, so dividing Bytes by ActiveNanos yields the rate // during active streaming rather than a value that decays as wall clock // keeps advancing past the last byte. IdleNanos is the gap between the // last byte and the snapshot, used to mark a side as "done". type SideBytes struct { Label string json:"label" Bytes int64 json:"bytes" Display string json:"display,omitempty" ActiveNanos int64 json:"activeNanos,omitempty" IdleNanos int64 json:"idleNanos,omitempty" }

// Stats holds the collected transfer statistics. type Stats struct { Sides []SideBytes json:"sides" }

// liveSides returns a snapshot of per-side byte totals for live rendering. // ActiveNanos and IdleNanos are computed against time.Now() at snapshot // time so callers do not need to know the collector's start instant. func (s *statsCollector) liveSides() []SideBytes { s.sidesMu.RLock() defer s.sidesMu.RUnlock() startNanos := s.startedAt.UnixNano() nowNanos := time.Now().UnixNano() out := make([]SideBytes, 0, len(s.sides)) for label, sc := range s.sides { bytes := sc.bytes.Load() lastByte := sc.lastByteAt.Load() var activeNanos, idleNanos int64 if lastByte > 0 { activeNanos = lastByte - startNanos if activeNanos < 0 { activeNanos = 0 } idleNanos = nowNanos - lastByte if idleNanos < 0 { idleNanos = 0 } } out = append(out, SideBytes{ Label: label, Bytes: sc.bytes.Load(), Display: sc.display, ActiveNanos: activeNanos, IdleNanos: idleNanos, }) } return out }