11
\$\begingroup\$

I am currently creating a Multiplayer Game using node.js and web sockets. The client side is receiving the positional information and applying it to all the players on the client side. Each client stores a list of all game elements.

The main issue I am experiencing at the moment is a large amount of Garbage Collection. I am unsure what could be causing this, since I followed all of the tips I have found online; Set Variables to null to make sure they are collected asap. Dont create objects during runtime etc.

After running a profile, this is the result:

enter image description here

As you can see, when the GC kicks in, the game stops for a long time.

How can I reduce/remove GC and object creation from my code. It is fully working, but it performs poorly on low end devices.

I initialize all the arrays at the start.

var energies = [];
var walls = [];
var users = [];
var leaderboard = [];

Those arrays are then filled with data by the server:

socket.on('serverTellPlayerMove', function(userData, strEnergy) {
    var visible = [];
    for (var i = 0; i < userData.length;) {
        if ((userData[i] == 1) == true) {
            visible.push({
                p : userData[0 + i] == 1,
                score : userData[1 + i],
                x : userData[2 + i],
                y : userData[3 + i],
                angle : userData[4 + i],
                hue : userData[5 + i],
                name : userData[6 + i],
                dead : userData[7 + i] == 1,
                dying : userData[8 + i] == 1
            });
            i += 9;
        } else {
            visible.push({
                x : userData[0 + i],
                y : userData[1 + i],
                angle : userData[2 + i],
                hue : userData[3 + i],
                name : userData[4 + i],
                dead : userData[5 + i] == 1,
                dying : userData[6 * i] == 1
            });
            i += 7;
        }
    }

    // SET DATA:
    for (var i = 0; i < visible.length; i++) {
        if (visible[i].p) {
            var playerData = visible[i];
            i = visible.length;
        }
    }
    if (userData != "") {
        if (playerType == 'player') {
            // HOW MUCH YOU MOVED BY:
            var xoffset = player.x - playerData.x;
            var yoffset = player.y - playerData.y;
            player.x = playerData.x;
            player.y = playerData.y;
            player.angle = playerData.angle;
            player.hue = playerData.hue;
            player.xoffset = isNaN(xoffset) ? 0 : xoffset;
            player.yoffset = isNaN(yoffset) ? 0 : yoffset;
            player.dead = playerData.dead;
            document.getElementById("killsText").innerHTML = "Score: "
                    + playerData.score;
        }
    }

    // DEATH ANIM:
    for (var i = 0; i < visible.length; i++) {
        if (visible[i].dying) {
            createExplosion(visible[i].x, visible[i].y);
        }
    }

    // DEATH ANIM:
    for (var i = 0; i < visible.length; i++) {
        if (visible[i].dying) {
            createExplosion(visible[i].x, visible[i].y);
        }
    }

    users = visible;
    visible = null;
    var energiesList = [];
    for (var i = 0; i < strEnergy.length; i += 5) {
        energiesList.push({
            x : strEnergy[0 + i],
            y : strEnergy[1 + i],
            index : strEnergy[2 + i],
            animate : strEnergy[3 + i] == 1,
            hue : strEnergy[4 + i]
        });
    }

    var energyT = null;
    for (var i = 0; i < energiesList.length; ++i) {
        energyT = energiesList[i];
        if (energyT.animate) {
            energyT.animScale = 0;
        } else {
            if (energies[energyT.index] != undefined)
                energyT.animScale = energies[energyT.index].animScale;
        }
        energies[energyT.index] = energyT;
    }
    energiesList = null;
    energyT = null;
    userData = null;
    strEnergy = null;
});

This code is called 30 times a second. This updates every users data on each client side. It also updates the pellet data. Pellets are objects that can be picked up by players in the game. The above snippet does most of the logical work on the client side.

Each object is also rendered as follows:

window.requestAnimFrame = (function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame
        || window.mozRequestAnimationFrame
        || window.msRequestAnimationFrame || function(callback) {
            window.setTimeout(callback, 1000 / 60);
        };
})();

function animloop() {
    gameLoop();
    animLoopHandle = window.requestAnimFrame(animloop);
}

function doGame() {
    now = Date.now();
    delta = now - lastUpdate;
    lastUpdate = now;

    startX = player.x - (screenWidth / 2);
    startY = player.y - (screenHeight / 2);

    graph.fillStyle = backgroundColor;
    graph.fillRect(0, 0, screenWidth, screenHeight);
    drawgrid();
    drawEnergies(energies);
    drawPlayers(users);
    drawWalls(walls);
}

