Skip to content

Commit fa8c711

Browse files
committed
add standard set of metrics/queries and a new test for them
1 parent f6e07ed commit fa8c711

File tree

6 files changed

+658
-20
lines changed

6 files changed

+658
-20
lines changed

wasp/benchspy/loki.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,70 @@ func (l *LokiQueryExecutor) TimeRange(start, end time.Time) {
159159
l.StartTime = start
160160
l.EndTime = end
161161
}
162+
163+
type StandardMetric string
164+
165+
const (
166+
AverageLatency StandardMetric = "average_latency"
167+
Percentile95Latency StandardMetric = "95th_percentile_latency"
168+
ErrorRate StandardMetric = "error_rate"
169+
)
170+
171+
var standardMetrics = []StandardMetric{AverageLatency, Percentile95Latency, ErrorRate}
172+
173+
func NewStandardMetricsLokiExecutor(lokiConfig *wasp.LokiConfig, testName, generatorName, branch, commit string, startTime, endTime time.Time) (*LokiQueryExecutor, error) {
174+
standardQueries, queryErr := generateStandardLokiQueries(testName, generatorName, branch, commit, startTime, endTime)
175+
if queryErr != nil {
176+
return nil, queryErr
177+
}
178+
179+
return &LokiQueryExecutor{
180+
Kind: "loki",
181+
Queries: standardQueries,
182+
LokiConfig: lokiConfig,
183+
QueryResults: make(map[string][]string),
184+
}, nil
185+
}
186+
187+
func standardQuery(standardMetric StandardMetric, testName, generatorName, branch, commit string, startTime, endTime time.Time) (string, error) {
188+
switch standardMetric {
189+
case AverageLatency:
190+
return fmt.Sprintf("avg_over_time({branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"responses\", gen_name=~\"%s\"} | json| unwrap duration [10s]) by (go_test_name, gen_name) / 1e6", branch, commit, testName, generatorName), nil
191+
case Percentile95Latency:
192+
return fmt.Sprintf("quantile_over_time(0.95, {branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"responses\", gen_name=~\"%s\"} | json| unwrap duration [10s]) by (go_test_name, gen_name) / 1e6", branch, commit, testName, generatorName), nil
193+
case ErrorRate:
194+
queryRange := calculateTimeRange(startTime, endTime)
195+
return fmt.Sprintf("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__)", branch, commit, testName, generatorName, queryRange), nil
196+
default:
197+
return "", fmt.Errorf("unsupported standard metric %s", standardMetric)
198+
}
199+
}
200+
201+
func generateStandardLokiQueries(testName, generatorName, branch, commit string, startTime, endTime time.Time) (map[string]string, error) {
202+
standardQueries := make(map[string]string)
203+
204+
for _, metric := range standardMetrics {
205+
query, err := standardQuery(metric, testName, generatorName, branch, commit, startTime, endTime)
206+
if err != nil {
207+
return nil, err
208+
}
209+
standardQueries[string(metric)] = query
210+
}
211+
212+
return standardQueries, nil
213+
}
214+
215+
func calculateTimeRange(startTime, endTime time.Time) string {
216+
totalSeconds := int(endTime.Sub(startTime).Seconds())
217+
218+
var rangeStr string
219+
if totalSeconds%3600 == 0 { // Exact hours
220+
rangeStr = fmt.Sprintf("%dh", totalSeconds/3600)
221+
} else if totalSeconds%60 == 0 { // Exact minutes
222+
rangeStr = fmt.Sprintf("%dm", totalSeconds/60)
223+
} else { // Use seconds for uneven intervals
224+
rangeStr = fmt.Sprintf("%ds", totalSeconds)
225+
}
226+
227+
return rangeStr
228+
}

wasp/benchspy/report.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/json"
66
"fmt"
77

8+
"github.com/pkg/errors"
9+
"github.com/smartcontractkit/chainlink-testing-framework/wasp"
810
"golang.org/x/sync/errgroup"
911
)
1012

