Skip to content

Commit 03983ac

Browse files
committed
use single error instead of a map[string][]error when comparing Direct queries
1 parent ff33e93 commit 03983ac

File tree

3 files changed

+183
-73
lines changed

3 files changed

+183
-73
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,17 @@ This function fetches the current report (for version passed as environment vari
8787
Let’s assume you want to ensure that none of the performance metrics degrade by more than **1%** between releases (and that error rate has not changed at all). Here's how you can write assertions using a convenient function for the `Direct` query executor:
8888

8989
```go
90-
hasErrors, errors := benchspy.CompareDirectWithThresholds(
90+
hasFailed, error := benchspy.CompareDirectWithThresholds(
9191
1.0, // Max 1% worse median latency
9292
1.0, // Max 1% worse p95 latency
9393
1.0, // Max 1% worse maximum latency
9494
0.0, // No increase in error rate
9595
currentReport, previousReport)
96-
require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors))
96+
require.False(t, hasError, fmt.Sprintf("issues found: %v", error))
9797
```
9898

99+
Error returned by this function is a concatenation of all threshold violations found for each standard metric and generator.
100+
99101
---
100102

101103
## Conclusion

wasp/benchspy/report.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package benchspy
33
import (
44
"context"
55
"encoding/json"
6+
goerrors "errors"
67
"fmt"
78
"os"
89
"strings"
@@ -166,11 +167,9 @@ func calculateDiffPercentage(current, previous float64) float64 {
166167

167168
// CompareDirectWithThresholds evaluates the current and previous reports against specified thresholds.
168169
// It checks for significant differences in metrics and returns any discrepancies found, aiding in performance analysis.
169-
func CompareDirectWithThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateThreshold float64, currentReport, previousReport *StandardReport) (bool, map[string][]error) {
170+
func CompareDirectWithThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateThreshold float64, currentReport, previousReport *StandardReport) (bool, error) {
170171
if currentReport == nil || previousReport == nil {
171-
return true, map[string][]error{
172-
"initialization": {errors.New("one or both reports are nil")},
173-
}
172+
return true, errors.New("one or both reports are nil")
174173
}
175174

176175
L.Info().
@@ -182,10 +181,8 @@ func CompareDirectWithThresholds(medianThreshold, p95Threshold, maxThreshold, er
182181
Float64("Error rate threshold", errorRateThreshold).
183182
Msg("Comparing Direct metrics with thresholds")
184183

185-
if thresholdsErrs := validateThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateThreshold); len(thresholdsErrs) > 0 {
186-
return true, map[string][]error{
187-
"initialization": thresholdsErrs,
188-
}
184+
if thresholdsErr := validateThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateThreshold); thresholdsErr != nil {
185+
return true, thresholdsErr
189186
}
190187

191188
allCurrentResults := MustAllDirectResults(currentReport)
@@ -253,10 +250,20 @@ func CompareDirectWithThresholds(medianThreshold, p95Threshold, maxThreshold, er
253250
Int("Number of meaningful differences", len(errors)).
254251
Msg("Finished comparing Direct metrics with thresholds")
255252

256-
return len(errors) > 0, errors
253+
return len(errors) > 0, concatenateGeneratorErrors(errors)
254+
}
255+
256+
func concatenateGeneratorErrors(errors map[string][]error) error {
257+
var errs []error
258+
for generatorName, errors := range errors {
259+
for _, err := range errors {
260+
errs = append(errs, fmt.Errorf("[%s] %w", generatorName, err))
261+
}
262+
}
263+
return goerrors.Join(errs...)
257264
}
258265

259-
func validateThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateThreshold float64) []error {
266+
func validateThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateThreshold float64) error {
260267
var errs []error
261268

262269
var validateThreshold = func(name string, threshold float64) error {
@@ -282,7 +289,7 @@ func validateThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateTh
282289
errs = append(errs, err)
283290
}
284291

285-
return errs
292+
return goerrors.Join(errs...)
286293
}
287294

288295
// PrintStandardDirectMetrics outputs a comparison of direct metrics between two reports.

wasp/benchspy/report_test.go

Lines changed: 161 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,13 +1384,10 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
13841384
},
13851385
}
13861386

