blob: d0472d075bad7c269cce14d7916b3960f91839a8 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package progress
import (
"bytes"
"fmt"
"io"
"log"
"strings"
"sync"
"time"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/rate"
)
const rateEstimationWindow = 5
// formatUnit optionally formats n with a size (K, M, G, T) suffix.
func formatUnit(n float64) string {
var unit string
for _, unit = range []string{"", "K", "M", "G", "T"} {
if n < 1000 {
break
}
n /= 1000
}
return strings.TrimRight(fmt.Sprintf("%#.3g", n), ".") + unit
}
// formatSize2 formats n, total as n/total, optionally with a size suffix.
func formatSize2(n, total int64) string {
if n == total {
return formatUnit(float64(n)) + "B"
}
return fmt.Sprintf("%sB/%sB", formatUnit(float64(n)), formatUnit(float64(total)))
}
type progressWriter struct {
name string
n int64
lastUpdate time.Time
total int64
rate *rate.Estimator
}
var _ io.WriteCloser = &progressWriter{}
// NewWriter creates a Writer that tracks the progress by the Write() call.
func NewWriter(name string, total int64) io.WriteCloser {
return &progressWriter{
name: name,
total: total,
rate: rate.NewEstimator(rateEstimationWindow),
}
}
func (w *progressWriter) Write(b []byte) (int, error) {
w.n += int64(len(b))
now := time.Now()
if now.Sub(w.lastUpdate) > time.Second {
w.Close()
w.lastUpdate = now
}
return len(b), nil
}
func (w *progressWriter) Close() error {
log.Printf(
"[%s] %5.1f%% %sbps %s\n",
w.name,
100*float64(w.n)/float64(w.total),
formatUnit(w.rate.AddRecord(float64(w.n*8))),
formatSize2(w.n, w.total),
)
return nil
}
// ProgressReporter aggregates the progress of multiple ReportingWriters.
type ProgressReporter struct {
sources []*ReportingWriter
rate *rate.Estimator
}
// NewProgressReporter a ProgressReporter.
func NewProgressReporter() *ProgressReporter {
return &ProgressReporter{
rate: rate.NewEstimator(rateEstimationWindow),
}
}
// Report returns the aggregated progress of r.
func (r *ProgressReporter) Report() string {
var b bytes.Buffer
var gN int64
var gTotal int64
for i, w := range r.sources {
if i > 0 {
b.WriteString(" ")
}
stats, n, total := w.Stats()
b.WriteString(stats)
gN += n
gTotal += total
}
if gTotal == 0 {
gTotal = 1 // prevent div by zero
}
return fmt.Sprintf("%5.1f%% %sbps %s",
float64(gN)/float64(gTotal)*100,
formatUnit(r.rate.AddRecord(float64(gN*8))),
b.String(),
)
}
// NewWriter creates a ReportingWriter reporting to r.
func (r *ProgressReporter) NewWriter(name string) *ReportingWriter {
rw := &ReportingWriter{
name: name,
}
r.sources = append(r.sources, rw)
return rw
}
// ReportingWriter is an io.Writer which reports its progress to a ProgressWriter.
type ReportingWriter struct {
mutex sync.Mutex
name string
n int64
total int64
}
var _ io.Writer = &ReportingWriter{}
// SetTotal sets to total size to be written to w.
func (w *ReportingWriter) SetTotal(total int64) {
w.mutex.Lock()
w.total = total
w.mutex.Unlock()
}
func (w *ReportingWriter) Write(b []byte) (int, error) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.n += int64(len(b))
return len(b), nil
}
// Stats returns the local statistics to w.
func (w *ReportingWriter) Stats() (stats string, n, total int64) {
w.mutex.Lock()
defer w.mutex.Unlock()
return fmt.Sprintf("[%s %s]", w.name, formatSize2(w.n, w.total)), w.n, w.total
}