2
\$\begingroup\$

My friend and I are making a node.js game, and we have been testing CPU. After profiling, it was determined that this process called zlib is sucking most of the CPU/RAM.

3 clients connected to a game is fine, but when 12~13 players are connected it uses 58% where zlib is using about 30% of this CPU.

inclusive      self           name
ticks   total  ticks   total
64775   58.5%  64775   58.5%  /lib/x86_64-linux-gnu/libc-2.19.so
25001   22.6%    224    0.2%  LazyCompile: *callback zlib.js:409
//this one is a different zlib
7435    6.7%     82    0.1%  LazyCompile: ~callback zlib.js:409

Is there any way to decrease the CPU usage from this? Or is there a reason why it is increasing so much?

I have done some reading and I am told it is from socket.io, so here is our section of socket sending most of the data:

for (var i = 0; i < users.length; i++) {
    if (u.room == users[i].room && users[i].x + users[i].radius >= u.x - u.screenWidth / 2 - 20 && users[i].x - users[i].radius <= u.x + u.screenWidth / 2 + 20 && users[i].y + users[i].radius >= u.y - u.screenHeight / 2 - 20 && users[i].y - users[i].radius <= u.y + u.screenHeight / 2 + 20) {
        if (users[i].id == u.id) {
            visiblePlayers.push({
                x: users[i].x,
                y: users[i].y,
                angle: users[i].angle,
                hue: users[i].hue,
                radius: users[i].radius,
                squeeze: users[i].squeeze,
                name: users[i].name,
                dead: users[i].dead,
                isPlayer: true,
                kills: users[i].kills
            });
        } else {
            visiblePlayers.push({
                x: users[i].x,
                y: users[i].y,
                angle: users[i].angle,
                hue: users[i].hue,
                radius: users[i].radius,
                squeeze: users[i].squeeze,
                name: users[i].name,
                dead: users[i].dead
            });
        }
        // SEND DYING INFO: (FOR OFFLINE ANIMATION):
        if (users[i].dying) {
            visiblePlayers[visiblePlayers.length - 1].dying = true;
        }
    }
}
var visibleEnergy = [];
for (var i = 0; i < energies.length; i++) {
    if (u.firstSend || (energies[i].updated && energies[i].room == u.room)) {
        var anim = energies[i].animate;
        if (u.firstSend)
            anim = true;
        visibleEnergy.push({
            x: energies[i].x,
            y: energies[i].y,
            radius: energies[i].radius,
            index: i,
            animate: anim,
            hue: energies[i].hue,
            room: energies[i].room
        });
    }
}

// SEND PLAYER UPDATES TO CLIENTS:
sockets[u.id].emit('serverTellPlayerMove', visiblePlayers,
    visibleEnergy);
\$\endgroup\$
  • \$\begingroup\$ Welcome to CodeReview. Great question, but it might be better suited for StackOverflow or maybe ServerFault. zlib is used to compress/extract data being sent to and from the server at lower level than your code. So it's not really your code that's at fault; it's more a configuration thing. There are certainly things to review in your code too, and we'd be happy to suggest some things, but changing your code won't change low-level data compression is handled since data is still sent the same way. \$\endgroup\$ – Flambino Oct 18 '15 at 9:15
  • \$\begingroup\$ Isn't the data being compressed every time .emit is called? So Wouldn't changing the code decrease the amount of compressions done by zlib? \$\endgroup\$ – TastyLemons Oct 18 '15 at 10:12
  • 1
    \$\begingroup\$ @TastyLemons Sure, but I doubt it'd make a really big difference. Still, less data to send is always better. However, in order to know how to pare down the data being sent, we'd need to know a lot about the game. Some data is necessary; some can be calculated client-side, and just checked against the server now and again; some data can maybe just be skipped entirely... OP is already doing some things to filter the data being sent, but figuring out the entire server/client communication is a little out of scope for a review \$\endgroup\$ – Flambino Oct 18 '15 at 10:27
  • \$\begingroup\$ Thanks guys, I tracked down the zlib it is definitely the socket doing it. So I will try and send less data see what happens. also thanks Flambino for the answer below. \$\endgroup\$ – Hawkhunter Oct 18 '15 at 10:39
