垃圾 collection 跟不上缓冲区的创建和删除

Garbage collection can't keep up with Buffer creation and removal

我有一种方法,每 2 秒 运行s 将视频流捕获到 canvas 并将其写入文件:

  function capture(streamName, callback) {
    var buffer,
      dataURL,
      dataSplit,
      _ctx;

    _ctx = _canvas[streamName].getContext('2d');
    _ctx.drawImage(_video[streamName], 0, 0);
    dataURL = _canvas[streamName].toDataURL('image/png');
    dataSplit = dataURL.split(",")[1];
    buffer = new Buffer(dataSplit, 'base64');

    fs.writeFileSync(directory + streamName + '.png', buffer);
  }

  setInterval(function() {
    // Called from here
    captureState.capture(activeScreens[currentScreenIndex]);
    gameState.pollForState(processId, activeScreens[currentScreenIndex], function() {
      // do things...
    });
  }, 2000);

假设 _video[streamName] 作为 运行 宁 <video> 存在并且 _canvas[streamName] 作为 <canvas> 存在。该方法有效,它只是导致内存泄漏。

问题:

垃圾 collection 跟不上该方法使用的内存量,随之而来的是内存泄漏。

我已将范围缩小到这一行:

buffer = new Buffer(dataSplit, 'base64');

如果我将其注释掉,就会有一些内存积累(~100MB),但它每 30 秒左右就会下降一次。

我尝试过的:

一些帖子建议 buffer = null; 删除引用和垃圾标记 collection,但这并没有改变任何东西。

有什么建议吗?

时间线: https://i.imgur.com/wH7yFjI.png https://i.imgur.com/ozFwuxY.png

分配概况: https://www.dropbox.com/s/zfezp46um6kin7g/Heap-20160929T140250.heaptimeline?dl=0

只是为了量化。经过大约 30 分钟的 运行 时间后,它占用了 2 GB 的内存。这是一个 Electron(chromium / 桌面)应用程序。

已解决 Pre-allocating 缓冲区是修复它的原因。这意味着除了作用域 buffer 之外的函数,您还需要使用 buffer.write 重用创建的缓冲区。为了保持正确的 headers,请确保使用 buffer.write.

encoded 参数

您的代码中没有缓冲区对象泄漏。

您不再在代码中保留引用的任何 Buffer 对象将立即可用于垃圾回收。

callback 引起的问题以及如何在capture 函数之外使用它。 请注意,只要回调是 运行.

,GC 就无法清除缓冲区或任何其他变量

我最近遇到了一个类似的问题,一个软件应用程序使用 arrayBuffer 形式的 ~500MB 数据。我以为我有内存泄漏,但事实证明 Chrome 正在尝试对一组大型 ArrayBuffer 和相应的操作进行优化(每个缓冲区的大小约为 60mb 和一些稍大的对象)。 CPU 用法似乎永远不允许 GC 到 运行,或者至少它是这样出现的。我必须做两件事来解决我的问题。我还没有阅读任何关于 GC 何时被安排证明或反驳这一点的具体规范。我必须做的事情:

  1. 我不得不中断对 arrayBuffers 和其他一些大对象中数据的引用。
  2. 我不得不强迫 Chrome 停机,这似乎给了它时间安排然后 运行 GC。

应用这两个步骤后,对我来说 运行 的东西被垃圾回收了。不幸的是,当彼此独立地应用这两个东西时,我的应用程序不断崩溃(在这样做之前爆炸到 GB 内存使用)。以下是我对您的代码尝试的想法。

垃圾收集器的问题是你不能强制它运行。所以你可以拥有准备好分配的对象,但无论出于何种原因,浏览器都不给垃圾收集器机会。 buffer = null 的另一种方法是用 delete operator 显式中断引用——这就是我所做的,但理论上 ... = null 是等效的。重要的是要注意 delete 不能是 运行 在 var 运算符创建的任何变量上。所以我的建议如下:

function capture(streamName, callback) {
    this._ctx = _canvas[streamName].getContext('2d');
    this._ctx.drawImage(_video[streamName], 0, 0);
    this.dataURL = _canvas[streamName].toDataURL('image/png');
    this.dataSplit = dataURL.split(",")[1];
    this.buffer = new Buffer(dataSplit, 'base64');

    fs.writeFileSync(directory + streamName + '.png', this.buffer);

    delete this._ctx;//because the context with the image used still exists
    delete this.dataURL;//because the data used in dataSplit exists here
    delete this.dataSplit;//because the data used in buffer exists here
    delete this.buffer;
    //again ... = null likely would work as well, I used delete
}

二、小破。所以看起来你有一些密集的流程正在进行并且系统无法跟上。它实际上并没有达到 2 秒保存标记,因为每次保存需要超过 2 秒。队列上总是有一个函数用于执行 captureState.capture(...) 方法,它从来没有时间进行垃圾收集。关于调度程序的一些有用的帖子以及 setInterval 和 setTimeout 之间的区别:

http://javascript.info/tutorial/settimeout-setinterval

