diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/runsummary/execution_summary.go | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'cli/internal/runsummary/execution_summary.go')
| -rw-r--r-- | cli/internal/runsummary/execution_summary.go | 282 |
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 +} |
