'Nodejs Game using too much cpu/ram

I am making a Nodejs Multiplayer game.

PROBLEM: After a period of time, around a few seconds, the latency between the server to client skyrockets. Not just a little. I have gotten up to 60 seconds of delay!!!! My server has 24GB of RAM and 8 CPU cores so this really surprised me. Also, the latency between the client and server is perfectly normal so I have to believe something is wrong with my code. This problem has been super annoying when I try to play it.

What I have tried: So, I have a temporary solution for this that DELAYS the problem, but doesn't remove it. When I set perMessageDeflate up to the defaults on the documentation, I have like 2x the play time before I have lag. I have done internet speed tests on my server. Both upload and download speed are above 1000 Mbps I have done websocket server tests and I can achieve pretty good results(around 30,000 to 40,000 messages per second)

WHAT HELP WITH THE PROBLEM: 1.) perMessageDeflate set to documentation

const io = require("socket.io")(httpServer, {
  perMessageDeflate: {
    threshold: 2048, // defaults to 1024

    zlibDeflateOptions: {
      chunkSize: 8 * 1024, // defaults to 16 * 1024
    },

    zlibInflateOptions: {
      windowBits: 14, // defaults to 15
      memLevel: 7, // defaults to 8
    },

    clientNoContextTakeover: true, // defaults to negotiated value.
    serverNoContextTakeover: true, // defaults to negotiated value.
    serverMaxWindowBits: 10, // defaults to negotiated value.

    concurrencyLimit: 20, // defaults to 10
  }
});

2.) Lowering the fps the server runs I want it to run at 120 fps(well not frames, more like updates) so ups, but I can only manage around 30 so I can play for a longer period of time.

CODE:

My code is kinda long(10000+ lines. I have too much free time) so I have removed unnecessary stuff:

Server:

Socket code:

io.on('connection', socket => {
  sockets.push(socket);
  socket.on('message', async function(msg) { // async is for other stuff I use my server for
    if (socket.username == undefined) {
      socket.username = JSON.parse(msg).username;
      if (JSON.parse(msg).task != 'auth') {
        if (bans.includes(socket.username)) {
          socket.disconnect();
        }
      }
    }
    var data = JSON.parse(msg);
    if (data.operation === 'multiplayer') { // I use my websocket server for other stuff then multiplayer things, but they dont have any performance issues.
      if (data.task == 'tag') {
        if (socket.room == undefined) {
          socket.room = 'main';
          if (tagRooms[0] == 0) {
            socket.tagRoom = 0;
            servers.tag[socket.room] = new HostTag();
            servers.tag[socket.room].control(socket.room);
            servers.tag[socket.room].sockets.push(socket);
            tagRooms[0]++;
          } else {
            socket.tagRoom = 0;
            servers.tag[socket.room].sockets.push(socket);
            tagRooms[0]++;
          }
        } else {
          if (data.event == 'joinerupdate') {
            servers.tag[socket.room].joinerupdate(data)
          }
          if (data.event == 'joinerjoin') {
            servers.tag[socket.room].joinerjoin(data);
          }
        }
      } else if (data.task == 'pixel-tanks') {
        if (data.gamemode == 'ffa') {
          if (socket.room == undefined) {
            socket.gamemode = 'ffa';
            if (JSON.parse(msg).mode == 'quick-join') {
              var l = 0, ip;
              while (l < pvpRooms.length) {
                if (pvpRooms[l] < 5) {
                  ip = 'quick-join' + l;
                  pvpRooms[l] += 1;
                  socket.pvpRoom = l;
                  socket.room = ip;
                  if (pvpRooms[l] == 1) {
                    console.log('[MULTI] => New FFA server created ' + ip);
                    servers.tanks.ffa[ip] = new HostTanks(ip, 'FFA');
                    console.log(util.inspect(servers));
                  }
                  servers.tanks.ffa[ip].sockets.push(socket);
                  l = pvpRooms.length;
                }
                l++;
              }
              if (socket.pvpRoom == undefined) {
                socket.disconnect();
                sockets = sockets.filter(s => s !== socket);
                return;
              }
            } else {
              if (servers.tanks.ffa[data.room] == undefined) {
                if (data.room == undefined) {
                  console.log(util.inspect(data.room));
                  return;
                }
                console.log('[MULTI] => New FFA room created ' + data.room);
                servers.tanks.ffa[data.room] = new HostTanks(data.room, 'FFA');
              }
              servers.tanks.ffa[data.room].sockets.push(socket);
              socket.room = data.room;
            }
          } else {
            if (data.event == 'joinerupdate') {
              servers.tanks.ffa[socket.room].joinerupdate(data);
            }
            if (data.event == 'joinerjoin') {
              servers.tanks.ffa[socket.room].joinerjoin(data.data);
            }
          }
        }
        // I have other gamemodes but they all have the same problem and similar code so i  will just have ffa
      }
    }
  });
  socket.on('disconnect', function() {
    if (socket.room != undefined && socket.tagRoom == undefined) {
      servers.tanks[socket.gamemode][socket.room].disconnect(socket.username);
    } else if (socket.room != undefined && socket.tagRoom != undefined) {
      servers.tag[socket.room].disconnect(socket.username);
    }
    if (socket.pvpRoom != undefined) {
      pvpRooms[socket.pvpRoom] -= 1;
    }
    if (socket.duelRoom != undefined) {
      duelRooms[socket.duelRoom] -= 1;
    }
    if (socket.tdmRoom != undefined) {
      tdmRooms[socket.tdmRoom] -= 1;
    }
    if (socket.defenseRoom != undefined) {
      defenseRooms[socket.tdmRoom] -= 1;
    }
    if (socket.tagRoom != undefined) {
      tagRooms[socket.tagRoom] -= 1;
    }
    sockets = sockets.filter(s => s !== socket);
  });
});

