Skip to content

Commit 8d96e64

Browse files
authored
Summary: Display thresholds values even when not in summaryTrendStats (#4698)
* Summary: Display thresholds values even when not in summaryTrendStats * Apply suggestions from code review
1 parent 80ff394 commit 8d96e64

File tree

4 files changed

+137
-3
lines changed

4 files changed

+137
-3
lines changed

internal/cmd/tests/cmd_run_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,38 @@ func TestMetricsAndThresholds(t *testing.T) {
342342
require.Equal(t, expected, teardownThresholds)
343343
}
344344

345+
func TestThresholdsWithCustomPercentile(t *testing.T) {
346+
t.Parallel()
347+
script := `
348+
export const options = {
349+
scenarios: {
350+
sc1: {
351+
executor: 'per-vu-iterations',
352+
vus: 1,
353+
iterations: 1,
354+
},
355+
},
356+
thresholds: {
357+
'iteration_duration': ['p(0)<50', 'p(90)<100', 'p(99.5)<150', 'p(99.99)<200'],
358+
},
359+
};
360+
361+
export default function () {}
362+
`
363+
ts := getSingleFileTestState(t, script, nil, 0)
364+
cmd.ExecuteWithGlobalState(ts.GlobalState)
365+
366+
stdout := ts.Stdout.String()
367+
t.Log(stdout)
368+
369+
// We want to make sure that all the thresholds expressions are accompanied
370+
// by their values, despite those being present in `options.summaryTrendStats` or not.
371+
assert.Regexp(t, `✓ 'p\(0\)<50' p\(0\)=\d+(\.\d+)?(µs|ms|ns|s)`, stdout)
372+
assert.Regexp(t, `✓ 'p\(90\)<100' p\(90\)=\d+(\.\d+)?(µs|ms|ns|s)`, stdout)
373+
assert.Regexp(t, `✓ 'p\(99.5\)<150' p\(99.5\)=\d+(\.\d+)?(µs|ms|ns|s)`, stdout)
374+
assert.Regexp(t, `✓ 'p\(99.99\)<200' p\(99.99\)=\d+(\.\d+)?(µs|ms|ns|s)`, stdout)
375+
}
376+
345377
func TestSSLKEYLOGFILEAbsolute(t *testing.T) {
346378
t.Parallel()
347379
ts := NewGlobalTestState(t)

internal/js/summary.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,7 @@ function renderThresholdResults(
839839
: formatter.decorate(failMark, 'red');
840840

841841
const sourceText = formatter.decorate(
842-
`'${threshold.source}'`,
842+
`'${threshold.source.trim()}'`,
843843
'white',
844844
);
845845

@@ -941,13 +941,16 @@ function renderMetricValueForThresholds(
941941
info,
942942
formatter,
943943
) {
944-
const {trendStats, trendCols, nonTrendValues, nonTrendExtras} = info;
944+
const {trendStats, trendCols, trendKeys, nonTrendValues, nonTrendExtras} = info;
945945
const thresholdAgg = threshold.source.split(/[=><]/)[0].trim();
946946

947947
let value;
948948
switch (metric.type) {
949949
case 'trend':
950-
value = trendCols[metric.name][trendStats.indexOf(thresholdAgg)]
950+
const trendStatIndex = trendStats.indexOf(thresholdAgg);
951+
value = (trendStatIndex !== -1)
952+
? trendCols[metric.name]?.[trendStatIndex]
953+
: trendKeys[metric.name]?.[thresholdAgg];
951954
break;
952955
case 'counter':
953956
value = (thresholdAgg === 'count')
@@ -1042,6 +1045,7 @@ function renderTrendValue(value, stat, metric, options) {
10421045
* @property {Object} nonTrendValues - The non-trend metric values.
10431046
* @property {Object} nonTrendExtras - The non-trend metric extras.
10441047
* @property {Object} trendCols - The trend columns.
1048+
* @property {Object} trendKeys - The trend keys (values that aren't included within `trendStats`).
10451049
* @property {number[]} trendColMaxLens - The trend column maximum lengths.
10461050
* @property {number} numTrendColumns - The number of trend columns.
10471051
* @property {string[]} trendStats - The trend statistics.
@@ -1061,6 +1065,10 @@ function computeSummaryInfo(metrics, renderContext, options) {
10611065
const nonTrendExtras = {};
10621066
const trendCols = {};
10631067

1068+
// While "trendCols" contain the values for each "trendStats" aggregation (e.g. p(90) as a sorted array,
1069+
// "trendKeys" is used to store specific aggregation values that aren't part of "trendStats"; mainly for thresholds.
1070+
const trendKeys = {};
1071+
10641072
let maxNameWidth = 0;
10651073
let maxNonTrendValueLen = 0;
10661074
let nonTrendExtraMaxLens = []; // FIXME: "lens"?
@@ -1082,6 +1090,13 @@ function computeSummaryInfo(metrics, renderContext, options) {
10821090
maxNameWidth = Math.max(maxNameWidth, strWidth(displayName));
10831091

10841092
if (metric.type === 'trend') {
1093+
const keys = Object.keys(metric.values).reduce((acc, key) => {
1094+
if (!trendStats.includes(key)) {
1095+
acc[key] = renderTrendValue(metric.values[key], key, metric, options);
1096+
}
1097+
return acc;
1098+
}, {});
1099+
10851100
const cols = trendStats.map((stat) =>
10861101
renderTrendValue(metric.values[stat], stat, metric, options),
10871102
);
@@ -1094,6 +1109,7 @@ function computeSummaryInfo(metrics, renderContext, options) {
10941109
);
10951110
});
10961111
trendCols[name] = cols;
1112+
trendKeys[name] = keys;
10971113
} else {
10981114
const values = nonTrendMetricValueForSum(
10991115
metric,
@@ -1127,6 +1143,7 @@ function computeSummaryInfo(metrics, renderContext, options) {
11271143
nonTrendExtras,
11281144
trendCols,
11291145
trendColMaxLens,
1146+
trendKeys,
11301147
numTrendColumns,
11311148
trendStats,
11321149
maxNonTrendValueLen,

internal/output/summary/data.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package summary
22

33
import (
4+
"regexp"
5+
"strconv"
46
"strings"
57
"sync/atomic"
68
"time"
@@ -245,6 +247,15 @@ func summaryThresholds(
245247
Source: threshold.Source,
246248
Ok: !threshold.LastFailed,
247249
})
250+
251+
// Additionally, if metric is a trend and the threshold source is a percentile,
252+
// we may need to add the percentile value to the metric values, in case it
253+
// isn't one of [summaryTrendStats].
254+
if trendSink, isTrend := mThresholds.Sink.(*metrics.TrendSink); isTrend {
255+
if agg, percentile, isPercentile := extractPercentileThresholdSource(threshold.Source); isPercentile {
256+
mt.Metric.Values[agg] = trendSink.P(percentile / 100)
257+
}
258+
}
248259
}
249260

250261
rts[mName] = mt
@@ -393,3 +404,31 @@ func metricValueGetter(summaryTrendStats []string) func(metrics.Sink, time.Durat
393404
return result
394405
}
395406
}
407+
408+
var percentileThresholdSourceRe = regexp.MustCompile(`^p\((\d+(?:\.\d+)?)\)\s*([<>=])`)
409+
410+
// TODO(@joanlopez): Evaluate whether we should expose the parsed expression from the threshold.
411+
// For now, and until we formally decide how do we want the `metrics` package to look like, we do "manually"
412+
// parse the threshold expression here in order to extract, if existing, the percentile aggregation.
413+
func extractPercentileThresholdSource(source string) (agg string, percentile float64, isPercentile bool) {
414+
// We capture the following three matches, in order to detect whether source is a percentile:
415+
// 1. The percentile definition: p(...)
416+
// 2. The percentile value: p(??)
417+
// 3. The beginning of the operator: '<', '>', or '='
418+
const expectedMatches = 3
419+
matches := percentileThresholdSourceRe.FindStringSubmatch(strings.TrimSpace(source))
420+
421+
if len(matches) == expectedMatches {
422+
var err error
423+
percentile, err = strconv.ParseFloat(matches[1], 64)
424+
if err != nil {
425+
return "", 0, false
426+
}
427+
428+
agg = "p(" + matches[1] + ")"
429+
isPercentile = true
430+
return
431+
}
432+
433+
return "", 0, false
434+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package summary
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func Test_extractPercentileThresholdSource(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := map[string]struct {
13+
source string
14+
expAgg string
15+
expPercentile float64
16+
expIsPercentile bool
17+
}{
18+
"empty source": {
19+
source: "", expAgg: "", expPercentile: 0, expIsPercentile: false,
20+
},
21+
"invalid source": {
22+
source: "rate<<<<0.01", expAgg: "", expPercentile: 0, expIsPercentile: false,
23+
},
24+
"rate threshold": {
25+
source: "rate<0.01", expAgg: "", expPercentile: 0, expIsPercentile: false,
26+
},
27+
"integer percentile": {
28+
source: "p(95)<200", expAgg: "p(95)", expPercentile: 95, expIsPercentile: true,
29+
},
30+
"floating-point percentile": {
31+
source: "p(99.9)<=200", expAgg: "p(99.9)", expPercentile: 99.9, expIsPercentile: true,
32+
},
33+
"percentile with whitespaces": {
34+
source: " p(99.9) < 200 ", expAgg: "p(99.9)", expPercentile: 99.9, expIsPercentile: true,
35+
},
36+
}
37+
for name, tc := range tests {
38+
t.Run(name, func(t *testing.T) {
39+
t.Parallel()
40+
agg, percentile, isPercentile := extractPercentileThresholdSource(tc.source)
41+
assert.Equal(t, tc.expAgg, agg)
42+
assert.Equal(t, tc.expPercentile, percentile)
43+
assert.Equal(t, tc.expIsPercentile, isPercentile)
44+
})
45+
}
46+
}

0 commit comments

Comments
 (0)