Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

javascript memory leak with HTML5 Canvas getImageData in Chrome browser for mac OSX

this problem was fixed in the new chrome version(Version 35.0.1916.114)


In chrome for mac osx, CanvasRenderingContext2D#getImageData function will make memory leaks, how can I avoid this problem, here is the test case and result, it's only happened in chrome browser, safari is OK

<!DOCTYPE html>
<html>
<head>
    <title>CanvasRenderingContext2D#getImageData bug in chrome</title>
    <script type="text/javascript">

    var g;
    function init(){
        g = document.getElementById('canvas').getContext('2d');
        g.fillStyle = "blue";
        g.fillRect(10, 10, 100, 100);
        g.fillStyle = "green";
        g.fillRect(60, 60, 100, 100);
    }

    function getImageData(){
        var i = 0;
        while(i++ < 100){
        var c = g.getImageData(0,0,1000, 1000);
        delete c;
        }
    }

    function toDataURL(){
        var i = 0;
        while(i++ < 100){
        var c = g.canvas.toDataURL();
        delete c;
        }
    }
    </script>
</head>
<body onload="init()">
<button onclick="getImageData()">call getImageData 100 times - then memory will grow, can't GC</button>
<button onclick="toDataURL()">call toDataURL 100 times - it is OK</button><br>
<canvas id='canvas' width='600px' height='500px'/>
</body>
</html>

enter image description here

like image 450
sam sha Avatar asked May 26 '13 09:05

sam sha


1 Answers

Your problem is not with the getImageData function. It's the way the variable that holds the getImageData is assigned that create the leaks.

The problem is that delete c will fail (delete doesn't affect variable names) and the browser silently returns false.

MDN delete reference

Try with c = null instead. Try to declare the c variable outside the for loop, to avoid recreate the variable in each step of the loop.

Here is the modified code:

function getImageData(){
    var i = 0;
    var c;
    while(i++ < 100){
        c = g.getImageData(0,0,1000, 1000);
        // c = null; // <= check UPDATE to see why this doesn't work as expected
    }   
}

function toDataURL(){
    var i = 0;
    var c;
    while(i++ < 100){
        c = g.canvas.toDataURL();
        // c = null; // <= check UPDATE to see why this doesn't work as expected
    }   
}

I tried the code exactly in the same browser and using the memory profile in developer tools I could see the memory being perfectly cleared by garbage collector.

Check the memory timeline in developer tools (Ctrl+Shift+i).

To enable the memory profile you need to start Chrome with the flag --enable-memory-info.

UPDATE:

As I’ve already said in comments, garbage collection works by reclaiming blocks of memory (objects) which are no longer reachable.

When a function returns, the object which c points to is automatically available for garbage collection, because there is nothing left that has a reference to it.

There are also misconceptions about how null works. Setting an object reference to null doesn’t “null” the object. It sets the object reference to null.

So, in this case, the memory allocated to store each getImageData information remains there until the function returns. Since the image data is a very large object, and it's larger as larger are the canvas dimensions, in huge loops (let's say 500 loops or above, that depends on the machine) will cause overflow in memory before the function returns and the garbage collector be triggered.

I recommend the following article: Writing Fast, Memory-Efficient JavaScript. It's well explained and easy to read.

SOLUTION !!!

Now we know that the garbage collector is triggered only after a function return, one solution that came into my mind is defer the function that call the getImageData by a fraction of millisecond. That way we guarantee that the function returns after each getImageData call.

I tried the code below and it work even for 10000 iterations! Spends a lot of time to finish, but it finishes and with no memory leaks!)

Try it by yourself:

<!DOCTYPE html>
<html>
<head>
<title>CanvasRenderingContext2D#getImageData bug fixed</title>
<script type="text/javascript">

var g;
function init(){
    g = document.getElementById('canvas').getContext('2d');
    g.fillStyle = "blue";
    g.fillRect(10, 10, 100, 100);
    g.fillStyle = "green";
    g.fillRect(60, 60, 100, 100);
}

function getImageData(){        
    var c = g.getImageData(0,0,1000, 1000);     
}

var total = 0;
var iterations = 100;

function test(){
    var i = 0;

    while(i++ < iterations){
        setTimeout(function(){          
            getImageData();
            total++;
            //console.log(total);
            if(total == iterations){
                alert("" + total+" getImageData functions were completed!!!")
            }
        }, 0.01); // defer
    }
    alert("" + (i-1) + " iterations completed. Wait for the return of all getImageData");
}
</script>
</head>
<body onload="init()">
<button onclick="test()">call getImageData several times</button><br>
<canvas id='canvas' width='600px' height='500px'/>
</body>
</html>
like image 105
Gustavo Carvalho Avatar answered Nov 05 '22 03:11

Gustavo Carvalho