Skip to content

Commit 1e2dca3

Browse files
Merge pull request #991 from exadel-inc/k6_load_tests
Added k6 load tests
2 parents ba1c464 + f151226 commit 1e2dca3

33 files changed

+7403
-0
lines changed

load-tests/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# K6 load tests for CompreFace services
2+
Each folder inside `tests` folder contains a separate loading test.
3+
To run tests, first you need to build an image:
4+
```
5+
cd ./docker
6+
docker build -t k6tests .
7+
```
8+
9+
Then you can run all tests or define a list of tests (as TESTS env variable):
10+
```
11+
# run only face_verify and recognize tests
12+
docker run \
13+
-e TESTS="face_verify;recognize" \
14+
-e HOSTNAME="http://myhost:8082" \
15+
-e INFLUXDB_HOSTNAME="http://myinfluxdbhost:8086"
16+
-e DB_CONNECTION_STRING="user=postgres password=postgres port=5432 dbname=frs host=mydbhost sslmode=disable" \
17+
k6tests
18+
```
19+
```
20+
# run all tests
21+
docker run \
22+
-e HOSTNAME="http://myhost:8082" \
23+
-e INFLUXDB_HOSTNAME="http://myinfluxdbhost:8086"
24+
-e DB_CONNECTION_STRING="user=postgres password=postgres port=5432 dbname=frs host=mydbhost sslmode=disable" \
25+
k6tests
26+
```
27+
28+
Any test from `tests` folder follows those steps:
29+
1. Apply db_init.sql to database
30+
2. Run recognition test according to `scenarios` defined in the script
31+
3. Apply db_truncate.sql to database
32+
33+
34+
### Run command details
35+
```
36+
docker run
37+
--env IMAGES="./faces/FACE_512KB.jpg;./faces/FACE_1024KB.jpg"
38+
--env HOSTNAME="<host>"
39+
--env INFLUXDB_HOSTNAME="<influxdb_host>"
40+
--env DB_CONNECTION_STRING="user=postgres password=<password> port=5432 dbname=frs host=<db_host> sslmode=disable"
41+
<image_id>
42+
```
43+
`IMAGES` list of images fot test (if images are needed for the test)
44+
`HOSTNAME` hostname of test server
45+
`INFLUXDB_HOSTNAME` hostname of influxdb
46+
`DB_CONNECTION_STRING` DB connection string, template is *"user=mydbuser password=mydbpass port=5432 dbname=mydbname host=mydbhost sslmode=disable"*

load-tests/docker/Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM alpine:3.13.4
2+
3+
ENV K6_HOME="/data/k6" \
4+
# run all tests by default
5+
TESTS="" \
6+
VUS="1" \
7+
ITERATIONS="1" \
8+
DURATION="2m" \
9+
HOSTNAME="http://localhost:8000" \
10+
INFLUXDB_HOSTNAME="http://localhost:8086" \
11+
DB_CONNECTION_STRING="user=postgres password=postgres port=5432 dbname=frs host=localhost sslmode=disable"
12+
13+
RUN mkdir -p ${K6_HOME}
14+
WORKDIR ${K6_HOME}
15+
ADD . .
16+
RUN chmod +x ./entrypoint.sh \
17+
&& chmod +x ./k6
18+
19+
ENTRYPOINT ["./entrypoint.sh"]

load-tests/docker/entrypoint.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/sh
2+
3+
FOLDERS=""
4+
5+
if [ -z "$TESTS" ]
6+
then
7+
echo "TESTS is empty, run all tests"
8+
FOLDERS=$(ls -F tests | tr "/ " "\n")
9+
else
10+
echo "TESTS to run: $TESTS"
11+
FOLDERS=$(echo $TESTS | tr ";" "\n")
12+
fi
13+
14+
for test_folder in $FOLDERS
15+
do
16+
echo "************************************************************************"
17+
echo "********************************** $test_folder"
18+
echo "************************************************************************"
19+
20+
chmod +x ./tests/${test_folder}/loadtest.k6.js
21+
./k6 run \
22+
--insecure-skip-tls-verify \
23+
--vus ${VUS} \
24+
--iterations ${ITERATIONS} \
25+
--duration ${DURATION} \
26+
-e HOSTNAME="$HOSTNAME" \
27+
-e DB_CONNECTION_STRING="$DB_CONNECTION_STRING" \
28+
--out influxdb=${INFLUXDB_HOSTNAME}/${test_folder} \
29+
./tests/${test_folder}/loadtest.k6.js
30+
done

