Merge pull request #269 from thecodingmachine/simulate-player
Create config file artillery websocket
This commit is contained in:
commit
483bb9de3a
@ -4,7 +4,7 @@ import * as http from "http";
|
||||
import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
||||
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
||||
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||
import {World} from "../Model/World";
|
||||
import {Group} from "_Model/Group";
|
||||
import {UserInterface} from "_Model/UserInterface";
|
||||
@ -18,6 +18,7 @@ import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
|
||||
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
|
||||
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
||||
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
||||
import {uuid} from 'uuidv4';
|
||||
|
||||
enum SockerIoEvent {
|
||||
CONNECTION = "connection",
|
||||
@ -60,10 +61,25 @@ export class IoSocketController {
|
||||
// Authentication with token. it will be decoded and stored in the socket.
|
||||
// Completely commented for now, as we do not use the "/login" route at all.
|
||||
this.Io.use((socket: Socket, next) => {
|
||||
console.log(socket.handshake.query.token);
|
||||
if (!socket.handshake.query || !socket.handshake.query.token) {
|
||||
console.error('An authentication error happened, a user tried to connect without a token.');
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
if(socket.handshake.query.token === 'test'){
|
||||
if (ALLOW_ARTILLERY) {
|
||||
(socket as ExSocketInterface).token = socket.handshake.query.token;
|
||||
(socket as ExSocketInterface).userId = uuid();
|
||||
(socket as ExSocketInterface).isArtillery = true;
|
||||
console.log((socket as ExSocketInterface).userId);
|
||||
next();
|
||||
return;
|
||||
} else {
|
||||
console.warn("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'");
|
||||
next();
|
||||
}
|
||||
}
|
||||
(socket as ExSocketInterface).isArtillery = false;
|
||||
if(this.searchClientByToken(socket.handshake.query.token)){
|
||||
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
||||
return next(new Error('Authentication error'));
|
||||
@ -155,6 +171,7 @@ export class IoSocketController {
|
||||
y: user y position on map
|
||||
*/
|
||||
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
||||
console.log(SockerIoEvent.JOIN_ROOM, message);
|
||||
try {
|
||||
if (!isJoinRoomMessageInterface(message)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'});
|
||||
@ -199,6 +216,7 @@ export class IoSocketController {
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
||||
console.log(SockerIoEvent.USER_POSITION, position);
|
||||
try {
|
||||
if (!isPointInterface(position)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
||||
@ -265,6 +283,7 @@ export class IoSocketController {
|
||||
|
||||
// Let's send the user id to the user
|
||||
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
||||
console.log(SockerIoEvent.SET_PLAYER_DETAILS, playerDetails);
|
||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
||||
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
||||
@ -273,10 +292,14 @@ export class IoSocketController {
|
||||
const Client = (socket as ExSocketInterface);
|
||||
Client.name = playerDetails.name;
|
||||
Client.characterLayers = playerDetails.characterLayers;
|
||||
answerFn(Client.userId);
|
||||
// Artillery fails when receiving an acknowledgement that is not a JSON object
|
||||
if (!Client.isArtillery) {
|
||||
answerFn(Client.userId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.SET_SILENT, (silent: unknown) => {
|
||||
console.log(SockerIoEvent.SET_SILENT, silent);
|
||||
if (typeof silent !== "boolean") {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_SILENT message.'});
|
||||
console.warn('Invalid SET_SILENT message received: ', silent);
|
||||
|
@ -2,10 +2,12 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
||||
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
||||
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
|
||||
|
||||
export {
|
||||
SECRET_KEY,
|
||||
URL_ROOM_STARTED,
|
||||
MINIMUM_DISTANCE,
|
||||
GROUP_RADIUS
|
||||
GROUP_RADIUS,
|
||||
ALLOW_ARTILLERY
|
||||
}
|
||||
|
@ -11,4 +11,5 @@ export interface ExSocketInterface extends Socket, Identificable {
|
||||
name: string;
|
||||
characterLayers: string[];
|
||||
position: PointInterface;
|
||||
isArtillery: boolean; // Whether this socket is opened by Artillery for load testing (hack)
|
||||
}
|
||||
|
69
benchmark/README.md
Normal file
69
benchmark/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Load testing
|
||||
|
||||
Load testing is performed with Artillery.
|
||||
|
||||
Install:
|
||||
|
||||
```bash
|
||||
cd benchmark
|
||||
npm install
|
||||
```
|
||||
|
||||
Running the tests (on one core):
|
||||
|
||||
```bash
|
||||
cd benchmark
|
||||
npm run start
|
||||
```
|
||||
|
||||
You can adapt the `socketio-load-test.yaml` file to increase/decrease load.
|
||||
|
||||
Default settings are:
|
||||
|
||||
```yaml
|
||||
phases:
|
||||
- duration: 20
|
||||
arrivalRate: 2
|
||||
```
|
||||
|
||||
which means: during 20 seconds, 2 users will be added every second (peaking at 40 simultaneous users).
|
||||
|
||||
Important: don't go above 40 simultaneous users for Artillery, otherwise, it is Artillery that will fail to run the tests properly.
|
||||
To know, simply run "top". The "node" process for Artillery should never reach 100%.
|
||||
|
||||
Reports are generated in `artillery_output.html`.
|
||||
|
||||
# Multicore tests
|
||||
|
||||
You will want to test with Artillery running on multiple cores.
|
||||
|
||||
You can use
|
||||
|
||||
```bash
|
||||
./artillery_multi_core.sh
|
||||
```
|
||||
|
||||
This will trigger 4 Artillery instances in parallel.
|
||||
|
||||
Beware, the report generated is generated for only one instance.
|
||||
|
||||
# How to test, what to track?
|
||||
|
||||
While testing, you can check:
|
||||
|
||||
- CPU load of WorkAdventure API node process (it should not reach 100%)
|
||||
- Get metrics at the end of the run: `http://api.workadventure.localhost/metrics`
|
||||
In particular, look for:
|
||||
```
|
||||
# HELP nodejs_eventloop_lag_max_seconds The maximum recorded event loop delay.
|
||||
# TYPE nodejs_eventloop_lag_max_seconds gauge
|
||||
nodejs_eventloop_lag_max_seconds 23.991418879
|
||||
```
|
||||
This is the maximum time it took Node to process an event (you need to restart node after each test to reset this counter)
|
||||
- Generate a profiling using "node --prof" by switching the command in docker-compose.yaml:
|
||||
```
|
||||
#command: yarn dev
|
||||
command: yarn run profile
|
||||
```
|
||||
Read https://nodejs.org/en/docs/guides/simple-profiling/ on how to generate a profile.
|
||||
|
17
benchmark/artillery_multi_core.sh
Executable file
17
benchmark/artillery_multi_core.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
npm run start &
|
||||
pid1=$!
|
||||
npm run start:nooutput &
|
||||
pid2=$!
|
||||
npm run start:nooutput &
|
||||
pid3=$!
|
||||
npm run start:nooutput &
|
||||
pid4=$!
|
||||
|
||||
wait $pid1
|
||||
wait $pid2
|
||||
wait $pid3
|
||||
wait $pid4
|
||||
|
||||
|
27
benchmark/package.json
Normal file
27
benchmark/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "workadventure-artillery",
|
||||
"version": "1.0.0",
|
||||
"description": "Load testing for WorkAdventure",
|
||||
"scripts": {
|
||||
"start": "artillery run socketio-load-test.yaml -o artillery_output.json && artillery report --output artillery_output.html artillery_output.json",
|
||||
"start:nooutput": "artillery run socketio-load-test.yaml"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Grégoire Parant",
|
||||
"email": "g.parant@thecodingmachine.com"
|
||||
},
|
||||
{
|
||||
"name": "David Négrier",
|
||||
"email": "d.negrier@thecodingmachine.com"
|
||||
},
|
||||
{
|
||||
"name": "Arthmaël Poly",
|
||||
"email": "a.poly@thecodingmachine.com"
|
||||
}
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"dependencies": {
|
||||
"artillery": "^1.6.1"
|
||||
}
|
||||
}
|
43
benchmark/socketio-load-test.yaml
Normal file
43
benchmark/socketio-load-test.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
config:
|
||||
target: "http://api.workadventure.localhost/"
|
||||
socketio:
|
||||
transports: ["websocket"]
|
||||
query:
|
||||
token: "test"
|
||||
phases:
|
||||
- duration: 20
|
||||
arrivalRate: 2
|
||||
processor: "./socketioLoadTest.js"
|
||||
scenarios:
|
||||
- name: "Connects and moves player for 20 seconds"
|
||||
weight: 90
|
||||
engine: "socketio"
|
||||
flow:
|
||||
- emit:
|
||||
channel: "set-player-details"
|
||||
data:
|
||||
name: 'TEST'
|
||||
characterLayers: ['male3']
|
||||
- think: 1
|
||||
- emit:
|
||||
channel: "join-room"
|
||||
data:
|
||||
roomId: 'global__api.workadventure.localhost/map/files/Floor0/floor0'
|
||||
position:
|
||||
x: 783
|
||||
y: 170
|
||||
direction: 'down'
|
||||
moving: false
|
||||
- think: 1
|
||||
- loop:
|
||||
- function: "setYRandom"
|
||||
- emit:
|
||||
channel: "user-position"
|
||||
data:
|
||||
x: "{{ x }}"
|
||||
y: "{{ y }}"
|
||||
direction: 'down'
|
||||
moving: false
|
||||
- think: 0.2
|
||||
count: 100
|
||||
- think: 10
|
11
benchmark/socketioLoadTest.js
Normal file
11
benchmark/socketioLoadTest.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
setYRandom
|
||||
};
|
||||
|
||||
function setYRandom(context, events, done) {
|
||||
context.vars.x = (883 + Math.round(Math.random() * 300));
|
||||
context.vars.y = (270 + Math.round(Math.random() * 300));
|
||||
return done();
|
||||
}
|
@ -50,6 +50,7 @@ services:
|
||||
environment:
|
||||
STARTUP_COMMAND_1: yarn install
|
||||
SECRET_KEY: yourSecretKey
|
||||
ALLOW_ARTILLERY: "true"
|
||||
volumes:
|
||||
- ./back:/usr/src/app
|
||||
labels:
|
||||
|
Loading…
Reference in New Issue
Block a user