diff options
Diffstat (limited to 'ui')
34 files changed, 2521 insertions, 434 deletions
diff --git a/ui/build/Android.bp b/ui/build/Android.bp index 1fe5b6f2..a48a314b 100644 --- a/ui/build/Android.bp +++ b/ui/build/Android.bp @@ -30,6 +30,8 @@ bootstrap_go_package { deps: [ "soong-ui-build-paths", "soong-ui-logger", + "soong-ui-status", + "soong-ui-terminal", "soong-ui-tracer", "soong-shared", "soong-finder", @@ -62,13 +64,11 @@ bootstrap_go_package { darwin: { srcs: [ "sandbox_darwin.go", - "util_darwin.go" ], }, linux: { srcs: [ "sandbox_linux.go", - "util_linux.go" ], }, } diff --git a/ui/build/build.go b/ui/build/build.go index acba2759..96cfdbb3 100644 --- a/ui/build/build.go +++ b/ui/build/build.go @@ -105,9 +105,7 @@ func checkCaseSensitivity(ctx Context, config Config) { func help(ctx Context, config Config, what int) { cmd := Command(ctx, config, "help.sh", "build/make/help.sh") cmd.Sandbox = dumpvarsSandbox - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() - cmd.RunOrFatal() + cmd.RunAndPrintOrFatal() } // Build the tree. The 'what' argument can be used to chose which components of diff --git a/ui/build/config_test.go b/ui/build/config_test.go index e4eab944..242e3afb 100644 --- a/ui/build/config_test.go +++ b/ui/build/config_test.go @@ -22,13 +22,14 @@ import ( "testing" "android/soong/ui/logger" + "android/soong/ui/terminal" ) func testContext() Context { return Context{&ContextImpl{ - Context: context.Background(), - Logger: logger.New(&bytes.Buffer{}), - StdioInterface: NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}), + Context: context.Background(), + Logger: logger.New(&bytes.Buffer{}), + Writer: terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})), }} } diff --git a/ui/build/context.go b/ui/build/context.go index 0636631d..c8b00c38 100644 --- a/ui/build/context.go +++ b/ui/build/context.go @@ -16,45 +16,14 @@ package build import ( "context" - "io" - "os" - "time" "android/soong/ui/logger" + "android/soong/ui/status" + "android/soong/ui/terminal" "android/soong/ui/tracer" ) -type StdioInterface interface { - Stdin() io.Reader - Stdout() io.Writer - Stderr() io.Writer -} - -type StdioImpl struct{} - -func (StdioImpl) Stdin() io.Reader { return os.Stdin } -func (StdioImpl) Stdout() io.Writer { return os.Stdout } -func (StdioImpl) Stderr() io.Writer { return os.Stderr } - -var _ StdioInterface = StdioImpl{} - -type customStdio struct { - stdin io.Reader - stdout io.Writer - stderr io.Writer -} - -func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface { - return customStdio{stdin, stdout, stderr} -} - -func (c customStdio) Stdin() io.Reader { return c.stdin } -func (c customStdio) Stdout() io.Writer { return c.stdout } -func (c customStdio) Stderr() io.Writer { return c.stderr } - -var _ StdioInterface = customStdio{} - -// Context combines a context.Context, logger.Logger, and StdIO redirection. +// Context combines a context.Context, logger.Logger, and terminal.Writer. // These all are agnostic of the current build, and may be used for multiple // builds, while the Config objects contain per-build information. type Context struct{ *ContextImpl } @@ -62,7 +31,8 @@ type ContextImpl struct { context.Context logger.Logger - StdioInterface + Writer terminal.Writer + Status *status.Status Thread tracer.Thread Tracer tracer.Tracer @@ -88,28 +58,3 @@ func (c ContextImpl) CompleteTrace(name string, begin, end uint64) { c.Tracer.Complete(name, c.Thread, begin, end) } } - -// ImportNinjaLog imports a .ninja_log file into the tracer. -func (c ContextImpl) ImportNinjaLog(filename string, startOffset time.Time) { - if c.Tracer != nil { - c.Tracer.ImportNinjaLog(c.Thread, filename, startOffset) - } -} - -func (c ContextImpl) IsTerminal() bool { - if term, ok := os.LookupEnv("TERM"); ok { - return term != "dumb" && isTerminal(c.Stdout()) && isTerminal(c.Stderr()) - } - return false -} - -func (c ContextImpl) IsErrTerminal() bool { - if term, ok := os.LookupEnv("TERM"); ok { - return term != "dumb" && isTerminal(c.Stderr()) - } - return false -} - -func (c ContextImpl) TermWidth() (int, bool) { - return termWidth(c.Stdout()) -} diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go index 8429a8a2..06bd74f2 100644 --- a/ui/build/dumpvars.go +++ b/ui/build/dumpvars.go @@ -18,6 +18,8 @@ import ( "bytes" "fmt" "strings" + + "android/soong/ui/status" ) // DumpMakeVars can be used to extract the values of Make variables after the @@ -60,7 +62,7 @@ func dumpMakeVars(ctx Context, config Config, goals, vars []string, write_soong_ } cmd.StartOrFatal() // TODO: error out when Stderr contains any content - katiRewriteOutput(ctx, pipe) + status.KatiReader(ctx.Status.StartTool(), pipe) cmd.WaitOrFatal() ret := make(map[string]string, len(vars)) @@ -175,7 +177,7 @@ func runMakeProductConfig(ctx Context, config Config) { } // Print the banner like make does - fmt.Fprintln(ctx.Stdout(), Banner(make_vars)) + ctx.Writer.Print(Banner(make_vars)) // Populate the environment env := config.Environment() diff --git a/ui/build/exec.go b/ui/build/exec.go index 90fb19de..5c312bcd 100644 --- a/ui/build/exec.go +++ b/ui/build/exec.go @@ -122,3 +122,20 @@ func (c *Cmd) CombinedOutputOrFatal() []byte { c.reportError(err) return ret } + +// RunAndPrintOrFatal will run the command, then after finishing +// print any output, then handling any errors with a call to +// ctx.Fatal +func (c *Cmd) RunAndPrintOrFatal() { + ret, err := c.CombinedOutput() + st := c.ctx.Status.StartTool() + if len(ret) > 0 { + if err != nil { + st.Error(string(ret)) + } else { + st.Print(string(ret)) + } + } + st.Finish() + c.reportError(err) +} diff --git a/ui/build/kati.go b/ui/build/kati.go index 7635c10c..7cfa1cf7 100644 --- a/ui/build/kati.go +++ b/ui/build/kati.go @@ -15,15 +15,14 @@ package build import ( - "bufio" "crypto/md5" "fmt" - "io" "io/ioutil" "path/filepath" - "regexp" "strconv" "strings" + + "android/soong/ui/status" ) var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_") @@ -117,77 +116,10 @@ func runKati(ctx Context, config Config) { cmd.Stderr = cmd.Stdout cmd.StartOrFatal() - katiRewriteOutput(ctx, pipe) + status.KatiReader(ctx.Status.StartTool(), pipe) cmd.WaitOrFatal() } -var katiIncludeRe = regexp.MustCompile(`^(\[\d+/\d+] )?including [^ ]+ ...$`) -var katiLogRe = regexp.MustCompile(`^\*kati\*: `) - -func katiRewriteOutput(ctx Context, pipe io.ReadCloser) { - haveBlankLine := true - smartTerminal := ctx.IsTerminal() - errSmartTerminal := ctx.IsErrTerminal() - - scanner := bufio.NewScanner(pipe) - for scanner.Scan() { - line := scanner.Text() - verbose := katiIncludeRe.MatchString(line) - - // Only put kati debug/stat lines in our verbose log - if katiLogRe.MatchString(line) { - ctx.Verbose(line) - continue - } - - // For verbose lines, write them on the current line without a newline, - // then overwrite them if the next thing we're printing is another - // verbose line. - if smartTerminal && verbose { - // Limit line width to the terminal width, otherwise we'll wrap onto - // another line and we won't delete the previous line. - // - // Run this on every line in case the window has been resized while - // we're printing. This could be optimized to only re-run when we - // get SIGWINCH if it ever becomes too time consuming. - if max, ok := termWidth(ctx.Stdout()); ok { - if len(line) > max { - // Just do a max. Ninja elides the middle, but that's - // more complicated and these lines aren't that important. - line = line[:max] - } - } - - // Move to the beginning on the line, print the output, then clear - // the rest of the line. - fmt.Fprint(ctx.Stdout(), "\r", line, "\x1b[K") - haveBlankLine = false - continue - } else if smartTerminal && !haveBlankLine { - // If we've previously written a verbose message, send a newline to save - // that message instead of overwriting it. - fmt.Fprintln(ctx.Stdout()) - haveBlankLine = true - } else if !errSmartTerminal { - // Most editors display these as garbage, so strip them out. - line = string(stripAnsiEscapes([]byte(line))) - } - - // Assume that non-verbose lines are important enough for stderr - fmt.Fprintln(ctx.Stderr(), line) - } - - // Save our last verbose line. - if !haveBlankLine { - fmt.Fprintln(ctx.Stdout()) - } - - if err := scanner.Err(); err != nil { - ctx.Println("Error from kati parser:", err) - io.Copy(ctx.Stderr(), pipe) - } -} - func runKatiCleanSpec(ctx Context, config Config) { ctx.BeginTrace("kati cleanspec") defer ctx.EndTrace() @@ -220,6 +152,6 @@ func runKatiCleanSpec(ctx Context, config Config) { cmd.Stderr = cmd.Stdout cmd.StartOrFatal() - katiRewriteOutput(ctx, pipe) + status.KatiReader(ctx.Status.StartTool(), pipe) cmd.WaitOrFatal() } diff --git a/ui/build/ninja.go b/ui/build/ninja.go index 96b5e9d6..c48fe0f6 100644 --- a/ui/build/ninja.go +++ b/ui/build/ninja.go @@ -21,15 +21,21 @@ import ( "strconv" "strings" "time" + + "android/soong/ui/status" ) func runNinja(ctx Context, config Config) { ctx.BeginTrace("ninja") defer ctx.EndTrace() + fifo := filepath.Join(config.OutDir(), ".ninja_fifo") + status.NinjaReader(ctx, ctx.Status.StartTool(), fifo) + executable := config.PrebuiltBuildTool("ninja") args := []string{ "-d", "keepdepfile", + fmt.Sprintf("--frontend=cat <&3 >%s", fifo), } args = append(args, config.NinjaArgs()...) @@ -47,9 +53,6 @@ func runNinja(ctx Context, config Config) { args = append(args, "-f", config.CombinedNinjaFile()) - if config.IsVerbose() { - args = append(args, "-v") - } args = append(args, "-w", "dupbuild=err") cmd := Command(ctx, config, "ninja", executable, args...) @@ -66,13 +69,6 @@ func runNinja(ctx Context, config Config) { cmd.Args = append(cmd.Args, strings.Fields(extra)...) } - if _, ok := cmd.Environment.Get("NINJA_STATUS"); !ok { - cmd.Environment.Set("NINJA_STATUS", "[%p %f/%t] ") - } - - cmd.Stdin = ctx.Stdin() - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() logPath := filepath.Join(config.OutDir(), ".ninja_log") ninjaHeartbeatDuration := time.Minute * 5 if overrideText, ok := cmd.Environment.Get("NINJA_HEARTBEAT_INTERVAL"); ok { @@ -99,10 +95,7 @@ func runNinja(ctx Context, config Config) { } }() - startTime := time.Now() - defer ctx.ImportNinjaLog(logPath, startTime) - - cmd.RunOrFatal() + cmd.RunAndPrintOrFatal() } type statusChecker struct { diff --git a/ui/build/soong.go b/ui/build/soong.go index a73082a4..6c94079e 100644 --- a/ui/build/soong.go +++ b/ui/build/soong.go @@ -15,12 +15,15 @@ package build import ( + "fmt" "os" "path/filepath" "strconv" - "time" + "strings" "github.com/google/blueprint/microfactory" + + "android/soong/ui/status" ) func runSoong(ctx Context, config Config) { @@ -41,9 +44,8 @@ func runSoong(ctx Context, config Config) { cmd.Environment.Set("SRCDIR", ".") cmd.Environment.Set("TOPNAME", "Android.bp") cmd.Sandbox = soongSandbox - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() - cmd.RunOrFatal() + + cmd.RunAndPrintOrFatal() }() func() { @@ -56,12 +58,18 @@ func runSoong(ctx Context, config Config) { if _, err := os.Stat(envTool); err == nil { cmd := Command(ctx, config, "soong_env", envTool, envFile) cmd.Sandbox = soongSandbox - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() + + var buf strings.Builder + cmd.Stdout = &buf + cmd.Stderr = &buf if err := cmd.Run(); err != nil { ctx.Verboseln("soong_env failed, forcing manifest regeneration") os.Remove(envFile) } + + if buf.Len() > 0 { + ctx.Verboseln(buf.String()) + } } else { ctx.Verboseln("Missing soong_env tool, forcing manifest regeneration") os.Remove(envFile) @@ -100,22 +108,18 @@ func runSoong(ctx Context, config Config) { ctx.BeginTrace(name) defer ctx.EndTrace() + fifo := filepath.Join(config.OutDir(), ".ninja_fifo") + status.NinjaReader(ctx, ctx.Status.StartTool(), fifo) + cmd := Command(ctx, config, "soong "+name, config.PrebuiltBuildTool("ninja"), "-d", "keepdepfile", "-w", "dupbuild=err", "-j", strconv.Itoa(config.Parallel()), + fmt.Sprintf("--frontend=cat <&3 >%s", fifo), "-f", filepath.Join(config.SoongOutDir(), file)) - if config.IsVerbose() { - cmd.Args = append(cmd.Args, "-v") - } cmd.Sandbox = soongSandbox - cmd.Stdin = ctx.Stdin() - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() - - defer ctx.ImportNinjaLog(filepath.Join(config.OutDir(), ".ninja_log"), time.Now()) - cmd.RunOrFatal() + cmd.RunAndPrintOrFatal() } ninja("minibootstrap", ".minibootstrap/build.ninja") diff --git a/ui/build/util.go b/ui/build/util.go index 96088fe7..0676a860 100644 --- a/ui/build/util.go +++ b/ui/build/util.go @@ -15,13 +15,9 @@ package build import ( - "bytes" - "io" "os" "path/filepath" "strings" - "syscall" - "unsafe" ) func absPath(ctx Context, p string) string { @@ -117,81 +113,3 @@ func decodeKeyValue(str string) (string, string, bool) { } return str[:idx], str[idx+1:], true } - -func isTerminal(w io.Writer) bool { - if f, ok := w.(*os.File); ok { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(), - ioctlGetTermios, uintptr(unsafe.Pointer(&termios)), - 0, 0, 0) - return err == 0 - } - return false -} - -func termWidth(w io.Writer) (int, bool) { - if f, ok := w.(*os.File); ok { - var winsize struct { - ws_row, ws_column uint16 - ws_xpixel, ws_ypixel uint16 - } - _, _, 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 0, false -} - -// stripAnsiEscapes strips ANSI control codes from a byte array in place. -func stripAnsiEscapes(input []byte) []byte { - // read represents the remaining part of input that needs to be processed. - read := input - // write represents where we should be writing in input. - // It will share the same backing store as input so that we make our modifications - // in place. - write := input - - // advance will copy count bytes from read to write and advance those slices - advance := func(write, read []byte, count int) ([]byte, []byte) { - copy(write, read[:count]) - return write[count:], read[count:] - } - - for { - // Find the next escape sequence - i := bytes.IndexByte(read, 0x1b) - // If it isn't found, or if there isn't room for <ESC>[, finish - if i == -1 || i+1 >= len(read) { - copy(write, read) - break - } - - // Not a CSI code, continue searching - if read[i+1] != '[' { - write, read = advance(write, read, i+1) - continue - } - - // Found a CSI code, advance up to the <ESC> - write, read = advance(write, read, i) - - // Find the end of the CSI code - i = bytes.IndexFunc(read, func(r rune) bool { - return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') - }) - if i == -1 { - // We didn't find the end of the code, just remove the rest - i = len(read) - 1 - } - - // Strip off the end marker too - i = i + 1 - - // Skip the reader forward and reduce final length by that amount - read = read[i:] - input = input[:len(input)-i] - } - - return input -} diff --git a/ui/build/util_test.go b/ui/build/util_test.go index 0e0dbdfb..89bfc778 100644 --- a/ui/build/util_test.go +++ b/ui/build/util_test.go @@ -49,48 +49,3 @@ func TestEnsureEmptyDirs(t *testing.T) { ensureEmptyDirectoriesExist(ctx, filepath.Join(tmpDir, "a")) } - -func TestStripAnsiEscapes(t *testing.T) { - testcases := []struct { - input string - output string - }{ - { - "", - "", - }, - { - "This is a test", - "This is a test", - }, - { - "interrupted: \x1b[12", - "interrupted: ", - }, - { - "other \x1bescape \x1b", - "other \x1bescape \x1b", - }, - { // from pretty-error macro - "\x1b[1mart/Android.mk: \x1b[31merror:\x1b[0m\x1b[1m art: test error \x1b[0m", - "art/Android.mk: error: art: test error ", - }, - { // from envsetup.sh make wrapper - "\x1b[0;31m#### make failed to build some targets (2 seconds) ####\x1b[00m", - "#### make failed to build some targets (2 seconds) ####", - }, - { // from clang (via ninja testcase) - "\x1b[1maffixmgr.cxx:286:15: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1musing the result... [-Wparentheses]\x1b[0m", - "affixmgr.cxx:286:15: warning: using the result... [-Wparentheses]", - }, - } - for _, tc := range testcases { - got := string(stripAnsiEscapes([]byte(tc.input))) - if got != tc.output { - t.Errorf("output strings didn't match\n"+ - "input: %#v\n"+ - " want: %#v\n"+ - " got: %#v", tc.input, tc.output, got) - } - } -} diff --git a/ui/status/Android.bp b/ui/status/Android.bp new file mode 100644 index 00000000..76caaef1 --- /dev/null +++ b/ui/status/Android.bp @@ -0,0 +1,42 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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. + +bootstrap_go_package { + name: "soong-ui-status", + pkgPath: "android/soong/ui/status", + deps: [ + "golang-protobuf-proto", + "soong-ui-logger", + "soong-ui-status-ninja_frontend", + ], + srcs: [ + "kati.go", + "log.go", + "ninja.go", + "status.go", + ], + testSrcs: [ + "kati_test.go", + "status_test.go", + ], +} + +bootstrap_go_package { + name: "soong-ui-status-ninja_frontend", + pkgPath: "android/soong/ui/status/ninja_frontend", + deps: ["golang-protobuf-proto"], + srcs: [ + "ninja_frontend/frontend.pb.go", + ], +} diff --git a/ui/status/kati.go b/ui/status/kati.go new file mode 100644 index 00000000..552a9e97 --- /dev/null +++ b/ui/status/kati.go @@ -0,0 +1,138 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 status + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strconv" + "strings" +) + +var katiError = regexp.MustCompile(`^(\033\[1m)?[^ ]+:[0-9]+: (\033\[31m)?error:`) +var katiIncludeRe = regexp.MustCompile(`^(\[(\d+)/(\d+)] )?((including [^ ]+|initializing build system|finishing build rules|writing build rules) ...)$`) +var katiLogRe = regexp.MustCompile(`^\*kati\*: `) +var katiNinjaMissing = regexp.MustCompile("^[^ ]+ is missing, regenerating...$") + +type katiOutputParser struct { + st ToolStatus + + count int + total int + extra int + + action *Action + buf strings.Builder + hasError bool +} + +func (k *katiOutputParser) flushAction() { + if k.action == nil { + return + } + + var err error + if k.hasError { + err = fmt.Errorf("makefile error") + } + + k.st.FinishAction(ActionResult{ + Action: k.action, + Output: k.buf.String(), + Error: err, + }) + + k.buf.Reset() + k.hasError = false +} + +func (k *katiOutputParser) parseLine(line string) { + // Only put kati debug/stat lines in our verbose log + if katiLogRe.MatchString(line) { + k.st.Verbose(line) + return + } + + if matches := katiIncludeRe.FindStringSubmatch(line); len(matches) > 0 { + k.flushAction() + k.count += 1 + + matches := katiIncludeRe.FindStringSubmatch(line) + if matches[2] != "" { + idx, err := strconv.Atoi(matches[2]) + + if err == nil && idx+k.extra != k.count { + k.extra = k.count - idx + k.st.SetTotalActions(k.total + k.extra) + } + } else { + k.extra += 1 + k.st.SetTotalActions(k.total + k.extra) + } + + if matches[3] != "" { + tot, err := strconv.Atoi(matches[3]) + + if err == nil && tot != k.total { + k.total = tot + k.st.SetTotalActions(k.total + k.extra) + } + } + + k.action = &Action{ + Description: matches[4], + } + k.st.StartAction(k.action) + } else if k.action != nil { + if katiError.MatchString(line) { + k.hasError = true + } + k.buf.WriteString(line) + k.buf.WriteString("\n") + } else { + // Before we've started executing actions from Kati + if line == "No need to regenerate ninja file" || katiNinjaMissing.MatchString(line) { + k.st.Status(line) + } else { + k.st.Print(line) + } + } +} + +// KatiReader reads the output from Kati, and turns it into Actions and +// messages that are passed into the ToolStatus API. +func KatiReader(st ToolStatus, pipe io.ReadCloser) { + parser := &katiOutputParser{ + st: st, + } + + scanner := bufio.NewScanner(pipe) + for scanner.Scan() { + parser.parseLine(scanner.Text()) + } + + parser.flushAction() + + if err := scanner.Err(); err != nil { + var buf strings.Builder + io.Copy(&buf, pipe) + st.Print(fmt.Sprintf("Error from kati parser: %s", err)) + st.Print(buf.String()) + } + + st.Finish() +} diff --git a/ui/status/kati_test.go b/ui/status/kati_test.go new file mode 100644 index 00000000..f2cb8132 --- /dev/null +++ b/ui/status/kati_test.go @@ -0,0 +1,175 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 status + +import ( + "testing" +) + +type lastOutput struct { + counterOutput + + action *Action + result ActionResult + + msgLevel MsgLevel + msg string +} + +func (l *lastOutput) StartAction(a *Action, c Counts) { + l.action = a + l.counterOutput.StartAction(a, c) +} +func (l *lastOutput) FinishAction(r ActionResult, c Counts) { + l.result = r + l.counterOutput.FinishAction(r, c) +} +func (l *lastOutput) Message(level MsgLevel, msg string) { + l.msgLevel = level + l.msg = msg +} +func (l *lastOutput) Flush() {} + +func TestKatiNormalCase(t *testing.T) { + status := &Status{} + output := &lastOutput{} + status.AddOutput(output) + + parser := &katiOutputParser{ + st: status.StartTool(), + } + + msg := "*kati*: verbose msg" + parser.parseLine(msg) + output.Expect(t, Counts{}) + + if output.msgLevel != VerboseLvl { + t.Errorf("Expected verbose message, but got %d", output.msgLevel) + } + if output.msg != msg { + t.Errorf("unexpected message contents:\nwant: %q\n got: %q\n", msg, output.msg) + } + + parser.parseLine("out/build-aosp_arm.ninja is missing, regenerating...") + output.Expect(t, Counts{}) + + parser.parseLine("[1/1] initializing build system ...") + output.Expect(t, Counts{ + TotalActions: 1, + RunningActions: 1, + StartedActions: 1, + FinishedActions: 0, + }) + + parser.parseLine("[2/5] including out/soong/Android-aosp_arm.mk ...") + output.Expect(t, Counts{ + TotalActions: 5, + RunningActions: 1, + StartedActions: 2, + FinishedActions: 1, + }) + + parser.parseLine("[3/5] including a ...") + msg = "a random message" + parser.parseLine(msg) + + // Start the next line to flush the previous result + parser.parseLine("[4/5] finishing build rules ...") + + msg += "\n" + if output.result.Output != msg { + t.Errorf("output for action did not match:\nwant: %q\n got: %q\n", msg, output.result.Output) + } + + parser.parseLine("[5/5] writing build rules ...") + parser.parseLine("*kati*: verbose msg") + parser.flushAction() + + if output.result.Output != "" { + t.Errorf("expected no output for last action, but got %q", output.result.Output) + } + + output.Expect(t, Counts{ + TotalActions: 5, + RunningActions: 0, + StartedActions: 5, + FinishedActions: 5, + }) +} + +func TestKatiExtraIncludes(t *testing.T) { + status := &Status{} + output := &lastOutput{} + status.AddOutput(output) + + parser := &katiOutputParser{ + st: status.StartTool(), + } + + parser.parseLine("[1/1] initializing build system ...") + parser.parseLine("[2/5] including out/soong/Android-aosp_arm.mk ...") + output.Expect(t, Counts{ + TotalActions: 5, + RunningActions: 1, + StartedActions: 2, + FinishedActions: 1, + }) + + parser.parseLine("including a ...") + + output.Expect(t, Counts{ + TotalActions: 6, + RunningActions: 1, + StartedActions: 3, + FinishedActions: 2, + }) + + parser.parseLine("including b ...") + + output.Expect(t, Counts{ + TotalActions: 7, + RunningActions: 1, + StartedActions: 4, + FinishedActions: 3, + }) + + parser.parseLine("[3/5] finishing build rules ...") + + output.Expect(t, Counts{ + TotalActions: 7, + RunningActions: 1, + StartedActions: 5, + FinishedActions: 4, + }) +} + +func TestKatiFailOnError(t *testing.T) { + status := &Status{} + output := &lastOutput{} + status.AddOutput(output) + + parser := &katiOutputParser{ + st: status.StartTool(), + } + + parser.parseLine("[1/1] initializing build system ...") + parser.parseLine("[2/5] inclduing out/soong/Android-aosp_arm.mk ...") + parser.parseLine("build/make/tools/Android.mk:19: error: testing") + parser.flushAction() + + if output.result.Error == nil { + t.Errorf("Expected the last action to be marked as an error") + } +} diff --git a/ui/status/log.go b/ui/status/log.go new file mode 100644 index 00000000..921aa440 --- /dev/null +++ b/ui/status/log.go @@ -0,0 +1,136 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 status + +import ( + "android/soong/ui/logger" + "compress/gzip" + "fmt" + "io" + "strings" +) + +type verboseLog struct { + w io.WriteCloser +} + +func NewVerboseLog(log logger.Logger, filename string) StatusOutput { + if !strings.HasSuffix(filename, ".gz") { + filename += ".gz" + } + + f, err := logger.CreateFileWithRotation(filename, 5) + if err != nil { + log.Println("Failed to create verbose log file:", err) + return nil + } + + w := gzip.NewWriter(f) + + return &verboseLog{ + w: w, + } +} + +func (v *verboseLog) StartAction(action *Action, counts Counts) {} + +func (v *verboseLog) FinishAction(result ActionResult, counts Counts) { + cmd := result.Command + if cmd == "" { + cmd = result.Description + } + + fmt.Fprintf(v.w, "[%d/%d] %s\n", counts.FinishedActions, counts.TotalActions, cmd) + + if result.Error != nil { + fmt.Fprintf(v.w, "FAILED: %s\n", strings.Join(result.Outputs, " ")) + } + + if result.Output != "" { + fmt.Fprintln(v.w, result.Output) + } +} + +func (v *verboseLog) Flush() { + v.w.Close() +} + +func (v *verboseLog) Message(level MsgLevel, message string) { + fmt.Fprintf(v.w, "%s%s\n", level.Prefix(), message) +} + +type errorLog struct { + w io.WriteCloser + + empty bool +} + +func NewErrorLog(log logger.Logger, filename string) StatusOutput { + f, err := logger.CreateFileWithRotation(filename, 5) + if err != nil { + log.Println("Failed to create error log file:", err) + return nil + } + + return &errorLog{ + w: f, + empty: true, + } +} + +func (e *errorLog) StartAction(action *Action, counts Counts) {} + +func (e *errorLog) FinishAction(result ActionResult, counts Counts) { + if result.Error == nil { + return + } + + cmd := result.Command + if cmd == "" { + cmd = result.Description + } + + if !e.empty { + fmt.Fprintf(e.w, "\n\n") + } + e.empty = false + + fmt.Fprintf(e.w, "FAILED: %s\n", result.Description) + if len(result.Outputs) > 0 { + fmt.Fprintf(e.w, "Outputs: %s\n", strings.Join(result.Outputs, " ")) + } + fmt.Fprintf(e.w, "Error: %s\n", result.Error) + if result.Command != "" { + fmt.Fprintf(e.w, "Command: %s\n", result.Command) + } + fmt.Fprintf(e.w, "Output:\n%s\n", result.Output) +} + +func (e *errorLog) Flush() { + e.w.Close() +} + +func (e *errorLog) Message(level MsgLevel, message string) { + if level < ErrorLvl { + return + } + + if !e.empty { + fmt.Fprintf(e.w, "\n\n") + } + e.empty = false + + fmt.Fprintf(e.w, "error: %s\n", message) +} diff --git a/ui/status/ninja.go b/ui/status/ninja.go new file mode 100644 index 00000000..4cce6815 --- /dev/null +++ b/ui/status/ninja.go @@ -0,0 +1,153 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 status + +import ( + "bufio" + "fmt" + "io" + "os" + "syscall" + + "github.com/golang/protobuf/proto" + + "android/soong/ui/logger" + "android/soong/ui/status/ninja_frontend" +) + +// NinjaReader reads the protobuf frontend format from ninja and translates it +// into calls on the ToolStatus API. +func NinjaReader(ctx logger.Logger, status ToolStatus, fifo string) { + os.Remove(fifo) + + err := syscall.Mkfifo(fifo, 0666) + if err != nil { + ctx.Fatalf("Failed to mkfifo(%q): %v", fifo, err) + } + + go ninjaReader(ctx, status, fifo) +} + +func ninjaReader(ctx logger.Logger, status ToolStatus, fifo string) { + defer os.Remove(fifo) + + f, err := os.Open(fifo) + if err != nil { + ctx.Fatal("Failed to open fifo:", err) + } + defer f.Close() + + r := bufio.NewReader(f) + + running := map[uint32]*Action{} + + for { + size, err := readVarInt(r) + if err != nil { + if err != io.EOF { + ctx.Println("Got error reading from ninja:", err) + } + return + } + + buf := make([]byte, size) + _, err = io.ReadFull(r, buf) + if err != nil { + if err == io.EOF { + ctx.Printf("Missing message of size %d from ninja\n", size) + } else { + ctx.Fatal("Got error reading from ninja:", err) + } + return + } + + msg := &ninja_frontend.Status{} + err = proto.Unmarshal(buf, msg) + if err != nil { + ctx.Printf("Error reading message from ninja: %v\n", err) + continue + } + + // Ignore msg.BuildStarted + if msg.TotalEdges != nil { + status.SetTotalActions(int(msg.TotalEdges.GetTotalEdges())) + } + if msg.EdgeStarted != nil { + action := &Action{ + Description: msg.EdgeStarted.GetDesc(), + Outputs: msg.EdgeStarted.Outputs, + Command: msg.EdgeStarted.GetCommand(), + } + status.StartAction(action) + running[msg.EdgeStarted.GetId()] = action + } + if msg.EdgeFinished != nil { + if started, ok := running[msg.EdgeFinished.GetId()]; ok { + delete(running, msg.EdgeFinished.GetId()) + + var err error + exitCode := int(msg.EdgeFinished.GetStatus()) + if exitCode != 0 { + err = fmt.Errorf("exited with code: %d", exitCode) + } + + status.FinishAction(ActionResult{ + Action: started, + Output: msg.EdgeFinished.GetOutput(), + Error: err, + }) + } + } + if msg.Message != nil { + message := "ninja: " + msg.Message.GetMessage() + switch msg.Message.GetLevel() { + case ninja_frontend.Status_Message_INFO: + status.Status(message) + case ninja_frontend.Status_Message_WARNING: + status.Print("warning: " + message) + case ninja_frontend.Status_Message_ERROR: + status.Error(message) + default: + status.Print(message) + } + } + if msg.BuildFinished != nil { + status.Finish() + } + } +} + +func readVarInt(r *bufio.Reader) (int, error) { + ret := 0 + shift := uint(0) + + for { + b, err := r.ReadByte() + if err != nil { + return 0, err + } + + ret += int(b&0x7f) << (shift * 7) + if b&0x80 == 0 { + break + } + shift += 1 + if shift > 4 { + return 0, fmt.Errorf("Expected varint32 length-delimited message") + } + } + + return ret, nil +} diff --git a/ui/status/ninja_frontend/README b/ui/status/ninja_frontend/README new file mode 100644 index 00000000..8c4b4510 --- /dev/null +++ b/ui/status/ninja_frontend/README @@ -0,0 +1,3 @@ +This comes from https://android.googlesource.com/platform/external/ninja/+/master/src/frontend.proto + +The only difference is the specification of a go_package. To regenerate frontend.pb.go, run regen.sh. diff --git a/ui/status/ninja_frontend/frontend.pb.go b/ui/status/ninja_frontend/frontend.pb.go new file mode 100644 index 00000000..7c05eed4 --- /dev/null +++ b/ui/status/ninja_frontend/frontend.pb.go @@ -0,0 +1,510 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: frontend.proto + +package ninja_frontend + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Status_Message_Level int32 + +const ( + Status_Message_INFO Status_Message_Level = 0 + Status_Message_WARNING Status_Message_Level = 1 + Status_Message_ERROR Status_Message_Level = 2 +) + +var Status_Message_Level_name = map[int32]string{ + 0: "INFO", + 1: "WARNING", + 2: "ERROR", +} +var Status_Message_Level_value = map[string]int32{ + "INFO": 0, + "WARNING": 1, + "ERROR": 2, +} + +func (x Status_Message_Level) Enum() *Status_Message_Level { + p := new(Status_Message_Level) + *p = x + return p +} +func (x Status_Message_Level) String() string { + return proto.EnumName(Status_Message_Level_name, int32(x)) +} +func (x *Status_Message_Level) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Status_Message_Level_value, data, "Status_Message_Level") + if err != nil { + return err + } + *x = Status_Message_Level(value) + return nil +} +func (Status_Message_Level) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 5, 0} +} + +type Status struct { + TotalEdges *Status_TotalEdges `protobuf:"bytes,1,opt,name=total_edges,json=totalEdges" json:"total_edges,omitempty"` + BuildStarted *Status_BuildStarted `protobuf:"bytes,2,opt,name=build_started,json=buildStarted" json:"build_started,omitempty"` + BuildFinished *Status_BuildFinished `protobuf:"bytes,3,opt,name=build_finished,json=buildFinished" json:"build_finished,omitempty"` + EdgeStarted *Status_EdgeStarted `protobuf:"bytes,4,opt,name=edge_started,json=edgeStarted" json:"edge_started,omitempty"` + EdgeFinished *Status_EdgeFinished `protobuf:"bytes,5,opt,name=edge_finished,json=edgeFinished" json:"edge_finished,omitempty"` + Message *Status_Message `protobuf:"bytes,6,opt,name=message" json:"message,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Status) Reset() { *m = Status{} } +func (m *Status) String() string { return proto.CompactTextString(m) } +func (*Status) ProtoMessage() {} +func (*Status) Descriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0} +} +func (m *Status) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Status.Unmarshal(m, b) +} +func (m *Status) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Status.Marshal(b, m, deterministic) +} +func (dst *Status) XXX_Merge(src proto.Message) { + xxx_messageInfo_Status.Merge(dst, src) +} +func (m *Status) XXX_Size() int { + return xxx_messageInfo_Status.Size(m) +} +func (m *Status) XXX_DiscardUnknown() { + xxx_messageInfo_Status.DiscardUnknown(m) +} + +var xxx_messageInfo_Status proto.InternalMessageInfo + +func (m *Status) GetTotalEdges() *Status_TotalEdges { + if m != nil { + return m.TotalEdges + } + return nil +} + +func (m *Status) GetBuildStarted() *Status_BuildStarted { + if m != nil { + return m.BuildStarted + } + return nil +} + +func (m *Status) GetBuildFinished() *Status_BuildFinished { + if m != nil { + return m.BuildFinished + } + return nil +} + +func (m *Status) GetEdgeStarted() *Status_EdgeStarted { + if m != nil { + return m.EdgeStarted + } + return nil +} + +func (m *Status) GetEdgeFinished() *Status_EdgeFinished { + if m != nil { + return m.EdgeFinished + } + return nil +} + +func (m *Status) GetMessage() *Status_Message { + if m != nil { + return m.Message + } + return nil +} + +type Status_TotalEdges struct { + // New value for total edges in the build. + TotalEdges *uint32 `protobuf:"varint,1,opt,name=total_edges,json=totalEdges" json:"total_edges,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Status_TotalEdges) Reset() { *m = Status_TotalEdges{} } +func (m *Status_TotalEdges) String() string { return proto.CompactTextString(m) } +func (*Status_TotalEdges) ProtoMessage() {} +func (*Status_TotalEdges) Descriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 0} +} +func (m *Status_TotalEdges) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Status_TotalEdges.Unmarshal(m, b) +} +func (m *Status_TotalEdges) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Status_TotalEdges.Marshal(b, m, deterministic) +} +func (dst *Status_TotalEdges) XXX_Merge(src proto.Message) { + xxx_messageInfo_Status_TotalEdges.Merge(dst, src) +} +func (m *Status_TotalEdges) XXX_Size() int { + return xxx_messageInfo_Status_TotalEdges.Size(m) +} +func (m *Status_TotalEdges) XXX_DiscardUnknown() { + xxx_messageInfo_Status_TotalEdges.DiscardUnknown(m) +} + +var xxx_messageInfo_Status_TotalEdges proto.InternalMessageInfo + +func (m *Status_TotalEdges) GetTotalEdges() uint32 { + if m != nil && m.TotalEdges != nil { + return *m.TotalEdges + } + return 0 +} + +type Status_BuildStarted struct { + // Number of jobs Ninja will run in parallel. + Parallelism *uint32 `protobuf:"varint,1,opt,name=parallelism" json:"parallelism,omitempty"` + // Verbose value passed to ninja. + Verbose *bool `protobuf:"varint,2,opt,name=verbose" json:"verbose,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Status_BuildStarted) Reset() { *m = Status_BuildStarted{} } +func (m *Status_BuildStarted) String() string { return proto.CompactTextString(m) } +func (*Status_BuildStarted) ProtoMessage() {} +func (*Status_BuildStarted) Descriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 1} +} +func (m *Status_BuildStarted) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Status_BuildStarted.Unmarshal(m, b) +} +func (m *Status_BuildStarted) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Status_BuildStarted.Marshal(b, m, deterministic) +} +func (dst *Status_BuildStarted) XXX_Merge(src proto.Message) { + xxx_messageInfo_Status_BuildStarted.Merge(dst, src) +} +func (m *Status_BuildStarted) XXX_Size() int { + return xxx_messageInfo_Status_BuildStarted.Size(m) +} +func (m *Status_BuildStarted) XXX_DiscardUnknown() { + xxx_messageInfo_Status_BuildStarted.DiscardUnknown(m) +} + +var xxx_messageInfo_Status_BuildStarted proto.InternalMessageInfo + +func (m *Status_BuildStarted) GetParallelism() uint32 { + if m != nil && m.Parallelism != nil { + return *m.Parallelism + } + return 0 +} + +func (m *Status_BuildStarted) GetVerbose() bool { + if m != nil && m.Verbose != nil { + return *m.Verbose + } + return false +} + +type Status_BuildFinished struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Status_BuildFinished) Reset() { *m = Status_BuildFinished{} } +func (m *Status_BuildFinished) String() string { return proto.CompactTextString(m) } +func (*Status_BuildFinished) ProtoMessage() {} +func (*Status_BuildFinished) Descriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 2} +} +func (m *Status_BuildFinished) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Status_BuildFinished.Unmarshal(m, b) +} +func (m *Status_BuildFinished) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Status_BuildFinished.Marshal(b, m, deterministic) +} +func (dst *Status_BuildFinished) XXX_Merge(src proto.Message) { + xxx_messageInfo_Status_BuildFinished.Merge(dst, src) +} +func (m *Status_BuildFinished) XXX_Size() int { + return xxx_messageInfo_Status_BuildFinished.Size(m) +} +func (m *Status_BuildFinished) XXX_DiscardUnknown() { + xxx_messageInfo_Status_BuildFinished.DiscardUnknown(m) +} + +var xxx_messageInfo_Status_BuildFinished proto.InternalMessageInfo + +type Status_EdgeStarted struct { + // Edge identification number, unique to a Ninja run. + Id *uint32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"` + // Edge start time in milliseconds since Ninja started. + StartTime *uint32 `protobuf:"varint,2,opt,name=start_time,json=startTime" json:"start_time,omitempty"` + // List of edge inputs. + Inputs []string `protobuf:"bytes,3,rep,name=inputs" json:"inputs,omitempty"` + // List of edge outputs. + Outputs []string `protobuf:"bytes,4,rep,name=outputs" json:"outputs,omitempty"` + // Description field from the edge. + Desc *string `protobuf:"bytes,5,opt,name=desc" json:"desc,omitempty"` + // Command field from the edge. + Command *string `protobuf:"bytes,6,opt,name=command" json:"command,omitempty"` + // Edge uses console. + Console *bool `protobuf:"varint,7,opt,name=console" json:"console,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Status_EdgeStarted) Reset() { *m = Status_EdgeStarted{} } +func (m *Status_EdgeStarted) String() string { return proto.CompactTextString(m) } +func (*Status_EdgeStarted) ProtoMessage() {} +func (*Status_EdgeStarted) Descriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 3} +} +func (m *Status_EdgeStarted) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Status_EdgeStarted.Unmarshal(m, b) +} +func (m *Status_EdgeStarted) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Status_EdgeStarted.Marshal(b, m, deterministic) +} +func (dst *Status_EdgeStarted) XXX_Merge(src proto.Message) { + xxx_messageInfo_Status_EdgeStarted.Merge(dst, src) +} +func (m *Status_EdgeStarted) XXX_Size() int { + return xxx_messageInfo_Status_EdgeStarted.Size(m) +} +func (m *Status_EdgeStarted) XXX_DiscardUnknown() { + xxx_messageInfo_Status_EdgeStarted.DiscardUnknown(m) +} + +var xxx_messageInfo_Status_EdgeStarted proto.InternalMessageInfo + +func (m *Status_EdgeStarted) GetId() uint32 { + if m != nil && m.Id != nil { + return *m.Id + } + return 0 +} + +func (m *Status_EdgeStarted) GetStartTime() uint32 { + if m != nil && m.StartTime != nil { + return *m.StartTime + } + return 0 +} + +func (m *Status_EdgeStarted) GetInputs() []string { + if m != nil { + return m.Inputs + } + return nil +} + +func (m *Status_EdgeStarted) GetOutputs() []string { + if m != nil { + return m.Outputs + } + return nil +} + +func (m *Status_EdgeStarted) GetDesc() string { + if m != nil && m.Desc != nil { + return *m.Desc + } + return "" +} + +func (m *Status_EdgeStarted) GetCommand() string { + if m != nil && m.Command != nil { + return *m.Command + } + return "" +} + +func (m *Status_EdgeStarted) GetConsole() bool { + if m != nil && m.Console != nil { + return *m.Console + } + return false +} + +type Status_EdgeFinished struct { + // Edge identification number, unique to a Ninja run. + Id *uint32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"` + // Edge end time in milliseconds since Ninja started. + EndTime *uint32 `protobuf:"varint,2,opt,name=end_time,json=endTime" json:"end_time,omitempty"` + // Exit status (0 for success). + Status *int32 `protobuf:"zigzag32,3,opt,name=status" json:"status,omitempty"` + // Edge output, may contain ANSI codes. + Output *string `protobuf:"bytes,4,opt,name=output" json:"output,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Status_EdgeFinished) Reset() { *m = Status_EdgeFinished{} } +func (m *Status_EdgeFinished) String() string { return proto.CompactTextString(m) } +func (*Status_EdgeFinished) ProtoMessage() {} +func (*Status_EdgeFinished) Descriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 4} +} +func (m *Status_EdgeFinished) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Status_EdgeFinished.Unmarshal(m, b) +} +func (m *Status_EdgeFinished) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Status_EdgeFinished.Marshal(b, m, deterministic) +} +func (dst *Status_EdgeFinished) XXX_Merge(src proto.Message) { + xxx_messageInfo_Status_EdgeFinished.Merge(dst, src) +} +func (m *Status_EdgeFinished) XXX_Size() int { + return xxx_messageInfo_Status_EdgeFinished.Size(m) +} +func (m *Status_EdgeFinished) XXX_DiscardUnknown() { + xxx_messageInfo_Status_EdgeFinished.DiscardUnknown(m) +} + +var xxx_messageInfo_Status_EdgeFinished proto.InternalMessageInfo + +func (m *Status_EdgeFinished) GetId() uint32 { + if m != nil && m.Id != nil { + return *m.Id + } + return 0 +} + +func (m *Status_EdgeFinished) GetEndTime() uint32 { + if m != nil && m.EndTime != nil { + return *m.EndTime + } + return 0 +} + +func (m *Status_EdgeFinished) GetStatus() int32 { + if m != nil && m.Status != nil { + return *m.Status + } + return 0 +} + +func (m *Status_EdgeFinished) GetOutput() string { + if m != nil && m.Output != nil { + return *m.Output + } + return "" +} + +type Status_Message struct { + // Message priority level (INFO, WARNING, or ERROR). + Level *Status_Message_Level `protobuf:"varint,1,opt,name=level,enum=ninja.Status_Message_Level,def=0" json:"level,omitempty"` + // Info/warning/error message from Ninja. + Message *string `protobuf:"bytes,2,opt,name=message" json:"message,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Status_Message) Reset() { *m = Status_Message{} } +func (m *Status_Message) String() string { return proto.CompactTextString(m) } +func (*Status_Message) ProtoMessage() {} +func (*Status_Message) Descriptor() ([]byte, []int) { + return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 5} +} +func (m *Status_Message) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Status_Message.Unmarshal(m, b) +} +func (m *Status_Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Status_Message.Marshal(b, m, deterministic) +} +func (dst *Status_Message) XXX_Merge(src proto.Message) { + xxx_messageInfo_Status_Message.Merge(dst, src) +} +func (m *Status_Message) XXX_Size() int { + return xxx_messageInfo_Status_Message.Size(m) +} +func (m *Status_Message) XXX_DiscardUnknown() { + xxx_messageInfo_Status_Message.DiscardUnknown(m) +} + +var xxx_messageInfo_Status_Message proto.InternalMessageInfo + +const Default_Status_Message_Level Status_Message_Level = Status_Message_INFO + +func (m *Status_Message) GetLevel() Status_Message_Level { + if m != nil && m.Level != nil { + return *m.Level + } + return Default_Status_Message_Level +} + +func (m *Status_Message) GetMessage() string { + if m != nil && m.Message != nil { + return *m.Message + } + return "" +} + +func init() { + proto.RegisterType((*Status)(nil), "ninja.Status") + proto.RegisterType((*Status_TotalEdges)(nil), "ninja.Status.TotalEdges") + proto.RegisterType((*Status_BuildStarted)(nil), "ninja.Status.BuildStarted") + proto.RegisterType((*Status_BuildFinished)(nil), "ninja.Status.BuildFinished") + proto.RegisterType((*Status_EdgeStarted)(nil), "ninja.Status.EdgeStarted") + proto.RegisterType((*Status_EdgeFinished)(nil), "ninja.Status.EdgeFinished") + proto.RegisterType((*Status_Message)(nil), "ninja.Status.Message") + proto.RegisterEnum("ninja.Status_Message_Level", Status_Message_Level_name, Status_Message_Level_value) +} + +func init() { proto.RegisterFile("frontend.proto", fileDescriptor_frontend_5a49d9b15a642005) } + +var fileDescriptor_frontend_5a49d9b15a642005 = []byte{ + // 496 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0xd1, 0x6e, 0xd3, 0x30, + 0x14, 0xa5, 0x69, 0xd3, 0x34, 0x37, 0x6d, 0x28, 0x96, 0x40, 0x59, 0x10, 0xa2, 0xda, 0xd3, 0x78, + 0x20, 0x48, 0xbc, 0x20, 0x10, 0x12, 0xa2, 0xd2, 0x06, 0x43, 0xd0, 0x49, 0xde, 0x24, 0x24, 0x5e, + 0xaa, 0x74, 0xf6, 0x86, 0x51, 0xe2, 0x54, 0xb1, 0xbb, 0x5f, 0xe0, 0x7f, 0x78, 0xe0, 0xfb, 0x90, + 0xaf, 0xed, 0x2c, 0x65, 0x7b, 0xcb, 0xf1, 0x3d, 0xe7, 0xde, 0x73, 0x8f, 0x1d, 0x48, 0xaf, 0xda, + 0x46, 0x6a, 0x2e, 0x59, 0xb1, 0x6d, 0x1b, 0xdd, 0x90, 0x50, 0x0a, 0xf9, 0xab, 0x3c, 0xfc, 0x13, + 0xc1, 0xf8, 0x5c, 0x97, 0x7a, 0xa7, 0xc8, 0x5b, 0x48, 0x74, 0xa3, 0xcb, 0x6a, 0xcd, 0xd9, 0x35, + 0x57, 0xd9, 0x60, 0x31, 0x38, 0x4a, 0x5e, 0x67, 0x05, 0xf2, 0x0a, 0xcb, 0x29, 0x2e, 0x0c, 0xe1, + 0xd8, 0xd4, 0x29, 0xe8, 0xee, 0x9b, 0x7c, 0x80, 0xd9, 0x66, 0x27, 0x2a, 0xb6, 0x56, 0xba, 0x6c, + 0x35, 0x67, 0x59, 0x80, 0xe2, 0x7c, 0x5f, 0xbc, 0x34, 0x94, 0x73, 0xcb, 0xa0, 0xd3, 0x4d, 0x0f, + 0x91, 0x25, 0xa4, 0xb6, 0xc1, 0x95, 0x90, 0x42, 0xfd, 0xe4, 0x2c, 0x1b, 0x62, 0x87, 0xa7, 0xf7, + 0x74, 0x38, 0x71, 0x14, 0x6a, 0x67, 0x7a, 0x48, 0xde, 0xc3, 0xd4, 0x38, 0xef, 0x3c, 0x8c, 0xb0, + 0xc3, 0xc1, 0x7e, 0x07, 0xe3, 0xd7, 0x5b, 0x48, 0xf8, 0x2d, 0x30, 0x2b, 0xa0, 0xba, 0x33, 0x10, + 0xde, 0xb7, 0x82, 0x91, 0x77, 0xf3, 0x71, 0x5c, 0x37, 0xfe, 0x15, 0x44, 0x35, 0x57, 0xaa, 0xbc, + 0xe6, 0xd9, 0x18, 0xa5, 0x8f, 0xf7, 0xa5, 0xdf, 0x6c, 0x91, 0x7a, 0x56, 0xfe, 0x12, 0xe0, 0x36, + 0x4e, 0xf2, 0xfc, 0x6e, 0xfa, 0xb3, 0x7e, 0xc6, 0xf9, 0x17, 0x98, 0xf6, 0x03, 0x24, 0x0b, 0x48, + 0xb6, 0x65, 0x5b, 0x56, 0x15, 0xaf, 0x84, 0xaa, 0x9d, 0xa0, 0x7f, 0x44, 0x32, 0x88, 0x6e, 0x78, + 0xbb, 0x69, 0x14, 0xc7, 0xfb, 0x98, 0x50, 0x0f, 0xf3, 0x87, 0x30, 0xdb, 0x8b, 0x32, 0xff, 0x3b, + 0x80, 0xa4, 0x17, 0x0d, 0x49, 0x21, 0x10, 0xcc, 0xf5, 0x0c, 0x04, 0x23, 0xcf, 0x00, 0x30, 0xd6, + 0xb5, 0x16, 0xb5, 0xed, 0x36, 0xa3, 0x31, 0x9e, 0x5c, 0x88, 0x9a, 0x93, 0x27, 0x30, 0x16, 0x72, + 0xbb, 0xd3, 0x2a, 0x1b, 0x2e, 0x86, 0x47, 0x31, 0x75, 0xc8, 0x38, 0x68, 0x76, 0x1a, 0x0b, 0x23, + 0x2c, 0x78, 0x48, 0x08, 0x8c, 0x18, 0x57, 0x97, 0x98, 0x72, 0x4c, 0xf1, 0xdb, 0xb0, 0x2f, 0x9b, + 0xba, 0x2e, 0x25, 0xc3, 0x04, 0x63, 0xea, 0xa1, 0xad, 0x48, 0xd5, 0x54, 0x3c, 0x8b, 0xec, 0x26, + 0x0e, 0xe6, 0x02, 0xa6, 0xfd, 0x3b, 0xb9, 0x63, 0xfc, 0x00, 0x26, 0x5c, 0xb2, 0xbe, 0xed, 0x88, + 0x4b, 0xe6, 0x4d, 0x2b, 0xbc, 0x1a, 0x7c, 0x6b, 0x8f, 0xa8, 0x43, 0xe6, 0xdc, 0xba, 0xc4, 0x17, + 0x14, 0x53, 0x87, 0xf2, 0xdf, 0x03, 0x88, 0xdc, 0x25, 0x92, 0x37, 0x10, 0x56, 0xfc, 0x86, 0x57, + 0x38, 0x29, 0xfd, 0xff, 0x99, 0x3a, 0x56, 0xf1, 0xd5, 0x50, 0xde, 0x8d, 0x4e, 0x57, 0x27, 0x67, + 0xd4, 0xf2, 0xcd, 0x26, 0xfe, 0x95, 0x04, 0x76, 0x47, 0x07, 0x0f, 0x5f, 0x40, 0x88, 0x7c, 0x32, + 0x01, 0x54, 0xcc, 0x1f, 0x90, 0x04, 0xa2, 0xef, 0x1f, 0xe9, 0xea, 0x74, 0xf5, 0x69, 0x3e, 0x20, + 0x31, 0x84, 0xc7, 0x94, 0x9e, 0xd1, 0x79, 0xb0, 0x24, 0x9f, 0x87, 0x3f, 0x52, 0x9c, 0xb8, 0xf6, + 0x7f, 0xf5, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2e, 0x8c, 0xef, 0xcb, 0xe0, 0x03, 0x00, 0x00, +} diff --git a/ui/status/ninja_frontend/frontend.proto b/ui/status/ninja_frontend/frontend.proto new file mode 100644 index 00000000..13fd535d --- /dev/null +++ b/ui/status/ninja_frontend/frontend.proto @@ -0,0 +1,84 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package ninja; +option go_package = "ninja_frontend"; + +message Status { + message TotalEdges { + // New value for total edges in the build. + optional uint32 total_edges = 1; + } + + message BuildStarted { + // Number of jobs Ninja will run in parallel. + optional uint32 parallelism = 1; + // Verbose value passed to ninja. + optional bool verbose = 2; + } + + message BuildFinished { + } + + message EdgeStarted { + // Edge identification number, unique to a Ninja run. + optional uint32 id = 1; + // Edge start time in milliseconds since Ninja started. + optional uint32 start_time = 2; + // List of edge inputs. + repeated string inputs = 3; + // List of edge outputs. + repeated string outputs = 4; + // Description field from the edge. + optional string desc = 5; + // Command field from the edge. + optional string command = 6; + // Edge uses console. + optional bool console = 7; + } + + message EdgeFinished { + // Edge identification number, unique to a Ninja run. + optional uint32 id = 1; + // Edge end time in milliseconds since Ninja started. + optional uint32 end_time = 2; + // Exit status (0 for success). + optional sint32 status = 3; + // Edge output, may contain ANSI codes. + optional string output = 4; + } + + message Message { + enum Level { + INFO = 0; + WARNING = 1; + ERROR = 2; + } + // Message priority level (INFO, WARNING, or ERROR). + optional Level level = 1 [default = INFO]; + // Info/warning/error message from Ninja. + optional string message = 2; + } + + optional TotalEdges total_edges = 1; + optional BuildStarted build_started = 2; + optional BuildFinished build_finished = 3; + optional EdgeStarted edge_started = 4; + optional EdgeFinished edge_finished = 5; + optional Message message = 6; +} diff --git a/ui/status/ninja_frontend/regen.sh b/ui/status/ninja_frontend/regen.sh new file mode 100755 index 00000000..d270731a --- /dev/null +++ b/ui/status/ninja_frontend/regen.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +aprotoc --go_out=paths=source_relative:. frontend.proto diff --git a/ui/status/status.go b/ui/status/status.go new file mode 100644 index 00000000..c851d7f5 --- /dev/null +++ b/ui/status/status.go @@ -0,0 +1,340 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 status tracks actions run by various tools, combining the counts +// (total actions, currently running, started, finished), and giving that to +// multiple outputs. +package status + +import ( + "sync" +) + +// Action describes an action taken (or as Ninja calls them, Edges). +type Action struct { + // Description is a shorter, more readable form of the command, meant + // for users. It's optional, but one of either Description or Command + // should be set. + Description string + + // Outputs is the (optional) list of outputs. Usually these are files, + // but they can be any string. + Outputs []string + + // Command is the actual command line executed to perform the action. + // It's optional, but one of either Description or Command should be + // set. + Command string +} + +// ActionResult describes the result of running an Action. +type ActionResult struct { + // Action is a pointer to the original Action struct. + *Action + + // Output is the output produced by the command (usually stdout&stderr + // for Actions that run commands) + Output string + + // Error is nil if the Action succeeded, or set to an error if it + // failed. + Error error +} + +// Counts describes the number of actions in each state +type Counts struct { + // TotalActions is the total number of expected changes. This can + // generally change up or down during a build, but it should never go + // below the number of StartedActions + TotalActions int + + // RunningActions are the number of actions that are currently running + // -- the number that have called StartAction, but not FinishAction. + RunningActions int + + // StartedActions are the number of actions that have been started with + // StartAction. + StartedActions int + + // FinishedActions are the number of actions that have been finished + // with FinishAction. + FinishedActions int +} + +// ToolStatus is the interface used by tools to report on their Actions, and to +// present other information through a set of messaging functions. +type ToolStatus interface { + // SetTotalActions sets the expected total number of actions that will + // be started by this tool. + // + // This call be will ignored if it sets a number that is less than the + // current number of started actions. + SetTotalActions(total int) + + // StartAction specifies that the associated action has been started by + // the tool. + // + // A specific *Action should not be specified to StartAction more than + // once, even if the previous action has already been finished, and the + // contents rewritten. + // + // Do not re-use *Actions between different ToolStatus interfaces + // either. + StartAction(action *Action) + + // FinishAction specifies the result of a particular Action. + // + // The *Action embedded in the ActionResult structure must have already + // been passed to StartAction (on this interface). + // + // Do not call FinishAction twice for the same *Action. + FinishAction(result ActionResult) + + // Verbose takes a non-important message that is never printed to the + // screen, but is in the verbose build log, etc + Verbose(msg string) + // Status takes a less important message that may be printed to the + // screen, but overwritten by another status message. The full message + // will still appear in the verbose build log. + Status(msg string) + // Print takes an message and displays it to the screen and other + // output logs, etc. + Print(msg string) + // Error is similar to Print, but treats it similarly to a failed + // action, showing it in the error logs, etc. + Error(msg string) + + // Finish marks the end of all Actions being run by this tool. + // + // SetTotalEdges, StartAction, and FinishAction should not be called + // after Finish. + Finish() +} + +// MsgLevel specifies the importance of a particular log message. See the +// descriptions in ToolStatus: Verbose, Status, Print, Error. +type MsgLevel int + +const ( + VerboseLvl MsgLevel = iota + StatusLvl + PrintLvl + ErrorLvl +) + +func (l MsgLevel) Prefix() string { + switch l { + case VerboseLvl: + return "verbose: " + case StatusLvl: + return "status: " + case PrintLvl: + return "" + case ErrorLvl: + return "error: " + default: + panic("Unknown message level") + } +} + +// StatusOutput is the interface used to get status information as a Status +// output. +// +// All of the functions here are guaranteed to be called by Status while +// holding it's internal lock, so it's safe to assume a single caller at any +// time, and that the ordering of calls will be correct. It is not safe to call +// back into the Status, or one of its ToolStatus interfaces. +type StatusOutput interface { + // StartAction will be called once every time ToolStatus.StartAction is + // called. counts will include the current counters across all + // ToolStatus instances, including ones that have been finished. + StartAction(action *Action, counts Counts) + + // FinishAction will be called once every time ToolStatus.FinishAction + // is called. counts will include the current counters across all + // ToolStatus instances, including ones that have been finished. + FinishAction(result ActionResult, counts Counts) + + // Message is the equivalent of ToolStatus.Verbose/Status/Print/Error, + // but the level is specified as an argument. + Message(level MsgLevel, msg string) + + // Flush is called when your outputs should be flushed / closed. No + // output is expected after this call. + Flush() +} + +// Status is the multiplexer / accumulator between ToolStatus instances (via +// StartTool) and StatusOutputs (via AddOutput). There's generally one of these +// per build process (though tools like multiproduct_kati may have multiple +// independent versions). +type Status struct { + counts Counts + outputs []StatusOutput + + // Protects counts and outputs, and allows each output to + // expect only a single caller at a time. + lock sync.Mutex +} + +// AddOutput attaches an output to this object. It's generally expected that an +// output is attached to a single Status instance. +func (s *Status) AddOutput(output StatusOutput) { + if output == nil { + return + } + + s.lock.Lock() + defer s.lock.Unlock() + + s.outputs = append(s.outputs, output) +} + +// StartTool returns a new ToolStatus instance to report the status of a tool. +func (s *Status) StartTool() ToolStatus { + return &toolStatus{ + status: s, + } +} + +// Finish will call Flush on all the outputs, generally flushing or closing all +// of their outputs. Do not call any other functions on this instance or any +// associated ToolStatus instances after this has been called. +func (s *Status) Finish() { + s.lock.Lock() + defer s.lock.Unlock() + + for _, o := range s.outputs { + o.Flush() + } +} + +func (s *Status) updateTotalActions(diff int) { + s.lock.Lock() + defer s.lock.Unlock() + + s.counts.TotalActions += diff +} + +func (s *Status) startAction(action *Action) { + s.lock.Lock() + defer s.lock.Unlock() + + s.counts.RunningActions += 1 + s.counts.StartedActions += 1 + + for _, o := range s.outputs { + o.StartAction(action, s.counts) + } +} + +func (s *Status) finishAction(result ActionResult) { + s.lock.Lock() + defer s.lock.Unlock() + + s.counts.RunningActions -= 1 + s.counts.FinishedActions += 1 + + for _, o := range s.outputs { + o.FinishAction(result, s.counts) + } +} + +func (s *Status) message(level MsgLevel, msg string) { + s.lock.Lock() + defer s.lock.Unlock() + + for _, o := range s.outputs { + o.Message(level, msg) + } +} + +type toolStatus struct { + status *Status + + counts Counts + // Protects counts + lock sync.Mutex +} + +var _ ToolStatus = (*toolStatus)(nil) + +func (d *toolStatus) SetTotalActions(total int) { + diff := 0 + + d.lock.Lock() + if total >= d.counts.StartedActions && total != d.counts.TotalActions { + diff = total - d.counts.TotalActions + d.counts.TotalActions = total + } + d.lock.Unlock() + + if diff != 0 { + d.status.updateTotalActions(diff) + } +} + +func (d *toolStatus) StartAction(action *Action) { + totalDiff := 0 + + d.lock.Lock() + d.counts.RunningActions += 1 + d.counts.StartedActions += 1 + + if d.counts.StartedActions > d.counts.TotalActions { + totalDiff = d.counts.StartedActions - d.counts.TotalActions + d.counts.TotalActions = d.counts.StartedActions + } + d.lock.Unlock() + + if totalDiff != 0 { + d.status.updateTotalActions(totalDiff) + } + d.status.startAction(action) +} + +func (d *toolStatus) FinishAction(result ActionResult) { + d.lock.Lock() + d.counts.RunningActions -= 1 + d.counts.FinishedActions += 1 + d.lock.Unlock() + + d.status.finishAction(result) +} + +func (d *toolStatus) Verbose(msg string) { + d.status.message(VerboseLvl, msg) +} +func (d *toolStatus) Status(msg string) { + d.status.message(StatusLvl, msg) +} +func (d *toolStatus) Print(msg string) { + d.status.message(PrintLvl, msg) +} +func (d *toolStatus) Error(msg string) { + d.status.message(ErrorLvl, msg) +} + +func (d *toolStatus) Finish() { + d.lock.Lock() + defer d.lock.Unlock() + + if d.counts.TotalActions != d.counts.StartedActions { + d.status.updateTotalActions(d.counts.StartedActions - d.counts.TotalActions) + } + + // TODO: update status to correct running/finished edges? + d.counts.RunningActions = 0 + d.counts.TotalActions = d.counts.StartedActions +} diff --git a/ui/status/status_test.go b/ui/status/status_test.go new file mode 100644 index 00000000..e62785f4 --- /dev/null +++ b/ui/status/status_test.go @@ -0,0 +1,166 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 status + +import "testing" + +type counterOutput Counts + +func (c *counterOutput) StartAction(action *Action, counts Counts) { + *c = counterOutput(counts) +} +func (c *counterOutput) FinishAction(result ActionResult, counts Counts) { + *c = counterOutput(counts) +} +func (c counterOutput) Message(level MsgLevel, msg string) {} +func (c counterOutput) Flush() {} + +func (c counterOutput) Expect(t *testing.T, counts Counts) { + if Counts(c) == counts { + return + } + t.Helper() + + if c.TotalActions != counts.TotalActions { + t.Errorf("Expected %d total edges, but got %d", counts.TotalActions, c.TotalActions) + } + if c.RunningActions != counts.RunningActions { + t.Errorf("Expected %d running edges, but got %d", counts.RunningActions, c.RunningActions) + } + if c.StartedActions != counts.StartedActions { + t.Errorf("Expected %d started edges, but got %d", counts.StartedActions, c.StartedActions) + } + if c.FinishedActions != counts.FinishedActions { + t.Errorf("Expected %d finished edges, but got %d", counts.FinishedActions, c.FinishedActions) + } +} + +func TestBasicUse(t *testing.T) { + status := &Status{} + counts := &counterOutput{} + status.AddOutput(counts) + s := status.StartTool() + + s.SetTotalActions(2) + + a := &Action{} + s.StartAction(a) + + counts.Expect(t, Counts{ + TotalActions: 2, + RunningActions: 1, + StartedActions: 1, + FinishedActions: 0, + }) + + s.FinishAction(ActionResult{Action: a}) + + counts.Expect(t, Counts{ + TotalActions: 2, + RunningActions: 0, + StartedActions: 1, + FinishedActions: 1, + }) + + a = &Action{} + s.StartAction(a) + + counts.Expect(t, Counts{ + TotalActions: 2, + RunningActions: 1, + StartedActions: 2, + FinishedActions: 1, + }) + + s.FinishAction(ActionResult{Action: a}) + + counts.Expect(t, Counts{ + TotalActions: 2, + RunningActions: 0, + StartedActions: 2, + FinishedActions: 2, + }) +} + +// For when a tool claims to have 2 actions, but finishes after one. +func TestFinishEarly(t *testing.T) { + status := &Status{} + counts := &counterOutput{} + status.AddOutput(counts) + s := status.StartTool() + + s.SetTotalActions(2) + + a := &Action{} + s.StartAction(a) + s.FinishAction(ActionResult{Action: a}) + s.Finish() + + s = status.StartTool() + s.SetTotalActions(2) + + a = &Action{} + s.StartAction(a) + + counts.Expect(t, Counts{ + TotalActions: 3, + RunningActions: 1, + StartedActions: 2, + FinishedActions: 1, + }) +} + +// For when a tool claims to have 1 action, but starts two. +func TestExtraActions(t *testing.T) { + status := &Status{} + counts := &counterOutput{} + status.AddOutput(counts) + s := status.StartTool() + + s.SetTotalActions(1) + + s.StartAction(&Action{}) + s.StartAction(&Action{}) + + counts.Expect(t, Counts{ + TotalActions: 2, + RunningActions: 2, + StartedActions: 2, + FinishedActions: 0, + }) +} + +// When a tool calls Finish() with a running Action +func TestRunningWhenFinished(t *testing.T) { + status := &Status{} + counts := &counterOutput{} + status.AddOutput(counts) + + s := status.StartTool() + s.SetTotalActions(1) + s.StartAction(&Action{}) + s.Finish() + + s = status.StartTool() + s.SetTotalActions(1) + s.StartAction(&Action{}) + + counts.Expect(t, Counts{ + TotalActions: 2, + RunningActions: 2, + StartedActions: 2, + FinishedActions: 0, + }) +} diff --git a/ui/terminal/Android.bp b/ui/terminal/Android.bp new file mode 100644 index 00000000..7104a504 --- /dev/null +++ b/ui/terminal/Android.bp @@ -0,0 +1,37 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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. + +bootstrap_go_package { + name: "soong-ui-terminal", + pkgPath: "android/soong/ui/terminal", + deps: ["soong-ui-status"], + srcs: [ + "status.go", + "writer.go", + "util.go", + ], + testSrcs: [ + "util_test.go", + ], + darwin: { + srcs: [ + "util_darwin.go", + ], + }, + linux: { + srcs: [ + "util_linux.go", + ], + }, +} diff --git a/ui/terminal/status.go b/ui/terminal/status.go new file mode 100644 index 00000000..5719456f --- /dev/null +++ b/ui/terminal/status.go @@ -0,0 +1,142 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 terminal + +import ( + "fmt" + "strings" + "time" + + "android/soong/ui/status" +) + +type statusOutput struct { + writer Writer + format string + + start time.Time +} + +// NewStatusOutput returns a StatusOutput that represents the +// current build status similarly to Ninja's built-in terminal +// output. +// +// statusFormat takes nearly all the same options as NINJA_STATUS. +// %c is currently unsupported. +func NewStatusOutput(w Writer, statusFormat string) status.StatusOutput { + return &statusOutput{ + writer: w, + format: statusFormat, + + start: time.Now(), + } +} + +func (s *statusOutput) Message(level status.MsgLevel, message string) { + if level > status.StatusLvl { + s.writer.Print(fmt.Sprintf("%s%s", level.Prefix(), message)) + } else if level == status.StatusLvl { + s.writer.StatusLine(message) + } +} + +func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) { + if !s.writer.isSmartTerminal() { + return + } + + str := action.Description + if str == "" { + str = action.Command + } + + s.writer.StatusLine(s.progress(counts) + str) +} + +func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) { + str := result.Description + if str == "" { + str = result.Command + } + + progress := s.progress(counts) + str + + if result.Error != nil { + hasCommand := "" + if result.Command != "" { + hasCommand = "\n" + } + + s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s%s%s", + strings.Join(result.Outputs, " "), result.Command, hasCommand, result.Output)) + } else if result.Output != "" { + s.writer.StatusAndMessage(progress, result.Output) + } else { + s.writer.StatusLine(progress) + } +} + +func (s *statusOutput) Flush() {} + +func (s *statusOutput) progress(counts status.Counts) string { + if s.format == "" { + return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions) + } + + buf := &strings.Builder{} + for i := 0; i < len(s.format); i++ { + c := s.format[i] + if c != '%' { + buf.WriteByte(c) + continue + } + + i = i + 1 + if i == len(s.format) { + buf.WriteByte(c) + break + } + + c = s.format[i] + switch c { + case '%': + buf.WriteByte(c) + case 's': + fmt.Fprintf(buf, "%d", counts.StartedActions) + case 't': + fmt.Fprintf(buf, "%d", counts.TotalActions) + case 'r': + fmt.Fprintf(buf, "%d", counts.RunningActions) + case 'u': + fmt.Fprintf(buf, "%d", counts.TotalActions-counts.StartedActions) + case 'f': + fmt.Fprintf(buf, "%d", counts.FinishedActions) + case 'o': + fmt.Fprintf(buf, "%.1f", float64(counts.FinishedActions)/time.Since(s.start).Seconds()) + case 'c': + // TODO: implement? + buf.WriteRune('?') + case 'p': + fmt.Fprintf(buf, "%3d%%", 100*counts.FinishedActions/counts.TotalActions) + case 'e': + fmt.Fprintf(buf, "%.3f", time.Since(s.start).Seconds()) + default: + buf.WriteString("unknown placeholder '") + buf.WriteByte(c) + buf.WriteString("'") + } + } + return buf.String() +} diff --git a/ui/terminal/util.go b/ui/terminal/util.go new file mode 100644 index 00000000..a85a517b --- /dev/null +++ b/ui/terminal/util.go @@ -0,0 +1,101 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// 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 terminal + +import ( + "bytes" + "io" + "os" + "syscall" + "unsafe" +) + +func isTerminal(w io.Writer) bool { + if f, ok := w.(*os.File); ok { + var termios syscall.Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(), + ioctlGetTermios, uintptr(unsafe.Pointer(&termios)), + 0, 0, 0) + return err == 0 + } + return false +} + +func termWidth(w io.Writer) (int, bool) { + if f, ok := w.(*os.File); ok { + var winsize struct { + ws_row, ws_column uint16 + ws_xpixel, ws_ypixel uint16 + } + _, _, 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 0, false +} + +// stripAnsiEscapes strips ANSI control codes from a byte array in place. +func stripAnsiEscapes(input []byte) []byte { + // read represents the remaining part of input that needs to be processed. + read := input + // write represents where we should be writing in input. + // It will share the same backing store as input so that we make our modifications + // in place. + write := input + + // advance will copy count bytes from read to write and advance those slices + advance := func(write, read []byte, count int) ([]byte, []byte) { + copy(write, read[:count]) + return write[count:], read[count:] + } + + for { + // Find the next escape sequence + i := bytes.IndexByte(read, 0x1b) + // If it isn't found, or if there isn't room for <ESC>[, finish + if i == -1 || i+1 >= len(read) { + copy(write, read) + break + } + + // Not a CSI code, continue searching + if read[i+1] != '[' { + write, read = advance(write, read, i+1) + continue + } + + // Found a CSI code, advance up to the <ESC> + write, read = advance(write, read, i) + + // Find the end of the CSI code + i = bytes.IndexFunc(read, func(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') + }) + if i == -1 { + // We didn't find the end of the code, just remove the rest + i = len(read) - 1 + } + + // Strip off the end marker too + i = i + 1 + + // Skip the reader forward and reduce final length by that amount + read = read[i:] + input = input[:len(input)-i] + } + + return input +} diff --git a/ui/build/util_darwin.go b/ui/terminal/util_darwin.go index 254a9b87..109a37f0 100644 --- a/ui/build/util_darwin.go +++ b/ui/terminal/util_darwin.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build +package terminal import ( "syscall" diff --git a/ui/build/util_linux.go b/ui/terminal/util_linux.go index 0a4e1d29..0a3d9dd1 100644 --- a/ui/build/util_linux.go +++ b/ui/terminal/util_linux.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build +package terminal import ( "syscall" diff --git a/ui/terminal/util_test.go b/ui/terminal/util_test.go new file mode 100644 index 00000000..82bde7c5 --- /dev/null +++ b/ui/terminal/util_test.go @@ -0,0 +1,64 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// 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 terminal + +import ( + "testing" +) + +func TestStripAnsiEscapes(t *testing.T) { + testcases := []struct { + input string + output string + }{ + { + "", + "", + }, + { + "This is a test", + "This is a test", + }, + { + "interrupted: \x1b[12", + "interrupted: ", + }, + { + "other \x1bescape \x1b", + "other \x1bescape \x1b", + }, + { // from pretty-error macro + "\x1b[1mart/Android.mk: \x1b[31merror:\x1b[0m\x1b[1m art: test error \x1b[0m", + "art/Android.mk: error: art: test error ", + }, + { // from envsetup.sh make wrapper + "\x1b[0;31m#### make failed to build some targets (2 seconds) ####\x1b[00m", + "#### make failed to build some targets (2 seconds) ####", + }, + { // from clang (via ninja testcase) + "\x1b[1maffixmgr.cxx:286:15: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1musing the result... [-Wparentheses]\x1b[0m", + "affixmgr.cxx:286:15: warning: using the result... [-Wparentheses]", + }, + } + for _, tc := range testcases { + got := string(stripAnsiEscapes([]byte(tc.input))) + if got != tc.output { + t.Errorf("output strings didn't match\n"+ + "input: %#v\n"+ + " want: %#v\n"+ + " got: %#v", tc.input, tc.output, got) + } + } +} diff --git a/ui/terminal/writer.go b/ui/terminal/writer.go new file mode 100644 index 00000000..dd322268 --- /dev/null +++ b/ui/terminal/writer.go @@ -0,0 +1,229 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 terminal provides a set of interfaces that can be used to interact +// with the terminal (including falling back when the terminal is detected to +// be a redirect or other dumb terminal) +package terminal + +import ( + "fmt" + "io" + "os" + "strings" + "sync" +) + +// Writer provides an interface to write temporary and permanent messages to +// the terminal. +// +// The terminal is considered to be a dumb terminal if TERM==dumb, or if a +// terminal isn't detected on stdout/stderr (generally because it's a pipe or +// file). Dumb terminals will strip out all ANSI escape sequences, including +// colors. +type Writer interface { + // Print prints the string to the terminal, overwriting any current + // status being displayed. + // + // On a dumb terminal, the status messages will be kept. + Print(str string) + + // Status prints the first line of the string to the terminal, + // overwriting any previous status line. Strings longer than the width + // of the terminal will be cut off. + // + // On a dumb terminal, previous status messages will remain, and the + // entire first line of the string will be printed. + StatusLine(str string) + + // StatusAndMessage prints the first line of status to the terminal, + // similarly to StatusLine(), then prints the full msg below that. The + // status line is retained. + // + // There is guaranteed to be no other output in between the status and + // message. + StatusAndMessage(status, msg string) + + // Finish ensures that the output ends with a newline (preserving any + // current status line that is current displayed). + // + // This does nothing on dumb terminals. + Finish() + + // Write implements the io.Writer interface. This is primarily so that + // the logger can use this interface to print to stderr without + // breaking the other semantics of this interface. + // + // Try to use any of the other functions if possible. + Write(p []byte) (n int, err error) + + isSmartTerminal() bool +} + +// NewWriter creates a new Writer based on the stdio and the TERM +// environment variable. +func NewWriter(stdio StdioInterface) Writer { + w := &writerImpl{ + stdio: stdio, + + haveBlankLine: true, + } + + if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" { + w.stripEscapes = !isTerminal(stdio.Stderr()) + w.smartTerminal = isTerminal(stdio.Stdout()) && !w.stripEscapes + } + + return w +} + +type writerImpl struct { + stdio StdioInterface + + haveBlankLine bool + + // Protecting the above, we assume that smartTerminal and stripEscapes + // does not change after initial setup. + lock sync.Mutex + + smartTerminal bool + stripEscapes bool +} + +func (w *writerImpl) isSmartTerminal() bool { + return w.smartTerminal +} + +func (w *writerImpl) requestLine() { + if !w.haveBlankLine { + fmt.Fprintln(w.stdio.Stdout()) + w.haveBlankLine = true + } +} + +func (w *writerImpl) Print(str string) { + if w.stripEscapes { + str = string(stripAnsiEscapes([]byte(str))) + } + + w.lock.Lock() + defer w.lock.Unlock() + w.print(str) +} + +func (w *writerImpl) print(str string) { + if !w.haveBlankLine { + fmt.Fprint(w.stdio.Stdout(), "\r", "\x1b[K") + w.haveBlankLine = true + } + fmt.Fprint(w.stdio.Stderr(), str) + if len(str) == 0 || str[len(str)-1] != '\n' { + fmt.Fprint(w.stdio.Stderr(), "\n") + } +} + +func (w *writerImpl) StatusLine(str string) { + w.lock.Lock() + defer w.lock.Unlock() + + w.statusLine(str) +} + +func (w *writerImpl) statusLine(str string) { + if !w.smartTerminal { + fmt.Fprintln(w.stdio.Stdout(), str) + return + } + + idx := strings.IndexRune(str, '\n') + if idx != -1 { + str = str[0:idx] + } + + // Limit line width to the terminal width, otherwise we'll wrap onto + // another line and we won't delete the previous line. + // + // Run this on every line in case the window has been resized while + // we're printing. This could be optimized to only re-run when we get + // SIGWINCH if it ever becomes too time consuming. + if max, ok := termWidth(w.stdio.Stdout()); ok { + if len(str) > max { + // TODO: Just do a max. Ninja elides the middle, but that's + // more complicated and these lines aren't that important. + str = str[:max] + } + } + + // Move to the beginning on the line, print the output, then clear + // the rest of the line. + fmt.Fprint(w.stdio.Stdout(), "\r", str, "\x1b[K") + w.haveBlankLine = false +} + +func (w *writerImpl) StatusAndMessage(status, msg string) { + if w.stripEscapes { + msg = string(stripAnsiEscapes([]byte(msg))) + } + + w.lock.Lock() + defer w.lock.Unlock() + + w.statusLine(status) + w.requestLine() + w.print(msg) +} + +func (w *writerImpl) Finish() { + w.lock.Lock() + defer w.lock.Unlock() + + w.requestLine() +} + +func (w *writerImpl) Write(p []byte) (n int, err error) { + w.Print(string(p)) + return len(p), nil +} + +// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers +type StdioInterface interface { + Stdin() io.Reader + Stdout() io.Writer + Stderr() io.Writer +} + +// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface +type StdioImpl struct{} + +func (StdioImpl) Stdin() io.Reader { return os.Stdin } +func (StdioImpl) Stdout() io.Writer { return os.Stdout } +func (StdioImpl) Stderr() io.Writer { return os.Stderr } + +var _ StdioInterface = StdioImpl{} + +type customStdio struct { + stdin io.Reader + stdout io.Writer + stderr io.Writer +} + +func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface { + return customStdio{stdin, stdout, stderr} +} + +func (c customStdio) Stdin() io.Reader { return c.stdin } +func (c customStdio) Stdout() io.Writer { return c.stdout } +func (c customStdio) Stderr() io.Writer { return c.stderr } + +var _ StdioInterface = customStdio{} diff --git a/ui/tracer/Android.bp b/ui/tracer/Android.bp index 9729c7e5..af588f1d 100644 --- a/ui/tracer/Android.bp +++ b/ui/tracer/Android.bp @@ -15,10 +15,13 @@ bootstrap_go_package { name: "soong-ui-tracer", pkgPath: "android/soong/ui/tracer", - deps: ["soong-ui-logger"], + deps: [ + "soong-ui-logger", + "soong-ui-status", + ], srcs: [ "microfactory.go", - "ninja.go", + "status.go", "tracer.go", ], } diff --git a/ui/tracer/microfactory.go b/ui/tracer/microfactory.go index acb9be4c..c4c37c25 100644 --- a/ui/tracer/microfactory.go +++ b/ui/tracer/microfactory.go @@ -17,10 +17,48 @@ package tracer import ( "bufio" "os" + "sort" "strconv" "strings" ) +type eventEntry struct { + Name string + Begin uint64 + End uint64 +} + +func (t *tracerImpl) importEvents(entries []*eventEntry) { + sort.Slice(entries, func(i, j int) bool { + return entries[i].Begin < entries[j].Begin + }) + + cpus := []uint64{} + for _, entry := range entries { + tid := -1 + for cpu, endTime := range cpus { + if endTime <= entry.Begin { + tid = cpu + cpus[cpu] = entry.End + break + } + } + if tid == -1 { + tid = len(cpus) + cpus = append(cpus, entry.End) + } + + t.writeEvent(&viewerEvent{ + Name: entry.Name, + Phase: "X", + Time: entry.Begin, + Dur: entry.End - entry.Begin, + Pid: 1, + Tid: uint64(tid), + }) + } +} + func (t *tracerImpl) ImportMicrofactoryLog(filename string) { if _, err := os.Stat(filename); err != nil { return diff --git a/ui/tracer/ninja.go b/ui/tracer/ninja.go deleted file mode 100644 index 1980559c..00000000 --- a/ui/tracer/ninja.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// -// 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 tracer - -import ( - "bufio" - "os" - "sort" - "strconv" - "strings" - "time" -) - -type eventEntry struct { - Name string - Begin uint64 - End uint64 -} - -func (t *tracerImpl) importEvents(entries []*eventEntry) { - sort.Slice(entries, func(i, j int) bool { - return entries[i].Begin < entries[j].Begin - }) - - cpus := []uint64{} - for _, entry := range entries { - tid := -1 - for cpu, endTime := range cpus { - if endTime <= entry.Begin { - tid = cpu - cpus[cpu] = entry.End - break - } - } - if tid == -1 { - tid = len(cpus) - cpus = append(cpus, entry.End) - } - - t.writeEvent(&viewerEvent{ - Name: entry.Name, - Phase: "X", - Time: entry.Begin, - Dur: entry.End - entry.Begin, - Pid: 1, - Tid: uint64(tid), - }) - } -} - -// ImportNinjaLog reads a .ninja_log file from ninja and writes the events out -// to the trace. -// -// startOffset is when the ninja process started, and is used to position the -// relative times from the ninja log into the trace. It's also used to skip -// reading the ninja log if nothing was run. -func (t *tracerImpl) ImportNinjaLog(thread Thread, filename string, startOffset time.Time) { - t.Begin("ninja log import", thread) - defer t.End(thread) - - if stat, err := os.Stat(filename); err != nil { - t.log.Println("Missing ninja log:", err) - return - } else if stat.ModTime().Before(startOffset) { - t.log.Verboseln("Ninja log not modified, not importing any entries.") - return - } - - f, err := os.Open(filename) - if err != nil { - t.log.Println("Error opening ninja log:", err) - return - } - defer f.Close() - - s := bufio.NewScanner(f) - header := true - entries := []*eventEntry{} - prevEnd := 0 - offset := uint64(startOffset.UnixNano()) / 1000 - for s.Scan() { - if header { - hdr := s.Text() - if hdr != "# ninja log v5" { - t.log.Printf("Unknown ninja log header: %q", hdr) - return - } - header = false - continue - } - - fields := strings.Split(s.Text(), "\t") - begin, err := strconv.Atoi(fields[0]) - if err != nil { - t.log.Printf("Unable to parse ninja entry %q: %v", s.Text(), err) - return - } - end, err := strconv.Atoi(fields[1]) - if err != nil { - t.log.Printf("Unable to parse ninja entry %q: %v", s.Text(), err) - return - } - if end < prevEnd { - entries = nil - } - prevEnd = end - entries = append(entries, &eventEntry{ - Name: fields[3], - Begin: offset + uint64(begin)*1000, - End: offset + uint64(end)*1000, - }) - } - if err := s.Err(); err != nil { - t.log.Println("Unable to parse ninja log:", err) - return - } - - t.importEvents(entries) -} diff --git a/ui/tracer/status.go b/ui/tracer/status.go new file mode 100644 index 00000000..af50e2d4 --- /dev/null +++ b/ui/tracer/status.go @@ -0,0 +1,87 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// 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 tracer + +import ( + "android/soong/ui/status" + "time" +) + +func (t *tracerImpl) StatusTracer() status.StatusOutput { + return &statusOutput{ + tracer: t, + + running: map[*status.Action]actionStatus{}, + } +} + +type actionStatus struct { + cpu int + start time.Time +} + +type statusOutput struct { + tracer *tracerImpl + + cpus []bool + running map[*status.Action]actionStatus +} + +func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) { + cpu := -1 + for i, busy := range s.cpus { + if !busy { + cpu = i + s.cpus[i] = true + break + } + } + + if cpu == -1 { + cpu = len(s.cpus) + s.cpus = append(s.cpus, true) + } + + s.running[action] = actionStatus{ + cpu: cpu, + start: time.Now(), + } +} + +func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) { + start, ok := s.running[result.Action] + if !ok { + return + } + delete(s.running, result.Action) + s.cpus[start.cpu] = false + + str := result.Action.Description + if len(result.Action.Outputs) > 0 { + str = result.Action.Outputs[0] + } + + s.tracer.writeEvent(&viewerEvent{ + Name: str, + Phase: "X", + Time: uint64(start.start.UnixNano()) / 1000, + Dur: uint64(time.Since(start.start).Nanoseconds()) / 1000, + Pid: 1, + Tid: uint64(start.cpu), + }) +} + +func (s *statusOutput) Flush() {} +func (s *statusOutput) Message(level status.MsgLevel, message string) {} diff --git a/ui/tracer/tracer.go b/ui/tracer/tracer.go index 87050403..b8fc87b2 100644 --- a/ui/tracer/tracer.go +++ b/ui/tracer/tracer.go @@ -31,6 +31,7 @@ import ( "time" "android/soong/ui/logger" + "android/soong/ui/status" ) type Thread uint64 @@ -46,7 +47,8 @@ type Tracer interface { Complete(name string, thread Thread, begin, end uint64) ImportMicrofactoryLog(filename string) - ImportNinjaLog(thread Thread, filename string, startOffset time.Time) + + StatusTracer() status.StatusOutput NewThread(name string) Thread } |