Problem
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);
Solution
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
andfilter
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;
});