1

I am using a temporary DOM div container and THREE.js renderer inside my javascript "Image SNAP Function" to produce a high-resolution image file of the current scene which the user can view outside the browser and/or save to disk as a .JPG file.

It all works fine except that another 0.14Gb of memory is used for each "snap" and not released. After a few snaps the PC performance degrades badly. The memory can be released by closing the "app" (browser tab window) but this is very inconvenient for the user.

I have tried various commands to release the memory within the Image Snap function, but they do not do the job.

Here is the code of the Image Snap function:-

function F_SNAP_JPG()           
{
    var bigContainer = document.createElement('div');
    bigContainer.style.cssText = 'id: "bigContainer", width: 4000, height:2200 '; 
    document.body.appendChild( bigContainer );
    bigContainer.innerHTML = "";

    var bigRenderer = new THREE.WebGLRenderer( {antialias:true , preserveDrawingBuffer: true} );    
    bigRenderer.setPixelRatio( window.devicePixelRatio );
    bigRenderer.setSize( 4000, 2200 );

    bigContainer.appendChild( bigRenderer.domElement ); 

//--------------------------------------------------------------------------------------------
// RENDER

    bigRenderer.render     ( scene, camera );
    
//--------------------------------------------------------------------------------------------
//... make an image of the rendition and allow user to view it outside the browser or save it as a file.
    var strMime = "image/jpg";
    var fileURL = bigRenderer.domElement.toDataURL( strMime );          

    //... after & thanks to http://muaz-khan.blogspot.co.uk/2012/10/save-files-on-disk-using-javascript-or.html
    var Anchor = document.createElement('a'); //... creates an html <a> anchor object.                      

    document.body.appendChild( Anchor ); //... for Firefox, not needed in Opera (?)
    Anchor.style    = "display: none";
    Anchor.href     = fileURL;
    Anchor.target   = '_blank';
    Anchor.download = filename || 'unknown';
    Anchor.click(); 
    document.body.removeChild( Anchor ); //... OK.
    
    //--------------------------------------------------------------------------------------------
    //...try various ways to release memory

    bigContainer.removeChild( bigRenderer.domElement ); //... removes container from screen.
    //... with no more instructions beyond here, memory increases 0.14Gb with every snap.

    bigRenderer.dispose();//... seems OK 
    //... but with no more instructions beyond here, memory increases 0.14Gb with every snap.

    document.body.removeChild( bigContainer );  //... seems OK 
    //... but with no more instructions beyond here, memory increases 0.14Gb with every snap.

    bigContainer.delete();  //... seems OK 
    //... but with no more instructions beyond here, memory increases 0.14Gb with every snap.

}//... End of function

What can be done to release the memory (short of closing down the browser tab window)?


Update (Solution)

I found a solution in this answer by Konstantin Eletskiy in this 2015 Stack Overflow post:- Clean up Threejs WebGl contexts.

The solution is to add a single line

bigRenderer.forceContextLoss();
//bigRenderer.context = null;    // not needed here.
//bigRenderer.domElement = null; // not needed here.
//bigRenderer = null;            // not needed here.

to the end of the F_SNAP_JPEG function. The other 3 lines were not needed here - possibly becuase I also removed all the DOM bigContainer stuff as suggested by George and theJim01. Now there is no detectable memory leakage for at least 9 snapshots.

steveOw
  • 879
  • 12
  • 41
  • try setting `fileURL = null` as that's a lot of data in that one variable. Also use `let` and `const` instead of `var`, because `var` can cause you problems. Additionally it looks like you should set preserveDrawingBuffer to false. – George Dec 13 '20 at 13:17
  • 1
    Furthermore, you don't need to create a new WebGLRenderer each time, instead initialise it once and re-use it. – George Dec 13 '20 at 13:21
  • @George thanks for the suggestions but (1) `fileURL=null` had no effect; (2) using `let` & `const` in place of `var` has no effect from what I have tried so far; (3) `preserveDrawingBuffer` set to false has no effect; (4) once-only initiation of `WebGLRenderer` has no effect. Additionally (5) .`clear` of the renderer has no effect. – steveOw Dec 13 '20 at 16:58
  • You could try chrome dev tools performance tab, recording the app over time and use that to detect exactly where the memory leak is occuring. – George Dec 14 '20 at 18:41
  • @George. Thanks for the suggestion but before I rolled up my sleeves to try it I found a working solution elsewhere on StackOverflow (see Update at end of my question). – steveOw Dec 15 '20 at 22:44

1 Answers1

2

I agree with George. Here's a re-write that might help reduce some of that build-up.

A couple things about the code below:

  • I used a closure to allow you to re-use the common elements.
  • I kept the parts where you create DOM structure (appendChild), but you really don't need it.
    • You actually don't need bigContainer at all.
const F_SNAP_JPG = ( function () { // TheJim01: Putting this in an IIFE closure for element re-use

    const bigContainer = document.createElement( 'div' );
    bigContainer.style.width = '4000px';
    bigContainer.style.height = '2200px';
    document.body.appendChild( bigContainer );

    const bigRenderer = new THREE.WebGLRenderer( { antialias: true, preserveDrawingBuffer: true } );
    bigRenderer.setPixelRatio( window.devicePixelRatio );
    bigRenderer.setSize( 4000, 2200 );
    bigContainer.appendChild( bigRenderer.domElement );

    const Anchor = document.createElement( 'a' );
    Anchor.style.display = 'none';
    Anchor.target = '_blank';
    document.body.appendChild( Anchor );

    return function () { // TheJim01: This is the actual function that gets called, so place breakpoints in here.

        bigRenderer.render( scene, camera );

        let fileURL = bigRenderer.domElement.toDataURL( 'image/jpg' );

        Anchor.href = fileURL;
        Anchor.download = filename || 'unknown'; // TheJim01: filename is always undefined! Mistake? Missing parameter?
        Anchor.click();
        Anchor.href = ''; // TheJim01: Removing the dataURL string.

    };

} )();
TheJim01
  • 8,411
  • 1
  • 30
  • 54
  • Thanks. My javascript skill level is not very advanced so I'm going to have to study that closure stuff carefully and get back to you! – steveOw Dec 14 '20 at 22:16
  • 1
    In a nutshell: For the most part, JS is functionally-scoped. When the code is first parsed, the IIFE is executed, creating the variables (like `bigRenderer`), and returning a function that is within the same scope as the variables, which is assigned to `F_SNAP_JPG`. (Kind of like a function pointer.) When you call `F_SNAP_JPG()`, it actually references the returned function, and that returned function still has access to the variables within its original scope (like `bigRenderer`), so it can reuse them. This is a very fast-and-loose description of closures, but I think it expresses the gist. – TheJim01 Dec 15 '20 at 17:11
  • Many thanks for the explanation. I tried your closured function (with and without the DOM structure) but the same memory leakage ensued. After 5 good snaps of ~200Mb I get a WebGL error. Currently the simplest way I have to keep on snapping is to close the browser tab (=close the app) then start it up again. – steveOw Dec 15 '20 at 22:04
  • I found a solution in this answer by Konstantin Eletskiy (in 2015) here:- [https://stackoverflow.com/questions/21548247/clean-up-threejs-webgl-contexts](https://stackoverflow.com/questions/21548247/clean-up-threejs-webgl-contexts). Thanks for your help - now I understand closures (better than before)! The solution is to run `bigRenderer.forceContextLoss()`; `bigRenderer.context = null`; `bigRenderer.domElement = null`; `bigRenderer = null`; – steveOw Dec 15 '20 at 22:30