aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/runsummary/execution_summary.go
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/runsummary/execution_summary.go
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'cli/internal/runsummary/execution_summary.go')
-rw-r--r--cli/internal/runsummary/execution_summary.go282
1 files changed, 282 insertions, 0 deletions
diff --git a/cli/internal/runsummary/execution_summary.go b/cli/internal/runsummary/execution_summary.go
new file mode 100644
index 0000000..fabb690
--- /dev/null
+++ b/cli/internal/runsummary/execution_summary.go
@@ -0,0 +1,282 @@
+package runsummary
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/vercel/turbo/cli/internal/chrometracing"
+ "github.com/vercel/turbo/cli/internal/fs"
+ "github.com/vercel/turbo/cli/internal/turbopath"
+
+ "github.com/mitchellh/cli"
+)
+
+// executionEvent represents a single event in the build process, i.e. a target starting or finishing
+// building, or reaching some milestone within those steps.
+type executionEvent struct {
+ // Timestamp of this event
+ Time time.Time
+ // Duration of this event
+ Duration time.Duration
+ // Target which has just changed
+ Label string
+ // Its current status
+ Status executionEventName
+ // Error, only populated for failure statuses
+ Err string
+
+ exitCode *int
+}
+
+// executionEventName represents the status of a target when we log a build result.
+type executionEventName int
+
+// The collection of expected build result statuses.
+const (
+ targetInitialized executionEventName = iota
+ TargetBuilding
+ TargetBuildStopped
+ TargetExecuted
+ TargetBuilt
+ TargetCached
+ TargetBuildFailed
+)
+
+func (en executionEventName) toString() string {
+ switch en {
+ case targetInitialized:
+ return "initialized"
+ case TargetBuilding:
+ return "building"
+ case TargetBuildStopped:
+ return "buildStopped"
+ case TargetExecuted:
+ return "executed"
+ case TargetBuilt:
+ return "built"
+ case TargetCached:
+ return "cached"
+ case TargetBuildFailed:
+ return "buildFailed"
+ }
+
+ return ""
+}
+
+// TaskExecutionSummary contains data about the state of a single task in a turbo run.
+// Some fields are updated over time as the task prepares to execute and finishes execution.
+type TaskExecutionSummary struct {
+ startAt time.Time // set once
+ status executionEventName // current status, updated during execution
+ err string // only populated for failure statuses
+ Duration time.Duration // updated during the task execution
+ exitCode *int // pointer so we can distinguish between 0 and unknown.
+}
+
+func (ts *TaskExecutionSummary) endTime() time.Time {
+ return ts.startAt.Add(ts.Duration)
+}
+
+// MarshalJSON munges the TaskExecutionSummary into a format we want
+// We'll use an anonmyous, private struct for this, so it's not confusingly duplicated
+func (ts *TaskExecutionSummary) MarshalJSON() ([]byte, error) {
+ serializable := struct {
+ Start int64 `json:"startTime"`
+ End int64 `json:"endTime"`
+ Err string `json:"error,omitempty"`
+ ExitCode *int `json:"exitCode"`
+ }{
+ Start: ts.startAt.UnixMilli(),
+ End: ts.endTime().UnixMilli(),
+ Err: ts.err,
+ ExitCode: ts.exitCode,
+ }
+
+ return json.Marshal(&serializable)
+}
+
+// ExitCode access exit code nil means no exit code was received
+func (ts *TaskExecutionSummary) ExitCode() *int {
+ var exitCode int
+ if ts.exitCode == nil {
+ return nil
+ }
+ exitCode = *ts.exitCode
+ return &exitCode
+}
+
+// executionSummary is the state of the entire `turbo run`. Individual task state in `Tasks` field
+type executionSummary struct {
+ // mu guards reads/writes to the `state` field
+ mu sync.Mutex
+ tasks map[string]*TaskExecutionSummary // key is a taskID
+ profileFilename string
+
+ // These get serialized to JSON
+ command string // a synthesized turbo command to produce this invocation
+ repoPath turbopath.RelativeSystemPath // the (possibly empty) path from the turborepo root to where the command was run
+ success int // number of tasks that exited successfully (does not include cache hits)
+ failure int // number of tasks that exited with failure
+ cached int // number of tasks that had a cache hit
+ attempted int // number of tasks that started
+ startedAt time.Time
+ endedAt time.Time
+ exitCode int
+}
+
+// MarshalJSON munges the executionSummary into a format we want
+// We'll use an anonmyous, private struct for this, so it's not confusingly duplicated.
+func (es *executionSummary) MarshalJSON() ([]byte, error) {
+ serializable := struct {
+ Command string `json:"command"`
+ RepoPath string `json:"repoPath"`
+ Success int `json:"success"`
+ Failure int `json:"failed"`
+ Cached int `json:"cached"`
+ Attempted int `json:"attempted"`
+ StartTime int64 `json:"startTime"`
+ EndTime int64 `json:"endTime"`
+ ExitCode int `json:"exitCode"`
+ }{
+ Command: es.command,
+ RepoPath: es.repoPath.ToString(),
+ StartTime: es.startedAt.UnixMilli(),
+ EndTime: es.endedAt.UnixMilli(),
+ Success: es.success,
+ Failure: es.failure,
+ Cached: es.cached,
+ Attempted: es.attempted,
+ ExitCode: es.exitCode,
+ }
+
+ return json.Marshal(&serializable)
+}
+
+// newExecutionSummary creates a executionSummary instance to track events in a `turbo run`.`
+func newExecutionSummary(command string, repoPath turbopath.RelativeSystemPath, start time.Time, tracingProfile string) *executionSummary {
+ if tracingProfile != "" {
+ chrometracing.EnableTracing()
+ }
+
+ return &executionSummary{
+ command: command,
+ repoPath: repoPath,
+ success: 0,
+ failure: 0,
+ cached: 0,
+ attempted: 0,
+ tasks: make(map[string]*TaskExecutionSummary),
+ startedAt: start,
+ profileFilename: tracingProfile,
+ }
+}
+
+// Run starts the Execution of a single task. It returns a function that can
+// be used to update the state of a given taskID with the executionEventName enum
+func (es *executionSummary) run(taskID string) (func(outcome executionEventName, err error, exitCode *int), *TaskExecutionSummary) {
+ start := time.Now()
+ taskExecutionSummary := es.add(&executionEvent{
+ Time: start,
+ Label: taskID,
+ Status: targetInitialized,
+ })
+
+ tracer := chrometracing.Event(taskID)
+
+ // This function can be called with an enum and an optional error to update
+ // the state of a given taskID.
+ tracerFn := func(outcome executionEventName, err error, exitCode *int) {
+ defer tracer.Done()
+ now := time.Now()
+ result := &executionEvent{
+ Time: now,
+ Duration: now.Sub(start),
+ Label: taskID,
+ Status: outcome,
+ // We'll assign this here regardless of whether it is nil, but we'll check for nil
+ // when we assign it to the taskExecutionSummary.
+ exitCode: exitCode,
+ }
+
+ if err != nil {
+ result.Err = err.Error()
+ }
+
+ // Ignore the return value here
+ es.add(result)
+ }
+
+ return tracerFn, taskExecutionSummary
+}
+
+func (es *executionSummary) add(event *executionEvent) *TaskExecutionSummary {
+ es.mu.Lock()
+ defer es.mu.Unlock()
+
+ var taskExecSummary *TaskExecutionSummary
+ if ts, ok := es.tasks[event.Label]; ok {
+ // If we already know about this task, we'll update it with the new event
+ taskExecSummary = ts
+ } else {
+ // If we don't know about it yet, init and add it into the parent struct
+ // (event.Status should always be `targetBuilding` here.)
+ taskExecSummary = &TaskExecutionSummary{startAt: event.Time}
+ es.tasks[event.Label] = taskExecSummary
+ }
+
+ // Update the Status, Duration, and Err fields
+ taskExecSummary.status = event.Status
+ taskExecSummary.err = event.Err
+ taskExecSummary.Duration = event.Duration
+
+ if event.exitCode != nil {
+ taskExecSummary.exitCode = event.exitCode
+ }
+
+ switch {
+ case event.Status == TargetBuilding:
+ es.attempted++
+ case event.Status == TargetBuildFailed:
+ es.failure++
+ case event.Status == TargetCached:
+ es.cached++
+ case event.Status == TargetBuilt:
+ es.success++
+ }
+
+ return es.tasks[event.Label]
+}
+
+// writeChromeTracing writes to a profile name if the `--profile` flag was passed to turbo run
+func writeChrometracing(filename string, terminal cli.Ui) error {
+ outputPath := chrometracing.Path()
+ if outputPath == "" {
+ // tracing wasn't enabled
+ return nil
+ }
+
+ name := fmt.Sprintf("turbo-%s.trace", time.Now().Format(time.RFC3339))
+ if filename != "" {
+ name = filename
+ }
+ if err := chrometracing.Close(); err != nil {
+ terminal.Warn(fmt.Sprintf("Failed to flush tracing data: %v", err))
+ }
+ cwdRaw, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ root, err := fs.GetCwd(cwdRaw)
+ if err != nil {
+ return err
+ }
+ // chrometracing.Path() is absolute by default, but can still be relative if overriden via $CHROMETRACING_DIR
+ // so we have to account for that before converting to turbopath.AbsoluteSystemPath
+ if err := fs.CopyFile(&fs.LstatCachedFile{Path: fs.ResolveUnknownPath(root, outputPath)}, name); err != nil {
+ return err
+ }
+ return nil
+}