【问题标题】:getImageData - Web workers - How can I reduce garbage collection?getImageData - Web 工作者 - 如何减少垃圾收集?
【发布时间】:2020-08-05 22:41:20
【问题描述】:

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

我猜这是垃圾收集 - 从探查器中看起来是这样。

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

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

【问题讨论】:

    标签: javascript canvas garbage-collection


    【解决方案1】:

    传递 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>

    【讨论】:

    • 哇! ....这很有趣: (imageData.data.buffer, [imageData.data.buffer]) .. 让它做一个零拷贝?谢谢你的例子——你是为我的帖子写的吗?我将在 codepen 中尝试您的示例,以更好地理解来龙去脉。
    • @SarahC,是的,就是这样。
    • 我是否仅限于在您演示的一个呼叫中发送缓冲区?有没有一种方法可以发送包含其他信息的对象而无需发布另一条消息? google 起来很麻烦,对我来说现在是凌晨 4:10。
    • @SarahC 不,您也可以将任何对象作为第一个参数传递,只要第二个对象包含您的 arrayBuffer :worker.postMessage({buffer:imageData.data.buffer, otherKey:'hello'}, [imageData.data.buffer]) 应该可以正常工作。
    • @SarahC,关于您之前和现在已删除的评论,最后的测试只是为了确保最终的 imageData 确实与已发送回工作人员的缓冲区保持相同的缓冲区。由于第一个丢失了他的内存分配,我们不能对其进行相等性检查。但是由于您报告它在进行结构化克隆时仍然有一个长度,这意味着 arrayBuffer 现在在内存中是我的回答中所述的三次(您可以进行相等性检查,以显示第一个 imageData 的 arrayBuffer 不一样) .我同意我的回答可能令人困惑。
    【解决方案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 上运行类似的代码并检查时间线显示没有超过 0.002 毫秒的 GC 命中

    // 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)
    

    【讨论】:

    • 我明白了 - 谢谢你帮我解决了这么多细节。我将阅读共享数组缓冲区并尝试使用它们。
    • 数组缓冲区可以很容易地分配在 GC 堆之外,而 JS 对象本身只是指针。因此,仅将它们发送出去不会造成显着的 GC 压力。更不用说 arraybuffers 和 ImageBitmap 是可转移的,这可以实现零拷贝。所以我不相信 postMessage 是问题
    • @the8472 ArrayBuffers 和 typedArrays 是将数据从一个上下文移动到另一个上下文的最佳方式。但是每个上下文都与其他上下文隔离。 ArrayBuffers 在发送给工作人员时会被复制,而不仅仅是发送的引用。这意味着发送方和接收方都需要分配保存数组所需的 RAM。更多developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/…
    • 对象必须被复制,但在内部它们可以只包含指向固定内存块的指针,不一定需要复制。例如,这就是 blob 的工作方式。此外,结构化克隆postMessage中的可转移对象不同。
    • @the8472 你是说当我向工作人员发送数组缓冲区消息时,它在原始上下文中占用的内存就会消失(移动到工作人员上下文)并且有新的内存资源可用。对我来说,接缝就像是转储内存的好方法。只需运行一个 worker,它会处理所有垃圾,让其上下文和 GC 处理混乱,而主上下文有取之不尽的新 RAM 可供使用,并且没有 GC 开销。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-30
    • 1970-01-01
    • 2014-02-05
    • 2011-09-30
    • 2015-04-30
    • 1970-01-01
    相关资源
    最近更新 更多