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 {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
||||||
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||||
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
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 {World} from "../Model/World";
|
||||||
import {Group} from "_Model/Group";
|
import {Group} from "_Model/Group";
|
||||||
import {UserInterface} from "_Model/UserInterface";
|
import {UserInterface} from "_Model/UserInterface";
|
||||||
@ -18,6 +18,7 @@ import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
|
|||||||
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
|
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
|
||||||
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
||||||
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
||||||
|
import {uuid} from 'uuidv4';
|
||||||
|
|
||||||
enum SockerIoEvent {
|
enum SockerIoEvent {
|
||||||
CONNECTION = "connection",
|
CONNECTION = "connection",
|
||||||
@ -60,10 +61,25 @@ export class IoSocketController {
|
|||||||
// Authentication with token. it will be decoded and stored in the socket.
|
// 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.
|
// Completely commented for now, as we do not use the "/login" route at all.
|
||||||
this.Io.use((socket: Socket, next) => {
|
this.Io.use((socket: Socket, next) => {
|
||||||
|
console.log(socket.handshake.query.token);
|
||||||
if (!socket.handshake.query || !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.');
|
console.error('An authentication error happened, a user tried to connect without a token.');
|
||||||
return next(new Error('Authentication error'));
|
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)){
|
if(this.searchClientByToken(socket.handshake.query.token)){
|
||||||
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
||||||
return next(new Error('Authentication error'));
|
return next(new Error('Authentication error'));
|
||||||
@ -155,6 +171,7 @@ export class IoSocketController {
|
|||||||
y: user y position on map
|
y: user y position on map
|
||||||
*/
|
*/
|
||||||
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
||||||
|
console.log(SockerIoEvent.JOIN_ROOM, message);
|
||||||
try {
|
try {
|
||||||
if (!isJoinRoomMessageInterface(message)) {
|
if (!isJoinRoomMessageInterface(message)) {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM 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 => {
|
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
||||||
|
console.log(SockerIoEvent.USER_POSITION, position);
|
||||||
try {
|
try {
|
||||||
if (!isPointInterface(position)) {
|
if (!isPointInterface(position)) {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
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
|
// Let's send the user id to the user
|
||||||
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
||||||
|
console.log(SockerIoEvent.SET_PLAYER_DETAILS, playerDetails);
|
||||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
||||||
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
||||||
@ -273,10 +292,14 @@ export class IoSocketController {
|
|||||||
const Client = (socket as ExSocketInterface);
|
const Client = (socket as ExSocketInterface);
|
||||||
Client.name = playerDetails.name;
|
Client.name = playerDetails.name;
|
||||||
Client.characterLayers = playerDetails.characterLayers;
|
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) => {
|
socket.on(SockerIoEvent.SET_SILENT, (silent: unknown) => {
|
||||||
|
console.log(SockerIoEvent.SET_SILENT, silent);
|
||||||
if (typeof silent !== "boolean") {
|
if (typeof silent !== "boolean") {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_SILENT message.'});
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_SILENT message.'});
|
||||||
console.warn('Invalid SET_SILENT message received: ', silent);
|
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 URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
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 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 {
|
export {
|
||||||
SECRET_KEY,
|
SECRET_KEY,
|
||||||
URL_ROOM_STARTED,
|
URL_ROOM_STARTED,
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
GROUP_RADIUS
|
GROUP_RADIUS,
|
||||||
|
ALLOW_ARTILLERY
|
||||||
}
|
}
|
||||||
|
@ -11,4 +11,5 @@ export interface ExSocketInterface extends Socket, Identificable {
|
|||||||
name: string;
|
name: string;
|
||||||
characterLayers: string[];
|
characterLayers: string[];
|
||||||
position: PointInterface;
|
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:
|
environment:
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
|
ALLOW_ARTILLERY: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
Loading…
Reference in New Issue
Block a user