Skip to content

Commit 87d67d9

Browse files
committed
update docs, divide loki/direct results when casting per generator
1 parent d389c00 commit 87d67d9

File tree

19 files changed

+754
-233
lines changed

19 files changed

+754
-233
lines changed

book/src/libs/wasp/benchspy/first_test.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ With load data available, let's generate a baseline performance report and store
4343
```go
4444
baseLineReport, err := benchspy.NewStandardReport(
4545
// random hash, this should be the commit or hash of the Application Under Test (AUT)
46-
"e7fc5826a572c09f8b93df3b9f674113372ce924",
46+
"v1.0.0",
4747
// use built-in queries for an executor that fetches data directly from the WASP generator
4848
benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct),
4949
// WASP generators
@@ -98,7 +98,7 @@ defer cancelFn()
9898
currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious(
9999
fetchCtx,
100100
// commit or tag of the new application version
101-
"e7fc5826a572c09f8b93df3b9f674113372ce925",
101+
"v2.0.0",
102102
benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct),
103103
benchspy.WithGenerators(newGen),
104104
)

book/src/libs/wasp/benchspy/loki_custom.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Now, let’s create a `StandardReport` using our custom queries:
2929

3030
```go
3131
baseLineReport, err := benchspy.NewStandardReport(
32-
"2d1fa3532656c51991c0212afce5f80d2914e34e",
32+
"v1.0.0",
3333
// notice the different functional option used to pass Loki executor with custom queries
3434
benchspy.WithQueryExecutors(lokiQueryExecutor),
3535
benchspy.WithGenerators(gen),

book/src/libs/wasp/benchspy/loki_std.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ require.NoError(t, err)
4444
gen.Run(true)
4545

4646
baseLineReport, err := benchspy.NewStandardReport(
47-
"c2cf545d733eef8bad51d685fcb302e277d7ca14",
47+
"v1.0.0",
4848
// notice the different standard query executor type
4949
benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Loki),
5050
benchspy.WithGenerators(gen),
@@ -67,16 +67,27 @@ Since the next steps are very similar to those in the first test, we’ll skip t
6767
By default, the `LokiQueryExecutor` returns results as the `[]string` data type. Let’s use dedicated convenience functions to cast them from `interface{}` to string slices:
6868

6969
```go
70-
currentAsStringSlice := benchspy.MustAllLokiResults(currentReport)
71-
previousAsStringSlice := benchspy.MustAllLokiResults(previousReport)
70+
allCurrentAsStringSlice := benchspy.MustAllLokiResults(currentReport)
71+
allPreviousAsStringSlice := benchspy.MustAllLokiResults(previousReport)
72+
73+
require.NotEmpty(t, allCurrentAsStringSlice, "current report is empty")
74+
require.NotEmpty(t, allPreviousAsStringSlice, "previous report is empty")
75+
76+
currentAsStringSlice := allCurrentAsStringSlice[gen.Cfg.GenName]
77+
previousAsStringSlice := allPreviousAsStringSlice[gen.Cfg.GenName]
7278
```
7379

80+
An explanation is needed here: this function separates metrics for each generator, hence it returns a `map[string]map[string][]string`. Let's break it down:
81+
- outer map's key is generator name
82+
- inner map's key is metric name and the value is a series of measurements
83+
In our case there's only a single generator, but in a complex test there might be a few.
84+
7485
## Step 4: Compare Metrics
7586

7687
Now, let’s compare metrics. Since we have `[]string`, we’ll first convert it to `[]float64`, calculate the median, and ensure the difference between the averages is less than 1%. Again, this is just an example—you should decide the best way to validate your metrics. Here we are explicitly aggregating them using an average to get a single number representation of each metric, but for your case a median or percentile or yet some other aggregate might be more appropriate.
7788

7889
```go
79-
var compareAverages = func(t *testing.T, metricName string, currentAsStringSlice, previousAsStringSlice map[string][]string) {
90+
var compareAverages = func(t *testing.T, metricName string, currentAsStringSlice, previousAsStringSlice map[string][]string, maxPrecentageDiff float64) {
8091
require.NotEmpty(t, currentAsStringSlice[metricName], "%s results were missing from current report", metricName)
8192
require.NotEmpty(t, previousAsStringSlice[metricName], "%s results were missing from previous report", metricName)
8293

@@ -98,13 +109,19 @@ var compareAverages = func(t *testing.T, metricName string, currentAsStringSlice
98109
} else {
99110
diffPrecentage = 100.0
100111
}
101-
assert.LessOrEqual(t, math.Abs(diffPrecentage), 1.0, "%s medians are more than 1% different", metricName, fmt.Sprintf("%.4f", diffPrecentage))
112+
assert.LessOrEqual(t, math.Abs(diffPrecentage), maxPrecentageDiff, "%s medians are more than 1% different", metricName, fmt.Sprintf("%.4f", diffPrecentage))
102113
}
103114

104-
compareAverages(t, string(benchspy.MedianLatency), currentAsStringSlice, previousAsStringSlice)
105-
compareAverages(t, string(benchspy.Percentile95Latency), currentAsStringSlice, previousAsStringSlice)
106-
compareAverages(t, string(benchspy.MaxLatency), currentAsStringSlice, previousAsStringSlice)
107-
compareAverages(t, string(benchspy.ErrorRate), currentAsStringSlice, previousAsStringSlice)
115+
compareAverages(
116+
t,
117+
string(benchspy.MedianLatency),
118+
currentAsStringSlice,
119+
previousAsStringSlice,
120+
1.0,
121+
)
122+
compareAverages(t, string(benchspy.Percentile95Latency), currentAsStringSlice, previousAsStringSlice, 1.0)
123+
compareAverages(t, string(benchspy.MaxLatency), currentAsStringSlice, previousAsStringSlice, 1.0)
124+
compareAverages(t, string(benchspy.ErrorRate), currentAsStringSlice, previousAsStringSlice, 1.0)
108125
```
109126

110127
> [!WARNING]

book/src/libs/wasp/benchspy/prometheus_std.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ currentMedianCPUUsageVector := currentMedianCPUUsage.(model.Vector)
9595
previousMedianCPUUsageVector := previousMedianCPUUsage.(model.Vector)
9696
```
9797

98+
Since these metrics are not related to load generation, the convenience function a `map[string](model.Value)`, where key is resource metric name.
99+
98100
> [!WARNING]
99101
> All standard Prometheus metrics bundled with `BenchSpy` return `model.Vector`.
100102
> However, if you use custom queries, you must manually verify their return types.

book/src/libs/wasp/benchspy/simplest_metrics.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ hasErrors, errors := benchspy.CompareDirectWithThresholds(
2323
require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors))
2424
```
2525

26+
If there are errors they will be returned as `map[string][]errors`, where key is the name of a generator.
27+
2628
> [!NOTE]
2729
> Both `Direct` and `Loki` query executors support following standard performance metrics out of the box:
2830
> - `median_latency`
@@ -32,14 +34,16 @@ require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors))
3234
3335
The function also prints a table with the differences between two reports, regardless whether they were meaningful:
3436
```bash
37+
Generator: vu1
38+
==============
3539
+-------------------------+---------+---------+---------+
36-
| METRIC | V1.0.0 | V1.1.0 | DIFF % |
40+
| METRIC | V1 | V2 | DIFF % |
3741
+-------------------------+---------+---------+---------+
38-
| median_latency | 50.4256 | 61.0009 | 20.9722 |
42+
| median_latency | 50.1300 | 50.1179 | -0.0242 |
3943
+-------------------------+---------+---------+---------+
40-
| 95th_percentile_latency | 51.0082 | 61.1052 | 19.7949 |
44+
| 95th_percentile_latency | 50.7387 | 50.7622 | 0.0463 |
4145
+-------------------------+---------+---------+---------+
42-
| max_latency | 52.1362 | 61.2028 | 17.3903 |
46+
| max_latency | 55.7195 | 51.7248 | -7.1692 |
4347
+-------------------------+---------+---------+---------+
4448
| error_rate | 0.0000 | 0.0000 | 0.0000 |
4549
+-------------------------+---------+---------+---------+

wasp/benchspy/direct.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/smartcontractkit/chainlink-testing-framework/wasp"
1313
)
1414

15-
type DirectQueryFn = func(responses *wasp.SliceBuffer[wasp.Response]) (float64, error)
15+
type DirectQueryFn = func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error)
1616

1717
type DirectQueryExecutor struct {
1818
KindName string `json:"kind"`
@@ -49,6 +49,10 @@ func NewDirectQueryExecutor(generator *wasp.Generator, queries map[string]Direct
4949
return g, nil
5050
}
5151

52+
func (g *DirectQueryExecutor) GeneratorName() string {
53+
return g.Generator.Cfg.GenName
54+
}
55+
5256
// Results returns the query results as a map of string keys to interface{} values.
5357
// It allows users to access the outcomes of executed queries, facilitating further processing or type assertions.
5458
func (g *DirectQueryExecutor) Results() map[string]interface{} {
@@ -122,14 +126,14 @@ func (g *DirectQueryExecutor) Execute(_ context.Context) error {
122126
return fmt.Errorf("generator %s has no data", g.Generator.Cfg.GenName)
123127
}
124128
length := len(g.Generator.GetData().FailResponses.Data) + len(g.Generator.GetData().OKData.Data)
125-
allResponses := wasp.NewSliceBuffer[wasp.Response](length)
129+
allResponses := wasp.NewSliceBuffer[*wasp.Response](length)
126130

127131
for _, response := range g.Generator.GetData().OKResponses.Data {
128-
allResponses.Append(*response)
132+
allResponses.Append(response)
129133
}
130134

131135
for _, response := range g.Generator.GetData().FailResponses.Data {
132-
allResponses.Append(*response)
136+
allResponses.Append(response)
133137
}
134138

135139
if len(allResponses.Data) == 0 {
@@ -170,7 +174,7 @@ func (g *DirectQueryExecutor) generateStandardQueries() (map[string]DirectQueryF
170174
func (g *DirectQueryExecutor) standardQuery(standardMetric StandardLoadMetric) (DirectQueryFn, error) {
171175
switch standardMetric {
172176
case MedianLatency:
173-
medianFn := func(responses *wasp.SliceBuffer[wasp.Response]) (float64, error) {
177+
medianFn := func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) {
174178
var asMiliDuration []float64
175179
for _, response := range responses.Data {
176180
// get duration as nanoseconds and convert to milliseconds in order to not lose precision
@@ -182,7 +186,7 @@ func (g *DirectQueryExecutor) standardQuery(standardMetric StandardLoadMetric) (
182186
}
183187
return medianFn, nil
184188
case Percentile95Latency:
185-
p95Fn := func(responses *wasp.SliceBuffer[wasp.Response]) (float64, error) {
189+
p95Fn := func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) {
186190
var asMiliDuration []float64
187191
for _, response := range responses.Data {
188192
// get duration as nanoseconds and convert to milliseconds in order to not lose precision
@@ -194,7 +198,7 @@ func (g *DirectQueryExecutor) standardQuery(standardMetric StandardLoadMetric) (
194198
}
195199
return p95Fn, nil
196200
case MaxLatency:
197-
maxFn := func(responses *wasp.SliceBuffer[wasp.Response]) (float64, error) {
201+
maxFn := func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) {
198202
var asMiliDuration []float64
199203
for _, response := range responses.Data {
200204
// get duration as nanoseconds and convert to milliseconds in order to not lose precision
@@ -206,7 +210,7 @@ func (g *DirectQueryExecutor) standardQuery(standardMetric StandardLoadMetric) (
206210
}
207211
return maxFn, nil
208212
case ErrorRate:
209-
errorRateFn := func(responses *wasp.SliceBuffer[wasp.Response]) (float64, error) {
213+
errorRateFn := func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) {
210214
if len(responses.Data) == 0 {
211215
return 0, nil
212216
}

wasp/benchspy/direct_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func TestBenchSpy_DirectQueryExecutor_Execute(t *testing.T) {
234234
// 4 responses with ~150ms latency (150ms sleep + some execution overhead)
235235
// and 2-3 responses with ~200ms latency (200ms sleep + some execution overhead)
236236
// expected median latency: (150ms, 151ms>
237-
resultsAsFloats, err := ResultsAs(0.0, []QueryExecutor{executor}, StandardQueryExecutor_Direct, string(MedianLatency), string(Percentile95Latency), string(ErrorRate))
237+
resultsAsFloats, err := ResultsAs(0.0, executor, string(MedianLatency), string(Percentile95Latency), string(ErrorRate))
238238
assert.NoError(t, err)
239239
require.Equal(t, 3, len(resultsAsFloats))
240240
require.InDelta(t, 151.0, resultsAsFloats[string(MedianLatency)], 1.0)
@@ -342,10 +342,10 @@ func TestBenchSpy_DirectQueryExecutor_MarshalJSON(t *testing.T) {
342342
original.QueryResults["test2"] = 12.1
343343

344344
original.Queries = map[string]DirectQueryFn{
345-
"test": func(responses *wasp.SliceBuffer[wasp.Response]) (float64, error) {
345+
"test": func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) {
346346
return 2.0, nil
347347
},
348-
"test2": func(responses *wasp.SliceBuffer[wasp.Response]) (float64, error) {
348+
"test2": func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) {
349349
return 12.1, nil
350350
},
351351
}

wasp/benchspy/loki.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,36 @@ var (
2323
Loki_ErrorRate = `sum(max_over_time({branch=~"%s", commit=~"%s", go_test_name=~"%s", test_data_type=~"stats", gen_name=~"%s"} | json| unwrap failed [%s]) by (node_id, go_test_name, gen_name)) by (__stream_shard__)`
2424
)
2525

26-
// NewLokiQueryExecutor creates a new LokiQueryExecutor instance.
27-
// It initializes the executor with provided queries and Loki configuration,
28-
// enabling efficient querying of logs from Loki in a structured manner.
29-
func NewLokiQueryExecutor(queries map[string]string, lokiConfig *wasp.LokiConfig) *LokiQueryExecutor {
26+
func NewLokiQueryExecutor(generatorName string, queries map[string]string, lokiConfig *wasp.LokiConfig) *LokiQueryExecutor {
3027
return &LokiQueryExecutor{
31-
KindName: string(StandardQueryExecutor_Loki),
32-
Queries: queries,
33-
Config: lokiConfig,
34-
QueryResults: make(map[string]interface{}),
28+
KindName: string(StandardQueryExecutor_Loki),
29+
GeneratorNameString: generatorName,
30+
Queries: queries,
31+
Config: lokiConfig,
32+
QueryResults: make(map[string]interface{}),
3533
}
3634
}
3735

3836
type LokiQueryExecutor struct {
39-
KindName string `json:"kind"`
37+
KindName string `json:"kind"`
38+
GeneratorNameString string `json:"generator_name"`
39+
4040
// Test metrics
4141
StartTime time.Time `json:"start_time"`
4242
EndTime time.Time `json:"end_time"`
4343

44-
// Performance queries
4544
// a map of name to query template, ex: "average cpu usage": "avg(rate(cpu_usage_seconds_total[5m]))"
4645
Queries map[string]string `json:"queries"`
47-
// Performance queries results
4846
// can be anything, avg RPS, amount of errors, 95th percentile of CPU utilization, etc
4947
QueryResults map[string]interface{} `json:"query_results"`
5048

5149
Config *wasp.LokiConfig `json:"-"`
5250
}
5351

52+
func (l *LokiQueryExecutor) GeneratorName() string {
53+
return l.GeneratorNameString
54+
}
55+
5456
// Results returns the query results as a map of string to interface{}.
5557
// It allows users to access the outcomes of executed queries, facilitating further processing or type assertions.
5658
func (l *LokiQueryExecutor) Results() map[string]interface{} {
@@ -73,6 +75,11 @@ func (l *LokiQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error
7375
return fmt.Errorf("expected type %s, got %s", reflect.TypeOf(l), otherType)
7476
}
7577

78+
otherAsLoki := otherQueryExecutor.(*LokiQueryExecutor)
79+
if l.GeneratorNameString != otherAsLoki.GeneratorNameString {
80+
return fmt.Errorf("generator name is different. Expected %s, got %s", l.GeneratorNameString, otherAsLoki.GeneratorNameString)
81+
}
82+
7683
return l.compareQueries(otherQueryExecutor.(*LokiQueryExecutor).Queries)
7784
}
7885

@@ -86,6 +93,9 @@ func (l *LokiQueryExecutor) Validate() error {
8693
if l.Config == nil {
8794
return errors.New("loki config is missing. Please set it and try again")
8895
}
96+
if l.GeneratorNameString == "" {
97+
return errors.New("generator name is missing. Please set it and try again")
98+
}
8999

90100
return nil
91101
}
@@ -220,9 +230,10 @@ func (l *LokiQueryExecutor) UnmarshalJSON(data []byte) error {
220230
// It generates queries based on provided test parameters and time range, returning the executor or an error if query generation fails.
221231
func NewStandardMetricsLokiExecutor(lokiConfig *wasp.LokiConfig, testName, generatorName, branch, commit string, startTime, endTime time.Time) (*LokiQueryExecutor, error) {
222232
lq := &LokiQueryExecutor{
223-
KindName: string(StandardQueryExecutor_Loki),
224-
Config: lokiConfig,
225-
QueryResults: make(map[string]interface{}),
233+
KindName: string(StandardQueryExecutor_Loki),
234+
GeneratorNameString: generatorName,
235+
Config: lokiConfig,
236+
QueryResults: make(map[string]interface{}),
226237
}
227238

228239
standardQueries, queryErr := lq.generateStandardQueries(testName, generatorName, branch, commit, startTime, endTime)

wasp/benchspy/loki_test.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestBenchSpy_NewLokiQueryExecutor(t *testing.T) {
2222
BasicAuth: "user:pass",
2323
}
2424

25-
executor := NewLokiQueryExecutor(queries, config)
25+
executor := NewLokiQueryExecutor("some_generator", queries, config)
2626
assert.Equal(t, "loki", executor.KindName)
2727
assert.Equal(t, queries, executor.Queries)
2828
assert.Equal(t, config, executor.Config)
@@ -63,26 +63,40 @@ func (a *anotherQueryExecutor) TimeRange(_, _ time.Time) {
6363

6464
func TestBenchSpy_LokiQueryExecutor_IsComparable(t *testing.T) {
6565
executor1 := &LokiQueryExecutor{
66-
Queries: map[string]string{"q1": "query1"},
66+
GeneratorNameString: "generator",
67+
Queries: map[string]string{"q1": "query1"},
6768
}
6869
executor2 := &LokiQueryExecutor{
69-
Queries: map[string]string{"q1": "query2"},
70+
GeneratorNameString: "generator",
71+
Queries: map[string]string{"q1": "query2"},
7072
}
7173
executor3 := &LokiQueryExecutor{
72-
Queries: map[string]string{"q2": "query1"},
74+
GeneratorNameString: "generator",
75+
Queries: map[string]string{"q2": "query1"},
7376
}
7477
executor4 := &LokiQueryExecutor{
75-
Queries: map[string]string{"q1": "query1", "q2": "query2"},
78+
GeneratorNameString: "generator",
79+
Queries: map[string]string{"q1": "query1", "q2": "query2"},
7680
}
7781
executor5 := &LokiQueryExecutor{
78-
Queries: map[string]string{"q1": "query1", "q3": "query3"},
82+
GeneratorNameString: "generator",
83+
Queries: map[string]string{"q1": "query1", "q3": "query3"},
84+
}
85+
executor6 := &LokiQueryExecutor{
86+
GeneratorNameString: "other",
87+
Queries: map[string]string{"q1": "query1"},
7988
}
8089

8190
t.Run("same queries", func(t *testing.T) {
8291
err := executor1.IsComparable(executor1)
8392
assert.NoError(t, err)
8493
})
8594

95+
t.Run("different generator names", func(t *testing.T) {
96+
err := executor1.IsComparable(executor6)
97+
assert.Error(t, err)
98+
})
99+
86100
t.Run("same queries, different names", func(t *testing.T) {
87101
err := executor1.IsComparable(executor3)
88102
assert.Error(t, err)
@@ -114,13 +128,22 @@ func TestBenchSpy_LokiQueryExecutor_IsComparable(t *testing.T) {
114128
func TestBenchSpy_LokiQueryExecutor_Validate(t *testing.T) {
115129
t.Run("valid configuration", func(t *testing.T) {
116130
executor := &LokiQueryExecutor{
117-
Queries: map[string]string{"q1": "query1"},
118-
Config: &wasp.LokiConfig{},
131+
GeneratorNameString: "generator",
132+
Queries: map[string]string{"q1": "query1"},
133+
Config: &wasp.LokiConfig{},
119134
}
120135
err := executor.Validate()
121136
assert.NoError(t, err)
122137
})
123138

139+
t.Run("missing generator name", func(t *testing.T) {
140+
executor := &LokiQueryExecutor{
141+
Config: &wasp.LokiConfig{},
142+
}
143+
err := executor.Validate()
144+
assert.Error(t, err)
145+
})
146+
124147
t.Run("missing queries", func(t *testing.T) {
125148
executor := &LokiQueryExecutor{
126149
Config: &wasp.LokiConfig{},

0 commit comments

Comments
 (0)