1387-
failed, errs := CompareDirectWithThresholds(10.0, 1.0, 1.0, 1.0, currentReport, previousReport)
1387+
failed, err := CompareDirectWithThresholds(10.0, 1.0, 1.0, 1.0, currentReport, previousReport)
13881388
assert.True(t, failed)
1389-
assert.Len(t, errs, 1)
1390-
assert.Len(t, errs["test-gen"], 1)
1391-
for _, err := range errs["test-gen"] {
1392-
assert.Contains(t, err.Error(), "different, which is higher than the threshold")
1393-
}
1389+
assert.NotNil(t, err)
1390+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 50.0000%% different, which is higher than the threshold", string(MedianLatency)))
13941391
})
13951392

13961393
t.Run("all metrics exceed thresholds", func(t *testing.T) {
@@ -1442,13 +1439,13 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
14421439
},
14431440
}
14441441

1445-
failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
1442+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
14461443
assert.True(t, failed)
1447-
assert.Len(t, errs, 1)
1448-
assert.Len(t, errs["test-gen"], 4)
1449-
for _, err := range errs["test-gen"] {
1450-
assert.Contains(t, err.Error(), "different, which is higher than the threshold")
1451-
}
1444+
assert.NotNil(t, err)
1445+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 50.0000%% different, which is higher than the threshold", string(MedianLatency)))
1446+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 50.0000%% different, which is higher than the threshold", string(Percentile95Latency)))
1447+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 50.0000%% different, which is higher than the threshold", string(MaxLatency)))
1448+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 100.0000%% different, which is higher than the threshold", string(ErrorRate)))
14521449
})
14531450

14541451
t.Run("handle zero values", func(t *testing.T) {
@@ -1500,12 +1497,67 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
15001497
},
15011498
}
15021499

1503-
failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
1500+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
15041501
assert.False(t, failed)
1505-
assert.Empty(t, errs)
1502+
assert.Nil(t, err)
15061503
})
15071504