load-tests/docker/k6

26.1 MB
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
INSERT INTO app(id, name, guid, api_key) VALUES (1, 'TEST_K6_1', 'a1111a11-ae6d-4636-8b47-49754ca2f54b', '6f348698-6985-4296-914e-a5e3270cfad1');
2+
INSERT INTO model(id, name, guid, api_key, app_id, type) VALUES (1, 'D_SERVICE', 'm1111a11-ae6d-4636-8b47-49754ca2f54b', '1f348698-6985-4296-914e-a5e3270cfad7', 1, 'D');
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DELETE FROM app WHERE id IN ('1', '2', '3');
2+
DELETE FROM model WHERE id IN ('1');
528 KB
Loading
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* FormData polyfill for k6
3+
* Copyright (C) 2021 Load Impact
4+
* License: MIT
5+
*
6+
* This simplifies the creation of multipart/form-data requests from k6 scripts.
7+
* It was adapted from the original version by Rob Wu[1] to remove references of
8+
* XMLHttpRequest and File related code which isn't supported in k6.
9+
*
10+
* [1]: https://gist.github.com/Rob--W/8b5adedd84c0d36aba64
11+
**/
12+
13+
if (exports.FormData) {
14+
// Don't replace FormData if it already exists
15+
return;
16+
}
17+
18+
// Export variable to the global scope
19+
exports.FormData = FormData;
20+
21+
function FormData() {
22+
// Force a Constructor
23+
if (!(this instanceof FormData)) return new FormData();
24+
// Generate a random boundary - This must be unique with respect to the
25+
// form's contents.
26+
this.boundary = '------RWWorkerFormDataBoundary' + Math.random().toString(36);
27+
this.parts = [];
28+
29+
/**
30+
* Internal method. Convert input to a byte array.
31+
* @param inp String | ArrayBuffer | Uint8Array Input
32+
*/
33+
this.__toByteArray = function(inp) {
34+
var arr = [];
35+
var i = 0, len;
36+
if (typeof inp === 'string') {
37+
for (len = inp.length; i < len; ++i)
38+
arr.push(inp.charCodeAt(i) & 0xff);
39+
} else if (inp && inp.byteLength) {/*If ArrayBuffer or typed array */
40+
if (!('byteOffset' in inp)) /* If ArrayBuffer, wrap in view */
41+
inp = new Uint8Array(inp);
42+
for (len = inp.byteLength; i < len; ++i)
43+
arr.push(inp[i] & 0xff);
44+
}
45+
return arr;
46+
};
47+
}
48+
49+
/**
50+
* @param fieldName String Form field name
51+
* @param data object|string An object or string field value.
52+
*
53+
* If data is an object, it should match the structure of k6's http.FileData
54+
* object (returned by http.file()) and consist of:
55+
* @param data.data String|Array|ArrayBuffer File data
56+
* @param data.filename String Optional file name
57+
* @param data.content_type String Optional content type, default is application/octet-stream
58+
**/
59+
FormData.prototype.append = function(fieldName, data) {
60+
if (arguments.length < 2) {
61+
throw new SyntaxError('Not enough arguments');
62+
}
63+
var file = data;
64+
if (typeof data === 'string') {
65+
file = {data: data, content_type: 'text/plain'};
66+
}
67+
this.parts.push({field: fieldName, file: file});
68+
};
69+
70+
/**
71+
* Return the assembled request body as an ArrayBuffer.
72+
**/
73+
FormData.prototype.body = function() {
74+
var body = [];
75+
var barr = this.__toByteArray('--' + this.boundary + '\r\n');
76+
for (var i=0; i < this.parts.length; i++) {
77+
Array.prototype.push.apply(body, barr);
78+
var p = this.parts[i];
79+
var cd = 'Content-Disposition: form-data; name="' + p.field + '"';
80+
if (p.file.filename) {
81+
cd += '; filename="' + p.file.filename.replace(/"/g,'%22') + '"';
82+
}
83+
cd += '\r\nContent-Type: '
84+
+ (p.file.content_type || 'application/octet-stream')
85+
+ '\r\n\r\n';
86+
Array.prototype.push.apply(body, this.__toByteArray(cd));
87+
var data = Array.isArray(p.file.data) ? p.file.data : this.__toByteArray(p.file.data);
88+
Array.prototype.push.apply(body, data);
89+
Array.prototype.push.apply(body, this.__toByteArray('\r\n'));
90+
}
91+
Array.prototype.push.apply(body, this.__toByteArray('--' + this.boundary + '--\r\n'));
92+
return new Uint8Array(body).buffer;
93+
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import sql from "k6/x/sql"
2+
import http from "k6/http"
3+
import { check } from 'k6'
4+
import { FormData } from './formdata.js'
5+
6+
7+
const PREDEFINED_IMAGES = [
8+
"./faces/FACE_1.jpg"
9+
]
10+
11+
const XAPIKEY = '1f348698-6985-4296-914e-a5e3270cfad7'
12+
const IMAGE_PATHS = __ENV.IMAGES ? __ENV.IMAGES.split(';') : PREDEFINED_IMAGES
13+
const IMAGES = getImageFiles(IMAGE_PATHS)
14+
const REQUEST_TIMEOUT = 360000
15+
const db_init_sql = open("./db_init.sql")
16+
const db_truncate_sql = open("./db_truncate.sql")
17+
const db = sql.open("postgres", __ENV.DB_CONNECTION_STRING)
18+
19+
20+
export let options = {
21+
scenarios: {
22+
my_awesome_api_test: {
23+
executor: 'constant-vus',
24+
vus: 8,
25+
duration: '1m', // possible opts "Xs" (X seconds), "Xm" (X minutes), "Xh" (X hours), "Xd" (X days)
26+
},
27+
},
28+
thresholds: {
29+
http_req_duration: ['p(99)<3000'], // 99% of requests must complete below 3s
30+
},
31+
};
32+
33+
export function setup() {
34+
console.log("DB: " + __ENV.DB_CONNECTION_STRING)
35+
console.log("Host: " + __ENV.HOSTNAME)
36+
37+
execute_sql(db_init_sql)
38+
return {}
39+
}
40+
41+
export function teardown(data) {
42+
execute_sql(db_truncate_sql)
43+
db.close()
44+
}
45+
46+
export default function(data) {
47+
let response = verify(IMAGES[IMAGE_PATHS[0]], IMAGES[IMAGE_PATHS[1]])
48+
check(response, {
49+
'status 200': (r) => r.status === 200,
50+
'probability': (r) => r.body.indexOf('probability') !== -1,
51+
})
52+
}
53+
54+
function verify(image_file) {
55+
let url = __ENV.HOSTNAME + '/api/v1/detection/detect'
56+
57+
const fd = new FormData()
58+
fd.append('file', http.file(image_file, 'file.jpg', 'image/jpeg'))
59+
fd.append('limit', '0')
60+
fd.append('prediction_count', '1')
61+
62+
let headers = {
63+
'Content-Type': 'multipart/form-data; boundary=' + fd.boundary,
64+
'x-api-key': XAPIKEY,
65+
}
66+
67+
let params = {headers: headers, timeout: REQUEST_TIMEOUT}
68+
69+
return http.post(url, fd.body(), params)
70+
}
71+
72+
function getImageFiles(image_paths) {
73+
let image_files = {}
74+
75+
for (let index = 0; index < image_paths.length; ++index) {
76+
image_files[IMAGE_PATHS[index]] = open(image_paths[index], 'b')
77+
}
78+
79+
return image_files
80+
}
81+
82+
export function execute_sql(sql_string) {
83+
db.exec(sql_string)
84+
}

0 commit comments

Comments
 (0)