github.com/grailbio/base@v0.0.11/status/stream.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package status
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/signal"
    12  	"syscall"
    13  	"text/tabwriter"
    14  	"time"
    15  )
    16  
    17  const (
    18  	refreshPeriod = 10 * time.Second
    19  	// the smallest interval between reports in "simple" mode
    20  	minSimpleReportingPeriod = time.Minute
    21  )
    22  
    23  var minDisplayInterval = 50 * time.Millisecond
    24  
    25  type result struct {
    26  	n   int
    27  	err error
    28  }
    29  
    30  type kind int
    31  
    32  const (
    33  	noop kind = iota
    34  	write
    35  	stop
    36  )
    37  
    38  type req struct {
    39  	kind kind
    40  	p    []byte
    41  	w    io.Writer
    42  	rc   chan result
    43  }
    44  
    45  type writer struct {
    46  	r Reporter
    47  	w io.Writer
    48  }
    49  
    50  func (w *writer) Write(p []byte) (n int, err error) {
    51  	if len(p) == 0 {
    52  		// To check EOF?
    53  		return w.w.Write(p)
    54  	}
    55  	c := make(chan result, 1)
    56  	w.r <- req{write, p, w.w, c}
    57  	r := <-c
    58  	return r.n, r.err
    59  }
    60  
    61  // Reporter displays regular updates of a Status. When updates are
    62  // displayed on a terminal, each update replaces the previous, so
    63  // only one update remains visible at a time. Otherwise update
    64  // snapshots are written periodically.
    65  type Reporter chan req
    66  
    67  // Wrap returns a writer whose writes are serviced by the reporter
    68  // and written to the underlying writer w. Wrap is used to allow an
    69  // application to write to the same set of file descriptors as are
    70  // used to render status updates. This permits the reporter's
    71  // terminal handling code to properly write log messages while also
    72  // rendering regular status updates.
    73  func (r Reporter) Wrap(w io.Writer) io.Writer {
    74  	return &writer{r: r, w: w}
    75  }
    76  
    77  // Go starts the Reporter's service routine, and will write regular
    78  // updates to the provided writer.
    79  func (r Reporter) Go(w io.Writer, status *Status) {
    80  	if term, err := openTerm(w); err == nil {
    81  		r.displayTerm(w, term, status)
    82  	} else {
    83  		r.displaySimple(w, status)
    84  	}
    85  }
    86  
    87  // Stop halts rendering of status updates; writes to writers
    88  // returned by Wrap are still serviced.
    89  func (r Reporter) Stop() {
    90  	c := make(chan result, 1)
    91  	r <- req{kind: stop, rc: c}
    92  	<-c
    93  }
    94  
    95  // displayTerm updates a status on the terminal w, with terminal
    96  // capabilities as described by term. displayTerm draws the status on
    97  // each update of status, and also at a regular refresh period to
    98  // update task elapsed times. Writes wrapped by the reporter are
    99  // serviced after first clearing the screen. This ensures that these
   100  // writes appear consistently on the screen above a persistent status
   101  // display. displayTerm handles window resize events.
   102  func (r Reporter) displayTerm(w io.Writer, term *term, status *Status) {
   103  	var nlines int
   104  	// TODO(marius): limit the maximum number of subtasks displayed
   105  	// Cursor is always at the end.
   106  	var (
   107  		tick    = time.NewTicker(refreshPeriod)
   108  		stopped bool
   109  		v       = -1
   110  		winch   = make(chan os.Signal, 1)
   111  	)
   112  	signal.Notify(winch, syscall.SIGWINCH)
   113  	defer tick.Stop()
   114  	defer signal.Stop(winch)
   115  	width, height := term.Dim()
   116  	for {
   117  		var req req
   118  		select {
   119  		case v = <-status.Wait(v):
   120  		case req = <-r:
   121  		case <-tick.C:
   122  		case <-winch:
   123  			width, height = term.Dim()
   124  		}
   125  		if nlines > height {
   126  			nlines = height
   127  		}
   128  		for i := 0; i < nlines; i++ {
   129  			term.Move(w, -1)
   130  			term.Clear(w)
   131  		}
   132  		nlines = 0
   133  		switch req.kind {
   134  		case noop:
   135  		case write:
   136  			n, err := req.w.Write(req.p)
   137  			req.rc <- result{n, err}
   138  		case stop:
   139  			// We stop reporting status but keep servicing writes.
   140  			stopped = true
   141  			close(req.rc)
   142  		}
   143  		if stopped {
   144  			continue
   145  		}
   146  		groups := status.Groups()
   147  		if len(groups) == 0 {
   148  			continue
   149  		}
   150  		// Take a snapshot of all the values to be rendered. The 0th value
   151  		// in each group is the group toplevel status. We then accomodate
   152  		// for our height budget by trimming task statuses (oldest first).
   153  		// We coalesce tasks with the same status to the first mention of
   154  		// the task.
   155  		var snapshot [][]Value
   156  		for _, g := range groups {
   157  			v := g.Value()
   158  			tasks := g.Tasks()
   159  			if v.Status == "" && len(tasks) == 0 {
   160  				continue
   161  			}
   162  			statuses := make(map[string]int)
   163  			values := []Value{v}
   164  			for _, task := range tasks {
   165  				value := task.Value()
   166  				key := value.Title + value.Status
   167  				if i, ok := statuses[key]; ok {
   168  					values[i].Count++
   169  					values[i].LastBegin = value.Begin
   170  				} else {
   171  					value.Count = 1
   172  					statuses[key] = len(values)
   173  					values = append(values, value)
   174  				}
   175  			}
   176  			snapshot = append(snapshot, values)
   177  		}
   178  		var n int
   179  		for _, g := range snapshot {
   180  			n += len(g) - 1
   181  		}
   182  		// Always make room for the toplevel status.
   183  		// We also need one extra line for the last newline.
   184  		for n > height-len(snapshot)-1 {
   185  			var (
   186  				mini = -1
   187  				min  time.Time
   188  			)
   189  			for i, g := range snapshot {
   190  				if len(g) > 1 && (mini < 0 || g[1].Begin.Before(min)) {
   191  					min = g[1].Begin
   192  					mini = i
   193  				}
   194  			}
   195  			if mini < 0 {
   196  				// Nothing we can do.
   197  				break
   198  			}
   199  			snapshot[mini] = append(snapshot[mini][:1], snapshot[mini][2:]...)
   200  			n--
   201  		}
   202  		now := time.Now()
   203  		for _, group := range snapshot {
   204  			v, tasks := group[0], group[1:]
   205  			top := fmt.Sprintf("%s: %s", v.Title, v.Status)
   206  			if len(top) > width {
   207  				top = top[:width]
   208  			}
   209  			fmt.Fprintln(w, top)
   210  			nlines++
   211  			tw := tabwriter.NewWriter(w, 2, 4, 2, ' ', 0)
   212  			type row struct{ title, value, elapsed string }
   213  			rows := make([]row, len(tasks))
   214  			var maxtitle, maxvalue, maxtime int
   215  			for i, v := range tasks {
   216  				elapsed := now.Sub(v.Begin)
   217  				row := row{
   218  					title:   v.Title,
   219  					value:   v.Status,
   220  					elapsed: round(elapsed).String(),
   221  				}
   222  				if v.Count > 1 {
   223  					row.title += fmt.Sprintf("[%d]", v.Count)
   224  					if lastElapsed := round(now.Sub(v.LastBegin)); elapsed-lastElapsed > time.Minute {
   225  						row.elapsed = fmt.Sprintf("%s-%s", lastElapsed, row.elapsed)
   226  					}
   227  				}
   228  				maxtitle = max(maxtitle, len(row.title))
   229  				maxvalue = max(maxvalue, len(row.value))
   230  				maxtime = max(maxtime, len(row.elapsed))
   231  				rows[i] = row
   232  			}
   233  			if trim := 2 + maxtitle + 3 + maxvalue + 2 + maxtime - width; trim > 0 {
   234  				if trim > maxvalue {
   235  					trim -= maxvalue
   236  					maxvalue = 0
   237  				} else {
   238  					maxvalue -= trim
   239  					trim = 0
   240  				}
   241  				if trim > 0 && maxtitle > 10 {
   242  					n := maxtitle - trim
   243  					if n < 10 {
   244  						n = 10
   245  					}
   246  					maxtitle = n
   247  				}
   248  			}
   249  			for _, row := range rows {
   250  				fmt.Fprintf(tw, "\t%s:\t%s\t%s\n",
   251  					trim(row.title, maxtitle),
   252  					trim(row.value, maxvalue),
   253  					trim(row.elapsed, maxtime),
   254  				)
   255  				nlines++
   256  			}
   257  			tw.Flush()
   258  		}
   259  	}
   260  }
   261  
   262  // displaySimple writes the provided status to writer w whenever
   263  // the status is updated, but at a minimum interval defined by
   264  // minSimpleReportingPeriod. Writes wrapped by the reporter
   265  // are serviced directly by displaySimple.
   266  func (r Reporter) displaySimple(w io.Writer, status *Status) {
   267  	var (
   268  		stopped    bool
   269  		v          = -1
   270  		lastReport time.Time
   271  		nextReport <-chan time.Time
   272  	)
   273  	for {
   274  		var req req
   275  		select {
   276  		case v = <-status.Wait(v):
   277  		case req = <-r:
   278  		case <-nextReport:
   279  			nextReport = nil
   280  		}
   281  		switch req.kind {
   282  		case noop:
   283  		case write:
   284  			n, err := req.w.Write(req.p)
   285  			req.rc <- result{n, err}
   286  			continue
   287  		case stop:
   288  			stopped = true
   289  			close(req.rc)
   290  			continue
   291  		}
   292  		if stopped {
   293  			continue
   294  		}
   295  		// In this case we're already waiting to report.
   296  		if nextReport != nil {
   297  			continue
   298  		}
   299  		if elapsed := time.Since(lastReport); elapsed < minSimpleReportingPeriod {
   300  			nextReport = time.After(minSimpleReportingPeriod - elapsed)
   301  			continue
   302  		}
   303  		// If writing fails, there's not much we can do besides try again next
   304  		// time.
   305  		_ = status.Marshal(w)
   306  		lastReport = time.Now()
   307  	}
   308  }
   309  
   310  func max(i, j int) int {
   311  	if i > j {
   312  		return i
   313  	}
   314  	return j
   315  }
   316  
   317  func trim(s string, n int) string {
   318  	if len(s) < n {
   319  		return s
   320  	}
   321  	return s[:n]
   322  }
   323  
   324  func round(d time.Duration) time.Duration {
   325  	return d - d%time.Second
   326  }