1508-
t.Run("handle missing metrics", func(t *testing.T) {
1505+
t.Run("handle missing metrics from current report", func(t *testing.T) {
1506+
previousReport := &StandardReport{
1507+
BasicData: BasicData{
1508+
GeneratorConfigs: map[string]*wasp.Config{
1509+
"test-gen": {
1510+
GenName: "test-gen",
1511+
},
1512+
},
1513+
},
1514+
QueryExecutors: []QueryExecutor{
1515+
&MockQueryExecutor{
1516+
KindFn: func() string { return string(StandardQueryExecutor_Direct) },
1517+
ResultsFn: func() map[string]interface{} {
1518+
return map[string]interface{}{
1519+
string(MedianLatency): 100.0,
1520+
string(Percentile95Latency): 0.0,
1521+
string(MaxLatency): 0.0,
1522+
string(ErrorRate): 0.0,
1523+
}
1524+
},
1525+
GeneratorNameFn: func() string { return "test-gen" },
1526+
},
1527+
},
1528+
}
1529+
1530+
currentReport := &StandardReport{
1531+
BasicData: BasicData{
1532+
GeneratorConfigs: map[string]*wasp.Config{
1533+
"test-gen": {
1534+
GenName: "test-gen",
1535+
},
1536+
},
1537+
},
1538+
QueryExecutors: []QueryExecutor{
1539+
&MockQueryExecutor{
1540+
KindFn: func() string { return string(StandardQueryExecutor_Direct) },
1541+
ResultsFn: func() map[string]interface{} {
1542+
return map[string]interface{}{
1543+
string(MedianLatency): 105.0,
1544+
// missing other metrics
1545+
}
1546+
},
1547+
GeneratorNameFn: func() string { return "test-gen" },
1548+
},
1549+
},
1550+
}
1551+
1552+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
1553+
assert.True(t, failed)
1554+
assert.NotNil(t, err)
1555+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from current report", string(Percentile95Latency)))
1556+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from current report", string(MaxLatency)))
1557+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from current report", string(ErrorRate)))
1558+
})
1559+
1560+
t.Run("handle missing metrics from previous report", func(t *testing.T) {
15091561
previousReport := &StandardReport{
15101562
BasicData: BasicData{
15111563
GeneratorConfigs: map[string]*wasp.Config{
@@ -1528,6 +1580,63 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
15281580
},
15291581
}
15301582

1583+
currentReport := &StandardReport{
1584+
BasicData: BasicData{
1585+
GeneratorConfigs: map[string]*wasp.Config{
1586+
"test-gen": {
1587+
GenName: "test-gen",
1588+
},
1589+
},
1590+
},
1591+
QueryExecutors: []QueryExecutor{
1592+
&MockQueryExecutor{
1593+
KindFn: func() string { return string(StandardQueryExecutor_Direct) },
1594+
ResultsFn: func() map[string]interface{} {
1595+
return map[string]interface{}{
1596+
string(MedianLatency): 105.0,
1597+
string(Percentile95Latency): 0.0,
1598+
string(MaxLatency): 0.0,
1599+
string(ErrorRate): 0.0,
1600+
}
1601+
},
1602+
GeneratorNameFn: func() string { return "test-gen" },
1603+
},
1604+
},
1605+
}
1606+
1607+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
1608+
assert.True(t, failed)
1609+
assert.NotNil(t, err)
1610+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from previous report", string(Percentile95Latency)))
1611+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from previous report", string(MaxLatency)))
1612+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from previous report", string(ErrorRate)))
1613+
})
1614+
1615+
t.Run("handle missing metrics from both reports", func(t *testing.T) {
1616+
previousReport := &StandardReport{
1617+
BasicData: BasicData{
1618+
GeneratorConfigs: map[string]*wasp.Config{
1619+
"test-gen": {
1620+
GenName: "test-gen",
1621+
},
1622+
},
1623+
},
1624+
QueryExecutors: []QueryExecutor{
1625+
&MockQueryExecutor{
1626+
KindFn: func() string { return string(StandardQueryExecutor_Direct) },
1627+
ResultsFn: func() map[string]interface{} {
1628+
return map[string]interface{}{
1629+
string(MedianLatency): 100.0,
1630+
string(Percentile95Latency): 0.0,
1631+
string(MaxLatency): 0.0,
1632+
string(ErrorRate): 0.0,
1633+
}
1634+
},
1635+
GeneratorNameFn: func() string { return "test-gen" },
1636+
},
1637+
},
1638+
}
1639+
15311640
currentReport := &StandardReport{
15321641
BasicData: BasicData{
15331642
GeneratorConfigs: map[string]*wasp.Config{
@@ -1550,13 +1659,12 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
15501659
},
15511660
}
15521661

1553-
failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
1662+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
15541663
assert.True(t, failed)
1555-
assert.Len(t, errs, 1)
1556-
assert.Len(t, errs["test-gen"], 3) // Should have errors for missing P95, Max, and Error Rate
1557-
for _, err := range errs["test-gen"] {
1558-
assert.Contains(t, err.Error(), "results were missing")
1559-
}
1664+
assert.NotNil(t, err)
1665+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from current report", string(Percentile95Latency)))
1666+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from current report", string(MaxLatency)))
1667+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s metric results were missing from current report", string(ErrorRate)))
15601668
})
15611669

15621670
t.Run("handle zero to non-zero transition", func(t *testing.T) {
@@ -1608,13 +1716,13 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
16081716
},
16091717
}
16101718

1611-
failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
1719+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
16121720
assert.True(t, failed)
1613-
assert.Len(t, errs, 1)
1614-
assert.Len(t, errs["test-gen"], 4)
1615-
for _, err := range errs["test-gen"] {
1616-
assert.Contains(t, err.Error(), "999.0000% different")
1617-
}
1721+
assert.NotNil(t, err)
1722+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 999.0000%% different, which is higher than the threshold", string(MedianLatency)))
1723+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 999.0000%% different, which is higher than the threshold", string(Percentile95Latency)))
1724+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 999.0000%% different, which is higher than the threshold", string(MaxLatency)))
1725+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 999.0000%% different, which is higher than the threshold", string(ErrorRate)))
16181726
})
16191727

16201728
t.Run("handle non-zero to zero transition", func(t *testing.T) {
@@ -1666,9 +1774,9 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
16661774
},
16671775
}
16681776

1669-
failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
1777+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport)
16701778
assert.False(t, failed)
1671-
assert.Len(t, errs, 0)
1779+
assert.Nil(t, err)
16721780
})
16731781

16741782
t.Run("handle edge-cases", func(t *testing.T) {
@@ -1720,11 +1828,10 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
17201828
},
17211829
}
17221830

1723-
failed, errs := CompareDirectWithThresholds(0.99, 0.9892, 10.0, 10.0, currentReport, previousReport)
1831+
failed, err := CompareDirectWithThresholds(0.99, 0.9892, 10.0, 10.0, currentReport, previousReport)
17241832
assert.True(t, failed)
1725-
assert.Equal(t, 1, len(errs))
1726-
assert.Equal(t, 1, len(errs["test-gen"]))
1727-
assert.Contains(t, errs["test-gen"][0].Error(), "0.9901% different")
1833+
assert.NotNil(t, err)
1834+
assert.Contains(t, err.Error(), fmt.Sprintf("[test-gen] %s is 0.9901%% different, which is higher than the threshold", string(MedianLatency)))
17281835
})
17291836

