aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorColin Cross <ccross@android.com>2019-06-11 11:19:06 -0700
committerLuca Stefani <luca.stefani.ge1@gmail.com>2019-09-04 15:14:54 +0200
commit7cd9f75a4458f4826c1edf916bdd0c3127b4a91b (patch)
treec07527c67ed9e71c91482743796639c3c144bcfb /ui
parent71fb7f3ee21a380f1187de83ae080f95a93f1fb6 (diff)
downloadbuild_soong-7cd9f75a4458f4826c1edf916bdd0c3127b4a91b.tar.gz
build_soong-7cd9f75a4458f4826c1edf916bdd0c3127b4a91b.tar.bz2
build_soong-7cd9f75a4458f4826c1edf916bdd0c3127b4a91b.zip
Support an action table that shows longest running actions
If SOONG_UI_TABLE_HEIGHT is set, enable a new smart terminal display that prints the normal scrolling build history in the top region of the screen and an action table of the longest currently running actions in the bottom region of the screen. This provides better visibility into which are the longest running actions and when the build parallelism is very low. Test: manual Change-Id: I677d7b6b008699febd259110d7f9e0f98d80c535
Diffstat (limited to 'ui')
-rw-r--r--ui/terminal/smart_status.go222
-rw-r--r--ui/terminal/status_test.go6
-rw-r--r--ui/terminal/util.go10
3 files changed, 227 insertions, 11 deletions
diff --git a/ui/terminal/smart_status.go b/ui/terminal/smart_status.go
index 82c04d46..9638cdf7 100644
--- a/ui/terminal/smart_status.go
+++ b/ui/terminal/smart_status.go
@@ -19,13 +19,22 @@ import (
"io"
"os"
"os/signal"
+ "strconv"
"strings"
"sync"
"syscall"
+ "time"
"android/soong/ui/status"
)
+const tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"
+
+type actionTableEntry struct {
+ action *status.Action
+ startTime time.Time
+}
+
type smartStatusOutput struct {
writer io.Writer
formatter formatter
@@ -34,7 +43,14 @@ type smartStatusOutput struct {
haveBlankLine bool
- termWidth int
+ tableMode bool
+ tableHeight int
+ requestedTableHeight int
+ termWidth, termHeight int
+
+ runningActions []actionTableEntry
+ ticker *time.Ticker
+ done chan bool
sigwinch chan os.Signal
sigwinchHandled chan bool
}
@@ -43,17 +59,41 @@ type smartStatusOutput struct {
// current build status similarly to Ninja's built-in terminal
// output.
func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+ tableHeight, _ := strconv.Atoi(os.Getenv(tableHeightEnVar))
+
s := &smartStatusOutput{
writer: w,
formatter: formatter,
haveBlankLine: true,
+ tableMode: tableHeight > 0,
+ requestedTableHeight: tableHeight,
+
+ done: make(chan bool),
sigwinch: make(chan os.Signal),
}
s.updateTermSize()
+ if s.tableMode {
+ // Add empty lines at the bottom of the screen to scroll back the existing history
+ // and make room for the action table.
+ // TODO: read the cursor position to see if the empty lines are necessary?
+ for i := 0; i < s.tableHeight; i++ {
+ fmt.Fprintln(w)
+ }
+
+ // Hide the cursor to prevent seeing it bouncing around
+ fmt.Fprintf(s.writer, ansi.hideCursor())
+
+ // Configure the empty action table
+ s.actionTable()
+
+ // Start a tick to update the action table periodically
+ s.startActionTableTick()
+ }
+
s.startSigwinch()
return s
@@ -77,6 +117,8 @@ func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
}
func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+ startTime := time.Now()
+
str := action.Description
if str == "" {
str = action.Command
@@ -87,6 +129,11 @@ func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Cou
s.lock.Lock()
defer s.lock.Unlock()
+ s.runningActions = append(s.runningActions, actionTableEntry{
+ action: action,
+ startTime: startTime,
+ })
+
s.statusLine(progress + str)
}
@@ -103,6 +150,13 @@ func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts stat
s.lock.Lock()
defer s.lock.Unlock()
+ for i, runningAction := range s.runningActions {
+ if runningAction.action == result.Action {
+ s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
+ break
+ }
+ }
+
if output != "" {
s.statusLine(progress)
s.requestLine()
@@ -119,6 +173,23 @@ func (s *smartStatusOutput) Flush() {
s.stopSigwinch()
s.requestLine()
+
+ s.runningActions = nil
+
+ if s.tableMode {
+ s.stopActionTableTick()
+
+ // Update the table after clearing runningActions to clear it
+ s.actionTable()
+
+ // Reset the scrolling region to the whole terminal
+ fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
+ _, height, _ := termSize(s.writer)
+ // Move the cursor to the top of the now-blank, previously non-scrolling region
+ fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 0))
+ // Turn the cursor back on
+ fmt.Fprintf(s.writer, ansi.showCursor())
+ }
}
func (s *smartStatusOutput) Write(p []byte) (int, error) {
@@ -137,7 +208,7 @@ func (s *smartStatusOutput) requestLine() {
func (s *smartStatusOutput) print(str string) {
if !s.haveBlankLine {
- fmt.Fprint(s.writer, "\r", "\x1b[K")
+ fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
s.haveBlankLine = true
}
fmt.Fprint(s.writer, str)
@@ -160,8 +231,8 @@ func (s *smartStatusOutput) statusLine(str string) {
// Move to the beginning on the line, turn on bold, print the output,
// turn off bold, then clear the rest of the line.
- start := "\r\x1b[1m"
- end := "\x1b[0m\x1b[K"
+ start := "\r" + ansi.bold()
+ end := ansi.regular() + ansi.clearToEndOfLine()
fmt.Fprint(s.writer, start, str, end)
s.haveBlankLine = false
}
@@ -176,12 +247,36 @@ func (s *smartStatusOutput) elide(str string) string {
return str
}
+func (s *smartStatusOutput) startActionTableTick() {
+ s.ticker = time.NewTicker(time.Second)
+ go func() {
+ for {
+ select {
+ case <-s.ticker.C:
+ s.lock.Lock()
+ s.actionTable()
+ s.lock.Unlock()
+ case <-s.done:
+ return
+ }
+ }
+ }()
+}
+
+func (s *smartStatusOutput) stopActionTableTick() {
+ s.ticker.Stop()
+ s.done <- true
+}
+
func (s *smartStatusOutput) startSigwinch() {
signal.Notify(s.sigwinch, syscall.SIGWINCH)
go func() {
for _ = range s.sigwinch {
s.lock.Lock()
s.updateTermSize()
+ if s.tableMode {
+ s.actionTable()
+ }
s.lock.Unlock()
if s.sigwinchHandled != nil {
s.sigwinchHandled <- true
@@ -196,7 +291,122 @@ func (s *smartStatusOutput) stopSigwinch() {
}
func (s *smartStatusOutput) updateTermSize() {
- if w, ok := termWidth(s.writer); ok {
- s.termWidth = w
+ if w, h, ok := termSize(s.writer); ok {
+ firstUpdate := s.termHeight == 0 && s.termWidth == 0
+ oldScrollingHeight := s.termHeight - s.tableHeight
+
+ s.termWidth, s.termHeight = w, h
+
+ if s.tableMode {
+ tableHeight := s.requestedTableHeight
+ if tableHeight > s.termHeight-1 {
+ tableHeight = s.termHeight - 1
+ }
+ s.tableHeight = tableHeight
+
+ scrollingHeight := s.termHeight - s.tableHeight
+
+ if !firstUpdate {
+ // If the scrolling region has changed, attempt to pan the existing text so that it is
+ // not overwritten by the table.
+ if scrollingHeight < oldScrollingHeight {
+ pan := oldScrollingHeight - scrollingHeight
+ if pan > s.tableHeight {
+ pan = s.tableHeight
+ }
+ fmt.Fprint(s.writer, ansi.panDown(pan))
+ }
+ }
+ }
+ }
+}
+
+func (s *smartStatusOutput) actionTable() {
+ scrollingHeight := s.termHeight - s.tableHeight
+
+ // Update the scrolling region in case the height of the terminal changed
+ fmt.Fprint(s.writer, ansi.setScrollingMargins(0, scrollingHeight))
+ // Move the cursor to the first line of the non-scrolling region
+ fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1, 0))
+
+ // Write as many status lines as fit in the table
+ var tableLine int
+ var runningAction actionTableEntry
+ for tableLine, runningAction = range s.runningActions {
+ if tableLine >= s.tableHeight {
+ break
+ }
+
+ seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
+
+ desc := runningAction.action.Description
+ if desc == "" {
+ desc = runningAction.action.Command
+ }
+
+ str := fmt.Sprintf(" %2d:%02d %s", seconds/60, seconds%60, desc)
+ str = s.elide(str)
+ fmt.Fprint(s.writer, str, ansi.clearToEndOfLine())
+ if tableLine < s.tableHeight-1 {
+ fmt.Fprint(s.writer, "\n")
+ }
}
+
+ // Clear any remaining lines in the table
+ for ; tableLine < s.tableHeight; tableLine++ {
+ fmt.Fprint(s.writer, ansi.clearToEndOfLine())
+ if tableLine < s.tableHeight-1 {
+ fmt.Fprint(s.writer, "\n")
+ }
+ }
+
+ // Move the cursor back to the last line of the scrolling region
+ fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 0))
+}
+
+var ansi = ansiImpl{}
+
+type ansiImpl struct{}
+
+func (ansiImpl) clearToEndOfLine() string {
+ return "\x1b[K"
+}
+
+func (ansiImpl) setCursor(row, column int) string {
+ // Direct cursor address
+ return fmt.Sprintf("\x1b[%d;%dH", row, column)
+}
+
+func (ansiImpl) setScrollingMargins(top, bottom int) string {
+ // Set Top and Bottom Margins DECSTBM
+ return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
+}
+
+func (ansiImpl) resetScrollingMargins() string {
+ // Set Top and Bottom Margins DECSTBM
+ return fmt.Sprintf("\x1b[r")
+}
+
+func (ansiImpl) bold() string {
+ return "\x1b[1m"
+}
+
+func (ansiImpl) regular() string {
+ return "\x1b[0m"
+}
+
+func (ansiImpl) showCursor() string {
+ return "\x1b[?25h"
+}
+
+func (ansiImpl) hideCursor() string {
+ return "\x1b[?25l"
+}
+
+func (ansiImpl) panDown(lines int) string {
+ return fmt.Sprintf("\x1b[%dS", lines)
+}
+
+func (ansiImpl) panUp(lines int) string {
+ return fmt.Sprintf("\x1b[%dT", lines)
}
diff --git a/ui/terminal/status_test.go b/ui/terminal/status_test.go
index 106d6515..81aa238b 100644
--- a/ui/terminal/status_test.go
+++ b/ui/terminal/status_test.go
@@ -17,6 +17,7 @@ package terminal
import (
"bytes"
"fmt"
+ "os"
"syscall"
"testing"
@@ -86,8 +87,11 @@ func TestStatusOutput(t *testing.T) {
},
}
+ os.Setenv(tableHeightEnVar, "")
+
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+
t.Run("smart", func(t *testing.T) {
smart := &fakeSmartTerminal{termWidth: 40}
stat := NewStatusOutput(smart, "", false)
@@ -251,6 +255,8 @@ func actionWithOuptutWithAnsiCodes(stat status.StatusOutput) {
}
func TestSmartStatusOutputWidthChange(t *testing.T) {
+ os.Setenv(tableHeightEnVar, "")
+
smart := &fakeSmartTerminal{termWidth: 40}
stat := NewStatusOutput(smart, "", false)
smartStat := stat.(*smartStatusOutput)
diff --git a/ui/terminal/util.go b/ui/terminal/util.go
index 3a11b79b..c9377f15 100644
--- a/ui/terminal/util.go
+++ b/ui/terminal/util.go
@@ -35,7 +35,7 @@ func isSmartTerminal(w io.Writer) bool {
return false
}
-func termWidth(w io.Writer) (int, bool) {
+func termSize(w io.Writer) (width int, height int, ok bool) {
if f, ok := w.(*os.File); ok {
var winsize struct {
ws_row, ws_column uint16
@@ -44,11 +44,11 @@ func termWidth(w io.Writer) (int, bool) {
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
0, 0, 0)
- return int(winsize.ws_column), err == 0
+ return int(winsize.ws_column), int(winsize.ws_row), err == 0
} else if f, ok := w.(*fakeSmartTerminal); ok {
- return f.termWidth, true
+ return f.termWidth, f.termHeight, true
}
- return 0, false
+ return 0, 0, false
}
// stripAnsiEscapes strips ANSI control codes from a byte array in place.
@@ -106,5 +106,5 @@ func stripAnsiEscapes(input []byte) []byte {
type fakeSmartTerminal struct {
bytes.Buffer
- termWidth int
+ termWidth, termHeight int
}