Show per-side throughput at end of run and via --progress ticker · Entire
Show per-side throughput at end of run and via --progress ticker
f3f7b3a→main·
Soph·2mo ago·8 files·+437 added/-0 removed
Two new outputs surfacing the per-side counters from the prior commit:
statsLines now appends a 'throughput: source=X @ Y/s · target=A @ B/s' line whenever --stats produced non-zero per-side bytes. Computed against the stats collector's own wall-clock window so it works without --measure-memory.
A new --progress flag wires a goroutine-driven stderr renderer that refreshes the same per-side line every 200ms in place. Auto-suppressed when stderr isn't a TTY so JSON pipes and CI logs stay clean. The reporter is started in newSession and torn down via a deferred syncSession.finish() in each public entry point so the final frame reflects the closing byte counts before the newline is emitted.
Plumbed through unstable.AdvancedOptions.Progress and exposed on sync, replicate, plan, bootstrap, and fetch.
Sessions
Changes
8
cmd/git-sync
Mbootstrap.go+1
Mfetch.go+1
Msyncplan.go+1
internal/syncer
Aprogress.go+146
Aprogress_test.go+119
Msyncer.go+65
Athroughput_test.go+99
unstable
Mclient.go+5
75 unmodified lines
cmd.Flags().BoolVar(&req.IncludeTags, "tags", false, "mirror tags") cmd.Flags().BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics") cmd.Flags().BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage") cmd.Flags().BoolVar(&req.Options.Progress, "progress", false, "show live per-side throughput on stderr (TTY only)") cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output") cmd.Flags().Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap if the streamed source pack exceeds this many bytes") cmd.Flags().Int64Var(&req.Options.TargetMaxPackBytes, "target-max-pack-bytes", 0, "target receive-pack body size limit; batches are planned and auto-subdivided to fit")
Mcmd/git-sync/bootstrap.go+1
71 unmodified lines
addProtocolFlag(cmd, &protocolVal) cmd.Flags().BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics") cmd.Flags().BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage") cmd.Flags().BoolVar(&req.Options.Progress, "progress", false, "show live per-side throughput on stderr (TTY only)") cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output") cmd.Flags().StringArrayVar(&haveRefs, "have-ref", nil, "source ref name to advertise as have; short names map to branches") cmd.Flags().StringArrayVar(&haveHashesRaw, "have", nil, "explicit object hash to advertise as have")
Mcmd/git-sync/fetch.go+1
112 unmodified lines
cmd.Flags().BoolVar(&req.Policy.Prune, "prune", false, "delete managed target refs that no longer exist on source") cmd.Flags().BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics") cmd.Flags().BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage") cmd.Flags().BoolVar(&req.Options.Progress, "progress", false, "show live per-side throughput on stderr (TTY only)") cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output") cmd.Flags().IntVar(&req.Options.MaterializedMaxObjects, "materialized-max-objects", unstable.DefaultMaterializedMaxObjects, "abort non-relay materialized syncs above this many objects") cmd.Flags().Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap-relay push if the streamed source pack exceeds this many bytes")
Mcmd/git-sync/syncplan.go+1
1-146 unmodified lines
package syncer
import ( "fmt" "io" "os" "sort" "strings" "sync" "time" )
// progressReporter renders live per-side throughput to a writer (typically // os.Stderr) by sampling the statsCollector's atomic byte counters on a // fixed interval. It is intentionally a one-line in-place renderer so it // stays out of the way of the final command output. type progressReporter struct { out io.Writer stats *statsCollector interval time.Duration start time.Time
stopOnce sync.Once stop chan struct{} done chan struct{}
mu sync.Mutex lastLen int }
func newProgressReporter(out io.Writer, stats *statsCollector, interval time.Duration) *progressReporter { if interval <= 0 { interval = 200 * time.Millisecond } return &progressReporter{ out: out, stats: stats, interval: interval, start: time.Now(), stop: make(chan struct{}), done: make(chan struct{}), } }
// run drives the ticker until stop() is called. Safe to call once. func (p *progressReporter) run() { defer close(p.done) ticker := time.NewTicker(p.interval) defer ticker.Stop() for { select { case <-p.stop: return case <-ticker.C: p.render(false) } } }
// terminate halts the ticker, draws one final frame so the printed line // reflects the closing byte counts, and emits a newline so subsequent // command output starts on a fresh row. func (p *progressReporter) terminate() { p.stopOnce.Do(func() { close(p.stop) <-p.done p.render(true) p.mu.Lock() if p.lastLen > 0 { fmt.Fprintln(p.out) } p.mu.Unlock() }) }
func (p *progressReporter) render(final bool) { sides := p.stats.liveSides() if len(sides) == 0 && !final { return } sort.Slice(sides, func(i, j int) bool { return sides[i].Label < sides[j].Label }) elapsed := time.Since(p.start)
var b strings.Builder for i, side := range sides { if i > 0 { b.WriteString(" · ") } fmt.Fprintf(&b, "%s: %s @ %s", side.Label, formatBytes(side.Bytes), formatRate(side.Bytes, elapsed)) } line := b.String()
p.mu.Lock() defer p.mu.Unlock() pad := 0 if p.lastLen > len(line) { pad = p.lastLen - len(line) } fmt.Fprint(p.out, "\r"+line+strings.Repeat(" ", pad)) p.lastLen = len(line) }
// stderrIsTTY reports whether stderr is attached to a terminal. The // progress ticker is suppressed otherwise because '\r' updates only make // sense on a TTY and would otherwise spam log files. func stderrIsTTY() bool { fi, err := os.Stderr.Stat() if err != nil { return false } return (fi.Mode() & os.ModeCharDevice) != 0 }
// formatBytes renders byte counts in IEC-ish human units (binary base). func formatBytes(n int64) string { const unit = 1024 if n < unit { return fmt.Sprintf("%d B", n) } ddiv, exp := int64(unit), 0 for x := n / unit; x >= unit; x /= unit { ddiv *= unit exp++ } value := float64(n) / float64(ddiv) suffix := []string{"KB", "MB", "GB", "TB", "PB"}[exp] if value >= 100 { return fmt.Sprintf("%.0f %s", value, suffix) } if value >= 10 { return fmt.Sprintf("%.1f %s", value, suffix) } return fmt.Sprintf("%.2f %s", value, suffix) }
// formatRate renders a bytes/second average over the supplied duration. // Returns "0 B/s" until the duration is large enough to be meaningful, // avoiding misleadingly large rates from sub-millisecond samples. func formatRate(bytes int64, dur time.Duration) string { if dur < 50*time.Millisecond || bytes <= 0 { return "0 B/s" } rate := float64(bytes) / dur.Seconds() return formatBytes(int64(rate)) + "/s" }