17301837
t.Run("handle nil reports", func(t *testing.T) {
@@ -1752,23 +1859,20 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
17521859
},
17531860
}
17541861

1755-
failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, report, nil)
1862+
failed, err := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, report, nil)
17561863
assert.True(t, failed)
1757-
assert.Equal(t, 1, len(errs))
1758-
assert.Equal(t, 1, len(errs["initialization"]))
1759-
assert.Contains(t, errs["initialization"][0].Error(), "one or both reports are nil")
1864+
assert.NotNil(t, err)
1865+
assert.Contains(t, err.Error(), "one or both reports are nil")
17601866

1761-
failed, errs = CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, nil, report)
1867+
failed, err = CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, nil, report)
17621868
assert.True(t, failed)
1763-
assert.Equal(t, 1, len(errs))
1764-
assert.Equal(t, 1, len(errs["initialization"]))
1765-
assert.Contains(t, errs["initialization"][0].Error(), "one or both reports are nil")
1869+
assert.NotNil(t, err)
1870+
assert.Contains(t, err.Error(), "one or both reports are nil")
17661871

1767-
failed, errs = CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, nil, nil)
1872+
failed, err = CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, nil, nil)
17681873
assert.True(t, failed)
1769-
assert.Equal(t, 1, len(errs))
1770-
assert.Equal(t, 1, len(errs["initialization"]))
1771-
assert.Contains(t, errs["initialization"][0].Error(), "one or both reports are nil")
1874+
assert.NotNil(t, err)
1875+
assert.Contains(t, err.Error(), "one or both reports are nil")
17721876
})
17731877

17741878
t.Run("handle incorrect thresholds", func(t *testing.T) {
@@ -1796,26 +1900,23 @@ func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) {
17961900
},
17971901
}
17981902

1799-
failed, errs := CompareDirectWithThresholds(-0.1, 100.0, 0.0, 100.0, report, report)
1903+
failed, err := CompareDirectWithThresholds(-0.1, 100.0, 0.0, 100.0, report, report)
18001904
assert.True(t, failed)
1801-
assert.Equal(t, 1, len(errs))
1802-
assert.Equal(t, 1, len(errs["initialization"]))
1803-
assert.Contains(t, errs["initialization"][0].Error(), "median threshold -0.1000 is not in the range [0, 100]")
1905+
assert.NotNil(t, err)
1906+
assert.Contains(t, err.Error(), "median threshold -0.1000 is not in the range [0, 100]")
18041907

1805-
failed, errs = CompareDirectWithThresholds(1.0, 101.0, 0.0, 100.0, report, report)
1908+
failed, err = CompareDirectWithThresholds(1.0, 101.0, 0.0, 100.0, report, report)
18061909
assert.True(t, failed)
1807-
assert.Equal(t, 1, len(errs))
1808-
assert.Equal(t, 1, len(errs["initialization"]))
1809-
assert.Contains(t, errs["initialization"][0].Error(), "p95 threshold 101.0000 is not in the range [0, 100]")
1910+
assert.NotNil(t, err)
1911+
assert.Contains(t, err.Error(), "p95 threshold 101.0000 is not in the range [0, 100]")
18101912

1811-
failed, errs = CompareDirectWithThresholds(-1, -1, -1, -1, report, report)
1913+
failed, err = CompareDirectWithThresholds(-1, -1, -1, -1, report, report)
18121914
assert.True(t, failed)
1813-
assert.Equal(t, 1, len(errs))
1814-
assert.Equal(t, 4, len(errs["initialization"]))
1815-
assert.Contains(t, errs["initialization"][0].Error(), "median threshold -1.0000 is not in the range [0, 100]")
1816-
assert.Contains(t, errs["initialization"][1].Error(), "p95 threshold -1.0000 is not in the range [0, 100]")
1817-
assert.Contains(t, errs["initialization"][2].Error(), "max threshold -1.0000 is not in the range [0, 100]")
1818-
assert.Contains(t, errs["initialization"][3].Error(), "error rate threshold -1.0000 is not in the range [0, 100]")
1915+
assert.NotNil(t, err)
1916+
assert.Contains(t, err.Error(), "median threshold -1.0000 is not in the range [0, 100]")
1917+
assert.Contains(t, err.Error(), "p95 threshold -1.0000 is not in the range [0, 100]")
1918+
assert.Contains(t, err.Error(), "max threshold -1.0000 is not in the range [0, 100]")
1919+
assert.Contains(t, err.Error(), "error rate threshold -1.0000 is not in the range [0, 100]")
18191920
})
18201921
}
18211922

0 commit comments

Comments
 (0)