And the actual rendering:

function drawPlayers(playersToDraw) {
    var playerC = null;
    var fontSize = Math.max(playerRadius / 1.2, 11);
    var radiusD = (playerRadius * 2.0);
    for (var z = 0; z < playersToDraw.length; z++) {
        playerC = playersToDraw[z];
        if (!playerC.dead) {
            var shapeX = playerC.x - startX;
            var shapeY = playerC.y - startY;
            var rotation = (Math.PI / 180) * playerC.angle;
            scale = radiusD;
            graph.fillStyle = 'hsl(' + playerC.hue + ', 50%, 45%)';
            graph.strokeStyle = 'hsl(' + playerC.hue + ', 50%, 60%)';
            graph.lineJoin = "round";
            graph.lineWidth = playerHRadius;
            graph.save();
            graph.translate(shapeX, shapeY);
            graph.rotate(rotation);
            graph.beginPath();
            graph.moveTo(0, (-2 * scale / 3));
            graph.lineTo((-scale / 2), scale / 3);
            graph.lineTo((scale / 2), scale / 3);
            graph.lineTo(0, (-2 * scale / 3));
            graph.closePath();
            graph.stroke();
            graph.fill();
            graph.restore();
        }
    }
    playerC = null;
}

As you can see I am setting playerC to null in an Attempt to make it be collected by the GC.

There are some more rendering functions as well, but they are all very similar to the player draw function.

How do I reduce GC on this code and does the data received from the server need to be collected as well?


I have attempted to implement v-sync, but then I realized that the movement data is sent by the server, to there is no way to use delta time or v-sync.

As suggested, I did a Heap Snapshot. Here is the result:

enter image description here

And here is what was actually removed:

enter image description here

After some further looking around, I found this: enter image description here

It points at the data received from the server. Also I noticed some throw TypeError statements, what could be causing that?

Here is a LIVE DEMO of the code in action: http://www.vertix.io

\$\endgroup\$
7
  • 1
    \$\begingroup\$ I suggest you try taking snapshots of your heap. The timeline can only tell us about the GC event, but not so much about what it was claiming back. And we're not even sure if the offending code is even in the given snippets. \$\endgroup\$
    – Joseph
    Commented Oct 26, 2015 at 12:42
  • \$\begingroup\$ Thanks for the suggestion, I have added the Heap Snapshot to the question. \$\endgroup\$
    – user87202
    Commented Oct 26, 2015 at 12:48
  • \$\begingroup\$ Here's an answer that references the "three-snapshot technique". Shows you what built up memory usage when you used it (first to second snapshot), and what was claimed or not claimed (second to third snapshot) \$\endgroup\$
    – Joseph
    Commented Oct 26, 2015 at 12:53
  • \$\begingroup\$ Thanks again. Very helpful. I have discovered that drawPlayer is one of the culprits. \$\endgroup\$
    – user87202
    Commented Oct 26, 2015 at 13:02
  • \$\begingroup\$ I cant figure out where this array could be coming from. \$\endgroup\$
    – user87202
    Commented Oct 26, 2015 at 22:05

3 Answers 3

2
+100
\$\begingroup\$

I think you should use a static approach for the visible array, avoiding the push instruction: declare and fill the array with dummy structures outside the socket.on handler and then iterate over the items to change their value.

Use a variable to keep the length of the received values, instead of calling visible.length().

You are also copying many times your data: in Javascript var playerData = visible[i]; makes another copy of the data, consider accessing the player's data by index instead of copying it in another structure.

The same problem arises for the energiesList array. Define it outside the event and only update its values inside, keeping the real lenght in a variable.

Hope it helps.


EDIT

This is how the code should be refactored (you need to test it)

MAX_VISIBLES = 500;
VISIBLE_PLAYER = 1;
VISIBLE_OTHER = 0;
var visible = [];
for (var i = 0; i < MAX_VISIBLES; i++) {
    visible.push({
        type: 0,
        p : 0,
        score : 0,
        x : 0,
        y : 0,
        angle : 0,
        hue : 0,
        name : 0,
        dead : 0,
        dying : 0
    });
}

// Just need it once (only if you use it anywhere else in the code)
users = visible;
// You'll need it if you want to iterate over users
var usersLength = 0;

