repo-sync/fetch/stats.go
James Coleman 5af1dd8928
Some checks failed
Go package / build (push) Has been cancelled
first commit
2026-07-28 19:22:20 -05:00

103 lines
2.9 KiB
Go

package fetch
import (
"context"
"fmt"
"sync/atomic"
)
// Counters accumulates transfer activity for one repository's summary log
// line. Counters are atomic because download workers run concurrently.
type Counters struct {
Fetched atomic.Int64
FetchedBytes atomic.Int64
Unchanged atomic.Int64
Pruned atomic.Int64
Planned atomic.Int64
PlannedBytes atomic.Int64
Missing atomic.Int64
}
// Stats collects transfer activity for the repository currently
// synchronizing. The CLI synchronizes repositories sequentially, so one
// package-level collector suffices there. The server's concurrent crawls
// interleave into these counters, so their values are only meaningful in
// the sequential path; the server must not report them.
var Stats = &Counters{}
// statsKey carries a caller's own collector on a context.
type statsKey struct{}
// WithStats returns a context whose transfers accumulate into c as well as
// into Stats. It gives one synchronization its own totals even while other
// synchronizations run concurrently, which is what the mirror server's
// crawls and their trace files need.
func WithStats(ctx context.Context, c *Counters) context.Context {
if c == nil {
return ctx
}
return context.WithValue(ctx, statsKey{}, c)
}
// runStats returns the caller's collector carried by ctx, or nil when the
// caller installed none.
func runStats(ctx context.Context) *Counters {
c, _ := ctx.Value(statsKey{}).(*Counters)
return c
}
// recordFetched counts a completed download in the shared collector and in
// the caller's own, when one is installed.
func recordFetched(ctx context.Context, bytes int64) {
Stats.Fetched.Add(1)
Stats.FetchedBytes.Add(bytes)
if c := runStats(ctx); c != nil {
c.Fetched.Add(1)
c.FetchedBytes.Add(bytes)
}
}
// recordUnchanged counts a file that needed no transfer in both collectors.
func recordUnchanged(ctx context.Context) {
Stats.Unchanged.Add(1)
if c := runStats(ctx); c != nil {
c.Unchanged.Add(1)
}
}
// recordMissing counts a file the upstream did not serve in both
// collectors, whether or not the run tolerates its absence.
func recordMissing(ctx context.Context) {
Stats.Missing.Add(1)
if c := runStats(ctx); c != nil {
c.Missing.Add(1)
}
}
// Reset clears the collector for the next repository.
func (s *Counters) Reset() {
s.Fetched.Store(0)
s.FetchedBytes.Store(0)
s.Unchanged.Store(0)
s.Pruned.Store(0)
s.Planned.Store(0)
s.PlannedBytes.Store(0)
s.Missing.Store(0)
}
// FormatBytes renders a byte count in a human readable unit.
func FormatBytes(n int64) string {
const unit = 1024
const prefixes = "KMGTPE"
if n < unit {
return fmt.Sprintf("%d B", n)
}
div, exp := int64(unit), 0
// Cap the exponent at the last available prefix so an unexpectedly
// large count can never index past the prefix table.
for m := n / unit; m >= unit && exp < len(prefixes)-1; m /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), prefixes[exp])
}