垃圾收集无法跟上缓冲区的创建和删除

时间:2016-09-29 04:04:27

标签: javascript garbage-collection buffer garbage

我有一个方法,每2秒运行一次,将视频流捕获到画布并将其写入文件:

  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>存在。该方法有效,它只会导致内存泄漏。

问题:

垃圾收集无法跟上方法使用的内存量,随之发生内存泄漏。

我已将其缩小到这一行:

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

如果我发表评论,会有一些内存累积(~100MB),但每30秒左右会下降一次。

我尝试了什么:

有些帖子建议buffer = null;删除引用并标记垃圾回收,但这并未改变任何内容。

有什么建议吗?

时间轴: 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内存。这是一个电子(铬/桌面)应用程序。

解决 预先分配缓冲区就是修复它的原因。这意味着除了在函数外部使用buffer作用域之外,还需要使用buffer.write重用创建的缓冲区。为了保持正确的标头,请确保使用encoded的{​​{1}}参数。

4 个答案:

答案 0 :(得分:2)

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;
}
你试过这样的事吗?它在哪里失败了?

答案 1 :(得分:0)

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

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

回调引起的问题以及如何将其用于捕获函数。 请注意,只要回调正在运行,GC就无法清除缓冲区或任何其他变量。

答案 2 :(得分:0)

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

  1. 我不得不打破对arrayBuffers和其他一些大对象中数据的引用。
  2. 我不得不强迫Chrome停机,这似乎让我有时间安排然后运行GC。
  3. 在应用这两个步骤之后,事情就为我而动,并且被垃圾收集了。不幸的是,当将这两个东西彼此独立地应用时,我的应用程序一直在崩溃(在这之前爆炸成GB的内存)。以下是我对您的代码尝试的看法。

    垃圾收集器的问题是你无法强制它运行。因此,您可以拥有准备进行malloced的对象,但无论出于何种原因,浏览器都无法为垃圾收集器提供机会。 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
    }
    
    第二,小休息。因此,您似乎已经进行了一些密集的流程,系统无法跟上。它实际上没有达到2s保存标记,因为每次保存需要超过2秒。队列上总有一个函数用于执行captureState.capture(...)方法,它从来没有时间进行垃圾收集。调度程序上的一些有用的帖子以及setInterval和setTimeout之间的差异:

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

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

    如果确实如此,为什么不使用setTimeout并简单检查大约2秒(或更长)的时间已经过去并执行。在执行该检查时,始终强制您的代码在保存之间等待一段时间。给浏览器时间来安排/运行GC - 类似于以下内容(pollForState中的100 ms setTimeout):

    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成为某种对象?如果上述问题无法解决您的问题(无法根据当前共享的内容进行任何评估),那么探讨这些问题可能是值得的。

    希望有所帮助!

答案 3 :(得分:0)

  

我已将其缩小到这一行:

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

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

您可以在不声明任何变量的情况下执行功能。按照NodeJS: Saving a base64-encoded image to diskAnswer所述,删除data返回的MIMEbase64类型和data URI部分HTMLCanvasElement.prototype.toDataURL()部分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);
  });