@@ -20,14 +22,20 @@ func (b *StandardReport) Store() (string, error) {
2022
return b.LocalStorage.Store(b.TestName, b.CommitOrTag, b)
2123
}
2224

23-
func (b *StandardReport) Load() error {
24-
return b.LocalStorage.Load(b.TestName, b.CommitOrTag, b)
25+
func (b *StandardReport) Load(testName, commitOrTag string) error {
26+
return b.LocalStorage.Load(testName, commitOrTag, b)
27+
}
28+
29+
func (b *StandardReport) LoadLatest(testName string) error {
30+
return b.LocalStorage.Load(testName, "", b)
2531
}
2632

2733
func (b *StandardReport) FetchData(ctx context.Context) error {
28-
startEndErr := b.BasicData.FillStartEndTimes()
29-
if startEndErr != nil {
30-
return startEndErr
34+
if b.TestStart.IsZero() || b.TestEnd.IsZero() {
35+
startEndErr := b.BasicData.FillStartEndTimes()
36+
if startEndErr != nil {
37+
return startEndErr
38+
}
3139
}
3240

3341
basicErr := b.BasicData.Validate()
@@ -91,6 +99,42 @@ func (b *StandardReport) IsComparable(otherReport Reporter) error {
9199
return nil
92100
}
93101

102+
func NewStandardReport(commitOrTag string, executionEnvironment ExecutionEnvironment, generators ...*wasp.Generator) (*StandardReport, error) {
103+
basicData, basicErr := NewBasicData(commitOrTag, generators...)
104+
if basicErr != nil {
105+
return nil, errors.Wrapf(basicErr, "failed to create basic data for generators %v", generators)
106+
}
107+
108+
startEndErr := basicData.FillStartEndTimes()
109+
if startEndErr != nil {
110+
return nil, startEndErr
111+
}
112+
113+
var queryExecutors []QueryExecutor
114+
for _, g := range generators {
115+
if !generatorHasLabels(g) {
116+
return nil, fmt.Errorf("generator %s is missing branch or commit labels", g.Cfg.GenName)
117+
}
118+
executor, executorErr := NewStandardMetricsLokiExecutor(g.Cfg.LokiConfig, basicData.TestName, g.Cfg.GenName, g.Cfg.Labels["branch"], g.Cfg.Labels["commit"], basicData.TestStart, basicData.TestEnd)
119+
if executorErr != nil {
120+
return nil, errors.Wrapf(executorErr, "failed to create standard Loki query executor for generator %s", g.Cfg.GenName)
121+
}
122+
queryExecutors = append(queryExecutors, executor)
123+
}
124+
125+
return &StandardReport{
126+
BasicData: *basicData,
127+
QueryExecutors: queryExecutors,
128+
ResourceReporter: ResourceReporter{
129+
ExecutionEnvironment: executionEnvironment,
130+
},
131+
}, nil
132+
}
133+
134+
func generatorHasLabels(g *wasp.Generator) bool {
135+
return g.Cfg.Labels["branch"] != "" && g.Cfg.Labels["commit"] != ""
136+
}
137+
94138
func (s *StandardReport) UnmarshalJSON(data []byte) error {
95139
// helper struct with QueryExecutors as json.RawMessage
96140
type Alias StandardReport

wasp/benchspy/types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ type Storer interface {
99
// Store stores the report in a persistent storage and returns the path to it, or an error
1010
Store() (string, error)
1111
// Load loads the report from a persistent storage and returns it, or an error
12-
Load() error
12+
Load(testName, commitOrTag string) error
13+
// LoadLatest loads the latest report from a persistent storage and returns it, or an error
14+
LoadLatest(testName string) error
1315
}
1416

1517
type DataFetcher interface {

wasp/benchspy_test.go

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ func TestBenchSpyWithLokiQuery(t *testing.T) {
6161
currentReport.QueryExecutors = append(currentReport.QueryExecutors, lokiQueryExecutor)
6262

6363
gen.Run(true)
64-
currentReport.TestEnd = time.Now()
6564

6665
fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
6766
defer cancelFn()
@@ -72,16 +71,14 @@ func TestBenchSpyWithLokiQuery(t *testing.T) {
7271
// path, storeErr := currentReport.Store()
7372
// require.NoError(t, storeErr, "failed to store current report", path)
7473

74+
// this is only needed, because we are using a non-standard directory
75+
// otherwise, the Load method would be able to find the file
7576
previousReport := benchspy.StandardReport{
76-
BasicData: benchspy.BasicData{
77-
TestName: t.Name(),
78-
CommitOrTag: "e7fc5826a572c09f8b93df3b9f674113372ce924",
79-
},
8077
LocalStorage: benchspy.LocalStorage{
8178
Directory: "test_performance_reports",
8279
},
8380
}
84-
loadErr := previousReport.Load()
81+
loadErr := previousReport.Load(t.Name(), "e7fc5826a572c09f8b93df3b9f674113372ce924")
8582
require.NoError(t, loadErr, "failed to load previous report")
8683

8784
isComparableErrs := previousReport.IsComparable(&currentReport)
@@ -110,7 +107,7 @@ func TestBenchSpyWithLokiQuery(t *testing.T) {
110107
require.NoError(t, err, "failed to parse float")
111108
previousSum += asFloat
112109
}
113-
previousAverage := currentSum / float64(len(previousReport.QueryExecutors[0].Results()["vu_over_time"]))
110+
previousAverage := previousSum / float64(len(previousReport.QueryExecutors[0].Results()["vu_over_time"]))
114111

115112
require.Equal(t, currentAverage, previousAverage, "vu_over_time averages are not the same")
116113
}
@@ -167,16 +164,14 @@ func TestBenchSpyWithTwoLokiQueries(t *testing.T) {
167164
// path, storeErr := currentReport.Store()
168165
// require.NoError(t, storeErr, "failed to store current report", path)
169166

167+
// this is only needed, because we are using a non-standard directory
168+
// otherwise, the Load method would be able to find the file
170169
previousReport := benchspy.StandardReport{
171-
BasicData: benchspy.BasicData{
172-
TestName: t.Name(),
173-
CommitOrTag: "e7fc5826a572c09f8b93df3b9f674113372ce924",
174-
},
175170
LocalStorage: benchspy.LocalStorage{
176171
Directory: "test_performance_reports",
177172
},
178173
}
179-
loadErr := previousReport.Load()
174+
loadErr := previousReport.Load(t.Name(), "e7fc5826a572c09f8b93df3b9f674113372ce924")
180175
require.NoError(t, loadErr, "failed to load previous report")
181176

182177
isComparableErrs := previousReport.IsComparable(&currentReport)
@@ -235,3 +230,90 @@ func TestBenchSpyWithTwoLokiQueries(t *testing.T) {
235230
diffPrecentage := (currentRespAverage - previousRespAverage) / previousRespAverage * 100
236231
require.LessOrEqual(t, math.Abs(diffPrecentage), 1.0, "responses_over_time averages are more than 1% different", fmt.Sprintf("%.4f", diffPrecentage))
237232
}
233+
234+
func TestBenchSpyWithStandardLokiMetrics(t *testing.T) {
235+
label := "benchspy-std"
236+
237+
gen, err := wasp.NewGenerator(&wasp.Config{
238+
T: t,
239+
LokiConfig: wasp.NewEnvLokiConfig(),
240+
GenName: "vu",
241+
Labels: map[string]string{
242+
"branch": label,
243+
"commit": label,
244+
},
245+
CallTimeout: 100 * time.Millisecond,
246+
LoadType: wasp.VU,
247+
Schedule: wasp.CombineAndRepeat(
248+
2,
249+
wasp.Steps(10, 1, 10, 10*time.Second),
250+
wasp.Plain(30, 15*time.Second),
251+
wasp.Steps(20, -1, 10, 5*time.Second),
252+
),
253+
VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{
254+
CallSleep: 50 * time.Millisecond,
255+
}),
256+
})
257+
require.NoError(t, err)
258+
259+
gen.Run(true)
260+
261+
currentReport, err := benchspy.NewStandardReport("e7fc5826a572c09f8b93df3b9f674113372ce925", benchspy.ExecutionEnvironment_Docker, gen)
262+
require.NoError(t, err)
263+
264+
fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
265+
defer cancelFn()
266+
267+
fetchErr := currentReport.FetchData(fetchCtx)
268+
require.NoError(t, fetchErr, "failed to fetch current report")
269+
270+
// path, storeErr := currentReport.Store()
271+
// require.NoError(t, storeErr, "failed to store current report", path)
272+
273+
// this is only needed, because we are using a non-standard directory
274+
// otherwise, the Load method would be able to find the file
275+
previousReport := benchspy.StandardReport{
276+
LocalStorage: benchspy.LocalStorage{
277+
Directory: "test_performance_reports",
278+
},
279+
}
280+
loadErr := previousReport.Load(t.Name(), "e7fc5826a572c09f8b93df3b9f674113372ce924")
281+
require.NoError(t, loadErr, "failed to load previous report")
282+
283+
isComparableErrs := previousReport.IsComparable(currentReport)
284+
require.Empty(t, isComparableErrs, "reports were not comparable", isComparableErrs)
285+
286+
var compareAverages = func(metricName benchspy.StandardMetric) {
287+
require.NotEmpty(t, currentReport.QueryExecutors[0].Results()[string(metricName)], "%s results were missing from current report", string(metricName))
288+
require.NotEmpty(t, previousReport.QueryExecutors[0].Results()[string(metricName)], "%s results were missing from previous report", string(metricName))
289+
require.Equal(t, len(currentReport.QueryExecutors[0].Results()[string(metricName)]), len(previousReport.QueryExecutors[0].Results()[string(metricName)]), "%s results are not the same length", string(metricName))
290+
291+
var currentAvgSum float64
292+
for _, value := range currentReport.QueryExecutors[0].Results()[string(metricName)] {
293+
asFloat, err := strconv.ParseFloat(value, 64)
294+
require.NoError(t, err, "failed to parse float")
295+
currentAvgSum += asFloat
296+
}
297+
currentAvgAverage := currentAvgSum / float64(len(currentReport.QueryExecutors[0].Results()[string(metricName)]))
298+
299+
var previousAvgSum float64
300+
for _, value := range previousReport.QueryExecutors[0].Results()[string(metricName)] {
301+
asFloat, err := strconv.ParseFloat(value, 64)
302+
require.NoError(t, err, "failed to parse float")
303+
previousAvgSum += asFloat
304+
}
305+
previousAvgAverage := previousAvgSum / float64(len(previousReport.QueryExecutors[0].Results()[string(metricName)]))
306+
307+
var diffPrecentage float64
308+
if previousAvgAverage != 0 {
309+
diffPrecentage = (currentAvgAverage - previousAvgAverage) / previousAvgAverage * 100
310+
} else {
311+
diffPrecentage = currentAvgAverage * 100
312+
}
313+
require.LessOrEqual(t, math.Abs(diffPrecentage), 1.0, "%s averages are more than 1% different", string(metricName), fmt.Sprintf("%.4f", diffPrecentage))
314+
}
315+
316+
compareAverages(benchspy.AverageLatency)
317+
compareAverages(benchspy.Percentile95Latency)
318+
compareAverages(benchspy.ErrorRate)
319+
}

wasp/go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ module github.com/smartcontractkit/chainlink-testing-framework/wasp
22

33
go 1.23
44

5-
toolchain go1.23.3
6-
75
require (
86
github.com/K-Phoen/grabana v0.22.2
97
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8

0 commit comments

Comments
 (0)