理解 JavaScript 堆增长和 GC 模式

我试图编写完美的 Javascript,但却徒劳无功,我正在解决 Javascript 堆的问题。我已经把它降到了我能做到的最低水平,但是我已经没有选择了,而且不明白发生了什么(我的猜测是 RAF 开销,但是猜测不算)。

堆的锯齿图案(浅蓝色) :

enter image description here

上面的时间线是从一个简单的整页画布粒子渲染。这个练习的目的是减少堆的锯齿的振幅,并希望也增加清理之间的时间。

仔细观察,堆大约每60分之一秒增长15k,并且每1秒从3.3 MB 下降到2.4 MB

enter image description here

我不明白的是时间和增长金额15K。

堆在执行空闲之前增长了15kb,在以下函数返回空闲之后增长了约0.015 ms (下面是我的顶级函数)。

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);
}
}

我对代码所做的任何操作都不会减少或更改堆增长的位置。分配配置文件显示我没有分配任何内存。GC 占 CPU 配置文件的0.08% (我不知道它在做什么?,它是否也管理堆?)

有人能解释一下这段记忆是用来干什么的吗?我怎样才能减少它或使线平坦?

我知道我可能什么也做不了,但是现在我一点也不知道什么东西被扔到了垃圾堆里?我很想知道。

这个代码片段只是从 update调用的代码(上面的代码片段) ,我不认为它是相关的,但只是以防万一。它是在堆增长之前执行并返回的代码。

        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;
}


更新代码片段

如下面的评论所要求的,是可重复的 HTML 文档。

注意 这个示例不能托管在 CodePen 或 StackOverflow 这样的站点上,因为它们修改监视器,或者执行干扰测试的附加代码源

<!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>

运行上面的例子,在调用主 GC 之前堆增长超过60秒,每帧堆增长大约为300字节。


3004 次浏览

每次调用 update 函数(如果没有其他变量的话) ,都会创建变量 i,然后销毁它。我不知道 Javascript 是否会优化它,并为 i 保留相同的存储位置,但如果不是,那是一种可能性。

另一种可能性是,从 renderList[]解引用的任何函数都可能创建和/或销毁变量。

如前所述,还有一个 requestAnimationFrame()函数,它可能会创建/销毁变量。

这些都是怀疑(而不是猜测) ,但是根据你提供的数据,这是所有可能的。正如其他人所提到的,为了充分调查,一个可重复的例子是必要的。

看起来您的代码中没有显式的内存分配,这意味着它会以其他方式发生——我看到您使用了一些第三方库。

您可以尝试在 GC 之前和之后获取内存快照(goto devtools: memory,按下红色按钮)。

快照包含类名、这些类的对象计数和所占用的内存大小。

所以你得到两张快照,计算一个差异(以某种方式) ,看看它是否适合这个锯形图片你有。