THREE.js Memory "leak" using 2D canvas for animated text billboards
<p>Not so much a leak as slow disposal causing rapid increase in system memory used by app.</p>

<p>I am making "Animated Bulletin Boards" by writing text onto a 2D canvas then making a texture for a material for a Plane mesh.</p>

<p>As I want multiple ABB's to update many times per second I thought I would set up an array of javascript ABB objects and assign a canvas, context, texture, material, geometry and mesh to each object.  Then when I update I just need to redraw the canvas, make a new texture, assign it to the material and update the material.</p>

<p>It all works fine except that as time passes the app grabs more and more memory.  If I pause the app (e.g. switch to another tab) then some disposal process slowly disposes of the memory used by the app.  Basically the rate of new memory acquisition is much faster than the rate of memory disposal.</p>

<p>Here is a <a href="http://jsfiddle.net/qL1skL0s/" rel="nofollow">jsfiddle</a> that shows the effect.</p>

<p>I have been trying various ways of re-using old objects or making new ones but nothing seems to work.  Note that the main application where this was detected was running OK under R71 and the problem appeared when upgrading to R77.  I have looked at the upgrade notes but cannot see the cause.</p>

<p>I would welcome any suggestions on how to solve or avoid this problem.</p>

<p>Here is some code showing how each ABB is updated:-</p>