MAX_ENERGIES = 1000;
var energiesList = [];
for (var i = 0; i < MAX_ENERGIES; i++) {
    energiesList.push({
        x : 0,
        y : 0,
        index : 0,
        animate : 0,
        hue : 0
    });
}

socket.on('serverTellPlayerMove', function(userData, strEnergy) {
    for (var i = 0, visibleLength = 0; i < userData.length; visibleLength++) {
        if ((userData[i] == 1) == true) {
            visible[visibleLength].type = VISIBLE_PLAYER;
            visible[visibleLength].p : userData[0 + i] == 1;
            visible[visibleLength].score : userData[1 + i];
            visible[visibleLength].x : userData[2 + i];
            visible[visibleLength].y : userData[3 + i];
            visible[visibleLength].angle : userData[4 + i];
            visible[visibleLength].hue : userData[5 + i];
            visible[visibleLength].name : userData[6 + i];
            visible[visibleLength].dead : userData[7 + i] == 1;
            visible[visibleLength].dying : userData[8 + i] == 1;
            i += 9;

            var playerData = visible[visibleLength];
        } else {
            visible[visibleLength].type = VISIBLE_OTHER;
            visible[visibleLength].x : userData[2 + i];
            visible[visibleLength].y : userData[3 + i];
            visible[visibleLength].angle : userData[4 + i];
            visible[visibleLength].hue : userData[5 + i];
            visible[visibleLength].name : userData[6 + i];
            visible[visibleLength].dead : userData[7 + i] == 1;
            visible[visibleLength].dying : userData[8 + i] == 1;
            i += 7;
        }
    }
// I don't think you need this cycle: playerdata can be assigned
// during userData parsing
//    // SET DATA:
//    for (var i = 0; i < visible.length; i++) {
//        if (visible[i].p) {
//            var playerData = visible[i];
//            i = visible.length;
//        }
//    }
    if (userData != "") {
        if (playerType == 'player') {
            // HOW MUCH YOU MOVED BY:
            var xoffset = player.x - playerData.x;
            var yoffset = player.y - playerData.y;
            player.x = playerData.x;
            player.y = playerData.y;
            player.angle = playerData.angle;
            player.hue = playerData.hue;
            player.xoffset = isNaN(xoffset) ? 0 : xoffset;
            player.yoffset = isNaN(yoffset) ? 0 : yoffset;
            player.dead = playerData.dead;
            document.getElementById("killsText").innerHTML = "Score: "
                    + playerData.score;
        }
    }

    // DEATH ANIM:
    for (var i = 0; i < visibleLength; i++) {
        if (visible[i].dying) {
            createExplosion(visible[i].x, visible[i].y);
        }
    }

    // DEATH ANIM:
    for (var i = 0; i < visibleLength; i++) {
        if (visible[i].dying) {
            createExplosion(visible[i].x, visible[i].y);
        }
    }

// See above
//    users = visible;
//    visible = null;
    usersLength = visibleLength;

    for (var i = 0, energiesListLength = 0; i < strEnergy.length; i += 5, energiesListLength++) {
        energiesList[energiesListLength].x : strEnergy[0 + i];
        energiesList[energiesListLength].y : strEnergy[1 + i],
        energiesList[energiesListLength].index : strEnergy[2 + i];
        energiesList[energiesListLength].animate : strEnergy[3 + i] == 1;
        energiesList[energiesListLength].hue : strEnergy[4 + i];
    }

    var energyT = null;
    for (var i = 0; i < energiesListLength; ++i) {
        energyT = energiesList[i];
        if (energyT.animate) {
            energyT.animScale = 0;
        } else {
            if (energies[energyT.index] != undefined)
                energyT.animScale = energies[energyT.index].animScale;
        }
        energies[energyT.index] = energyT;
    }

// You don't need to force GC: you need to avoid GC
//    energyT = null;
//    userData = null;
//    strEnergy = null;
});
\$\endgroup\$
5
  • 1
    \$\begingroup\$ "makes another copy of the data" - false. Objects are always copied by reference in JavaScript. This is why clone() and deepClone() functions exist. The performance difference in copying this reference would be incredibly superfluous. \$\endgroup\$
    – Dan
    Commented Nov 3, 2015 at 10:40
  • \$\begingroup\$ Ok, I must agree with you @dan. Still the line var visible = []; has to be removed from inside the socket handler. \$\endgroup\$
    – N74
    Commented Nov 3, 2015 at 11:31
  • \$\begingroup\$ @N74: Setting variables to null does not trigger garbage collection. It can make it easier to garbage-collect those objects. Even the delete keyword does not trigger the garbage collector. This happens at the browser's discretion. The best you can do is limit the amount of new objects that get created, which can be difficult if you are receiving JSON over the network and then parsing it into objects. \$\endgroup\$ Commented Nov 3, 2015 at 21:53
  • \$\begingroup\$ @GregBurghardt thanks for the clarification. In the comment I just wanted to warn the OP that setting these variables to null is almost of no use in his code. \$\endgroup\$
    – N74
    Commented Nov 4, 2015 at 8:03
  • \$\begingroup\$ Very true. Unless a function closure is involved, null-ing object references doesn't do much but bulk up your code. \$\endgroup\$ Commented Nov 4, 2015 at 13:20
2
\$\begingroup\$

Continuing from your last comment 'drawPlayer is one of the culprits', you use graph for drawing operations. The graph context is a huge bitmap to manipulate since it is as big as the screen:

graph.fillRect(0, 0, screenWidth, screenHeight);

To rotate and translate such a big bitmap costs a lot of memory and processing power.

In such cases I recommend using the following technique:

Create an 'offscreen' canvas with the same width and height of the player image and draw your player on that context. If you want to rotate within your offscreen canvas, make sure the rotated image fits in the offscreen canvas.

After that, you draw the content of your offscreen canvas onto your main stage.

You can write the contents of a canvas on another canvas with the drawImage function:

In this article the technique called pre-rendering is explained further with a mario character resembling your 'player'.

Other remarks about your drawplayer routine:

Use length caching with for loops : store the length in a variable:

for (var z = 0; z < playersToDraw.length; z++) {

var len = playersToDraw.length;
for (var z = 0; z < len; z++) {

or:

for(var z=0, len=playersToDraw.length; z < len; z++){

Another thing, just as N74 mentioned, don't copy the variable if it is not really nescessary.

Don't use this:

playerC = playersToDraw[z];
playerC.blabla

Just use:

playersToDraw[z].blabla

And another thing, move the line:

scale = radiusD;

Out of the for-loop, now it runs every iteration and that isn't nescessary. Or instead of:

var radiusD = (playerRadius * 2.0);

use this at once and be done with it:

var scale = (playerRadius * 2.0);
\$\endgroup\$
7
  • \$\begingroup\$ @ your point about copying the variable; objects are copied by reference. The difference between array[index].value and const host = array[index]; host.value are for legibility's sake only. \$\endgroup\$
    – Dan
    Commented Nov 3, 2015 at 10:39
  • \$\begingroup\$ I say that in a short function like this, where performance is of the most importance, code should be concise as possible and prevail over readability. I understand that player.blabla reads nicer than playersToDraw[z].blabla, but there is just no need to make a copy of that variable (that references to the same value indeed) \$\endgroup\$ Commented Nov 3, 2015 at 10:51
  • \$\begingroup\$ If you like, you can suggest how I should rephrase my words and I will gladly update it. :) \$\endgroup\$ Commented Nov 3, 2015 at 10:53
  • 1
    \$\begingroup\$ Well, one of the principles of Law of Demeter is that a function may only ever access a.b or a.c, but never a.b.c. Ideally, the loop should be in a separate function for testing purposes which passes the function a.b, and then that function accesses b.c. Further, OP is reusing the variable array[index]. In my opinion, it makes more sense to assign that to a temporary variable to avoid extra lookups (even if they are O(1)) and to DRY up the code. \$\endgroup\$
    – Dan
    Commented Nov 3, 2015 at 10:56
  • \$\begingroup\$ in short: player.dead player.x and player.y is significantly more legible than array[index].dead, array[index].x and array[index].y \$\endgroup\$
    – Dan
    Commented Nov 3, 2015 at 10:57
1
\$\begingroup\$
  1. Create arrays with required size to prevent reallocation overhead
var visible = new Array(required_size);

You can look at good overview for array allocations from codereview, but different topic.

  1. GC cleans temporary objects and time it need to clean them depends on their number, so you can use closure compiler to inline everything that can be inlined to decrease number of objects GC had to process.

  2. Not GC optimization, but related to performance: do not search DOM element frequently, save link to them and reuse variable.

var killsLabel = document.getElementById("killsText");
// when you need it
killsLabel.innerHTML = "Score: " + playerData.score;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.