75

In a vain attempt to write perfect javascript I am tackling the issue of the Javascript heap. I have got it down to the lowest level I can but I have run out of options, and don't understand what is going on (well my guess is rAF overhead, but guesses don't count).

The heap's sawtooth pattern (in light blue):

enter image description here

The above timeline is from a simple full page canvas particle render. The aim of the exercise is to reduce the amplitude of the heap's sawtooth, and hopefully also increase the period between cleanups.

Looking closer, the heap is growing about 15k every 60th of a second, and falling from 3.3MB to 2.4MB every ~1 second

enter image description here

What I do not understand is the timing and the growth amount 15K.

The heap grows by 15kb just before execution idle, and ~0.015ms after the following function has return to idle (below is my top level function).

var renderList = [];    
var stop = false;
var i;

function update(timer) { // Main update loop
    if(!stop){
        requestAnimationFrame(update);
    }
    for(i = 0; i < renderList.length; i ++){
        renderList[i](timer,ctx, w, h);            
    }
}

Nothing I do to the code is reducing or changing the location of the heap growth. The allocation profile shows that am not allocating any memory. GC is at 0.08% on the CPU profile (what it is doing I don't know?, does it also manage the heap?)

Can someone please explain to me what this memory is being used for? and how I can reduce it or make the line flat?

I understand that there may be nothing I can do, but at the moment I dont have the vaguest idea what is being put on the heap? It would be nice to know.

The snippet is just the code called from update (code snippet above) I don't think it is relevant, but just in case. It is the code that has executed and returned just before the heap grows.

        var p,d,s;
        var renderCount = 0;
        var fxId = 0;
        var lastTime;
        var frameTime = 0;
        var minParticles = 10;
        var particleCount = minParticles;
        var frameSum = 0;
        var frameAve = 0;
        var frameWorkTime = 0;
        var lastFrameWorkTime = 0;
        var particleRenderTimeMax = 0;
        var m = 0;
        var mC = 0;
        var mR = 0;
        var then,tx,ty;
        var renderTime,then1; 

        //=====================================================================================
        // the following function is out of context and just placed here as reference
        /*
        draw : function (image, index, x, y, scale, rotation, alpha) {
            spr = image.sprites[index];
            ctx.setTransform(scale, 0, 0, scale, x, y);
            ctx.rotate(rotation);
            ctx.globalAlpha = alpha;
            sh = spr.h;
            sw = spr.w;
            if(spr.vx !== undefined){  // virtual sprite dimensions
                _x = -spr.vw / 2 + spr.vx;
                _y = -spr.vh / 2 + spr.vy;
                ctx.drawImage(image, spr.x, spr.y, sw, sh, _x, _y, sw, sh);
                return;
            }
            ctx.drawImage(image, spr.x, spr.y, sw, sh, -sw / 2, -sh / 2, sw, sh);
        },
        */        
        //=====================================================================================        
        
        // Add particle
        function addP(x,y,spr){
            p = particles.fNextFree();
            if(particles.fLength >= particleCount || p === undefined){ // no room for more
                return;
            }
            p.x = x;
            p.y = y;
            p.spr = spr;
            p.life = 100;
            p.s = Math.random() +0.1
            d = Math.random() * Math.PI * 2;
            s = Math.random() * Math.PI * 2;
            p.dx = Math.cos(d) * s;
            p.dy = Math.sin(d) * s;
            p.dr = Math.random()-0.5;
            p.maxLife = p.life = 100-spr*10;
        }
        // move and draw particle
        function updateDrawP(p,i){
            if(i >= particleCount){
                p.life = undefined;
                return;
            }
            s =  p.life/p.maxLife;
            p.x += p.dx * s;
            p.y += p.dy * s;
            p.r += p.dr;
            p.life -= 1;
            
            if(p.life === 0){
                p.life = undefined;
                return;
            }
            renderCount += 1;
            sDraw(spriteSheet, p.spr, p.x, p.y, p.s, p.r, s); // Calls draw (function example above)
        }
      
        
        function renderAll(time) { // this is called from a requestAnimationFrame controlled function
            var then = performance.now(); // get frame start time
            var tx, ty;
            if (lastTime !== undefined) {
                frameTime = time - lastTime;
                frameSum *= 0.5;
                frameSum += frameTime;
                frameAve = frameSum * 0.5; // a running mean render time
            }
            lastTime = time;
            ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
            ctx.globalAlpha = 1; // reset alpha
            ctx.clearRect(0, 0, w, h);
            if (spriteSheet.sprites) { 
                mouseWorld = EZSprites.world.screen2World(mouse.x, mouse.y, mouseWorld);
                if (mouse.buttonRaw & 1) {
                    fxId += 1;
                    fxId %= EZSprites.FX.namedFX.length;
                    mouse.buttonRaw = 0;
                }
                if (mouse.buttonRaw & 4) {
                    world.posX += mouse.x - mouse.lastX;
                    world.posY += mouse.y - mouse.lastY;
                    EZSprites.world.setPosition(world.posX, world.posY);
                    mouseWorld = EZSprites.world.screen2World(mouse.x, mouse.y, mouseWorld);
                }
                if (mouse.w !== 0) {
                    if (mouse.w > 0) {
                        EZSprites.world.zoom2Screen(mouse.x, mouse.y, ZOOM_AMOUNT, true);
                        mouse.w -= ZOOM_WHEEL_STEPS;
                    } else {
                        EZSprites.world.zoom2Screen(mouse.x, mouse.y, ZOOM_AMOUNT, false);
                        mouse.w += ZOOM_WHEEL_STEPS
                    }
                    mouseWorld = EZSprites.world.screen2World(mouse.x, mouse.y, mouseWorld);
                    EZSprites.world.getWorld(currentWorld);
                    world.posX = currentWorld.x;
                    world.posY = currentWorld.y;
                }

                // sets the current composite operation (only using source-over)
                EZSprites.FX[EZSprites.FX.namedFX[fxId]]();

                // render and time particles
                renderCount = 0;
                var then1 = performance.now();
                
                particles.fEach(updateDrawP); // render all particles
                
                var renderTime = performance.now() - then1;

                EZSprites.context.setDefaults();

                // gets the total time spent inside this function
                frameWorkTime += performance.now() - then;
                lastFrameWorkTime = frameWorkTime;
                if (renderCount > 0) {
                    particleRenderTimeMax = Math.max(particleRenderTimeMax, renderTime / renderCount);
                    particleRenderTimeMax *= 10;
                    particleRenderTimeMax += renderTime / renderCount
                    particleRenderTimeMax /= 11;
                    // Smooth out per particle render time max
                    m = particleRenderTimeMax;
                    mC += (m - mR) * 0.1;
                    mC *= 0.1;
                    mR += mC;
                    // Particle count is being balanced to keep ensure there is plenty of idle time before
                    // the next frame. Mean time spent in this function is about 8 to 9ms
                    particleCount = Math.floor(((1000 / 120) - (frameWorkTime - renderTime)) / (mR));
                }
                // This is where frameWorkTime begins its timing of the function
                then = performance.now();
                frameWorkTime = 0;

                if (particleCount <= maxParticles) {
                    particles.fMaxLength = particleCount;
                }
                // Add particles. 
                addP(mouse.x, mouse.y, 1);
                addP(mouse.x, mouse.y, 2);
                addP(mouse.x, mouse.y, 3);
                addP(mouse.x, mouse.y, 4);
                addP(mouse.x, mouse.y, 5);
                addP(mouse.x, mouse.y, 1);
                addP(mouse.x, mouse.y, 2);
                addP(mouse.x, mouse.y, 3);
                addP(mouse.x, mouse.y, 4);
                addP(mouse.x, mouse.y, 5);
            }
            mouse.lastX = mouse.x;
            mouse.lastY = mouse.y;
            frameWorkTime = performance.now() - then;
        }

Update snippet

As asked in comments below is reproducible HTML doc.

Note this example can not be hosted in sites like CodePen or StackOverflow as they modify monitor and or execute source of addition code that interferes with the test

<!DOCTYPE html>
<html>
    <head><meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-8"></head>
    <body><script> 
    "use strict";
    (() => {
        var renderList = [], stop = false, i, ctx;
        requestAnimationFrame(function update(timer) { // Main loop
            if (!stop) { requestAnimationFrame(update) }
            for (i = 0; i < renderList.length; i ++){
                renderList[i](timer, ctx, w, h);            
            }
        });
    })();
    </script></body>
</html>

Running the above example has the heap grow over 60secs before a major GC is called with the heap growing approx ~300bytes per frame.


Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • 4
    In Profiles -> Heap snapshot, allocation timeline and allocation profile you can find what and why allocates exactly. PS: it's rather V8 heap, since JS by itself does not have a notion of a heap and GC. – zerkms Nov 18 '16 at 02:38
  • 3
    I realize much time has passed, but it would have been helpful if you had created a runnable snippet, or a codepen (or something similar) that reproduces the issue. – Wyck Dec 19 '20 at 02:04
  • 2
    If I had to guess I'd say that `requestAnimationFrame(update)` allocates some form of closure. – Jonas Wilms Apr 05 '21 at 10:22
  • 2
    can you please provide a fully running example illustrating the issue, for Ex. in jsfiddle? – Danny Apostolov Jun 18 '21 at 05:14
  • Can you provide minimal reproducible example? I would like to try to profile this. – Monsieur Merso Jul 27 '21 at 09:06
  • Could this be caused by the stack trace of your rAF calls? Chrome does save the trace of async calls whenever the devtools is open. See: https://crbug.com/1069425 You can disable this by running the command (ctrl + shift + P from the dev-tools) `Do not capture async stack traces`. – Kaiido Aug 02 '21 at 03:20
  • 2
    @Kaiido Odd thing is with Dev Tool option `disable async stack trace` on the performance monitor (dev tools) stops showing JS heap size growth. However when using `performance` tab to record, the heap growth is still there. Could it be Dev tools overhead? maybe – Blindman67 Aug 02 '21 at 04:16
  • "*the heap growing approx ~300bytes per frame*" - that's fine though, nothing like the 15kB you mentioned originally. And the memory is collected eventually just fine, no memory leak. – Bergi Aug 02 '21 at 08:19

2 Answers2

1

It looks like you do not have explicit memory allocations in your code, which means it happends some other way - I see you use some third party libs.

You could try taking a memory snapshot before and after GC (goto devtools: memory, push the red button).

Snapshots have class names, count of objects of those classes and the memory size taken.

So you get 2 snapshots, calculate a diff (somehow), and see if it fits to this saw-shaped picture you have.

Damask
  • 1,754
  • 1
  • 13
  • 24
-4

Each time the update function is called, if nothing else, variable i is created and then destroyed. I don't know if Javascript will optimize that out and preserve the same storage location for i, but if not, that's one possibility.

Another possibility is that any of the functions dereferenced from renderList[] may create and/or destroy variables.

As previously mentioned, there's also the requestAnimationFrame() function which may be creating/destroying variables.

These are suspicions (rather than guesses), but with the data you've provided, that is all that is possible. As others have mentioned, a repeatable example would be necessary in order to fully investigate.

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • `i` is stored on the stack, so it won't spill into the heap. This would not be true if `i` was an array or object, though. – Carson Graham Aug 23 '21 at 20:53