<pre><code>function SOW_F_grob_BB_Update_a_BB ( thisBB, newText function SOW_F_grob_BB_Update_a_BB ( thisBB, newText )
{
            thisBB.params.text = newText;
            //var thisCan = thisBB.canvas = document.createElement('canvas');
            //var thisCan_Ctx_1 = thisBB.context = thisCan.getContext('2d');

            var new_or_old = "new";
            if ( new_or_old == "new")
            {
                //... Getting NEW CANVAS and CONTEXT
                var thisCan = document.createElement('canvas');
                var thisCan_Ctx_1 = thisCan.getContext('2d');
            }
            else
            {
                //... RE-USING OLD  CANVAS and CONTEXT trying to solve the memory leak problem.
                var thisCan = thisBB.canvas;
                var thisCan_Ctx_1 = thisBB.context;
            }

            thisCan.width = thisBB.params.Pennant_rect_width;//... stretches rectangle

            //... ***** BEWARE: Setting the width or height of a &lt;canvas&gt; element 
            //... will erase its contents 
            //... and reset all the properties of its drawing context 
            //... to their default values (http://diveintohtml5.info/canvas.html).


            thisCan_Ctx_1.fillStyle = thisBB.params.back_color;
            //thisCan_Ctx_1.fillRect(0,0,temp_canvas2.width, temp_canvas2.height);
            thisCan_Ctx_1.fillRect(0,0,thisBB.params.Pennant_rect_width, thisBB.params.rect_depth);//... fixed size rectangle, using powers of two

            thisCan_Ctx_1.font = thisBB.params.font;
            thisCan_Ctx_1.fillStyle = thisBB.params.text_color;
            thisCan_Ctx_1.fillText(thisBB.params.text, thisBB.params.text_offset_x, thisBB.params.text_offset_y); //... offset_width, offset_depth of start point of text string (bottom left corner of 1st char).

            //... Memory unblocking attempts.
              {
               //scene.remove( thisBB.mesh );
                //thisBB.mesh.material.map.dispose();
                //thisBB.mesh.geometry.dispose();
                //thisBB.mesh.material.dispose(); //... NOT REQUIRED
                //canvas1.parentNode.removeChild(canvas1);
                //thisBB.mesh.dispose(); //... not a function, but without this command the old ABBs didn't get removed.
              }         

            //... N.B. texture needs just the name of the source/donor canvas.
            var thisNewTexture = thisBB.texture = new THREE.Texture( thisCan );
            thisNewTexture.minFilter = THREE.LinearFilter;//THREE.NearestFilter;//... to avoid console warnings  about texture .NE. power of two.
            thisNewTexture.needsUpdate = true; //... important.

            //thisCan_Ctx_1.dispose(); //... not a function, anyway we are keeping them in array.
            //thisCan.dispose(); //... not a function, anyway we are keeping them in array.         
            //thisCan.parentNode.removeChild( thisCan );//... ERROR Uncaught TypeError: Cannot read property 'removeChild' of null
            //document.removeElement( thisCan );// ERROR: TypeError: document.removeElement is not a function


            //-------------------------------------------------------------------------------------------

            //... If plane geometry unchanged then we can just update the material? (maybe).

            //var  geometry_changed = true;
            //... ***** HARD-WIRED ***** , works for now.
            var  geometry_changed = false;                                  
            if ( geometry_changed == true ) //... so must make new mesh and presumably new material.
                {
            /*
                    scene.remove( thisBB.mesh );

                    thisBB.mesh.material.map.dispose();
                    thisBB.mesh.material.dispose();
                    thisBB.mesh.geometry.dispose();
                    thisBB.mesh.dispose();

                    //... New Material
                    var thisNewMaterial = thisBB.material = new THREE.MeshBasicMaterial( 
                        { map: thisNewTexture, side:THREE.DoubleSide } );
                    thisNewMaterial.transparent = true;

                    //... New Mesh ( inc. new Geometry).
                    var thisNewMesh = thisBB.mesh = new THREE.Mesh(
                        new THREE.PlaneBufferGeometry( thisCan.width, thisCan.height ),
                        thisNewMaterial
                        );

                    //... set mesh position &amp; add to the scene
                    thisNewMesh.position.set( thisBB.px, thisBB.py, thisBB.pz );
                    scene.add( thisNewMesh );

            */
                }
            else //... geometry_changed == false, no need for New Material or New Mesh, 
            //... just do update because texture and .map have changed.
                {   
                    //... Works OK now that we are using the BB array with retained individual textures and materials.


                    thisBB.mesh.material.map = thisNewTexture;  //... no exceptions or warnings
                    thisBB.mesh.material.map.needsUpdate = true;

                }  //... if geometry_changed true or false.

        thisBB.mesh.position.set( thisBB.params.px, thisBB.params.py, thisBB.params.pz );
}
{
            thisBB.params.text = newText;
            //var thisCan = thisBB.canvas = document.createElement('canvas');
            //var thisCan_Ctx_1 = thisBB.context = thisCan.getContext('2d');

            var new_or_old = "new";
            if ( new_or_old == "new")
            {
                //... Getting NEW CANVAS and CONTEXT
                var thisCan = document.createElement('canvas');
                var thisCan_Ctx_1 = thisCan.getContext('2d');
            }
            else
            {
                //... RE-USING OLD  CANVAS and CONTEXT trying to solve the memory leak problem.
                var thisCan = thisBB.canvas;
                var thisCan_Ctx_1 = thisBB.context;
            }

            thisCan.width = thisBB.params.Pennant_rect_width;//... stretches rectangle

            //... ***** BEWARE: Setting the width or height of a &lt;canvas&gt; element 
            //... will erase its contents 
            //... and reset all the properties of its drawing context 
            //... to their default values (http://diveintohtml5.info/canvas.html).


            thisCan_Ctx_1.fillStyle = thisBB.params.back_color;
            //thisCan_Ctx_1.fillRect(0,0,temp_canvas2.width, temp_canvas2.height);
            thisCan_Ctx_1.fillRect(0,0,thisBB.params.Pennant_rect_width, thisBB.params.rect_depth);//... fixed size rectangle, using powers of two

            thisCan_Ctx_1.font = thisBB.params.font;
            thisCan_Ctx_1.fillStyle = thisBB.params.text_color;
            thisCan_Ctx_1.fillText(thisBB.params.text, thisBB.params.text_offset_x, thisBB.params.text_offset_y); //... offset_width, offset_depth of start point of text string (bottom left corner of 1st char).

            //... Memory unblocking attempts.
              {
               //scene.remove( thisBB.mesh );
                //thisBB.mesh.material.map.dispose();
                //thisBB.mesh.geometry.dispose();
                //thisBB.mesh.material.dispose(); //... NOT REQUIRED
                //canvas1.parentNode.removeChild(canvas1);
                //thisBB.mesh.dispose(); //... not a function, but without this command the old ABBs didn't get removed.
              }         

            //... N.B. texture needs just the name of the source/donor canvas.
            var thisNewTexture = thisBB.texture = new THREE.Texture( thisCan );
            thisNewTexture.minFilter = THREE.LinearFilter;//THREE.NearestFilter;//... to avoid console warnings  about texture .NE. power of two.
            thisNewTexture.needsUpdate = true; //... important.

            //thisCan_Ctx_1.dispose(); //... not a function, anyway we are keeping them in array.
            //thisCan.dispose(); //... not a function, anyway we are keeping them in array.         
            //thisCan.parentNode.removeChild( thisCan );//... ERROR Uncaught TypeError: Cannot read property 'removeChild' of null
            //document.removeElement( thisCan );// ERROR: TypeError: document.removeElement is not a function


            //-------------------------------------------------------------------------------------------

            //... If plane geometry unchanged then we can just update the material? (maybe).

            //var  geometry_changed = true;
            //... ***** HARD-WIRED ***** , works for now.
            var  geometry_changed = false;                                  
            if ( geometry_changed == true ) //... so must make new mesh and presumably new material.
                {
            /*
                    scene.remove( thisBB.mesh );

                    thisBB.mesh.material.map.dispose();
                    thisBB.mesh.material.dispose();
                    thisBB.mesh.geometry.dispose();
                    thisBB.mesh.dispose();

                    //... New Material
                    var thisNewMaterial = thisBB.material = new THREE.MeshBasicMaterial( 
                        { map: thisNewTexture, side:THREE.DoubleSide } );
                    thisNewMaterial.transparent = true;

                    //... New Mesh ( inc. new Geometry).
                    var thisNewMesh = thisBB.mesh = new THREE.Mesh(
                        new THREE.PlaneBufferGeometry( thisCan.width, thisCan.height ),
                        thisNewMaterial
                        );

                    //... set mesh position &amp; add to the scene
                    thisNewMesh.position.set( thisBB.px, thisBB.py, thisBB.pz );
                    scene.add( thisNewMesh );

            */
                }
            else //... geometry_changed == false, no need for New Material or New Mesh, 
            //... just do update because texture and .map have changed.
                {   
                    //... Works OK now that we are using the BB array with retained individual textures and materials.


                    thisBB.mesh.material.map = thisNewTexture;  //... no exceptions or warnings
                    thisBB.mesh.material.map.needsUpdate = true;

                }  //... if geometry_changed true or false.

        thisBB.mesh.position.set( thisBB.params.px, thisBB.params.py, thisBB.params.pz );
}
</code></pre>

<p><strong>UPDATE</strong>
I found a solution was to insert the command thisBB.texture.dispose() just before creating a new texture.  The alternative idea ( simply to never make the texture a property of thisBB ) does not work.</p>

<p>Here is an <a href="http://jsfiddle.net/steveow/w9wnzb60/" rel="nofollow">updated jsfiddle</a> which allows you to toggle the above-mentioned dispose() command.</p>