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):
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
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; }
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With