blob: 52ed56a1379e2085f39e69f99c5818a2c585072e [file] [log] [blame] [edit]
// 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"
"time"
)
// 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
// Inputs is the (optional) list of inputs. Usually these are files,
// but they can be any string.
Inputs []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
// ChangedInputs is the (optional) list of inputs that have changed
// since last time this action was run.
ChangedInputs []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
Stats ActionResultStats
}
type ActionResultStats struct {
// Number of milliseconds spent executing in user mode
UserTime uint32
// Number of milliseconds spent executing in kernel mode
SystemTime uint32
// Max resident set size in kB
MaxRssKB uint64
// Minor page faults
MinorPageFaults uint64
// Major page faults
MajorPageFaults uint64
// IO input in kB
IOInputKB uint64
// IO output in kB
IOOutputKB uint64
// Voluntary context switches
VoluntaryContextSwitches uint64
// Involuntary context switches
InvoluntaryContextSwitches uint64
Tags string
}
// 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
EstimatedTime time.Time
}
// 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)
SetEstimatedTime(estimatedTime time.Time)
// 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()
// Write lets StatusOutput implement io.Writer
Write(p []byte) (n int, err error)
}
// 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) SetEstimatedTime(estimatedTime time.Time) {
s.lock.Lock()
defer s.lock.Unlock()
s.counts.EstimatedTime = estimatedTime
}
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)
}
}
func (s *Status) Status(msg string) {
s.message(StatusLvl, 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) SetEstimatedTime(estimatedTime time.Time) {
d.status.SetEstimatedTime(estimatedTime)
}
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
}