getImageData - Web worker - 如何减少垃圾回收?

时间:2017-01-18 14:31:27

标签: javascript canvas garbage-collection

我有一个示例网络工作者画布更新脚本正在运行,但我注意到它每隔几秒就会停止约200毫秒。通常的循环时间约为15ms。

我猜这是垃圾收集 - 它看起来像是来自探查器。

http://codepen.io/SarahC/pen/bgBoMM

我认为这是在这个功能中:

function nextFrame(){
  timeChart.start();
  workersWorking = workerCount;
  var stripHeight = ~~( h / workerCount );
  for(var i = 0; i < workerCount; i++){
    var localImageData = ctx.getImageData(0, stripHeight * i, w, stripHeight); /// This needs putting in constant memory.... GC takes ages here.
    workers[i].postMessage({imageData: localImageData, YPosition: stripHeight * i, threadNumber: i});
  }
}

如果这个位是所有垃圾内存,我不知道我能做些什么来继续为这些数据块使用相同的内存区域。

2 个答案:

答案 0 :(得分:5)

传递imageData的缓冲区而不是imageData本身。

这样,您的缓冲区为transferred(带有零拷贝操作),并且不会再污染主线程的内存。
否则,当您不传输它时,您的对象是结构化克隆(就像您执行JSON.parse(JSON.stringify(yourObject));一样),这意味着当您使用计算机时,计算机将三个相同数据的副本保存在内存中从工人发回主线程。

请注意,当在worker中传递时,imageData的数据在主线程中不再可用(如果你尝试putImageData()它会抛出错误)。 遗憾的是,我不知道改变ImageData缓冲区的好方法,但是你可以在创建时设置一个缓冲区,这要归功于ImageData()构造函数(显然在IE中仍然不支持...),这将是实际上只是创建一个指向arrayBuffer的指针。

因此,当支持所有这些时,只创建ImageData结构(基本上是对象{width:XXX, height:XXX})而不是重缓冲区。其他所有东西都被移动了,不会污染记忆。

let workerURL = URL.createObjectURL(new Blob([workerScript.textContent], {
  type: 'application/javascript'
}));

const worker = new Worker(workerURL);
worker.onmessage = e => {
  let buf = e.data,
    arr = new Uint8ClampedArray(buf),
    processedImageData;
  try {
    processedImageData = new ImageData(arr, imageData.width, imageData.height);
  } catch (e) {
    processedImageData = ctx.createImageData(imageData.width, imageData.height);
    processedImageData.data.set(arr);
  }
  // checks that we didn't created an useless buffer in this last step
  // IE will because it doesn't support new ImageData(buf)
  console.log('Does our TypedArray share the same buffer as the one we received ? ',
              arr.buffer === buf);
  console.log('Does our new imageData share the same buffer as the one we received ? ',
              processedImageData.data.buffer === buf);
  // Note that here a check for the original imageData's buffer has no sense
  //       since it has been emptied
  ctx.putImageData(processedImageData, 0, 0);
}

const ctx = canvas.getContext('2d');
ctx.fillStyle = 'green';
ctx.fillRect(20, 20, 60, 80);
let imageData = ctx.getImageData(0, 0, 300, 150);
// pass it as transferable
worker.postMessage(imageData.data.buffer, [imageData.data.buffer]);
console.log(imageData.data.length, 'now empty')
<script type="worker-script" id="workerScript">
	self.onmessage = e => {
		let buf = e.data,
		arr = new Uint8Array(buf);
		console.log('worker received', buf);
		for(let i =0; i<arr.length; i+=4){
			arr[i] = (arr[i] + 128) % 255;
			arr[i+1] = (arr[i+1] + 128) % 255;
			arr[i+2] = (arr[i+2] + 128) % 255;			
			}
		self.postMessage(buf, [buf]);
		// this won't print in stacksnippet's console
		// you have to check your dev tools' one
		console.log('worker now holds', buf.byteLength, 'empty');

		};
</script>
<canvas id="canvas"></canvas>

使用结构克隆的反例:

let workerURL = URL.createObjectURL(new Blob([workerScript.textContent], {
  type: 'application/javascript'
}));

