blob: e32a4130b4f3ed2d073c6293f461048d5981e978 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package progress
import (
"errors"
"fmt"
"io"
"strconv"
"strings"
"sync"
"time"
"go.chromium.org/luci/client/internal/common"
)
// Group identifies a column group to keep progress for.
type Group int
// Section identifies a particular column in a column group.
type Section int
// Progress outputs information about the progress of a long task.
//
// It's implementation must be thread safe.
type Progress interface {
io.Closer
// Update increases the count of a column.
Update(group Group, section Section, count int64)
}
// Formatter formats numbers in a Column.
type Formatter func(i int64) string
// Column represent a column to be printed in the progress status.
type Column struct {
Name string
Formatter Formatter
Value int64
}
// New returns an initialized thread-safe Progress implementation.
//
// columns is the number of stages each item must go through, then with a set
// of numbers for each state, which will be displayed as a number in each box.
//
// For:
// columns = [][]Column{{Name:"found"}, {"hashed", Name:"to hash"}}
// It'll print:
// [found] [hashed/to hash]
func New(columns [][]Column, out io.Writer) Progress {
p := &progress{
start: time.Now().UTC(),
columns: make([][]Column, len(columns)),
interval: time.Second,
EOL: "\n",
out: out,
}
if common.IsTerminal(out) {
p.interval = 50 * time.Millisecond
p.EOL = "\r"
}
for i, c := range columns {
p.columns[i] = make([]Column, len(c))
for j, d := range c {
p.columns[i][j] = d
if p.columns[i][j].Formatter == nil {
p.columns[i][j].Formatter = formatInt
}
}
}
if out != nil {
go p.printLoop()
}
return p
}
// Private stuff.
type progress struct {
// Immutable.
start time.Time
interval time.Duration
EOL string
out io.Writer
// Mutable.
lock sync.Mutex
columns [][]Column // Only the .Value are updated.
valueChanged bool
}
func (p *progress) Update(group Group, section Section, count int64) {
go func() {
p.lock.Lock()
defer p.lock.Unlock()
p.columns[group][section].Value += count
p.valueChanged = true
}()
}
func (p *progress) Close() error {
p.lock.Lock()
defer p.lock.Unlock()
if p.out == nil {
return errors.New("was already closed")
}
_, _ = p.out.Write([]byte("\n"))
p.out = nil
return nil
}
func (p *progress) printLoop() {
line := renderNames(p.columns) + "\n"
p.lock.Lock()
out := p.out
p.lock.Unlock()
if _, err := io.WriteString(out, line); err != nil {
return
}
for {
p.lock.Lock()
out, line := p.printStep()
p.lock.Unlock()
if out == nil {
return
}
if line != "" {
if _, err := io.WriteString(out, line); err != nil {
return
}
}
time.Sleep(p.interval)
}
}
func (p *progress) printStep() (io.Writer, string) {
if p.out == nil || !p.valueChanged {
return p.out, ""
}
p.valueChanged = false
// Zap resolution at .1s level. We're slow anyway.
duration := time.Since(p.start).Round(100 * time.Millisecond)
return p.out, fmt.Sprintf("%s %s%s", renderValues(p.columns), duration, p.EOL)
}
func renderSectionName(section []Column) string {
parts := make([]string, len(section))
for i, s := range section {
parts[i] = s.Name
}
return "[" + strings.Join(parts, "/") + "]"
}
func renderNames(c [][]Column) string {
sections := make([]string, len(c))
for i, section := range c {
sections[i] = renderSectionName(section)
}
return strings.Join(sections, " ")
}
func renderSectionValue(section []Column) string {
parts := make([]string, len(section))
for i, part := range section {
parts[i] = part.Formatter(part.Value)
}
return "[" + strings.Join(parts, "/") + "]"
}
func renderValues(c [][]Column) string {
sections := make([]string, len(c))
for i, section := range c {
sections[i] = renderSectionValue(section)
}
return strings.Join(sections, " ")
}
func formatInt(i int64) string {
return strconv.FormatInt(i, 10)
}