diff --git a/README.md b/README.md index 5e81419..32e4997 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,16 @@ For example, save the CSV to a file, upload it to from Google Spreadsheets, and ![Execution Chart](./images/flat-chart.png) +### Retrieve an interactive chart + +Execute the `histogram_graph` query to retrieve an HTML page powered by the `go-echarts` library: + +``` +$ tctl --namespace benchtest workflow query --query_type histogram_graph --workflow_id 2 | tail -n +2 | sed 's/^\["//' | sed 's/"\]$//' | sed 's/\\n/\n/g' | sed 's/\\u003c//g' | sed 's/\\"/"/g' > benchmark.html +``` + +The resulting HTML can be opened directly in a browser and shows the benchmark run statistics. + ## Retrieve the metrics If you have Prometheus installed and configured, you can pass its URL via `PROMETHEUS_URL` environment variable (default: `http://prometheus-server`), diff --git a/worker/bench/graph.go b/worker/bench/graph.go new file mode 100644 index 0000000..b2f407e --- /dev/null +++ b/worker/bench/graph.go @@ -0,0 +1,78 @@ +package bench + +import ( + "bytes" + "fmt" + "strconv" + + "github.com/go-echarts/go-echarts/v2/charts" + "github.com/go-echarts/go-echarts/v2/components" + "github.com/go-echarts/go-echarts/v2/opts" +) + +func printHistogramGraph(request benchWorkflowRequest, values []histogramValue) string { + chart := charts.NewLine() + chart.SetGlobalOptions( + charts.WithTitleOpts(opts.Title{ + Title: fmt.Sprintf("Benchmark: %s", request.Workflow.Name), + //Subtitle: fmt.Sprintf("Steps:\n%+v", request.Steps), + }), + charts.WithLegendOpts(opts.Legend{Show: true}), + charts.WithTooltipOpts(opts.Tooltip{Show: true, Trigger: "axis"}), + charts.WithXAxisOpts(opts.XAxis{Name: "Time (s)"}), + charts.WithInitializationOpts(opts.Initialization{ + Width: "1440px", + Height: "900px", + }), + ) + + interval := request.Report.IntervalInSeconds + times := make([]string, len(values)) + workflowsStartedRate := make([]float32, len(values)) + workflowsExecutionRate := make([]float32, len(values)) + workflowsClosedRate := make([]float32, len(values)) + backlog := make([]float32, len(values)) + for i, v := range values { + times[i] = strconv.Itoa((i + 1) * interval) + workflowsStartedRate[i] = float32(v.Started) / float32(interval) + workflowsExecutionRate[i] = float32(v.Execution) / float32(interval) + workflowsClosedRate[i] = float32(v.Closed) / float32(interval) + backlog[i] = float32(v.Backlog) + } + + seriesOpts := []charts.SeriesOpts{ + charts.WithLabelOpts(opts.Label{Show: true, Position: "top"}), + charts.WithLineChartOpts(opts.LineChart{Smooth: true}), + } + + chart.SetXAxis(times). + AddSeries("Workflows Started Rate", generateLineData(workflowsStartedRate), seriesOpts...). + AddSeries("Workflows Execution Rate", generateLineData(workflowsExecutionRate), seriesOpts...). + AddSeries("Workflows Closed Rate", generateLineData(workflowsClosedRate), seriesOpts...). + AddSeries("Backlog", generateLineData(backlog), seriesOpts...) + + // TODO: + // 1. Add raw data as a table (collapsed if possible) + // 2. Multiple charts / page? + // 3. Add the remaining data in a second chart: workflows started (v.Started), + // workflow executions (v.Execution), workflows closed (v.Closed) + // 4. Example used: https://github.com/go-echarts/examples/blob/master/examples/line.go + + page := components.NewPage() + page.AddCharts(chart) + + var b bytes.Buffer + if err := page.Render(&b); err != nil { + return err.Error() + } + + return b.String() +} + +func generateLineData(data []float32) []opts.LineData { + items := make([]opts.LineData, 0) + for i := 0; i < len(data); i++ { + items = append(items, opts.LineData{Value: data[i]}) + } + return items +} diff --git a/worker/bench/graph_test.go b/worker/bench/graph_test.go new file mode 100644 index 0000000..ed8ee80 --- /dev/null +++ b/worker/bench/graph_test.go @@ -0,0 +1,68 @@ +package bench + +import ( + "os" + "testing" +) + +// TODO: make this a runnable example? +func Test_printHistogramGraph(t *testing.T) { + type args struct { + request benchWorkflowRequest + values []histogramValue + } + tests := []struct { + name string + args args + }{ + { + name: "Benchmark const 500", + args: args{ + request: benchWorkflowRequest{ + Workflow: benchWorkflowRequestWorkflow{ + Name: "basic-workflow", + Args: struct { + SequenceCount int `json:"sequenceCount"` + ParallelCount int `json:"parallelCount"` + ActivityDurationMilliseconds int `json:"activityDurationMilliseconds"` + Payload string `json:"payload"` + ResultPayload string `json:"resultPayload"` + }{ + SequenceCount: 3, + }, + }, + Steps: []benchWorkflowRequestStep{ + { + Count: 500, + RatePerSecond: 50, + Concurrency: 5, + }, + }, + Report: benchWorkflowRequestReporting{IntervalInSeconds: 5}, + }, + values: []histogramValue{ + {Started: 64, Execution: 64, Closed: 11, Backlog: 53}, + {Started: 35, Execution: 35, Closed: 63, Backlog: 25}, + {Started: 16, Execution: 16, Closed: 88, Backlog: 53}, + {Started: 10, Execution: 10, Closed: 99, Backlog: 53}, + {Started: 23, Execution: 23, Closed: 87, Backlog: 78}, + {Started: 11, Execution: 11, Closed: 93, Backlog: 96}, + {Started: 41, Execution: 41, Closed: 99, Backlog: 50}, + {Started: 10, Execution: 10, Closed: 60, Backlog: 0}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := printHistogramGraph(tt.args.request, tt.args.values) + f, err := os.Create("test.html") + if err != nil { + t.Fatal(err) + } + if _, err := f.WriteString(got); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/worker/bench/workflow.go b/worker/bench/workflow.go index a862cbe..96c677b 100644 --- a/worker/bench/workflow.go +++ b/worker/bench/workflow.go @@ -234,6 +234,12 @@ func (w *benchWorkflow) setupQueries(res []histogramValue, startTime time.Time) return err } + if err := workflow.SetQueryHandler(w.ctx, "histogram_graph", func(input []byte) (string, error) { + return printHistogramGraph(w.request, res), nil + }); err != nil { + return err + } + return nil } diff --git a/worker/go.mod b/worker/go.mod index 0219f3a..f82154d 100644 --- a/worker/go.mod +++ b/worker/go.mod @@ -3,6 +3,7 @@ module github.com/temporalio/maru go 1.18 require ( + github.com/go-echarts/go-echarts/v2 v2.2.4 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.11.0 github.com/prometheus/common v0.26.0 diff --git a/worker/go.sum b/worker/go.sum index 96d5d2a..27a36eb 100644 --- a/worker/go.sum +++ b/worker/go.sum @@ -314,6 +314,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-echarts/go-echarts/v2 v2.2.4 h1:SKJpdyNIyD65XjbUZjzg6SwccTNXEgmh+PlaO23g2H0= +github.com/go-echarts/go-echarts/v2 v2.2.4/go.mod h1:6TOomEztzGDVDkOSCFBq3ed7xOYfbOqhaBzD0YV771A= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -508,6 +510,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=