Vitim.us Vitim.us - 3 months ago 19
Javascript Question

Canvas memory leak

I was trying to generate a canvas with random noise, but I couldn't afford to generate a entire canvas of random pixels at 60fps, so I ended up using a temporary canvas in memory to generate a small 64x64 tile, and then using context fill to repeat the pattern, and let the browser push those bytes to the screen, instead of using the javascript engine.

It was much faster, and I could get a solid 60fps on a iOS device even on fullscreen, but I noticed that after some minutes the fps stated to drop until it got very slow.

On this fiddle I'm not using requestAnimationFrame that should limit to 60Hz, instead I'm using a custom loop, on my macbook it starts at around 500Hz and quickly slows down to emphasize the problem.

http://jsfiddle.net/Victornpb/m42NT/2/

function loop(){
drawNoise();
}


function drawNoise(){
var context = canvas.getContext("2d");
var pattern = context.createPattern(generatePattern(), "repeat");
context.rect(0,0, canvas.width, canvas.height);
context.fillStyle = pattern;
context.fill()
}

//create a on memory canvas to generate a tile with 64x64 pixels of noise and return it
function generatePattern(){

var canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 64;
var context = canvas.getContext("2d");

var image = context.getImageData(0, 0, canvas.width, canvas.height);
var imageData = image.data; // here we detach the pixels array from DOM

var p;
var pixels = canvas.width*canvas.height;
while(pixels--){
p = pixels*4;
imageData[p+0] = Math.random() >= 0.5 ? 255 : 0; // Red
imageData[p+1] = Math.random() >= 0.5 ? 255 : 0; // Green
imageData[p+2] = Math.random() >= 0.5 ? 255 : 0; // Blue
imageData[p+3] = 255; // Alpha
}

image.data = imageData;
context.putImageData(image, 0, 0);

return canvas;
}

Answer

You are using context.rect in your main draw function, without creating a new path (beginPath). So all your rect sub-path add, and needs a re-draw on each frame ==>> soon enough it is too slow.

==>> Either use beginPath() before using rect or use fillRect.

function drawNoise() {
    var context = canvas.getContext("2d");
    var pattern = context.createPattern(generatePattern(), "repeat");
    context.fillStyle = pattern;
    context.fillRect(0, 0, canvas.width, canvas.height);
}

Remark that you can win a great deal of time by not creating a canvas, and creating an image data on each call of generatePattern, but rather re-use the same imageData again and again. What's more, you can only set the alpha once :

//create a on memory canvas to generate a tile with 64x64 pixels of noise and return it
var generatePattern = (function () {
    var canvas = document.createElement("canvas");
    canvas.width = 64;
    canvas.height = 64;
    var context = canvas.getContext("2d");
    var image = context.getImageData(0, 0, canvas.width, canvas.height);
    var imageData = image.data; // here we detach the pixels array from DOM
    // set the alpha only once.
    var p = 0,
        pixels = canvas.width * canvas.height;
    while (pixels--) {
        imageData[p + 3] = 255; // Alpha
        p += 4;
    }
    var _generatePattern = function () {
        var p = 0;
        var pixels = canvas.width * canvas.height;
        var data = imageData;
        var rnd = Math.random;
        while (pixels--) {
            data[p++ ] = rnd() >= 0.5 ? 255 : 0; // Red
            data[p++ ] = rnd() >= 0.5 ? 255 : 0; // Green
            data[p++ ] = rnd() >= 0.5 ? 255 : 0; // Blue
            p++;
        }
        context.putImageData(image, 0, 0);
        return canvas;
    }
    return _generatePattern;
})();

Updated fiddle is here :

http://jsfiddle.net/gamealchemist/m42NT/15/

Edit : using one call to random() just to get one random bit is an overkill : use math.random() to get a bitfield, then re-fill this bitfield when it is empty. Here i took 21 bits from Math.random(), because it has not much more significant bits. This way you use 21 times less call to this function (!!) for same result.

http://jsfiddle.net/gamealchemist/m42NT/18/

//create a on memory canvas to generate a tile with 64x64 pixels of noise and return it
var generatePattern = (function () {
    var canvas = document.createElement("canvas");
    canvas.width = 64;
    canvas.height = 64;
    var context = canvas.getContext("2d");
    var image = context.getImageData(0, 0, canvas.width, canvas.height);
    var imageData = image.data; // here we detach the pixels array from DOM
    // set the alpha only once.
    var p = 0,
        pixels = canvas.width * canvas.height;
    while (pixels--) {
        imageData[p + 3] = 255; // Alpha
        p += 4;
    }
    var _generatePattern = function () {
        var p = 0;
        var pixels = canvas.width * canvas.height;
        var data = imageData;
        var rnd = Math.random;
        var bitsLeft = 0;
        var multiplier = (1<<22)-1;
        var mask = 0;
        while (pixels--) {
            if (!bitsLeft) {
                bitsLeft=21;
                mask= 0 | (Math.random()*multiplier);
            }
            data[p++ ] = (mask & 1) && 255 ; // Red
            data[p++ ] = (mask & 2 )  && 255 ; // Green
            data[p++ ] = (mask & 4) && 255; // Blue
            p++;
            mask>>=3;
            bitsLeft-=3;
        }
        context.putImageData(image, 0, 0);
        return canvas;
    }
    return _generatePattern;
})();
Comments