From 424d53ac70d1960770f3bb3a7aa44afea9f2dbeb Mon Sep 17 00:00:00 2001 From: Michael Yang Date: Sat, 18 Nov 2023 16:23:03 -0800 Subject: [PATCH] progress: fix bar rate --- format/bytes.go | 2 + progress/bar.go | 200 +++++++++++++++++++++++++++----------------- progress/spinner.go | 4 +- 3 files changed, 125 insertions(+), 81 deletions(-) diff --git a/format/bytes.go b/format/bytes.go index 4e5d43d1..01be17ce 100644 --- a/format/bytes.go +++ b/format/bytes.go @@ -37,6 +37,8 @@ func HumanBytes(b int64) string { switch { case value >= 100: return fmt.Sprintf("%d %s", int(value), unit) + case value >= 10: + return fmt.Sprintf("%d %s", int(value), unit) case value != math.Trunc(value): return fmt.Sprintf("%.1f %s", value, unit) default: diff --git a/progress/bar.go b/progress/bar.go index a482bfe4..9a0da825 100644 --- a/progress/bar.go +++ b/progress/bar.go @@ -2,7 +2,6 @@ package progress import ( "fmt" - "math" "os" "strings" "time" @@ -11,12 +10,6 @@ import ( "golang.org/x/term" ) -type Stats struct { - rate int64 - value int64 - remaining time.Duration -} - type Bar struct { message string messageWidth int @@ -26,33 +19,45 @@ type Bar struct { currentValue int64 started time.Time + stopped time.Time - stats Stats - statted time.Time + maxBuckets int + buckets []bucket +} + +type bucket struct { + updated time.Time + value int64 } func NewBar(message string, maxValue, initialValue int64) *Bar { - return &Bar{ + b := Bar{ message: message, messageWidth: -1, maxValue: maxValue, initialValue: initialValue, currentValue: initialValue, started: time.Now(), + maxBuckets: 10, } + + if initialValue >= maxValue { + b.stopped = time.Now() + } + + return &b } // formatDuration limits the rendering of a time.Duration to 2 units func formatDuration(d time.Duration) string { - if d >= 100*time.Hour { + switch { + case d >= 100*time.Hour: return "99h+" - } - - if d >= time.Hour { + case d >= time.Hour: return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60) + default: + return d.Round(time.Second).String() } - - return d.Round(time.Second).String() } func (b *Bar) String() string { @@ -61,59 +66,85 @@ func (b *Bar) String() string { termWidth = 80 } - var pre, mid, suf strings.Builder - - if b.message != "" { + var pre strings.Builder + if len(b.message) > 0 { message := strings.TrimSpace(b.message) if b.messageWidth > 0 && len(message) > b.messageWidth { message = message[:b.messageWidth] } fmt.Fprintf(&pre, "%s", message) - if b.messageWidth-pre.Len() >= 0 { - pre.WriteString(strings.Repeat(" ", b.messageWidth-pre.Len())) + if padding := b.messageWidth - pre.Len(); padding > 0 { + pre.WriteString(repeat(" ", padding)) } pre.WriteString(" ") } - fmt.Fprintf(&pre, "%3.0f%% ", math.Floor(b.percent())) + fmt.Fprintf(&pre, "%3.0f%%", b.percent()) - fmt.Fprintf(&suf, "(%s/%s", format.HumanBytes(b.currentValue), format.HumanBytes(b.maxValue)) + var suf strings.Builder + // max 13 characters: "999 MB/999 MB" + if b.stopped.IsZero() { + curValue := format.HumanBytes(b.currentValue) + suf.WriteString(repeat(" ", 6-len(curValue))) + suf.WriteString(curValue) + suf.WriteString("/") - stats := b.Stats() - rate := stats.rate - if stats.value > b.initialValue && stats.value < b.maxValue { - fmt.Fprintf(&suf, ", %s/s", format.HumanBytes(int64(rate))) + maxValue := format.HumanBytes(b.maxValue) + suf.WriteString(repeat(" ", 6-len(maxValue))) + suf.WriteString(maxValue) + } else { + maxValue := format.HumanBytes(b.maxValue) + suf.WriteString(repeat(" ", 6-len(maxValue))) + suf.WriteString(maxValue) + suf.WriteString(repeat(" ", 7)) } - fmt.Fprintf(&suf, ")") - - var timing string - if stats.value > b.initialValue && stats.value < b.maxValue { - timing = fmt.Sprintf("[%s:%s]", formatDuration(time.Since(b.started)), formatDuration(stats.remaining)) + rate := b.rate() + // max 10 characters: " 999 MB/s" + if b.stopped.IsZero() && rate > 0 { + suf.WriteString(" ") + humanRate := format.HumanBytes(int64(rate)) + suf.WriteString(repeat(" ", 6-len(humanRate))) + suf.WriteString(humanRate) + suf.WriteString("/s") + } else { + suf.WriteString(repeat(" ", 10)) } - // 44 is the maximum width for the stats on the right of the progress bar - pad := 44 - suf.Len() - len(timing) - if pad > 0 { - suf.WriteString(strings.Repeat(" ", pad)) - } - suf.WriteString(timing) + // max 8 characters: " 59m59s" + if b.stopped.IsZero() && rate > 0 { + suf.WriteString(" ") + var remaining time.Duration + if rate > 0 { + remaining = time.Duration(int64(float64(b.maxValue-b.currentValue)/rate)) * time.Second + } - // add 3 extra spaces: 2 boundary characters and 1 space at the end - f := termWidth - pre.Len() - suf.Len() - 3 + humanRemaining := formatDuration(remaining) + suf.WriteString(repeat(" ", 6-len(humanRemaining))) + suf.WriteString(humanRemaining) + } else { + suf.WriteString(repeat(" ", 8)) + } + + var mid strings.Builder + // add 5 extra spaces: 2 boundary characters and 1 space at each end + f := termWidth - pre.Len() - suf.Len() - 5 n := int(float64(f) * b.percent() / 100) - if f > 0 { - mid.WriteString("▕") - mid.WriteString(strings.Repeat("█", n)) - if f-n > 0 { - mid.WriteString(strings.Repeat(" ", f-n)) - } - mid.WriteString("▏") + mid.WriteString(" ▕") + + if n > 0 { + mid.WriteString(repeat("█", n)) } + if f-n > 0 { + mid.WriteString(repeat(" ", f-n)) + } + + mid.WriteString("▏ ") + return pre.String() + mid.String() + suf.String() } @@ -123,6 +154,21 @@ func (b *Bar) Set(value int64) { } b.currentValue = value + if b.currentValue >= b.maxValue { + b.stopped = time.Now() + } + + // throttle bucket updates to 1 per second + if len(b.buckets) == 0 || time.Since(b.buckets[len(b.buckets)-1].updated) > time.Second { + b.buckets = append(b.buckets, bucket{ + updated: time.Now(), + value: value, + }) + + if len(b.buckets) > b.maxBuckets { + b.buckets = b.buckets[1:] + } + } } func (b *Bar) percent() float64 { @@ -133,41 +179,37 @@ func (b *Bar) percent() float64 { return 0 } -func (b *Bar) Stats() Stats { - if time.Since(b.statted) < time.Second { - return b.stats - } +func (b *Bar) rate() float64 { + var numerator, denominator float64 - switch { - case b.statted.IsZero(): - b.stats = Stats{ - value: b.initialValue, - rate: 0, - remaining: 0, - } - case b.currentValue >= b.maxValue: - b.stats = Stats{ - value: b.maxValue, - rate: 0, - remaining: 0, - } - default: - rate := b.currentValue - b.stats.value - var remaining time.Duration - if rate > 0 { - remaining = time.Second * time.Duration((float64(b.maxValue-b.currentValue))/(float64(rate))) - } else { - remaining = time.Duration(math.MaxInt64) - } - - b.stats = Stats{ - value: b.currentValue, - rate: rate, - remaining: remaining, + if !b.stopped.IsZero() { + numerator = float64(b.currentValue - b.initialValue) + denominator = b.stopped.Sub(b.started).Round(time.Second).Seconds() + } else { + switch len(b.buckets) { + case 0: + // noop + case 1: + numerator = float64(b.buckets[0].value - b.initialValue) + denominator = b.buckets[0].updated.Sub(b.started).Round(time.Second).Seconds() + default: + first, last := b.buckets[0], b.buckets[len(b.buckets)-1] + numerator = float64(last.value - first.value) + denominator = last.updated.Sub(first.updated).Round(time.Second).Seconds() } } - b.statted = time.Now() + if denominator != 0 { + return numerator / denominator + } - return b.stats + return 0 +} + +func repeat(s string, n int) string { + if n > 0 { + return strings.Repeat(s, n) + } + + return "" } diff --git a/progress/spinner.go b/progress/spinner.go index 62ed4f09..02f3f9fb 100644 --- a/progress/spinner.go +++ b/progress/spinner.go @@ -40,8 +40,8 @@ func (s *Spinner) String() string { } fmt.Fprintf(&sb, "%s", message) - if s.messageWidth-sb.Len() >= 0 { - sb.WriteString(strings.Repeat(" ", s.messageWidth-sb.Len())) + if padding := s.messageWidth - sb.Len(); padding > 0 { + sb.WriteString(strings.Repeat(" ", padding)) } sb.WriteString(" ")