diff options
Diffstat (limited to 'lib/textui')
-rw-r--r-- | lib/textui/progress.go | 42 |
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() { |