Skip to content

Commit 5f6b816

Browse files
authored
Add support for k6 perf tests (#436)
Add performance test support for endpoints using k6 tooling - Add env file with default duration, virtual users (vus), base urls and limits - Add README show casing how to run - Add a run bash script with docker command for easy spin up - Add entry `api.js` k6 script - Add lib folder with files for shared logic around report generation, constants and default value calculation - Add a scenario run file per endpoint Signed-off-by: Nana-EC <[email protected]>
1 parent a66f3df commit 5f6b816

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2534
-0
lines changed

k6/ENV.csh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
setenv DEFAULT_DURATION 1s
2+
setenv DEFAULT_VUS 1
3+
setenv MIRROR_BASE_URL https://testnet.mirrornode.hedera.com/api/v1
4+
setenv RELAY_BASE_URL https://testnet.hashio.io/api
5+
setenv DEFAULT_LIMIT 10
6+

k6/README.md

Lines changed: 410 additions & 0 deletions
Large diffs are not rendered by default.

k6/run.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
testname=${1}
3+
4+
docker run --rm \
5+
-e DEFAULT_VUS="100" \
6+
-e DEFAULT_DURATION="90s" \
7+
-e MIRROR_BASE_URL="https://testnet.mirrornode.hedera.com" \
8+
-e RELAY_BASE_URL="https://testnet.hashio.io/api" \
9+
-v ${PWD}:/mnt \
10+
loadimpact/k6 run /mnt/${testname}
11+

k6/src/lib/common.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/*-
2+
* ‌
3+
* Hedera JSON RPC Relay
4+
*
5+
* Copyright (C) 2022 Hedera Hashgraph, LLC
6+
* ​
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* ‍
19+
*/
20+
21+
import {check} from "k6";
22+
import {Gauge} from 'k6/metrics';
23+
import {setDefaultValuesForEnvParameters} from "./parameters.js";
24+
25+
setDefaultValuesForEnvParameters();
26+
27+
const SCENARIO_DURATION_METRIC_NAME = 'scenario_duration';
28+
29+
const options = {
30+
thresholds: {
31+
checks: [`rate>=${__ENV['DEFAULT_PASS_RATE']}`], // min % that should pass the checks,
32+
http_req_duration: [`p(95)<${__ENV['DEFAULT_MAX_DURATION']}`], // 95% requests should receive response in less than max duration
33+
},
34+
insecureSkipTLSVerify: true,
35+
noConnectionReuse: true,
36+
noVUConnectionReuse: true,
37+
};
38+
39+
const scenario = {
40+
duration: __ENV.DEFAULT_DURATION,
41+
exec: 'run',
42+
executor: 'constant-vus',
43+
gracefulStop: (__ENV.DEFAULT_GRACEFUL_STOP != null && __ENV.DEFAULT_GRACEFUL_STOP) || '5s',
44+
vus: __ENV.DEFAULT_VUS,
45+
};
46+
47+
function getMetricNameWithTags(name, ...tags) {
48+
return tags.length === 0 ? name : `${name}{${tags}}`;
49+
}
50+
51+
const timeRegex = /^\d+s$/;
52+
53+
function getNextStartTime(startTime, duration, gracefulStop) {
54+
if (!timeRegex.test(startTime)) {
55+
throw new Error(`Invalid startTime ${startTime}`);
56+
}
57+
58+
if (!timeRegex.test(duration)) {
59+
throw new Error(`Invalid duration ${duration}`);
60+
}
61+
62+
if (!timeRegex.test(gracefulStop)) {
63+
throw new Error(`Invalid gracefulStop ${gracefulStop}`);
64+
}
65+
66+
return `${parseInt(startTime) + parseInt(duration) + parseInt(gracefulStop)}s`;
67+
}
68+
69+
function getOptionsWithScenario(name, tags = {}) {
70+
return Object.assign({}, options, {
71+
scenarios: {
72+
[name]: Object.assign({}, scenario, {tags}),
73+
},
74+
});
75+
}
76+
77+
function getSequentialTestScenarios(tests) {
78+
let startTime = '0s';
79+
let duration = '0s';
80+
let gracefulStop = '0s';
81+
82+
const funcs = {};
83+
const scenarios = {};
84+
const thresholds = {};
85+
for (const testName of Object.keys(tests).sort()) {
86+
const testModule = tests[testName];
87+
const testScenarios = testModule.options.scenarios;
88+
const testThresholds = testModule.options.thresholds;
89+
for (const [scenarioName, testScenario] of Object.entries(testScenarios)) {
90+
const scenario = Object.assign({}, testScenario);
91+
funcs[scenarioName] = testModule[scenario.exec];
92+
scenarios[scenarioName] = scenario;
93+
94+
// update the scenario's startTime, so scenarios run in sequence
95+
scenario.startTime = getNextStartTime(startTime, duration, gracefulStop);
96+
startTime = scenario.startTime;
97+
duration = scenario.duration;
98+
gracefulStop = scenario.gracefulStop;
99+
100+
// thresholds
101+
const tag = `scenario:${scenarioName}`;
102+
for (const [name, threshold] of Object.entries(testThresholds)) {
103+
if (name === 'http_req_duration') {
104+
thresholds[getMetricNameWithTags(name, tag, 'expected_response:true')] = threshold;
105+
} else {
106+
thresholds[getMetricNameWithTags(name, tag)] = threshold;
107+
}
108+
}
109+
thresholds[getMetricNameWithTags('http_reqs', tag)] = ['count>0'];
110+
thresholds[getMetricNameWithTags(SCENARIO_DURATION_METRIC_NAME, tag)] = ['value>0'];
111+
}
112+
}
113+
114+
const testOptions = Object.assign({}, options, {scenarios, thresholds});
115+
116+
return {funcs, options: testOptions, scenarioDurationGauge: new Gauge(SCENARIO_DURATION_METRIC_NAME)};
117+
}
118+
119+
const checksRegex = /^checks{.*scenario:.*}$/;
120+
const httpReqDurationRegex = /^http_req_duration{.*scenario:.*}$/;
121+
const httpReqsRegex = /^http_reqs{.*scenario:.*}$/;
122+
const scenarioDurationRegex = /^scenario_duration{.*scenario:.*}$/;
123+
const scenarioRegex = /scenario:([^,}]+)/;
124+
125+
function getScenario(metricKey) {
126+
const match = scenarioRegex.exec(metricKey);
127+
return match[1];
128+
}
129+
130+
function defaultMetrics() {
131+
return {
132+
"checks": {
133+
"values": {
134+
"rate": 0
135+
},
136+
},
137+
"http_req_duration": {
138+
"values": {
139+
"avg": 0
140+
}
141+
},
142+
"http_reqs": {
143+
"values": {
144+
"count": 0
145+
},
146+
},
147+
"scenario_duration": {
148+
"values": {
149+
"value": 0
150+
}
151+
}
152+
};
153+
}
154+
155+
function markdownReport(data, isFirstColumnUrl, scenarios) {
156+
const firstColumnName = isFirstColumnUrl ? 'URL' : 'Scenario';
157+
const header = `| ${firstColumnName} | VUS | Pass% | RPS | Pass RPS | Avg. Req Duration | Comment |
158+
|----------|-----|-------|-----|----------|-------------------|---------|`;
159+
160+
// collect the metrics
161+
const {metrics} = data;
162+
const scenarioMetrics = {};
163+
164+
for (const [key, value] of Object.entries(metrics)) {
165+
let name;
166+
if (checksRegex.test(key)) {
167+
name = 'checks';
168+
} else if (httpReqDurationRegex.test(key)) {
169+
name = 'http_req_duration';
170+
} else if (httpReqsRegex.test(key)) {
171+
name = 'http_reqs';
172+
} else if (scenarioDurationRegex.test(key)) {
173+
name = 'scenario_duration';
174+
} else {
175+
continue;
176+
}
177+
178+
const scenario = getScenario(key);
179+
const existingMetrics = scenarioMetrics[scenario] || defaultMetrics();
180+
scenarioMetrics[scenario] = Object.assign(existingMetrics, {[name]: value});
181+
}
182+
183+
const scenarioUrls = {};
184+
if (isFirstColumnUrl) {
185+
for (const [name, scenario] of Object.entries(scenarios)) {
186+
scenarioUrls[name] = scenario.tags.url;
187+
}
188+
}
189+
190+
// Generate the markdown report
191+
let markdown = `${header}\n`;
192+
for (const scenario of Object.keys(scenarioMetrics).sort()) {
193+
try {
194+
const scenarioMetric = scenarioMetrics[scenario];
195+
const passPercentage = (scenarioMetric['checks'].values.rate * 100.0).toFixed(2);
196+
const httpReqs = scenarioMetric['http_reqs'].values.count;
197+
const duration = scenarioMetric['scenario_duration'].values.value; // in ms
198+
const rps = ((httpReqs * 1.0 / duration) * 1000).toFixed(2);
199+
const passRps = (rps * passPercentage / 100.0).toFixed(2);
200+
const httpReqDuration = scenarioMetric['http_req_duration'].values.avg.toFixed(2);
201+
202+
const firstColumn = isFirstColumnUrl ? scenarioUrls[scenario] : scenario;
203+
markdown += `| ${firstColumn} | ${__ENV.DEFAULT_VUS} | ${passPercentage} | ${rps}/s | ${passRps}/s | ${httpReqDuration}ms | |\n`;
204+
} catch (err) {
205+
console.error(`Unable to render report for scenario ${scenario}`);
206+
}
207+
}
208+
209+
return markdown;
210+
}
211+
212+
function TestScenarioBuilder() {
213+
this._checks = {};
214+
this._name = null;
215+
this._request = null;
216+
this._tags = {};
217+
218+
this.build = function () {
219+
const that = this;
220+
return {
221+
options: getOptionsWithScenario(that._name, that._tags),
222+
run: function (testParameters) {
223+
const response = that._request(testParameters);
224+
check(response, that._checks);
225+
},
226+
};
227+
}
228+
229+
this.check = function (name, func) {
230+
this._checks[name] = func;
231+
return this;
232+
}
233+
234+
this.name = function (name) {
235+
this._name = name;
236+
return this;
237+
}
238+
239+
this.request = function (func) {
240+
this._request = func;
241+
return this;
242+
}
243+
244+
this.tags = function (tags) {
245+
this._tags = tags;
246+
return this;
247+
}
248+
249+
return this;
250+
}
251+
252+
export {getSequentialTestScenarios, markdownReport, TestScenarioBuilder};

k6/src/lib/constants.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*-
2+
* ‌
3+
* Hedera JSON RPC Relay
4+
*
5+
* Copyright (C) 2022 Hedera Hashgraph, LLC
6+
* ​
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* ‍
19+
*/
20+
21+
export const logListName = "logs";
22+
export const resultListName = "results";
23+
export const transactionListName = "transactions";

0 commit comments

Comments
 (0)