Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding JavaScript heap growth and GC pattern

Tags:

javascript

v8

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.


like image 776
Blindman67 Avatar asked Nov 18 '16 02:11

Blindman67


Video Answer


1 Answers

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.

like image 60
Damask Avatar answered Oct 12 '22 23:10

Damask