This is the actual rendering of the game(on server). It's what runs the game.


class HostTanks {
  constructor(channelname, gamemode) {
    this.spawn = {
      x: 0,
      y: 0,
    }
    this.channelname = channelname;
    this.gamemode = gamemode;
    this.gamestate = 0; // 0 -> Waiting for Players, 1 -> Game started!
    this.s = [];
    this.b = [];
    this.pt = [];
    this.ai = [];
    this.scaffolding = [];
    this.sockets = [];
    this.i = [];
    this.t = [];
    if (this.gamemode == 'DUELS' || this.gamemode == 'RAID' || this.gamemode == 'DEFENSE' || this.gamemode == 'TDM') {
      this.i.push(setInterval(function(host) {
        host.gameRunner(host);
      }, 30, this));
    }
    ... game running and stuff
  }
  gameRunner(host) {
    // runs the game // not really important
  }
  joinerupdate(data) {
    var tank = data.data;
    var l = 0;
    while (l < this.pt.length) {
      if (this.pt[l].username == tank.username) {
        // Apply updates to server //
// This part is where it sends the data back to the player
        var m = 0;
        while (m<this.sockets.length) {
          if (this.sockets[m].username == this.pt[l].username) {
            var gameMessage;
            if (!this.gameMessage) {
              if (this.pt[l].ded) {
                gameMessage = 'Back in '+new String(new Date().getTime() - this.pt[l].dedTime.getTime());
              } else {
                var kills = this.pt[l].kills;
                gameMessage = 'Kills: ' + kills;
              }
            } else {
              gameMessage = this.gameMessage;
            }
            this.sockets[m].emit('message', JSON.stringify({
              sendTime: new Date().toISOString(),
              event: 'hostupdate',
              tanks: this.pt,
              blocks: this.b,
              scaffolding: this.scaffolding,
              bullets: this.s,
              gameMessage: gameMessage,
            }, replacer));
          }
          m++;
        } // Send update as soon as joiner sends data
      }
      l++;
    }
  }
  joinerjoin(data) { //done
    // registers a new tank to the server
    // pt = playertanks, teamData = team core hp and team playertanks
    var tank = data;
    if (this.gamemode == 'FFA') {
      var l = 0;
      while (l<this.sockets.length) {
        if (this.sockets[l].username == tank.username) {
          this.sockets[l].emit('message', JSON.stringify({
            event: 'override',
            data: {
              key: 'x',
              value: this.spawn.x,
              key2: 'y',
              value2: this.spawn.y,
            }
          }));
        }
        l++;
      }
    } else if (this.gamemode == 'DUELS') {
      var l = 0;
      while (l < this.sockets.length) {
        if (this.sockets[l].username == tank.username) {
          if (this.pt.length == 0) {
            this.sockets[l].emit('message', JSON.stringify({
              event: 'override',
              data: {
                key: 'x',
                value: 0,
                key2: 'y',
                value2: 0,
              }
            }));
          } else if (this.pt.length == 1) {
            this.sockets[l].emit('message', JSON.stringify({
              event: 'override',
              data: {
                key: 'x',
                value: 1460,
                key2: 'y',
                value2: 1460,
              }
            }));
          }
        }
        l++;
      }
    } else if (this.gamemode == 'DEFENSE') {
      var l = 0;
      while (l < this.sockets.length) {
        if (this.sockets[l].username == tank.username) {
          this.sockets[l].emit('message', JSON.stringify({
            event: 'override',
            data: {
              key: 'team',
              value: 'Players',
            }
          }));
        }
        l++;
      }
    } else if (this.gamemode == 'TDM') {
      tank.team = 'lobby';
      tank.damagedRecent = false;
      this.pt.push(tank);
      if (this.pt.length == 6) {
        var l = 0, blue = [], red = [];
        while (l<this.pt.length) {
          var canBlue, canRed;
          if (blue.length != 3) {
            canBlue = true;
          } else {
            canBlue = false;
          }
          if (red.length != 3) {
            canRed = true;
          } else {
            canRed = false;
          }
          if (canBlue && canRed) {
            if (Math.random() < 0.5) {
              blue.push(this.pt[l]);
              this.pt[l].team = 'blue';
            } else {
              red.push(this.pt[l]);
              this.pt[l].team = 'red';
            }
          } else {
            if (canBlue) {
              blue.push(this.pt[l]);
              this.pt[l].team = 'blue';
            } else if (canRed) {
              red.push(this.pt[l]);
              this.pt[l].team = 'red';
            }
          }
          l++;
        }
        this.teams = {
          blue: blue,
          red: red,
        }
        this.gamestate = 1;
        levelReader(['                              ','           #      #           ','  #        #      #  ####  #  ','  #   ##   ###  ###        #  ','  #                           ','  #                           ','  #  #####   ####   #####  #  ','                        #  #  ','                        #  #  ','###  #########  ############  ','  #  #                     #  ','  #  #                     #  ','     #    ##  ##  ##   #####  ','     #   #          #         ','  ########  #    #  #         ','         #  #    #  ########  ','         #          #   #     ','  #####   ##  ##  ##    #     ','  #                     #  #  ','  #                     #  #  ','  ############  #########  ###','  #  #                        ','  #  #                        ','  #  #####   ####   #####  #  ','                           #  ','                           #  ','  #        ###  ###        #  ','  #  ####  #      #   ##   #  ','           #      #           ','                              '], true, this);
        this.count = 10;
        this.gameMessage = 'Game Starting in '+this.count;
        this.countdown = setInterval(function() {
          var l = 0;
          while (l<this.pt.length) {
            if (this.pt[l].team == 'blue') {
              var q = 0;
              while (q<this.sockets.length) {
                if (this.pt[l].username == this.sockets[q].username) {
                  this.sockets[q].emit('message', JSON.stringify({
                    event: 'override',
                    data: {
                      key: 'x',
                      value: 0,
                      key: 'y',
                      value: 730,
                    }
                  }))
                }
                q++;
              }
            } else if (this.pt[l].team == 'red') {
              var q = 0;
              while (q<this.sockets.length) {
                if (this.pt[l].username == this.sockets[q].username) {
                  this.sockets[q].emit('message', JSON.stringify({
                    event: 'override',
                    data: {
                      key: 'x',
                      value: 1460,
                      key2: 'y',
                      value2: 730,
                    }
                  }))
                }
                q++;
              }
            }
            l++;
          }
          this.count -= 1;
          this.gameMessage = 'Game Starting in '+this.count;
          if (this.count == 0) {
            this.gamestate = 2;
            this.gameMessage = 'FIGHT'
            clearInterval(this.countdown);
          }
        }.bind(this), 1000);
      }
      return;
    }
    tank.damagedRecent = false;
    this.pt.push(tank);
  }
  disconnect(username) { // done?
    if (this.gamemode == 'DUELS') {
      this.gamestate = 0; // If someone leaves, prevent game crashing by going back to waiting for opponent.
    }
    var l = 0;
    while (l < this.pt.length) {
      if (this.pt[l].username == username) {
        var q = 0;
        while (q<this.pt[l].t.length) {
          clearTimeout(this.pt[l].t[q]);
          q++;
        }
        var q = 0;
        while (q<this.pt[l].i.length) {
          clearInterval(this.pt[l].i[q]);
          q++;
        }
        clearTimeout(this.pt[l].inactiveTimer);
        clearTimeout(this.pt[l].inactiveTimer2);
        this.pt.splice(l, 1);
      }
      l++;
    }
    var l = 0,
      gamemode;
    while (l < this.sockets.length) {
      if (this.sockets[l].username == username) {
        gamemode = this.sockets[l].gamemode;
        this.sockets.splice(l, 1);
      }
      l++;
    }
    if (this.sockets.length == 0) {
      var l = 0;
      while (l < this.i) {
        clearInterval(this.i[l]);
        l++;
      }
      servers.tanks[gamemode][this.channelname] = undefined;
      console.log('[MULTI] => Server shutdown - ' + this.channelname);
      delete this;
    }
  }
}

