github.com/mckael/restic@v0.8.3/internal/restic/progress.go (about)

     1  package restic
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strconv"
     7  	"sync"
     8  	"time"
     9  
    10  	"golang.org/x/crypto/ssh/terminal"
    11  )
    12  
    13  // minTickerTime limits how often the progress ticker is updated. It can be
    14  // overridden using the RESTIC_PROGRESS_FPS (frames per second) environment
    15  // variable.
    16  var minTickerTime = time.Second / 60
    17  
    18  var isTerminal = terminal.IsTerminal(int(os.Stdout.Fd()))
    19  var forceUpdateProgress = make(chan bool)
    20  
    21  func init() {
    22  	fps, err := strconv.ParseInt(os.Getenv("RESTIC_PROGRESS_FPS"), 10, 64)
    23  	if err == nil && fps >= 1 {
    24  		if fps > 60 {
    25  			fps = 60
    26  		}
    27  		minTickerTime = time.Second / time.Duration(fps)
    28  	}
    29  }
    30  
    31  // Progress reports progress on an operation.
    32  type Progress struct {
    33  	OnStart  func()
    34  	OnUpdate ProgressFunc
    35  	OnDone   ProgressFunc
    36  	fnM      sync.Mutex
    37  
    38  	cur        Stat
    39  	curM       sync.Mutex
    40  	start      time.Time
    41  	c          *time.Ticker
    42  	cancel     chan struct{}
    43  	o          *sync.Once
    44  	d          time.Duration
    45  	lastUpdate time.Time
    46  
    47  	running bool
    48  }
    49  
    50  // Stat captures newly done parts of the operation.
    51  type Stat struct {
    52  	Files  uint64
    53  	Dirs   uint64
    54  	Bytes  uint64
    55  	Trees  uint64
    56  	Blobs  uint64
    57  	Errors uint64
    58  }
    59  
    60  // ProgressFunc is used to report progress back to the user.
    61  type ProgressFunc func(s Stat, runtime time.Duration, ticker bool)
    62  
    63  // NewProgress returns a new progress reporter. When Start() is called, the
    64  // function OnStart is executed once. Afterwards the function OnUpdate is
    65  // called when new data arrives or at least every d interval. The function
    66  // OnDone is called when Done() is called. Both functions are called
    67  // synchronously and can use shared state.
    68  func NewProgress() *Progress {
    69  	var d time.Duration
    70  	if isTerminal {
    71  		d = time.Second
    72  	}
    73  	return &Progress{d: d}
    74  }
    75  
    76  // Start resets and runs the progress reporter.
    77  func (p *Progress) Start() {
    78  	if p == nil || p.running {
    79  		return
    80  	}
    81  
    82  	p.o = &sync.Once{}
    83  	p.cancel = make(chan struct{})
    84  	p.running = true
    85  	p.Reset()
    86  	p.start = time.Now()
    87  	p.c = nil
    88  	if p.d != 0 {
    89  		p.c = time.NewTicker(p.d)
    90  	}
    91  
    92  	if p.OnStart != nil {
    93  		p.OnStart()
    94  	}
    95  
    96  	go p.reporter()
    97  }
    98  
    99  // Reset resets all statistic counters to zero.
   100  func (p *Progress) Reset() {
   101  	if p == nil {
   102  		return
   103  	}
   104  
   105  	if !p.running {
   106  		panic("resetting a non-running Progress")
   107  	}
   108  
   109  	p.curM.Lock()
   110  	p.cur = Stat{}
   111  	p.curM.Unlock()
   112  }
   113  
   114  // Report adds the statistics from s to the current state and tries to report
   115  // the accumulated statistics via the feedback channel.
   116  func (p *Progress) Report(s Stat) {
   117  	if p == nil {
   118  		return
   119  	}
   120  
   121  	if !p.running {
   122  		panic("reporting in a non-running Progress")
   123  	}
   124  
   125  	p.curM.Lock()
   126  	p.cur.Add(s)
   127  	cur := p.cur
   128  	needUpdate := false
   129  	if isTerminal && time.Since(p.lastUpdate) > minTickerTime {
   130  		p.lastUpdate = time.Now()
   131  		needUpdate = true
   132  	}
   133  	p.curM.Unlock()
   134  
   135  	if needUpdate {
   136  		p.updateProgress(cur, false)
   137  	}
   138  
   139  }
   140  
   141  func (p *Progress) updateProgress(cur Stat, ticker bool) {
   142  	if p.OnUpdate == nil {
   143  		return
   144  	}
   145  
   146  	p.fnM.Lock()
   147  	p.OnUpdate(cur, time.Since(p.start), ticker)
   148  	p.fnM.Unlock()
   149  }
   150  
   151  func (p *Progress) reporter() {
   152  	if p == nil {
   153  		return
   154  	}
   155  
   156  	updateProgress := func() {
   157  		p.curM.Lock()
   158  		cur := p.cur
   159  		p.curM.Unlock()
   160  		p.updateProgress(cur, true)
   161  	}
   162  
   163  	var ticker <-chan time.Time
   164  	if p.c != nil {
   165  		ticker = p.c.C
   166  	}
   167  
   168  	for {
   169  		select {
   170  		case <-ticker:
   171  			updateProgress()
   172  		case <-forceUpdateProgress:
   173  			updateProgress()
   174  		case <-p.cancel:
   175  			if p.c != nil {
   176  				p.c.Stop()
   177  			}
   178  			return
   179  		}
   180  	}
   181  }
   182  
   183  // Done closes the progress report.
   184  func (p *Progress) Done() {
   185  	if p == nil || !p.running {
   186  		return
   187  	}
   188  
   189  	p.running = false
   190  	p.o.Do(func() {
   191  		close(p.cancel)
   192  	})
   193  
   194  	cur := p.cur
   195  
   196  	if p.OnDone != nil {
   197  		p.fnM.Lock()
   198  		p.OnUpdate(cur, time.Since(p.start), false)
   199  		p.OnDone(cur, time.Since(p.start), false)
   200  		p.fnM.Unlock()
   201  	}
   202  }
   203  
   204  // Add accumulates other into s.
   205  func (s *Stat) Add(other Stat) {
   206  	s.Bytes += other.Bytes
   207  	s.Dirs += other.Dirs
   208  	s.Files += other.Files
   209  	s.Trees += other.Trees
   210  	s.Blobs += other.Blobs
   211  	s.Errors += other.Errors
   212  }
   213  
   214  func (s Stat) String() string {
   215  	b := float64(s.Bytes)
   216  	var str string
   217  
   218  	switch {
   219  	case s.Bytes > 1<<40:
   220  		str = fmt.Sprintf("%.3f TiB", b/(1<<40))
   221  	case s.Bytes > 1<<30:
   222  		str = fmt.Sprintf("%.3f GiB", b/(1<<30))
   223  	case s.Bytes > 1<<20:
   224  		str = fmt.Sprintf("%.3f MiB", b/(1<<20))
   225  	case s.Bytes > 1<<10:
   226  		str = fmt.Sprintf("%.3f KiB", b/(1<<10))
   227  	default:
   228  		str = fmt.Sprintf("%dB", s.Bytes)
   229  	}
   230  
   231  	return fmt.Sprintf("Stat(%d files, %d dirs, %v trees, %v blobs, %d errors, %v)",
   232  		s.Files, s.Dirs, s.Trees, s.Blobs, s.Errors, str)
   233  }