Understanding JavaScript heap growth and GC pattern

JavascriptV8

Javascript Problem Overview


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.


Javascript Solutions


Solution 1 - Javascript

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.

Solution 2 - Javascript

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBlindman67View Question on Stackoverflow
Solution 1 - JavascriptDamaskView Answer on Stackoverflow
Solution 2 - JavascriptOwen DeLongView Answer on Stackoverflow