http://ejohn.org/blog/how-javascript-timers-work/

如果确实如此,为什么不使用 setTimeout 并简单地检查大约 2 秒(或更多)的时间已经过去并执行。在进行该检查时,始终强制您的代码在两次保存之间等待一段设定的时间。给浏览器时间 schedule/run GC——如下所示(pollForState 中的 setTimeout 为 100 毫秒):

var MINIMUM_DELAY_BETWEEN_SAVES = 100;
var POLLING_DELAY = 100;

//get the time in ms
var ts = Date.now();
function interValCheck(){
   //check if 2000 ms have passed
   if(Date.now()-ts > 2000){
      //reset the timestamp of the last time save was run
      ts = Date.now();
      // Called from here
      captureState.capture(activeScreens[currentScreenIndex]);
      //upon callback, force the system to take a break.
      setTimeout(function(){
        gameState.pollForState(processId, activeScreens[currentScreenIndex], function() {
          // do things...
          //and then schedule the interValCheck again, but give it some time
          //to potentially garbage collect.
          setTimeout(intervalCheck,MINIMUM_DELAY_BETWEEN_SAVES);
       });
      }
   }else{
      //reschedule check back in 1/10th of a second.
      //or after whatever may be executing next.
      setTimeout(intervalCheck,POLLING_DELAY);
   }
}

这意味着捕获不会超过每 2 秒发生一次,但在某种意义上也会诱使浏览器有时间进行 GC 并删除任何剩余的数据。

最后的想法,采用更传统的内存泄漏定义,根据我在您的代码中看到的内容,内存泄漏的候选对象是 activeScreens_canvas_video哪些看起来是某种物体?如果以上内容不能解决您的问题(将无法根据当前共享的内容进行任何评估),可能值得探索这些内容。

希望对您有所帮助!

I have narrowed it down to this line:

buffer = new Buffer(dataSplit, 'base64');

简短的解决方案是不使用 Buffer,因为没有必要将文件写入文件系统,其中文件引用存在于 data URIbase64 部分。 setInterval 似乎没有被清除。您可以为 setInterval 定义引用,然后在 <video> ended 事件中调用 clearInterval()

您可以在不声明任何变量的情况下执行功能。删除 HTMLCanvasElement.prototype.toDataURL() 返回的 dataMIME 类型和 data URIbase64 部分,如 NodeJS: Saving a base64-encoded image to disk , this Answer at NodeJS write base64 image-file

所述
  function capture(streamName, callback) {

    _canvas[streamName].getContext("2d")
    .drawImage(_video[streamName], 0, 0);

    fs.writeFileSync(directory + streamName + ".png"
      , _canvas[streamName].toDataURL("image/png").split(",")[1], "base64");
  }

  var interval = setInterval(function() {
    // Called from here
    captureState.capture(activeScreens[currentScreenIndex]);
    gameState.pollForState(processId, activeScreens[currentScreenIndex]
    , function() {
    // do things...
    });
  }, 2000);

  video[/* streamName */].addEventListener("ended", function(e) {
    clearInterval(interval);
  });

Matt,我不确定预分配缓冲区有什么问题,所以我发布了一个关于如何使用此类预分配缓冲区的算法。这里的关键是缓冲区只分配一次,因此不应该有任何内存泄漏。

var buffers = [];
var bsize = 10000;

// allocate buffer pool
for(var i = 0; i < 10; i++ ){
    buffers.push({free:true, buf: new Buffer(bsize)});
}

// sample method that picks one of the buffers into use
function useOneBuffer(data){
    // find a free buffer
    var theBuf;
    var i = 10;
    while((typeof theBuf==='undefined')&& i < 10){
        if(buffers[i].free){
            theBuf = buffers[i];
        }
        i++;
    }

    theBuf.free = false;
    // start doing whatever you need with the buffer, write data in needed format to it first
    // BUT do not allocate
    // also, you may want to clear-write the existing data int he buffer, just in case before reuse or after the use.
    if(typeof theBuf==='undefined'){
        // return or throw... no free buffers left for now
        return;
    }
    theBuf.buf.write(data);
    // .... continue using

    // dont forget to pass the reference to the buffers member along because 
    // when you are done, toy have to mark it as free, so that it could be used again
    // theBuf.free = true;
}

你试过这样的事情吗?哪里失败了?

一般来说,我会建议使用 UUID 的本地映射/可以让您在处理 getImageData 和其他缓冲区时控制内存的东西。 UUID 可以是 pre-defined 标识符,例如:“current-image”和“prev-image”如果在幻灯片之间进行比较

例如

existingBuffers: Record<string, UInt8ClampedArray> = {} 
existingBuffers[ptrUid] = ImageData.data (OR something equivalent) 

然后,如果你想覆盖 ("current-image"),你可以(在这里矫枉过正):

existingBuffers[ptrUid] = new UInt8ClampedArray();
delete existingBuffers[ptrUid]

此外,您将始终能够检查缓冲区并确保它们不会失控。

可能有点old-school,但我觉得很舒服。