Here is a brief description of what the code does

  • Client makes request to server to join a room
  • If empty the server creates a new one
  • A room is a HostTanks object
  • Client joins server and it socket is registered
  • Client sends joinerjoin event and the socket.io server redirects it to the appropriate HostTanks object where the data is processed by the game
  • Client sends joinerupdate events at a rate determined by the client.
    • I have noticed it lags more the more fps I try to run this at
    • The lag doesn't occur instantly but happens after a few seconds
    • permessagedeflate(on the socket.io server) set to true actually improve performance for me
  • Socket.io server redirects the data to the HostTanks object and the game processes it
  • The HostTanks object sends back data immediately after applying updates(to provide best ping to players)
  • Gameplay continues until client disconnects where socket server tell the HostTanks that the client has left and the game ends.


Solution 1:[1]

Because node runs a javascript VM with dynamic heap and garbage collection… so, contrary to native languages with manual memory management, there will always be some “garbage” occupying RAM before it gets garbage collected.

Plus there is an overhead for JIT-compiled code (assembly representation of the loaded javascript code) residing in memory.

Plus there is a management overhead for memory internal to the VM (V8 engine) and JIT.

This is a “problem” of all VM-based platforms. Be it Javascript, Java, .NET, etc.

If you want to save memory that much, you should use C,C++, or Rust.

Luckily memory is cheap nowadays so almost no one cares for extra MBs as long as it speeds up development, provides runtime safety, eliminates some security vulnerabilities (of the family of buffer overflows etc.) or similar benefits.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 clarabrand