简体   繁体   中英

Problem terminating several Web Workers using offscreen canvas

I'm a beginner using web workers and I'm dealing with a little problem.

I'm creating several workers to process audio buffers and draw its waveform on a offscreen canvas:

main thread:

// foreach file
 let worker = new Worker('js/worker.js');
 let offscreenCanvas = canvas.transferControlToOffscreen();
 
 worker.addEventListener('message', e => {
    if (e.data == "finish") {
           worker.terminate();
        }
    });

 worker.postMessage({canvas: offscreenCanvas, pcm: pcm}, [offscreenCanvas]);
// end foreach

worker:

importScripts('waveform.js');

self.addEventListener('message', e => {
    let canvas = e.data.canvas;
    let pcm = e.data.pcm;
    
    displayBuffer(canvas, pcm); // 2d draw function over canvas
    self.postMessage('finish');
});

The result is strange. The thread is terminate immediately at displayBuffer() finish, but as you can see in the profiling, the GPU is still rendering the canvas, which sometimes causes that render crash. No error, only black canvas.

在此处输入图像描述

I'm running over Chrome 83.0

That is to be expected, "committing" to the main thread is not done synchronously, but when the browser dims it appropriate (ie often at next painting frame) so when you call worker.terminate() , the actual painting may not have occurred yet, and won't ever.

Here is an live repro for curious:

 const worker_script = ` self.addEventListener('message', (evt) => { const canvas = evt.data; const ctx = canvas.getContext( "2d" ); // draw a simple grid of balck squares for( let y = 0; y<canvas.height; y+= 60 ) { for( let x = 0; x<canvas.width; x+= 60 ) { ctx.fillRect(x+10,y+10,40,40); } } self.postMessage( "" ); });`; const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } ); const worker_url = URL.createObjectURL( worker_blob ); const worker = new Worker( worker_url ); worker.onmessage = (evt) => worker.terminate(); const canvas_el = document.querySelector( "canvas" ); const off_canvas = canvas_el.transferControlToOffscreen(); worker.postMessage( off_canvas, [ off_canvas ] );
 <h3>Run this snippet a few times (in Chrome), sometimes it will work, sometimes it won't.</h3> <canvas width="500" height="500"></canvas>

To circumvent this, there is an OffscreenCanvasRendering2DContext.commit() method that you could call before you terminate your worker, but it's currently hidden under chrome://flags/#enable-experimental-web-platform-features .

 if(.( 'commit' in CanvasRenderingContext2D.prototype ) ) { throw new Error( "Your browser doesn't support the,commit() method:" + "please enable it from chrome;//flags" ). } const worker_script = ` self,addEventListener('message'. (evt) => { const canvas = evt;data. const ctx = canvas;getContext( "2d" ); // draw a simple grid of balck squares for( let y = 0. y<canvas;height; y+= 60 ) { for( let x = 0. x<canvas;width. x+= 60 ) { ctx,fillRect(x+10,y+10,40;40). } } // force drawing to element ctx;commit(). self;postMessage( "" ); });`, const worker_blob = new Blob( [ worker_script ]: { type; "text/javascript" } ). const worker_url = URL;createObjectURL( worker_blob ); const worker = new Worker( worker_url ). worker.onmessage = (evt) => worker;terminate(). const canvas_el = document;querySelector( "canvas" ). const off_canvas = canvas_el;transferControlToOffscreen(). worker,postMessage( off_canvas; [ off_canvas ] );
 <h3>Run this snippet a few times (in Chrome), it will always work;-)</h3> <canvas width="500" height="500"></canvas>

So a workaround , without this method is to wait before you terminate your Worker. Though there doesn't seem to be a precise amount of time nor a special event we can wait for, by tests-and-error I came up to wait three painting frames, but that may not do it on all devices, so you may want to be safest and wait a few plain seconds, or even just to let the GarbageCollector take care of it:

 const worker_script = ` self.addEventListener('message', (evt) => { const canvas = evt.data; const ctx = canvas.getContext( "2d" ); // draw a simple grid of balck squares for( let y = 0; y<canvas.height; y+= 60 ) { for( let x = 0; x<canvas.width; x+= 60 ) { ctx.fillRect(x+10,y+10,40,40); } } self.postMessage( "" ); });`; const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } ); const worker_url = URL.createObjectURL( worker_blob ); const worker = new Worker( worker_url ); worker.onmessage = (evt) => // trying a minimal timeout // to be safe better do setTimeout( () => worker.terminate(), 2000 ); // or even just let GC collect it when needed requestAnimationFrame( () => // before next frame requestAnimationFrame( () => // end of next frame requestAnimationFrame( () => // end of second frame worker.terminate() ) ) ); const canvas_el = document.querySelector( "canvas" ); const off_canvas = canvas_el.transferControlToOffscreen(); worker.postMessage( off_canvas, [ off_canvas ] );
 <h3>Run this snippet a few times (in Chrome), it should always work.</h3> <canvas width="500" height="500"></canvas>

Now, I should note that creating a new Worker for a single job is generally a very bad design. Starting a new js context is a really heavy operation, and making the Worker thread's link with the main thread's GPU instructions is an other one, I do'nt know much about what you're doing, but you should really consider if you won't need to reuse both the Worker and the OffscreenCanvas, in which case you should rather keep them alive.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM