@@ -4,12 +4,74 @@ import (
44 "context"
55 "fmt"
66 "log"
7+ "os"
8+ "path/filepath"
79 "time"
10+
11+ "github.com/vicanso/go-charts/v2"
812)
913
14+ type LatencySnapshot struct {
15+ t time.Time
16+ latency time.Duration
17+ }
18+
19+ type LatencyResult struct {
20+ snapshots []LatencySnapshot
21+ }
22+
23+ func (lr * LatencyResult ) GeneratePlot (plotPath string , plotName string ) error {
24+ bytes , err := lr .PlotBytes (plotName )
25+ if err != nil {
26+ return err
27+ }
28+
29+ // save to file
30+ f , err := os .Create (filepath .Join (plotPath , fmt .Sprintf ("%s_plot.png" , plotName )))
31+ if err != nil {
32+ return err
33+ }
34+ defer f .Close ()
35+ _ , err = f .Write (bytes )
36+ return err
37+ }
38+
39+ func (lr * LatencyResult ) PlotBytes (plotName string ) ([]byte , error ) {
40+ if len (lr .snapshots ) == 0 {
41+ return nil , fmt .Errorf ("no snapshots available" )
42+ }
43+
44+ xvals := make ([]string , 0 , len (lr .snapshots ))
45+ yvals := make ([]float64 , 0 , len (lr .snapshots ))
46+
47+ start := lr .snapshots [0 ].t
48+
49+ for _ , s := range lr .snapshots {
50+ elapsed := s .t .Sub (start ).Seconds ()
51+ xvals = append (xvals , fmt .Sprintf ("%.2f" , elapsed ))
52+
53+ latencyMs := float64 (s .latency .Microseconds ()) / 1000.0
54+ yvals = append (yvals , latencyMs )
55+ }
56+
57+ p , err := charts .LineRender (
58+ [][]float64 {yvals },
59+ charts .TitleTextOptionFunc (fmt .Sprintf ("Task %s (ms)" , plotName )),
60+ charts .XAxisDataOptionFunc (xvals ),
61+ charts .LegendLabelsOptionFunc ([]string {"Latency" }),
62+ charts .HeightOptionFunc (500 ),
63+ charts .WidthOptionFunc (1000 ),
64+ )
65+ if err != nil {
66+ return nil , err
67+ }
68+ return p .Bytes ()
69+ }
70+
1071type avgResult struct {
11- count int64
12- avg time.Duration
72+ count int64
73+ avg time.Duration
74+ latencyResult LatencyResult
1375}
1476
1577func do (config LoadTestConfig ) error {
@@ -25,22 +87,28 @@ func do(config LoadTestConfig) error {
2587 defer cancel ()
2688
2789 ch := make (chan int64 , 2 )
28- durations := make (chan time. Duration , config .Events )
90+ durations := make (chan executionEvent , config .Events )
2991
3092 // Compute running average for executed durations using a rolling average.
3193 durationsResult := make (chan avgResult )
3294 go func () {
3395 var count int64
3496 var avg time.Duration
97+ var snapshots []LatencySnapshot
98+
3599 for d := range durations {
36100 count ++
37101 if count == 1 {
38- avg = d
102+ avg = d . duration
39103 } else {
40- avg += (d - avg ) / time .Duration (count )
104+ avg += (d . duration - avg ) / time .Duration (count )
41105 }
106+ snapshots = append (snapshots , LatencySnapshot {
107+ t : d .startedAt ,
108+ latency : d .duration ,
109+ })
42110 }
43- durationsResult <- avgResult {count : count , avg : avg }
111+ durationsResult <- avgResult {count : count , avg : avg , latencyResult : LatencyResult { snapshots : snapshots } }
44112 }()
45113
46114 // Start worker and ensure it has time to register
@@ -77,15 +145,20 @@ func do(config LoadTestConfig) error {
77145 go func () {
78146 var count int64
79147 var avg time.Duration
148+ var snapshots []LatencySnapshot
80149 for d := range scheduled {
81150 count ++
82151 if count == 1 {
83152 avg = d
84153 } else {
85154 avg += (d - avg ) / time .Duration (count )
86155 }
156+ snapshots = append (snapshots , LatencySnapshot {
157+ t : time .Now (),
158+ latency : d ,
159+ })
87160 }
88- scheduledResult <- avgResult {count : count , avg : avg }
161+ scheduledResult <- avgResult {count : count , avg : avg , latencyResult : LatencyResult { snapshots : snapshots } }
89162 }()
90163
91164 emitted := emit (ctx , config .Namespace , config .Events , config .Duration , scheduled , config .PayloadSize )
@@ -118,7 +191,37 @@ func do(config LoadTestConfig) error {
118191
119192 log .Printf ("ℹ️ final average duration per executed event: %s" , finalDurationResult .avg )
120193 log .Printf ("ℹ️ final average scheduling time per event: %s" , finalScheduledResult .avg )
121-
194+ if ShouldSendSlack () {
195+ log .Printf ("ℹ️ sending scheduling/duration plots to Slack" )
196+ slackSender := NewSlackSender ("hatchet-staging-loadtest-us-west-2" )
197+ durationBytes , err := finalDurationResult .latencyResult .PlotBytes ("duration" )
198+ if err != nil {
199+ log .Printf ("❌ failed to generate duration plot: %v " , err )
200+ }
201+ schedulingBytes , err := finalScheduledResult .latencyResult .PlotBytes ("scheduling" )
202+ if err != nil {
203+ log .Printf ("❌ failed to generate scheduling plot: %v " , err )
204+ }
205+ err = slackSender .Send (durationBytes , schedulingBytes , finalDurationResult .avg , finalScheduledResult .avg )
206+ if err != nil {
207+ log .Printf ("❌ failed to send duration plots to slack: %v " , err )
208+ }
209+ log .Printf ("ℹ️ scheduling/duration successfully plots to Slack" )
210+ } else {
211+ log .Printf ("ℹ️ not all environment vars for sending plots to Slack enabled...skipping" )
212+ }
213+ if config .PlotDir != "" {
214+ log .Printf ("ℹ️ exporting scheduling/duration snapshot data" )
215+ err := finalScheduledResult .latencyResult .GeneratePlot (config .PlotDir , "scheduling" )
216+ if err != nil {
217+ return err
218+ }
219+ err = finalDurationResult .latencyResult .GeneratePlot (config .PlotDir , "duration" )
220+ if err != nil {
221+ return err
222+ }
223+ log .Printf ("ℹ️ exported scheduling/duration snapshot data" )
224+ }
122225 if expected != executed {
123226 log .Printf ("⚠️ warning: pushed and executed counts do not match: expected=%d got=%d" , expected , executed )
124227 }
0 commit comments