Skip to content

Commit eeb8fa1

Browse files
authored
test: add load tests (#93)
* setup env for k6 * test: load tests to package.json * test: add tests * test: add load test * test: granualize metrics * test: have a standard workload * test: load 50 users 1 request/sec/user * test: log the response failures * test: randomize the iteration interval * test: adjust the load scenario * test: doc update * test: move globals to shared esconfig
1 parent 6e3f8e9 commit eeb8fa1

File tree

9 files changed

+152
-1
lines changed

9 files changed

+152
-1
lines changed

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"version": "latest",
2121
"installBicep": true
2222
},
23-
"ghcr.io/azure/azure-dev/azd:latest": {}
23+
"ghcr.io/azure/azure-dev/azd:latest": {},
24+
"ghcr.io/devcontainers-contrib/features/k6:1": {}
2425
},
2526

2627
// Configure tool-specific properties.

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"start:indexer": "npm run dev --workspace=indexer",
1414
"test": "npm run test -ws --if-present",
1515
"test:playwright": "npx playwright test",
16+
"test:load": "k6 run tests/load/index.js",
1617
"build": "npm run build -ws --if-present",
1718
"clean": "npm run clean -ws --if-present",
1819
"docker:build": "npm run docker:build -ws --if-present",
@@ -33,6 +34,7 @@
3334
"@playwright/test": "^1.39.0",
3435
"@tapjs/nock": "^3.1.13",
3536
"@types/node": "^18.15.3",
37+
"@types/k6": "^0.47.1",
3638
"concurrently": "^8.2.1",
3739
"eslint-config-shared": "^1.0.0",
3840
"lint-staged": "^14.0.1",

packages/eslint-config/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
module.exports = {
2+
globals: {
3+
__ENV: 'readonly',
4+
},
25
parserOptions: {
36
ecmaVersion: 'latest',
47
sourceType: 'module',

tests/load/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The tests use [k6](https://k6.io/) to perform load testing.
2+
3+
# Install k6
4+
5+
k6 is already included in the dev container, so no further installation is required.
6+
7+
For manual installation, refer to [k6 installation docs](https://k6.io/docs/get-started/installation/).
8+
9+
# To run the test
10+
11+
Set the following environment variables to point to the deployment.
12+
13+
```
14+
export WEBAPP_URI=<webapp_uri>
15+
export SEARCH_API_URI=<search_api_uri>
16+
```
17+
18+
Once set, you can now run load tests using the following command:
19+
20+
```
21+
npm run test:load
22+
```

tests/load/chat.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import http from 'k6/http';
2+
import { Trend } from 'k6/metrics';
3+
import { group, sleep } from 'k6';
4+
5+
const chatStreamLatency = new Trend('chat_stream_duration');
6+
const chatNoStreamLatency = new Trend('chat_nostream_duration');
7+
8+
function between(min, max) {
9+
min = Math.ceil(min);
10+
max = Math.floor(max);
11+
return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive
12+
}
13+
14+
function choose(list) {
15+
return list[between(0, list.length)];
16+
}
17+
18+
export function chat(baseUrl, stream = true) {
19+
group('Chat flow', function () {
20+
const defaultPrompts = [
21+
'How to search and book rentals?',
22+
'What is the refund policy?',
23+
'How to contact a representative?',
24+
];
25+
26+
const payload = JSON.stringify({
27+
messages: [{ content: choose(defaultPrompts), role: 'user' }],
28+
context: {
29+
retrieval_mode: 'hybrid',
30+
semantic_ranker: true,
31+
semantic_captions: false,
32+
suggest_followup_questions: true,
33+
retrievalMode: 'hybrid',
34+
top: 3,
35+
useSemanticRanker: true,
36+
useSemanticCaptions: false,
37+
excludeCategory: '',
38+
promptTemplate: '',
39+
promptTemplatePrefix: '',
40+
promptTemplateSuffix: '',
41+
suggestFollowupQuestions: true,
42+
approach: 'rrr',
43+
},
44+
stream,
45+
});
46+
47+
const parameters = {
48+
headers: {
49+
'Content-Type': 'application/json',
50+
},
51+
tags: { type: 'API' },
52+
};
53+
54+
const response = http.post(`${baseUrl}/chat`, payload, parameters);
55+
56+
if (response.status !== 200) {
57+
console.log(`Response: ${response.status} ${response.body}`);
58+
}
59+
60+
// add duration property to metric
61+
const latencyMetric = stream ? chatStreamLatency : chatNoStreamLatency;
62+
latencyMetric.add(response.timings.duration, { type: 'API' });
63+
64+
sleep(between(5, 20)); // wait between 5 and 20 seconds between each user iteration
65+
});
66+
}

tests/load/config.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const thresholdsSettings = {
2+
'http_req_failed{type:API}': [{ threshold: 'rate<0.01' }], // less than 1% failed requests
3+
'http_req_failed{type:content}': [{ threshold: 'rate<0.01' }], // less than 1% failed requests
4+
'http_req_duration{type:API}': ['p(90)<40000'], // 90% of the API requests must complete below 40s
5+
'http_req_duration{type:content}': ['p(99)<200'], // 99% of the content requests must complete below 200ms
6+
};
7+
8+
// 5.00 iterations/s for 1m0s (maxVUs: 100-200, gracefulStop: 30s)
9+
export const standardWorkload = {
10+
executor: 'constant-arrival-rate',
11+
rate: 5,
12+
timeUnit: '1s',
13+
duration: '1m',
14+
preAllocatedVUs: 100,
15+
maxVUs: 200,
16+
};

tests/load/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { mainpage } from './mainpage.js';
2+
import { chat } from './chat.js';
3+
import { thresholdsSettings, standardWorkload } from './config.js';
4+
5+
export const options = {
6+
scenarios: {
7+
staged: standardWorkload,
8+
},
9+
thresholds: thresholdsSettings,
10+
};
11+
12+
const webappUrl = __ENV.WEBAPP_URI;
13+
const searchUrl = __ENV.SEARCH_API_URI;
14+
15+
export default function () {
16+
mainpage(webappUrl);
17+
chat(searchUrl, true);
18+
//chat(searchUrl, false);
19+
}

tests/load/mainpage.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import http from 'k6/http';
2+
import { Trend } from 'k6/metrics';
3+
import { group, sleep } from 'k6';
4+
5+
const mainpageLatency = new Trend('mainpage_duration');
6+
7+
export function mainpage(baseUrl) {
8+
group('Mainpage', function () {
9+
// save response as variable
10+
const response = http.get(`${baseUrl}`, { tags: { type: 'content' } });
11+
// add duration property to metric
12+
mainpageLatency.add(response.timings.duration, { type: 'content' });
13+
sleep(1);
14+
});
15+
}

0 commit comments

Comments
 (0)