1
\$\begingroup\$

As I wrote in a comment, changing the code here won't change zlib usage, as that's something that's happening at a lower level. Still, I wanted to review the code. However, as TastyLemons rightfully notes in a comment, if you can simply send less stuff all the time, it'll help. But with that much CPU usage for zlib, I'm guessing there's something wonky in the node/zlib/server configuration.

For the first loop (users loop):

  • That is a very long and complex if condition! And it's filled with magic numbers, as well as numbers that I assume you don't need to calculate over and over again.

  • The object you're building is mostly the same in both the inner branches, so it'd be easier to build the common part first, and then only add the parts that differ.

  • There are other small changes that would make things simpler, like setting the dying boolean in the object before pushing it to the array.

  • You can make good use of the map and filter array methods

For the condition, you're checking if the user is in the same room, and in view of the u user (Sidenote: u is not a terribly descriptive label, and it doesn't help that you sometimes use player and other times use user to refer to the same kind of entity).

I'd make that a method, for instance on the User prototype, if you have such a thing:

User.prototype.canSee = function (otherUser) {
  // initial check
  if(this.room !== otherUser.room) {
    return false;
  }

  // x and y distances between user and other's "radius"
  var dx = Math.abs(otherUser.x - this.x) - otherUser.radius,
      dy = Math.abs(otherUser.y - this.y) - otherUser.radius;

  return dx <= this.xViewDistance && dx <= this.yViewDistance;
};

(if you don't have a User prototype, you can change the function to be a plain function that takes two arguments.)

I'm assuming here, that the *ViewDistance values are already calculated, since I imagine they don't really change (or at least don't change all the time). So if the screenWidth doesn't change much, you could calculate a screenHalfWidth and keep it around, so you don't always have to divide by 2 to get the same result. And the 20 needs to be a needs constant (I know JS doesn't have constants; I mean a variable you treat as constant). Something like SCREEN_WIDTH_MARGIN and maybe a similar thing for height.

So xViewDistance might be defined as user.screenHalfWidth - SCREEN_WIDTH_MARGIN, which itself is a value that probably won't change all the time, and can thus be stored.

Point is, you end up with code, where:

  • your magic numbers are labelled so you know what they mean
  • your magic numbers are defined in exactly 1 place, so you only need to hunt down the same number in 23 different places if it needs to change
  • calculations are done once, and the results stored, rather than recalculated constantly despite not having changed.

For building the objects I'd again suggest a method on the User prototype (though again, it can also be a function that just takes an argument). Something like:

User.prototype.getInfo = function (isPlayer) {
  var info = {
    x: this.x,
    y: this.y,
    angle: this.angle,
    hue: this.hue,
    radius: this.radius,
    squeeze: this.squeeze,
    name: this.name,
    dead: this.dead,
    dying: this.dying
  };

  if(isPlayer) {
    info.isPlayer = true;
    info.kills = this.kills;
  }

  return info;
};

Putting it together, you might get:

var visibleUsers = users.filter(function (user) {
  return user.id === currentUser.id || currentUser.canSee(user);
}).map(function (user) {
  return user.getInfo(user.id === currentUser.id);
});

That's everyone's info, including the current user/the player, ready to be sent.

For the energies, the story's similar: Add some methods/functions to do the heavy lifting, and use filter/map to iterate the array. Only trick is that if firstSend is true, we could really skip the filter function, but I've left it pretty much as-is:

var visibleEnergies = energies.filter(function (energy) {
  return currentUser.firstSend || (energy.updated && energy.room === currentUser.room);
}).map(function (energy) {
  var info = energy.getInfo();
  info.anim = info.anim || currentUser.firstSend; // override anim if necessary
  return info;
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.