Browse Source

progress: fix bar rate

Michael Yang 1 year ago
parent
commit
424d53ac70
3 changed files with 126 additions and 82 deletions
  1. 2 0
      format/bytes.go
  2. 122 80
      progress/bar.go
  3. 2 2
      progress/spinner.go

+ 2 - 0
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:

+ 122 - 80
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
+
+	maxBuckets int
+	buckets    []bucket
+}
 
-	stats   Stats
-	statted time.Time
+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(&suf, "(%s/%s", format.HumanBytes(b.currentValue), format.HumanBytes(b.maxValue))
-
-	stats := b.Stats()
-	rate := stats.rate
-	if stats.value > b.initialValue && stats.value < b.maxValue {
-		fmt.Fprintf(&suf, ", %s/s", format.HumanBytes(int64(rate)))
+	fmt.Fprintf(&pre, "%3.0f%%", b.percent())
+
+	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("/")
+
+		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))
+	// 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
+		}
+
+		humanRemaining := formatDuration(remaining)
+		suf.WriteString(repeat(" ", 6-len(humanRemaining)))
+		suf.WriteString(humanRemaining)
+	} else {
+		suf.WriteString(repeat(" ", 8))
 	}
-	suf.WriteString(timing)
 
-	// add 3 extra spaces: 2 boundary characters and 1 space at the end
-	f := termWidth - pre.Len() - suf.Len() - 3
+	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
-	}
-
-	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)
+func (b *Bar) rate() float64 {
+	var numerator, denominator float64
+
+	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.stats = Stats{
-			value:     b.currentValue,
-			rate:      rate,
-			remaining: remaining,
-		}
+	if denominator != 0 {
+		return numerator / denominator
 	}
 
-	b.statted = time.Now()
+	return 0
+}
+
+func repeat(s string, n int) string {
+	if n > 0 {
+		return strings.Repeat(s, n)
+	}
 
-	return b.stats
+	return ""
 }

+ 2 - 2
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(" ")