Merge branch 'master' of github.com:thecodingmachine/workadventure into develop

This commit is contained in:
David Négrier 2020-09-11 13:42:07 +02:00
commit 1879c550c4
9 changed files with 197 additions and 3 deletions

View File

@ -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;
// Artillery fails when receiving an acknowledgement that is not a JSON object
if (!Client.isArtillery) {
answerFn(Client.userId); 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);

View File

@ -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
} }

View File

@ -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
View 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.

View 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
View 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"
}
}

View 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

View 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();
}

View File

@ -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: