mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Storage: k6 tests (#57496)
* object store k6 * update script * refactor * rename scripts * fix paths * fixes * fix client - check connected state * add teardown timeout * rename to grpc object store client * fail if health check fails * abort rather than fail * stale import * create `run.sh` * adjust for dummy server * fix mkdir * clean up dependencies * remove name and version * bring back name and version! * remove clean webpackk plugin * remove copy plugin * update yarn lock * remove stale import * update yarn lock * move perf tests to devenv/docker/loadtest-ts * add codeownres
This commit is contained in:
parent
10fdfa8583
commit
616db7f68b
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@ -46,6 +46,9 @@ go.sum @grafana/backend-platform
|
|||||||
/devenv/docker/blocks/loki* @grafana/observability-logs
|
/devenv/docker/blocks/loki* @grafana/observability-logs
|
||||||
/devenv/docker/blocks/elastic* @grafana/observability-logs
|
/devenv/docker/blocks/elastic* @grafana/observability-logs
|
||||||
|
|
||||||
|
# Performance tests
|
||||||
|
/devenv/docker/loadtests-ts @grafana/grafana-edge-squad
|
||||||
|
|
||||||
# Continuous Integration
|
# Continuous Integration
|
||||||
.drone.yml @grafana/grafana-release-eng
|
.drone.yml @grafana/grafana-release-eng
|
||||||
.drone.star @grafana/grafana-release-eng
|
.drone.star @grafana/grafana-release-eng
|
||||||
|
10
devenv/docker/loadtest-ts/.babelrc
Normal file
10
devenv/docker/loadtest-ts/.babelrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/env",
|
||||||
|
"@babel/typescript"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/proposal-class-properties",
|
||||||
|
"@babel/proposal-object-rest-spread"
|
||||||
|
]
|
||||||
|
}
|
3
devenv/docker/loadtest-ts/.gitignore
vendored
Normal file
3
devenv/docker/loadtest-ts/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
scripts/tmp
|
||||||
|
dist/
|
||||||
|
.yarn
|
14
devenv/docker/loadtest-ts/README.md
Normal file
14
devenv/docker/loadtest-ts/README.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Grafana load tests written in typescript - EXPERIMENTAL
|
||||||
|
|
||||||
|
Runs load tests written in typescript and checks Grafana's performance using [k6](https://k6.io/)
|
||||||
|
|
||||||
|
This is **experimental** - please consider adding new tests to devenv/docker/loadtest while we are testing the typescript approach!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# How to run
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
GRPC_TOKEN={REPLACE_WITH_SERVICE_ACCOUNT_ADMIN_TOKEN} ./run.sh test=object-store-test grpcAddress=127.0.0.1:10000 execution=local
|
||||||
|
```
|
28
devenv/docker/loadtest-ts/package.json
Normal file
28
devenv/docker/loadtest-ts/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"name": "@grafana/perf-tests",
|
||||||
|
"version": "9.3.0-pre",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "7.19.0",
|
||||||
|
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||||
|
"@babel/plugin-proposal-object-rest-spread": "7.18.9",
|
||||||
|
"@babel/preset-env": "7.19.0",
|
||||||
|
"@babel/preset-typescript": "7.18.6",
|
||||||
|
"@types/k6": "0.39.0",
|
||||||
|
"@types/shortid": "0.0.29",
|
||||||
|
"@types/webpack": "5.28.0",
|
||||||
|
"babel-loader": "8.2.5",
|
||||||
|
"shortid": "2.2.16",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
|
"typescript": "4.8.2",
|
||||||
|
"webpack": "5.74.0",
|
||||||
|
"webpack-cli": "4.10.0",
|
||||||
|
"webpack-glob-entries": "1.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack",
|
||||||
|
"prepare-testdata": "yarn run prepare-testdata:object-store-test",
|
||||||
|
"prepare-testdata:object-store-test": "ts-node scripts/prepareDashboardFileNames.ts ../../dev-dashboards ./scripts/tmp/filenames.json"
|
||||||
|
}
|
||||||
|
}
|
64
devenv/docker/loadtest-ts/run.sh
Executable file
64
devenv/docker/loadtest-ts/run.sh
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if ((BASH_VERSINFO[0] < 4)); then
|
||||||
|
echo "Bash ver >= 4 is needed to run this script"
|
||||||
|
echo "Please upgrade your bash - run 'brew install bash' if you use Homebrew on MacOS"
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -A cfg=(
|
||||||
|
[grpcToken]=$GRPC_TOKEN
|
||||||
|
[grpcAddress]="127.0.0.1:10000"
|
||||||
|
[execution]="local"
|
||||||
|
[test]="object-store-test"
|
||||||
|
[k6CloudToken]=$K6_CLOUD_TOKEN
|
||||||
|
)
|
||||||
|
|
||||||
|
for ARGUMENT in "$@"
|
||||||
|
do
|
||||||
|
KEY=$(echo $ARGUMENT | cut -f1 -d=)
|
||||||
|
|
||||||
|
KEY_LENGTH=${#KEY}
|
||||||
|
VALUE="${ARGUMENT:$KEY_LENGTH+1}"
|
||||||
|
cfg["$KEY"]="$VALUE"
|
||||||
|
done
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
echo "$0 grpcAddress= grpcToken= execution= k6CloudToken= test=
|
||||||
|
- 'grpcAddress' is the address of Grafana gRPC server. 127.0.0.1:10000 is the default.
|
||||||
|
- 'grpcToken' is the service account admin token used for Grafana gRPC server authentication.
|
||||||
|
- 'execution' is the test execution mode; one of 'local', 'cloud-output', 'cloud'. 'local' is the default.
|
||||||
|
- 'k6CloudToken' is the k6 cloud token required for 'cloud-output' and 'cloud' execution modes.
|
||||||
|
- 'test' is the filepath of the test to execute relative to ./src, without the extension. example 'object-store-test'"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${cfg[grpcToken]}" == "" ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if [ "${cfg[execution]}" == "cloud" ]; then
|
||||||
|
echo "cloud execution mode is not yet implemented"
|
||||||
|
exit 0
|
||||||
|
elif [ "${cfg[execution]}" == "cloud-output" ]; then
|
||||||
|
if [ "${cfg[k6CloudToken]}" == "" ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
elif [ "${cfg[execution]}" != "local" ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
yarn run build
|
||||||
|
yarn run prepare-testdata
|
||||||
|
|
||||||
|
TEST_PATH="./dist/${cfg[test]}.js"
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S'): Executing test ${TEST_PATH} in ${cfg[execution]} mode"
|
||||||
|
|
||||||
|
if [ "${cfg[execution]}" == "cloud-output" ]; then
|
||||||
|
GRPC_TOKEN="${cfg[grpcToken]}" GRPC_ADDRESS="${cfg[grpcAddress]}" K6_CLOUD_TOKEN="${cfg[k6CloudToken]}" k6 run --out cloud "$TEST_PATH"
|
||||||
|
elif [ "${cfg[execution]}" == "local" ]; then
|
||||||
|
GRPC_TOKEN="${cfg[grpcToken]}" GRPC_ADDRESS="${cfg[grpcAddress]}" k6 run "$TEST_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
|||||||
|
import { readdirSync, writeFileSync, mkdirSync } from 'fs';
|
||||||
|
import { dirname, resolve } from 'path';
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length !== 2) {
|
||||||
|
throw new Error('expected dev dashboards dir and the output file path');
|
||||||
|
}
|
||||||
|
|
||||||
|
const devDashboardsDir = args[0];
|
||||||
|
const outputFilePath = args[1];
|
||||||
|
|
||||||
|
const getFiles = (dirPath: string, ext?: string): string[] =>
|
||||||
|
readdirSync(dirPath, { withFileTypes: true })
|
||||||
|
.flatMap((dirEntry) => {
|
||||||
|
const res = resolve(dirPath, dirEntry.name);
|
||||||
|
return dirEntry.isDirectory() ? getFiles(res) : res;
|
||||||
|
})
|
||||||
|
.filter((path) => (ext?.length ? path.endsWith(ext) : true));
|
||||||
|
|
||||||
|
const files = getFiles(devDashboardsDir, '.json');
|
||||||
|
|
||||||
|
mkdirSync(dirname(outputFilePath), { recursive: true });
|
||||||
|
writeFileSync(outputFilePath, JSON.stringify(files, null, 2));
|
116
devenv/docker/loadtest-ts/src/get-large-dashboard.ts
Normal file
116
devenv/docker/loadtest-ts/src/get-large-dashboard.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
const testDash = {
|
||||||
|
annotations: { list: [] },
|
||||||
|
editable: true,
|
||||||
|
fiscalYearStartMonth: 0,
|
||||||
|
graphTooltip: 0,
|
||||||
|
id: 100,
|
||||||
|
links: [],
|
||||||
|
liveNow: false,
|
||||||
|
panels: [
|
||||||
|
{
|
||||||
|
datasource: {
|
||||||
|
type: 'testdata',
|
||||||
|
uid: 'testdata',
|
||||||
|
},
|
||||||
|
fieldConfig: {
|
||||||
|
defaults: {
|
||||||
|
color: {
|
||||||
|
mode: 'thresholds',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
align: 'auto',
|
||||||
|
displayMode: 'auto',
|
||||||
|
inspect: false,
|
||||||
|
},
|
||||||
|
mappings: [],
|
||||||
|
thresholds: {
|
||||||
|
mode: 'absolute',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
color: 'green',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'red',
|
||||||
|
value: 80,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [],
|
||||||
|
},
|
||||||
|
gridPos: {
|
||||||
|
h: 9,
|
||||||
|
w: 12,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
id: 2,
|
||||||
|
options: {
|
||||||
|
footer: {
|
||||||
|
fields: '',
|
||||||
|
reducer: ['sum'],
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
showHeader: true,
|
||||||
|
},
|
||||||
|
pluginVersion: '9.3.0-pre',
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
csvContent: '',
|
||||||
|
datasource: {
|
||||||
|
type: 'testdata',
|
||||||
|
uid: 'PD8C576611E62080A',
|
||||||
|
},
|
||||||
|
refId: 'A',
|
||||||
|
scenarioId: 'csv_content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: 'Panel Title',
|
||||||
|
type: 'table',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schemaVersion: 37,
|
||||||
|
style: 'dark',
|
||||||
|
tags: [],
|
||||||
|
templating: {
|
||||||
|
list: [],
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
from: 'now-6h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
timepicker: {},
|
||||||
|
timezone: '',
|
||||||
|
title: 'New dashboard',
|
||||||
|
uid: '5v6e5VH4z',
|
||||||
|
version: 1,
|
||||||
|
weekStart: '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const getCsvContent = (lengthInKb: number): string => {
|
||||||
|
const lines: string[] = ['id,name'];
|
||||||
|
for (let i = 0; i < lengthInKb; i++) {
|
||||||
|
const prefix = `${i},`;
|
||||||
|
lines.push(prefix + 'a'.repeat(1024 - prefix.length));
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareDashboard = (lengthInKb: number): Record<string, unknown> => {
|
||||||
|
const firstPanel = testDash.panels[0];
|
||||||
|
return {
|
||||||
|
...testDash,
|
||||||
|
panels: [
|
||||||
|
{
|
||||||
|
...firstPanel,
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
...firstPanel.targets[0],
|
||||||
|
csvContent: getCsvContent(lengthInKb),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
214
devenv/docker/loadtest-ts/src/object-store-client.ts
Normal file
214
devenv/docker/loadtest-ts/src/object-store-client.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { check } from 'k6';
|
||||||
|
import { b64encode } from 'k6/encoding';
|
||||||
|
import grpc from 'k6/net/grpc';
|
||||||
|
|
||||||
|
import { Object } from './prepare-data';
|
||||||
|
|
||||||
|
enum GRPCMethods {
|
||||||
|
ServerHealth = 'grpc.health.v1.Health/Check',
|
||||||
|
ObjectWrite = 'object.ObjectStore/Write',
|
||||||
|
ObjectDelete = 'object.ObjectStore/Delete',
|
||||||
|
ObjectRead = 'object.ObjectStore/Read',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GRPCObjectStoreClient {
|
||||||
|
private connected = false;
|
||||||
|
constructor(private client: grpc.Client, private grpcAddress: string, private grpcToken: string) {}
|
||||||
|
|
||||||
|
connect = () => {
|
||||||
|
if (!this.connected) {
|
||||||
|
this.client.connect(this.grpcAddress, { plaintext: true, reflect: true });
|
||||||
|
this.connected = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
grpcRequestParams = () => {
|
||||||
|
return {
|
||||||
|
metadata: {
|
||||||
|
authorization: `Bearer ${this.grpcToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
healthCheck = (): boolean => {
|
||||||
|
this.connect();
|
||||||
|
const response = this.client.invoke(GRPCMethods.ServerHealth, {});
|
||||||
|
|
||||||
|
return check(response, {
|
||||||
|
'server is healthy': (r) => {
|
||||||
|
const statusOK = r && r.status === grpc.StatusOK;
|
||||||
|
if (!statusOK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = r.message;
|
||||||
|
// @ts-ignore
|
||||||
|
return 'status' in body && body.status === 'SERVING';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteObject = (uid: string, kind: string, _?: {}) => {
|
||||||
|
this.connect();
|
||||||
|
|
||||||
|
const response = this.client.invoke(
|
||||||
|
GRPCMethods.ObjectDelete,
|
||||||
|
{
|
||||||
|
kind: kind,
|
||||||
|
UID: uid,
|
||||||
|
},
|
||||||
|
this.grpcRequestParams()
|
||||||
|
);
|
||||||
|
|
||||||
|
check(response, {
|
||||||
|
'object was deleted': (r) => {
|
||||||
|
const statusOK = r && r.status === grpc.StatusOK;
|
||||||
|
if (!statusOK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDeleteObjectResponse(r.message)) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'invalid_delete_response',
|
||||||
|
uid: uid,
|
||||||
|
kind: kind,
|
||||||
|
resp: r,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
readObject = (uid: string, kind: string, _?: {}) => {
|
||||||
|
this.connect();
|
||||||
|
|
||||||
|
const response = this.client.invoke(
|
||||||
|
GRPCMethods.ObjectRead,
|
||||||
|
{
|
||||||
|
kind: kind,
|
||||||
|
UID: uid,
|
||||||
|
with_body: true,
|
||||||
|
with_summary: true,
|
||||||
|
},
|
||||||
|
this.grpcRequestParams()
|
||||||
|
);
|
||||||
|
|
||||||
|
check(response, {
|
||||||
|
'object exists': (r) => {
|
||||||
|
const statusOK = r && r.status === grpc.StatusOK;
|
||||||
|
if (!statusOK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const respBody = r.message;
|
||||||
|
if (!isReadObjectResponse(respBody)) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'invalid_read_response',
|
||||||
|
uid: uid,
|
||||||
|
kind: kind,
|
||||||
|
resp: r,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof respBody.object.body === 'string';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
writeObject = (object: Object, opts?: { randomizeData?: boolean; checkCreatedOrUpdated?: boolean }) => {
|
||||||
|
this.connect();
|
||||||
|
|
||||||
|
const data = opts?.randomizeData
|
||||||
|
? {
|
||||||
|
...object.data,
|
||||||
|
__random: `${Date.now() - Math.random()}`,
|
||||||
|
}
|
||||||
|
: object.data;
|
||||||
|
|
||||||
|
const response = this.client.invoke(
|
||||||
|
GRPCMethods.ObjectWrite,
|
||||||
|
{
|
||||||
|
body: b64encode(JSON.stringify(data)),
|
||||||
|
comment: '',
|
||||||
|
kind: object.kind,
|
||||||
|
UID: object.uid,
|
||||||
|
},
|
||||||
|
this.grpcRequestParams()
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkName = opts?.checkCreatedOrUpdated ? 'object was created or updated' : 'object was created';
|
||||||
|
check(response, {
|
||||||
|
[checkName]: (r) => {
|
||||||
|
const statusOK = r && r.status === grpc.StatusOK;
|
||||||
|
if (!statusOK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const respBody = r.message;
|
||||||
|
if (!isWriteObjectResponse(respBody)) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'invalid_write_response',
|
||||||
|
uid: object.uid,
|
||||||
|
kind: object.kind,
|
||||||
|
resp: r,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts?.checkCreatedOrUpdated
|
||||||
|
? respBody.status === WriteObjectResponseStatus.UPDATED ||
|
||||||
|
respBody.status === WriteObjectResponseStatus.CREATED
|
||||||
|
: respBody.status === WriteObjectResponseStatus.CREATED;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteObjectResponse = {
|
||||||
|
OK: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDeleteObjectResponse = (resp: object): resp is DeleteObjectResponse => {
|
||||||
|
return resp.hasOwnProperty('OK');
|
||||||
|
};
|
||||||
|
|
||||||
|
enum WriteObjectResponseStatus {
|
||||||
|
CREATED = 'CREATED',
|
||||||
|
UPDATED = 'UPDATED',
|
||||||
|
}
|
||||||
|
|
||||||
|
type WriteObjectResponse = {
|
||||||
|
status: WriteObjectResponseStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWriteObjectResponse = (resp: object): resp is WriteObjectResponse => {
|
||||||
|
return resp.hasOwnProperty('status');
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReadObjectResponse = {
|
||||||
|
object: {
|
||||||
|
UID: string;
|
||||||
|
kind: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isReadObjectResponse = (resp: object): resp is ReadObjectResponse => {
|
||||||
|
if (!resp.hasOwnProperty('object')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const object = resp.object;
|
||||||
|
return Boolean(object && typeof object === 'object' && object.hasOwnProperty('body'));
|
||||||
|
};
|
145
devenv/docker/loadtest-ts/src/object-store-test.ts
Executable file
145
devenv/docker/loadtest-ts/src/object-store-test.ts
Executable file
@ -0,0 +1,145 @@
|
|||||||
|
import { SharedArray } from 'k6/data';
|
||||||
|
import execution from 'k6/execution';
|
||||||
|
import grpc from 'k6/net/grpc';
|
||||||
|
|
||||||
|
import { GRPCObjectStoreClient } from './object-store-client';
|
||||||
|
import { Data, prepareData } from './prepare-data';
|
||||||
|
|
||||||
|
const grpcToken = __ENV.GRPC_TOKEN;
|
||||||
|
const grpcAddress = __ENV.GRPC_ADDRESS;
|
||||||
|
|
||||||
|
if (typeof grpcToken !== 'string' || !grpcToken.length) {
|
||||||
|
throw new Error('GRPC_TOKEN env variable is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof grpcAddress !== 'string' || !grpcAddress.length) {
|
||||||
|
throw new Error('GRPC_ADDRESS env variable is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new grpc.Client();
|
||||||
|
const objectStoreClient = new GRPCObjectStoreClient(client, grpcAddress, grpcToken);
|
||||||
|
|
||||||
|
const data: Data = new SharedArray('data', () => {
|
||||||
|
return [prepareData(JSON.parse(open('../scripts/tmp/filenames.json')), 50)];
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const scenarioDuration = '2m';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
setupTimeout: '5m',
|
||||||
|
teardownTimeout: '5m',
|
||||||
|
noConnectionReuse: true,
|
||||||
|
scenarios: {
|
||||||
|
writer: {
|
||||||
|
exec: 'writer',
|
||||||
|
executor: 'constant-arrival-rate',
|
||||||
|
rate: 1,
|
||||||
|
timeUnit: '2s',
|
||||||
|
duration: scenarioDuration,
|
||||||
|
preAllocatedVUs: 1,
|
||||||
|
maxVUs: 1,
|
||||||
|
},
|
||||||
|
reader: {
|
||||||
|
exec: 'reader',
|
||||||
|
executor: 'constant-arrival-rate',
|
||||||
|
rate: 10,
|
||||||
|
timeUnit: '2s',
|
||||||
|
duration: scenarioDuration,
|
||||||
|
preAllocatedVUs: 1,
|
||||||
|
maxVUs: 10,
|
||||||
|
},
|
||||||
|
writer1mb: {
|
||||||
|
exec: 'writer1mb',
|
||||||
|
executor: 'constant-arrival-rate',
|
||||||
|
rate: 1,
|
||||||
|
timeUnit: '20s',
|
||||||
|
duration: scenarioDuration,
|
||||||
|
preAllocatedVUs: 1,
|
||||||
|
maxVUs: 5,
|
||||||
|
},
|
||||||
|
reader1mb: {
|
||||||
|
startTime: '2s',
|
||||||
|
exec: 'reader1mb',
|
||||||
|
executor: 'constant-arrival-rate',
|
||||||
|
rate: 1,
|
||||||
|
timeUnit: '1s',
|
||||||
|
duration: scenarioDuration,
|
||||||
|
preAllocatedVUs: 1,
|
||||||
|
maxVUs: 5,
|
||||||
|
},
|
||||||
|
writer4mb: {
|
||||||
|
exec: 'writer4mb',
|
||||||
|
executor: 'constant-arrival-rate',
|
||||||
|
rate: 1,
|
||||||
|
timeUnit: '30s',
|
||||||
|
duration: scenarioDuration,
|
||||||
|
preAllocatedVUs: 1,
|
||||||
|
maxVUs: 5,
|
||||||
|
},
|
||||||
|
reader4mb: {
|
||||||
|
startTime: '3s',
|
||||||
|
exec: 'reader4mb',
|
||||||
|
executor: 'constant-arrival-rate',
|
||||||
|
rate: 1,
|
||||||
|
timeUnit: '5s',
|
||||||
|
duration: scenarioDuration,
|
||||||
|
preAllocatedVUs: 1,
|
||||||
|
maxVUs: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// thresholds: { http_req_duration: ['avg<100', 'p(95)<200'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
if (!objectStoreClient.healthCheck()) {
|
||||||
|
execution.test.abort('server should be healthy');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('inserting base objects');
|
||||||
|
for (let i = 0; i < data.base.length; i++) {
|
||||||
|
if (i % 100 === 0) {
|
||||||
|
console.log(`inserted ${i} / ${data.base.length}`);
|
||||||
|
}
|
||||||
|
objectStoreClient.writeObject(data.base[i], { randomizeData: false, checkCreatedOrUpdated: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function teardown() {
|
||||||
|
const toDelete = [...data.base, ...data.toWrite, data.size1mb, data.size4mb, data.size100kb];
|
||||||
|
|
||||||
|
console.log('deleting base objects');
|
||||||
|
for (let i = 0; i < toDelete.length; i++) {
|
||||||
|
if (i % 100 === 0) {
|
||||||
|
console.log(`deleted ${i} / ${data.base.length}`);
|
||||||
|
}
|
||||||
|
objectStoreClient.deleteObject(toDelete[i].uid, toDelete[i].kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reader() {
|
||||||
|
const item = data.base[execution.scenario.iterationInTest % data.base.length];
|
||||||
|
objectStoreClient.readObject(item.uid, item.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writer() {
|
||||||
|
const item = data.toWrite[execution.scenario.iterationInTest % data.toWrite.length];
|
||||||
|
objectStoreClient.writeObject(item, { randomizeData: true, checkCreatedOrUpdated: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writer1mb() {
|
||||||
|
objectStoreClient.writeObject(data.size1mb, { randomizeData: true, checkCreatedOrUpdated: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reader1mb() {
|
||||||
|
const item = data.size1mb;
|
||||||
|
objectStoreClient.readObject(item.uid, item.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writer4mb() {
|
||||||
|
objectStoreClient.writeObject(data.size4mb, { randomizeData: true, checkCreatedOrUpdated: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reader4mb() {
|
||||||
|
const item = data.size4mb;
|
||||||
|
objectStoreClient.readObject(item.uid, item.kind);
|
||||||
|
}
|
57
devenv/docker/loadtest-ts/src/prepare-data.ts
Normal file
57
devenv/docker/loadtest-ts/src/prepare-data.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import shortid from 'shortid';
|
||||||
|
|
||||||
|
import { prepareDashboard } from './get-large-dashboard';
|
||||||
|
|
||||||
|
export type Object = {
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
kind: string;
|
||||||
|
uid: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Data = {
|
||||||
|
base: Object[]; // objects that are inserted in the test setup and removed only in the teardown
|
||||||
|
toWrite: Object[]; // objects that are inserted by scenarios and removed after a short period of time: Object;
|
||||||
|
size100kb: Object;
|
||||||
|
size1mb: Object;
|
||||||
|
size4mb: Object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readAsObjects = (paths: string[], kind: string): Object[] => {
|
||||||
|
return paths.map((p) => ({
|
||||||
|
data: JSON.parse(open(p)),
|
||||||
|
uid: shortid.generate(),
|
||||||
|
kind,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBase = (uniqueObjects: Object[], no: number): Object[] => {
|
||||||
|
const base = new Array<Object>(no);
|
||||||
|
for (let i = 0; i < no; i++) {
|
||||||
|
const obj = uniqueObjects[Math.floor(i % uniqueObjects.length)];
|
||||||
|
base[i] = {
|
||||||
|
...obj,
|
||||||
|
uid: `${obj.uid}-${Math.floor(i / uniqueObjects.length)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareObject = (lengthInKb: number): Object => {
|
||||||
|
return {
|
||||||
|
data: prepareDashboard(lengthInKb),
|
||||||
|
kind: 'dashboard',
|
||||||
|
uid: shortid(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareData = (dashboardFilePaths: string[], baseNumber: number): Data => {
|
||||||
|
const objects = readAsObjects(dashboardFilePaths, 'dashboard');
|
||||||
|
return {
|
||||||
|
base: getBase(objects, baseNumber),
|
||||||
|
toWrite: objects,
|
||||||
|
size100kb: prepareObject(100),
|
||||||
|
size1mb: prepareObject(1000),
|
||||||
|
size4mb: prepareObject(4000),
|
||||||
|
};
|
||||||
|
};
|
26
devenv/docker/loadtest-ts/tsconfig.json
Normal file
26
devenv/docker/loadtest-ts/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "commonjs",
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"removeComments": false,
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
37
devenv/docker/loadtest-ts/webpack.config.js
Normal file
37
devenv/docker/loadtest-ts/webpack.config.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const GlobEntries = require('webpack-glob-entries');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
entry: GlobEntries('./src/*test*.ts'), // Generates multiple entry for each test
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
libraryTarget: 'commonjs',
|
||||||
|
filename: '[name].js',
|
||||||
|
clean: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: 'babel-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
target: 'web',
|
||||||
|
externals: /^(k6|https?\:\/\/)(\/.*)?/,
|
||||||
|
// Generate map files for compiled scripts
|
||||||
|
devtool: 'source-map',
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
optimization: {
|
||||||
|
// Don't minimize, as it's not used in the browser
|
||||||
|
minimize: false,
|
||||||
|
},
|
||||||
|
};
|
3108
devenv/docker/loadtest-ts/yarn.lock
Normal file
3108
devenv/docker/loadtest-ts/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user