aboutsummaryrefslogtreecommitdiff
path: root/internal/progress/meter.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/progress/meter.go')
-rw-r--r--internal/progress/meter.go126
1 files changed, 79 insertions, 47 deletions
diff --git a/internal/progress/meter.go b/internal/progress/meter.go
index e5e64fb4..9d4f1155 100644
--- a/internal/progress/meter.go
+++ b/internal/progress/meter.go
@@ -1,17 +1,22 @@
package progress
import (
+ "sync/atomic"
"time"
"lindenii.org/go/lgo/iowrap"
)
const (
- updateInterval = time.Second
+ renderInterval = 100 * time.Millisecond
+ forceInterval = time.Second
throughputInterval = 500 * time.Millisecond
)
// Meter renders one in-place progress line.
+//
+// Add is safe for concurrent use; a single background goroutine renders.
+// Stop must be called exactly once to flush the final line and release it.
type Meter struct {
writer iowrap.WriteFlusher
@@ -21,24 +26,29 @@ type Meter struct {
sparse bool
throughput bool
- startedAt time.Time
- nextUpdateAt time.Time
- nextThroughput time.Time
+ done atomic.Int64
+ bytes atomic.Int64
+ sawValue atomic.Bool
- lastDone int
- lastBytes int
- lastPercent int
- lastCounterW int
- sawValue bool
+ startedAt time.Time
+ stop chan struct{}
+ exited chan struct{}
+
+ // The following are owned by the render goroutine while it runs,
+ // then by Stop once exited is closed.
+ nextForceAt time.Time
+ nextThroughput time.Time
+ lastPercent int
+ lastCounterW int
throughputSuffix string
}
-// New creates one progress meter.
+// New creates one progress meter and starts its render goroutine.
func New(opts Options) *Meter {
now := time.Now()
- return &Meter{
+ meter := &Meter{
writer: opts.Writer,
title: opts.Title,
total: opts.Total,
@@ -46,10 +56,20 @@ func New(opts Options) *Meter {
sparse: opts.Sparse,
throughput: opts.Throughput,
startedAt: now,
- nextUpdateAt: now.Add(updateInterval),
+ stop: make(chan struct{}),
+ exited: make(chan struct{}),
+ nextForceAt: now.Add(forceInterval),
nextThroughput: now.Add(throughputInterval),
lastPercent: -1,
}
+
+ if meter.writer != nil {
+ go meter.loop()
+ } else {
+ close(meter.exited)
+ }
+
+ return meter
}
// Options configures one progress meter.
@@ -67,59 +87,71 @@ type Options struct {
Throughput bool
}
-// Set records current progress
-// and renders when percent changed or the 1s tick elapsed.
-func (meter *Meter) Set(done int, bytes int) {
- meter.lastDone = done
- meter.lastBytes = bytes
- meter.sawValue = true
+// Add increments the done and byte counters.
+//
+// Labels: MT-Safe.
+func (meter *Meter) Add(done, bytes int64) {
+ meter.done.Add(done)
+ meter.bytes.Add(bytes)
+ meter.sawValue.Store(true)
+}
+
+// Stop ends the render goroutine, forces the final line, and appends ", <msg>.".
+func (meter *Meter) Stop(msg string) {
+ close(meter.stop)
+ <-meter.exited
- if meter.writer == nil {
+ if !meter.sawValue.Load() || meter.writer == nil {
return
}
- now := time.Now()
- forced := meter.consumeUpdateTick(now)
-
- percentChanged := false
-
- if meter.total > 0 {
- percent := int(int64(done) * 100 / int64(meter.total))
- percentChanged = percent != meter.lastPercent
+ if msg == "" {
+ msg = "done"
}
- if !percentChanged && !forced {
- return
+ if meter.sparse && meter.total > 0 && int(meter.done.Load()) != meter.total {
+ meter.done.Store(int64(meter.total))
}
- meter.render(now, "\r")
+ meter.render(time.Now(), ", "+msg+".\n")
}
-// Stop forces the final progress line and appends ", <msg>.".
-func (meter *Meter) Stop(msg string) {
- if !meter.sawValue || meter.writer == nil {
- return
- }
+func (meter *Meter) loop() {
+ defer close(meter.exited)
- if msg == "" {
- msg = "done"
+ ticker := time.NewTicker(renderInterval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-meter.stop:
+ return
+ case now := <-ticker.C:
+ meter.maybeRender(now)
+ }
}
+}
- if meter.sparse && meter.total > 0 && meter.lastDone != meter.total {
- meter.lastDone = meter.total
+func (meter *Meter) maybeRender(now time.Time) {
+ if !meter.sawValue.Load() {
+ return
}
- meter.render(time.Now(), ", "+msg+".\n")
-}
+ forced := false
-func (meter *Meter) consumeUpdateTick(now time.Time) bool {
- if now.Before(meter.nextUpdateAt) {
- return false
+ for !now.Before(meter.nextForceAt) {
+ meter.nextForceAt = meter.nextForceAt.Add(forceInterval)
+ forced = true
}
- for !now.Before(meter.nextUpdateAt) {
- meter.nextUpdateAt = meter.nextUpdateAt.Add(updateInterval)
+ percentChanged := false
+
+ if meter.total > 0 {
+ percent := int(meter.done.Load() * 100 / int64(meter.total))
+ percentChanged = percent != meter.lastPercent
}
- return true
+ if percentChanged || forced {
+ meter.render(now, "\r")
+ }
}