const worker = new Worker(workerURL);
worker.onmessage = e => {
  let buf = e.data;
  // so our original imageData's arrayBuffer is still available
  imageData.data.set(buf);
  // Here we can check for equality with the first arrayBuffer
  console.log('Is the first bufferArray the same as the one we received ?', imageData.data.buffer === buf);  
  ctx.putImageData(imageData, 0, 0);
}

const ctx = canvas.getContext('2d');
ctx.fillStyle = 'green';
ctx.fillRect(20, 20, 60, 80);
let imageData = ctx.getImageData(0, 0, 300, 150);
// pass it as transferable
worker.postMessage(imageData.data.buffer);
console.log(imageData.data.length, 'not empty')
<script type="worker-script" id="workerScript">
	self.onmessage = e => {
		let buf = e.data,
		arr = new Uint8Array(buf);
		console.log('worker received', buf);
		for(let i =0; i<arr.length; i+=4){
			arr[i] = (arr[i] + 128) % 255;
			arr[i+1] = (arr[i+1] + 128) % 255;
			arr[i+2] = (arr[i+2] + 128) % 255;			
			}
		console.log(arr);
		self.postMessage(buf);
		// this won't print in stacksnippet's console
		// you have to check your dev tools' one
		console.log('worker now holds', buf.byteLength, 'full');
		};
</script>
<canvas id="canvas"></canvas>

答案 1 :(得分:2)

您只能通过重用像素数据阵列来减少每个JS上下文中的GC命中总数,但是您无法进行重大更改。问题是传入和传出工人的数据。

当你向工作人员发布数据时,工作人员必须分配内存来接收数据,不能说,这里有一些我已经使用过的RAM请把它放在这里。可悲的是,发布的消息及其内容每次都作为新对象到达工作者的上下文。这同样适用于返回数据,每条消息都是一条新消息,带有新的依据,分配和最终删除。

您可以一次发送较小的数据块,这可能会导致GC点击,因此您无法获得大的GC峰值。但是内存使用率与内存吞吐量有关,除非您降低吞吐量,否则不会降低GC负载。

您可能希望查看sharedArrayBuffers,因为它们证明了Javascript线程之间的共享内存资源。目前的支持是Chrome(需要标记)和Firefox。但是有一种推动来实现这种类型的内存管理,所以在尝试它时可能值得。

SharedArrayBuffers应该可以在很大程度上消除此类应用程序的GC命中。

更新

根据新信息,您可以尝试在worker.postMessage调用中使用transferable transfer参数。请参阅草案W3C worker postmessage以便发布给工作人员和worker global scope postmessage以便从工作人员返回数据。

此处定义了可转移对象transferable objects ,它指出您只能转移一次对象。当工人收到对象时,它不能将其作为可转移对象返回(根据文档)。如果您希望将其作为可转让的方式返回,则必须从您收到的任何数组中创建一个新的类型数组。

文档中不清楚的是这会如何影响发件人上下文中的内存管理。

更新2

在播放可传输数据后,我发现要停止任何内存开销,你需要有两个数据副本(在imageData的情况下)

保存像素数据的imageData.data属性在传输后无法使用。您需要创建一个新的imageData数组或复制数据以发送到另一个数组,并在使用typedArray set函数返回时将其复制回来。

以下是使用可传输数据向工作人员传输数据和从工作人员传输数据而不会产生过多GC开销的示例(代码不是全部)。在Chrome上运行类似的代码并检查时间线,显示没有GC命中超过0.002毫秒

// one time set up
var imgData = ctx.getImageData(0, 0, 512, 512);
var tData = new Uint8ClampedArray(512 * 512 * 4); 
tData.set(imgData.data);

// repeats from here
worker.sendMessage(tData.buffer, [tData.buffer]);

// In the worker
onmessage(event){
    var data = new Uint8ClampedArray(event.data);
    // process the data
    // return data
    postMessage(event.data,[event.data]);
}

// Back on the main thread
onmessage(event){
    tData = new Uint8ClampedArray(event.data);
    imgData.data.set(tData);
    ctx.putImageData(imgdata, 0, 0);
}
// now you can resend tData as it is a new typedArray referance (to the same data)
相关问题