Memory Optimization

Posted the 08 Aug 2014

Do you know about the Garbage Collector ?

Javascript doesn't have an explicit memory management, you can create but not delete objects. The Garbage collector help to regular this problem by pausing the javascript execution randomly to clean up unused memory.
In theory it's awesome, but in practice if you create too many objects in a rendering loop then your game can freeze each seconds to delete objects making a very bad experience for your users.

Since d33283, a big change where made to avoid object creation. Since this change I'm now able to render 3 times more poring (monster) in my screen than before.
Since it's actually an interesting change, I think it's a good idea to share the process.


Packets reception

One of the primary things I would like to improve was the packet parser. Each time you received a packet from the server, it instantiate a new packet class to parse it. It's really annoying, particularly when you have around 100 monsters in your screen moving at the same time, it will create a hell lot of objects.

console packets

Fixing it was pretty easy, since the packet were not stored anywhere after its used, then we can just create one instance per packets and re-use it each time. This process is also known as singleton. As I didn't want to modify manually hundred of packets, I just applied a little hack:

-	var data = new packet.struct(fp, offset);

+	if (!packet.instance) { // no instance create it
+		packet.instance = new packet.struct(fp, offset);
+	}
+	else { // use the instance
+		packet.struct.call(packet.instance, fp, offset);
+	}

Path Finding

Another leak I found is from the PathFinder algorithm. When this class is called (each time a monster/player move), a list of cells is generated in the format array[cellcount][2], meaning 1 + cellcount * 2 objects are created each time. It's really expensive, just imagine when there are dozens of monsters following you to attack...

To optimize this, I fixed the problem differently. Instead of storing on multi-dimentionals array, I stored the data on a static Int16Array. The position is then aligned each 2 indexes [x0, y0, x1, y1, x2, y2] and the array stored in Entity's class to be re-used the next time.

As typedarray is not a dynamical array, you must set a limit to its size. Happily in RO there is a server limitation avoiding to move more than 32 cells a time, so the maximum defined size of the array can not exceed 32*2. The over thing to thing about, is to not depend of the array.length property anymore, you have to add a variable to stored the movement size (to not execute a movement from an old path).

-		this.path  =  [];
+		this.path  =  new Int16Array(32*2);
+		this.total =  0;

Another problem I saw in my script, is the use of path.shift() method to move in the path array. This method (as .slice(), .shift(), .splice(), ...) are bad practices : they are creating new array. This function was replaced with a new internal variables storing the progression in the path list.

 // Calculate new position, base on time and walk speed.
-while (path.length) {
-	x = path[0][0] - walk.pos[0];
-	y = path[0][1] - walk.pos[1];
+while (index < total) {
+	x = path[index+0] - walk.pos[0];
+	y = path[index+1] - walk.pos[1];
 
 	// Seems like walking on diagonal is slower ?
 	speed = (x && y) ? walk.speed / 0.6 : walk.speed;
 @@ -111,13 +116,16 @@ define( function( require )
 	}
 
 	walk.tick += speed;
-	walk.pos.set(path.shift());
+	walk.pos[0] = path[index+0];
+	walk.pos[1] = path[index+1];
+	index += 2;
 }
  
-delay  = Math.min(speed, TICK-walk.tick);
+delay      = Math.min(speed, TICK-walk.tick);
+walk.index = index;

With this, no object creating at all while calling this function, and the average speed should be faster because optimized in a typedarray.


SoundManager optimization

Finally the biggest improvement, optimizing memory used by Audio objects.

Sounds are one of the most important things in games, and because of the complexity of the objects, using them can lead to a lot of GC pauses. So what can we do about it ?

One thing, is maybe to avoid playing a sound more than one time at a time. It will not modify user experience and avoid to create multiple sound object for nothing when only one is really needed.
The idea here, is to stored sounds you play in a list, add a tick parameter. Remove them from the list when the sound just ended. And add a check while wanted to add a new sound to check if it's already playing since a depending time duration.

 Client.loadFile( 'data/wav/' + filename, function( url ) {
-		var sound = document.createElement('audio');
+		var i, count = _sounds.length;
+		var sound, tick = Date.now();
+
+		// Wait a delay to replay a sound
+		for (i = 0; i < count; ++i) {
+			if (_sounds[i].src === url && _sounds[i].tick > tick - 100) {
+				return;
+			}
+		}

An other idea, is to avoid deleting sound if they are going to be played soon. When the sound just ended, cache them in a new list. When asking to play a new sound, check in the list if the sound already exist, then use it.
It's important in this case to add a timer to sound in list to automatically remove them (it's not a good idea to store indefinitely sounds in cache, it will just lead to crash your browser because it's out of memory).

// Is a not used sound is ready ?
var sound = getSoundFromCache(filename);
if (sound) {
	sound.play();
	_sounds.push(sound); // play list
	return;
}

// Not from cache, load it
Client.loadFile( 'data/wav/' + filename, function( url ) {
	sound = document.createElement('audio');
	sound.src = url;
	...
	sound.addEventListener('ended', storeSoundInCache, false);
});

Finally, I let you enjoy the difference: roBrowser memory improvement

Latest Posts

ALL POSTS