github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/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 }