summaryrefslogtreecommitdiff
path: root/lib/textui/progress.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/textui/progress.go')
-rw-r--r--lib/textui/progress.go42
1 files changed, 42 insertions, 0 deletions
diff --git a/lib/textui/progress.go b/lib/textui/progress.go
index 48a3901..04c8212 100644
--- a/lib/textui/progress.go
+++ b/lib/textui/progress.go
@@ -18,6 +18,16 @@ type Stats interface {
fmt.Stringer
}
+// Progress helps display to the user the ongoing progress of a long
+// task.
+//
+// There are few usage requirements to watch out for:
+//
+// - .Set() must have been called at least once before you call
+// .Done(). The easiest way to ensure this is to call .Set right
+// after creating the progress, or right before calling .Done(). I
+// advise against counting on a loop to have called .Set() at least
+// once.
type Progress[T Stats] struct {
ctx context.Context //nolint:containedctx // captured for separate goroutine
lvl dlog.LogLevel
@@ -29,6 +39,10 @@ type Progress[T Stats] struct {
cur typedsync.Value[T]
oldStat T
oldLine string
+
+ // This isn't a functional part, but is useful for helping us
+ // to detect misuse.
+ last time.Time
}
func NewProgress[T Stats](ctx context.Context, lvl dlog.LogLevel, interval time.Duration) *Progress[T] {
@@ -44,18 +58,43 @@ func NewProgress[T Stats](ctx context.Context, lvl dlog.LogLevel, interval time.
return ret
}
+// Set update the Progress. Rate-limiting prevents this from being
+// expensive, or from spamming the user; it is reasonably safe to call
+// .Set in a tight inner loop.
+//
+// It is safe to call Set concurrently.
func (p *Progress[T]) Set(val T) {
if _, hadOld := p.cur.Swap(val); !hadOld {
go p.run()
}
}
+// Done closes the Progress; it flushes out one last status update (if
+// nescessary), and releases resources associated with the Progress.
+//
+// It is safe to call Done multiple times, or concurrently.
+//
+// It will panic if Done is called without having called Set at least
+// once.
func (p *Progress[T]) Done() {
p.cancel()
+ if _, started := p.cur.Load(); !started {
+ panic("textui.Progress: .Done called without ever calling .Set")
+ }
<-p.done
}
func (p *Progress[T]) flush(force bool) {
+ // Check how long it's been since we last printed something.
+ // If this grows too big, it probably means that either the
+ // program deadlocked or that we forgot to call .Done().
+ now := time.Now()
+ if !p.last.IsZero() && now.Sub(p.last) > Tunable(2*time.Minute) {
+ dlog.Error(p.ctx, "stale Progress")
+ panic("stale Progress")
+ }
+
+ // Load the data to print.
cur, ok := p.cur.Load()
if !ok {
panic("should not happen")
@@ -65,13 +104,16 @@ func (p *Progress[T]) flush(force bool) {
}
defer func() { p.oldStat = cur }()
+ // Format the data as text.
line := cur.String()
if !force && line == p.oldLine {
return
}
defer func() { p.oldLine = line }()
+ // Print.
dlog.Log(p.ctx, p.lvl, line)
+ p.last = now
}
func (p *Progress[T]) run() {