-
-
Notifications
You must be signed in to change notification settings - Fork 693
feat: execution trace gantt chart #2114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
78d87ac
65e3e34
cc235f9
7ef95da
5300eaf
c18570e
45f7b93
7998a75
463602c
e0170dd
70e0505
4e57b99
6c54b94
0d38e75
5d152e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,6 +73,8 @@ var ( | |
Offline bool | ||
ClearCache bool | ||
Timeout time.Duration | ||
|
||
ExecutionTraceOutput string | ||
) | ||
|
||
func init() { | ||
|
@@ -133,6 +135,8 @@ func init() { | |
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.") | ||
} | ||
|
||
pflag.StringVar(&ExecutionTraceOutput, "execution-trace-output", "", "When supplied, generates a Mermaid Gantt chart of each task's invocation. Useful to visualize highly parallel execution.") | ||
|
||
pflag.Parse() | ||
} | ||
|
||
|
@@ -221,5 +225,9 @@ func WithExecutorOptions() task.ExecutorOption { | |
task.ExecutorWithTaskSorter(sorter), | ||
task.ExecutorWithVersionCheck(true), | ||
) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think here, the same pattern as other executors should be used (above calls). |
||
if ExecutionTraceOutput != "" { | ||
task.ExecutorWithTracer(ExecutionTraceOutput)(e) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package tracing | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here perhaps an interface, and then implementation for the Mermaid tracer.
|
||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"strings" | ||
"sync" | ||
"time" | ||
) | ||
|
||
type Tracer struct { | ||
mu sync.Mutex | ||
spans []*Span | ||
outFile string | ||
|
||
timeFn func() time.Time | ||
} | ||
|
||
func NewTracer(outFile string) *Tracer { | ||
return &Tracer{ | ||
outFile: outFile, | ||
} | ||
} | ||
|
||
type Span struct { | ||
parent *Tracer | ||
name string | ||
startedAt time.Time | ||
endedAt time.Time | ||
} | ||
|
||
func (t *Tracer) Start(name string) *Span { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
|
||
if t.timeFn == nil { | ||
t.timeFn = time.Now | ||
} | ||
|
||
result := &Span{ | ||
parent: t, | ||
name: name, | ||
startedAt: t.timeFn(), | ||
} | ||
t.spans = append(t.spans, result) | ||
return result | ||
} | ||
|
||
func (s *Span) Stop() { | ||
s.parent.mu.Lock() | ||
defer s.parent.mu.Unlock() | ||
|
||
s.endedAt = s.parent.timeFn() | ||
} | ||
|
||
func (t *Tracer) WriteOutput() error { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
|
||
if t.outFile == "" { | ||
return nil | ||
} | ||
return os.WriteFile(t.outFile, []byte(t.toMermaidOutput()), 0o644) | ||
} | ||
|
||
func (t *Tracer) toMermaidOutput() string { | ||
out := `gantt | ||
title Task Execution Timeline | ||
dateFormat YYYY-MM-DD HH:mm:ss.SSS | ||
axisFormat %X | ||
` | ||
dateFormat := "2006-01-02 15:04:05.000" | ||
for _, span := range t.spans { | ||
if span.endedAt.IsZero() { | ||
continue | ||
} | ||
name := strings.Replace(span.name, ":", "|", -1) | ||
duration := span.endedAt.Sub(span.startedAt).Truncate(time.Millisecond * 100) | ||
out += fmt.Sprintf(" %s [%v] :done, %s, %s\n", name, duration, span.startedAt.Format(dateFormat), span.endedAt.Format(dateFormat)) | ||
} | ||
|
||
return out | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package tracing | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestTracer_Start(t *testing.T) { | ||
t.Parallel() | ||
tracer := NewTracer(t.TempDir() + "/tracing.txt") | ||
|
||
currentTime, err := time.Parse(time.DateTime, "2025-01-02 15:42:23") | ||
require.NoError(t, err) | ||
tracer.timeFn = func() time.Time { | ||
return currentTime | ||
} | ||
|
||
task1 := tracer.Start("task one") | ||
currentTime = currentTime.Add(time.Second) | ||
|
||
// special chars handling: will be replaced with "namespace|task two" in the output | ||
task2 := tracer.Start("namespace:task two") | ||
tracer.Start("task three - did not finish, should not show up in end result") | ||
currentTime = currentTime.Add(time.Second * 2) | ||
|
||
task1.Stop() | ||
currentTime = currentTime.Add(time.Second * 3) | ||
task2.Stop() | ||
|
||
// very short tasks should still show up as a point in timeline | ||
tracer.Start("very short task").Stop() | ||
|
||
r := require.New(t) | ||
r.NoError(tracer.WriteOutput()) | ||
|
||
contents, err := os.ReadFile(tracer.outFile) | ||
r.NoError(err) | ||
|
||
expectedContents := `gantt | ||
title Task Execution Timeline | ||
dateFormat YYYY-MM-DD HH:mm:ss.SSS | ||
axisFormat %X | ||
task one [3s] :done, 2025-01-02 15:42:23.000, 2025-01-02 15:42:26.000 | ||
namespace|task two [5s] :done, 2025-01-02 15:42:24.000, 2025-01-02 15:42:29.000 | ||
very short task [0s] :done, 2025-01-02 15:42:29.000, 2025-01-02 15:42:29.000 | ||
` | ||
|
||
r.Equal(expectedContents, string(contents)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might it be possible to consider a more generic operation: