Skip to content

Commit 616bd35

Browse files
committed
Summary: Display thresholds values even when not in summaryTrendStats
1 parent 20369d7 commit 616bd35

File tree

3 files changed

+107
-22
lines changed

3 files changed

+107
-22
lines changed

internal/cmd/tests/cmd_run_test.go

Lines changed: 51 additions & 19 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`, stdout)
372+
assert.Regexp(t, `✓ 'p\(90\)<100' p\(90\)=\d+(\.\d+)?µs`, stdout)
373+
assert.Regexp(t, `✓ 'p\(99.5\)<150' p\(99.5\)=\d+(\.\d+)?µs`, stdout)
374+
assert.Regexp(t, `✓ 'p\(99.99\)<200' p\(99.99\)=\d+(\.\d+)?µs`, stdout)
375+
}
376+
345377
func TestSSLKEYLOGFILEAbsolute(t *testing.T) {
346378
t.Parallel()
347379
ts := NewGlobalTestState(t)
@@ -2102,14 +2134,14 @@ func TestEventSystemError(t *testing.T) {
21022134
test.abort('oops!');
21032135
}
21042136
`, expLog: []string{
2105-
"got event Init with data '<nil>'",
2106-
"got event TestStart with data '<nil>'",
2107-
"got event IterStart with data '{Iteration:0 VUID:1 ScenarioName:default Error:<nil>}'",
2108-
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:test aborted: oops! at default (file:///-:11:16(5))}'",
2109-
"got event TestEnd with data '<nil>'",
2110-
"got event Exit with data '&{Error:test aborted: oops! at default (file:///-:11:16(5))}'",
2111-
"test aborted: oops! at default (file:///-:11:16(5))",
2112-
},
2137+
"got event Init with data '<nil>'",
2138+
"got event TestStart with data '<nil>'",
2139+
"got event IterStart with data '{Iteration:0 VUID:1 ScenarioName:default Error:<nil>}'",
2140+
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:test aborted: oops! at default (file:///-:11:16(5))}'",
2141+
"got event TestEnd with data '<nil>'",
2142+
"got event Exit with data '&{Error:test aborted: oops! at default (file:///-:11:16(5))}'",
2143+
"test aborted: oops! at default (file:///-:11:16(5))",
2144+
},
21132145
expExitCode: exitcodes.ScriptAborted,
21142146
},
21152147
{
@@ -2134,17 +2166,17 @@ func TestEventSystemError(t *testing.T) {
21342166
throw new Error('oops!');
21352167
}
21362168
`, expLog: []string{
2137-
"got event Init with data '<nil>'",
2138-
"got event TestStart with data '<nil>'",
2139-
"got event IterStart with data '{Iteration:0 VUID:1 ScenarioName:default Error:<nil>}'",
2140-
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:Error: oops!\n\tat default (file:///-:9:12(3))\n}'",
2141-
"Error: oops!\n\tat default (file:///-:9:12(3))\n",
2142-
"got event IterStart with data '{Iteration:1 VUID:1 ScenarioName:default Error:<nil>}'",
2143-
"got event IterEnd with data '{Iteration:1 VUID:1 ScenarioName:default Error:Error: oops!\n\tat default (file:///-:9:12(3))\n}'",
2144-
"Error: oops!\n\tat default (file:///-:9:12(3))\n",
2145-
"got event TestEnd with data '<nil>'",
2146-
"got event Exit with data '&{Error:<nil>}'",
2147-
},
2169+
"got event Init with data '<nil>'",
2170+
"got event TestStart with data '<nil>'",
2171+
"got event IterStart with data '{Iteration:0 VUID:1 ScenarioName:default Error:<nil>}'",
2172+
"got event IterEnd with data '{Iteration:0 VUID:1 ScenarioName:default Error:Error: oops!\n\tat default (file:///-:9:12(3))\n}'",
2173+
"Error: oops!\n\tat default (file:///-:9:12(3))\n",
2174+
"got event IterStart with data '{Iteration:1 VUID:1 ScenarioName:default Error:<nil>}'",
2175+
"got event IterEnd with data '{Iteration:1 VUID:1 ScenarioName:default Error:Error: oops!\n\tat default (file:///-:9:12(3))\n}'",
2176+
"Error: oops!\n\tat default (file:///-:9:12(3))\n",
2177+
"got event TestEnd with data '<nil>'",
2178+
"got event Exit with data '&{Error:<nil>}'",
2179+
},
21482180
expExitCode: 0,
21492181
},
21502182
}

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: 36 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's
253+
// not 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,28 @@ 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+
func extractPercentileThresholdSource(source string) (agg string, percentile float64, isPercentile bool) {
411+
// We capture the following three matches, in order to detect whether source is a percentile:
412+
// 1. The percentile definition: p(...)
413+
// 2. The percentile value: p(??)
414+
// 3. The beginning of the operator: '<', '>', or '='
415+
const expectedMatches = 3
416+
matches := percentileThresholdSourceRe.FindStringSubmatch(strings.TrimSpace(source))
417+
418+
if len(matches) == expectedMatches {
419+
var err error
420+
percentile, err = strconv.ParseFloat(matches[1], 64)
421+
if err != nil {
422+
return "", 0, false
423+
}
424+
425+
agg = "p(" + matches[1] + ")"
426+
isPercentile = true
427+
return
428+
}
429+
430+
return "", 0, false
431+
}

0 commit comments

Comments
 (0)