垃圾 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 何时被安排证明或反驳这一点的具体规范。我必须做的事情:
- 我不得不中断对 arrayBuffers 和其他一些大对象中数据的引用。
- 我不得不强迫 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 URI
的 base64
部分。 setInterval
似乎没有被清除。您可以为 setInterval
定义引用,然后在 <video>
ended
事件中调用 clearInterval()
。
您可以在不声明任何变量的情况下执行功能。删除 HTMLCanvasElement.prototype.toDataURL()
返回的 data
、MIME
类型和 data URI
的 base64
部分,如 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,但我觉得很舒服。
我有一种方法,每 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 何时被安排证明或反驳这一点的具体规范。我必须做的事情:
- 我不得不中断对 arrayBuffers 和其他一些大对象中数据的引用。
- 我不得不强迫 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 URI
的 base64
部分。 setInterval
似乎没有被清除。您可以为 setInterval
定义引用,然后在 <video>
ended
事件中调用 clearInterval()
。
您可以在不声明任何变量的情况下执行功能。删除 HTMLCanvasElement.prototype.toDataURL()
返回的 data
、MIME
类型和 data URI
的 base64
部分,如 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,但我觉得很舒服。