progress: fix bar rate

This commit is contained in:
Michael Yang 2023-11-18 16:23:03 -08:00
parent e1a69d44c9
commit 424d53ac70
3 changed files with 125 additions and 81 deletions

View file

@ -37,6 +37,8 @@ func HumanBytes(b int64) string {
switch { switch {
case value >= 100: case value >= 100:
return fmt.Sprintf("%d %s", int(value), unit) return fmt.Sprintf("%d %s", int(value), unit)
case value >= 10:
return fmt.Sprintf("%d %s", int(value), unit)
case value != math.Trunc(value): case value != math.Trunc(value):
return fmt.Sprintf("%.1f %s", value, unit) return fmt.Sprintf("%.1f %s", value, unit)
default: default:

View file

@ -2,7 +2,6 @@ package progress
import ( import (
"fmt" "fmt"
"math"
"os" "os"
"strings" "strings"
"time" "time"
@ -11,12 +10,6 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
type Stats struct {
rate int64
value int64
remaining time.Duration
}
type Bar struct { type Bar struct {
message string message string
messageWidth int messageWidth int
@ -26,33 +19,45 @@ type Bar struct {
currentValue int64 currentValue int64
started time.Time started time.Time
stopped time.Time
stats Stats maxBuckets int
statted time.Time buckets []bucket
}
type bucket struct {
updated time.Time
value int64
} }
func NewBar(message string, maxValue, initialValue int64) *Bar { func NewBar(message string, maxValue, initialValue int64) *Bar {
return &Bar{ b := Bar{
message: message, message: message,
messageWidth: -1, messageWidth: -1,
maxValue: maxValue, maxValue: maxValue,
initialValue: initialValue, initialValue: initialValue,
currentValue: initialValue, currentValue: initialValue,
started: time.Now(), 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 // formatDuration limits the rendering of a time.Duration to 2 units
func formatDuration(d time.Duration) string { func formatDuration(d time.Duration) string {
if d >= 100*time.Hour { switch {
case d >= 100*time.Hour:
return "99h+" return "99h+"
} case d >= time.Hour:
if d >= time.Hour {
return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60) 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 { func (b *Bar) String() string {
@ -61,59 +66,85 @@ func (b *Bar) String() string {
termWidth = 80 termWidth = 80
} }
var pre, mid, suf strings.Builder var pre strings.Builder
if len(b.message) > 0 {
if b.message != "" {
message := strings.TrimSpace(b.message) message := strings.TrimSpace(b.message)
if b.messageWidth > 0 && len(message) > b.messageWidth { if b.messageWidth > 0 && len(message) > b.messageWidth {
message = message[:b.messageWidth] message = message[:b.messageWidth]
} }
fmt.Fprintf(&pre, "%s", message) fmt.Fprintf(&pre, "%s", message)
if b.messageWidth-pre.Len() >= 0 { if padding := b.messageWidth - pre.Len(); padding > 0 {
pre.WriteString(strings.Repeat(" ", b.messageWidth-pre.Len())) pre.WriteString(repeat(" ", padding))
} }
pre.WriteString(" ") 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() maxValue := format.HumanBytes(b.maxValue)
rate := stats.rate suf.WriteString(repeat(" ", 6-len(maxValue)))
if stats.value > b.initialValue && stats.value < b.maxValue { suf.WriteString(maxValue)
fmt.Fprintf(&suf, ", %s/s", format.HumanBytes(int64(rate))) } else {
maxValue := format.HumanBytes(b.maxValue)
suf.WriteString(repeat(" ", 6-len(maxValue)))
suf.WriteString(maxValue)
suf.WriteString(repeat(" ", 7))
} }
fmt.Fprintf(&suf, ")") rate := b.rate()
// max 10 characters: " 999 MB/s"
var timing string if b.stopped.IsZero() && rate > 0 {
if stats.value > b.initialValue && stats.value < b.maxValue { suf.WriteString(" ")
timing = fmt.Sprintf("[%s:%s]", formatDuration(time.Since(b.started)), formatDuration(stats.remaining)) 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 // max 8 characters: " 59m59s"
pad := 44 - suf.Len() - len(timing) if b.stopped.IsZero() && rate > 0 {
if pad > 0 { suf.WriteString(" ")
suf.WriteString(strings.Repeat(" ", pad)) var remaining time.Duration
} if rate > 0 {
suf.WriteString(timing) 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 humanRemaining := formatDuration(remaining)
f := termWidth - pre.Len() - suf.Len() - 3 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) n := int(float64(f) * b.percent() / 100)
if f > 0 { mid.WriteString(" ▕")
mid.WriteString("▕")
mid.WriteString(strings.Repeat("█", n)) if n > 0 {
if f-n > 0 { mid.WriteString(repeat("█", n))
mid.WriteString(strings.Repeat(" ", f-n))
}
mid.WriteString("▏")
} }
if f-n > 0 {
mid.WriteString(repeat(" ", f-n))
}
mid.WriteString("▏ ")
return pre.String() + mid.String() + suf.String() return pre.String() + mid.String() + suf.String()
} }
@ -123,6 +154,21 @@ func (b *Bar) Set(value int64) {
} }
b.currentValue = value 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 { func (b *Bar) percent() float64 {
@ -133,41 +179,37 @@ func (b *Bar) percent() float64 {
return 0 return 0
} }
func (b *Bar) Stats() Stats { func (b *Bar) rate() float64 {
if time.Since(b.statted) < time.Second { var numerator, denominator float64
return b.stats
}
switch { if !b.stopped.IsZero() {
case b.statted.IsZero(): numerator = float64(b.currentValue - b.initialValue)
b.stats = Stats{ denominator = b.stopped.Sub(b.started).Round(time.Second).Seconds()
value: b.initialValue, } else {
rate: 0, switch len(b.buckets) {
remaining: 0, case 0:
} // noop
case b.currentValue >= b.maxValue: case 1:
b.stats = Stats{ numerator = float64(b.buckets[0].value - b.initialValue)
value: b.maxValue, denominator = b.buckets[0].updated.Sub(b.started).Round(time.Second).Seconds()
rate: 0, default:
remaining: 0, first, last := b.buckets[0], b.buckets[len(b.buckets)-1]
} numerator = float64(last.value - first.value)
default: denominator = last.updated.Sub(first.updated).Round(time.Second).Seconds()
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,
} }
} }
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 ""
} }

View file

@ -40,8 +40,8 @@ func (s *Spinner) String() string {
} }
fmt.Fprintf(&sb, "%s", message) fmt.Fprintf(&sb, "%s", message)
if s.messageWidth-sb.Len() >= 0 { if padding := s.messageWidth - sb.Len(); padding > 0 {
sb.WriteString(strings.Repeat(" ", s.messageWidth-sb.Len())) sb.WriteString(strings.Repeat(" ", padding))
} }
sb.WriteString(" ") sb.WriteString(" ")