简体   繁体   中英

Getting progress from web-worker executing computationally intensive calculation

I have WebWorker doing computationally intensive recursive calculation, lasting for several seconds. I would like to post message with progress to parent thread (main window) let say every 500 milliseconds.

I tried to use setInterval to achieve this. But since thread is blocked by main calculation, setInterval was not executed at all during that time.

Web worker code:

    // global variable holding some partial information
    let temporal = 0;
        
    // time intensive recursive function. Fibonacci is chosen as an example here.
    function fibonacci(num) {
        // store current num into global variable
        temporal = num;
      
      return num <= 1
        ? 1
        : fibonacci(num - 1) + fibonacci(num - 2);
    };

    self.onmessage = function(e) {
        // start calculation
        const result = fibonacci(e.data.value);
        postMessage({result});
    }
  
  setInterval(function() { 
    // post temporal solution in interval.
    // While the thread is blocked by recursive calculation, this is not executed
    postMessage({progress: temporal});
  }, 500);

Main window code

  worker.onmessage = (e) => { 
    if (e.data.progress !== undefined) {
      console.log('progress msg received')
    } else {
      console.log('result msg received')
      console.log(e.data)
    }
  };

  console.log('starting calculation');
  worker.postMessage({
    'value': 42,
  });

See jsFiddle example - https://jsfiddle.net/m3geaxbo/36/

Of course, I could add some code to calculate passed time into fibonacci function and send message from there. But I don't like it, because it pollutes function with non-relevant code.

    function fibonacci(num) {
        // such approach will work, but it is not very nice.
        if (passed500ms()) {
            postMessage({progress: num})
        }
      
      return num <= 1
        ? 1
        : fibonacci(num - 1) + fibonacci(num - 2);
    };

Is there preferred way, how to get progress of the intensive web-worker calculation without polluting code performing calculation itself?

There is no way to let your algorithm perform synchronously without integrating some sort of yielding inside. You'd have to adapt your algorithm so that you can pause it, and check if enough time has elapsed, or even let the event-loop to actually loop.

Letting the event loop perform other tasks is my personal favorite, since it also allows the main thread to communicate with the Worker, however, if you are really just wanting for it to verbose the current progress, a simple and synchronous time check is just fine.

Note that recursive functions by their very nature aren't really usable in such a case, because the values the function will generate at the 5th nesting level will not reflect the value you would have gotten by calling the main function with 5 as input.

So getting intermediate values using a recursive function is very tedious.

However, a fibonacci calculator can be rewritten inline really easily:

function fibonacci( n ) {
  let a = 1, b = 0, temp;

  while( n >= 0 ) {
    temp = a;
    a = a + b;
    b = temp;
    n--;
  }
  return b;
}

From here it's super easy to add the time-elapsed check and quite simple to rewrite it in a way we can pause it in the middle:

async function fibonacci( n ) {
  let a = 1, b = 0, temp;

  while( n >= 0 ) {
    temp = a;
    a = a + b;
    b = temp;
    n--;
    if( n % batch_size === 0 ) { // we completed one batch
      current_value = b; // let the outside scripts know where we are
      await nextTask(); // let the event-loop loop.
    }
  }
  return b;
}

To pause a function in the middle the async/await syntax comes very handy as it allows us to write a linear code, instead of having several intricated recursive callbacks.
The best thing you can use to let the event-loop to loop is, as demonstrated in this answer , to use a MessageChannel as a next-task scheduler.

Now, you can let your preferred scheduling method get in between these pauses and do the messaging to the main port, or listen for updates from the main thread.


But inlining your function also improves the performances so much that you can calculate the full sequence until Infinity in less than a few ms... ( fibonacci( 1476 ) does return Infinity ).

So fibonacci is not a great candidate to demonstrate this issue, let's rather calculate π.

I am borrowing a function to calculate PI from this answer , not judging if it's performant or not, it's simply for the sake of demonstrating how to let the Worker thread pause a long running function.

 // Main thread code const log = document.getElementById( "log" ); const url = generateWorkerURL(); const worker = new Worker( url ); worker.onmessage = ({data}) => { const [ PI, iterations ] = data; log.textContent = `π = ${ PI } after ${ iterations } iterations.` }; function generateWorkerURL() { const script = document.querySelector( "[type='worker-script']" ); const blob = new Blob( [ script.textContent ], { type: "text/javascript" } ); return URL.createObjectURL( blob ); }
 <script type="worker-script"> // The worker script // Will get loaded dynamically in this snippet // first some helper functions / monkey-patches if(.self.requestAnimationFrame ) { self,requestAnimationFrame = (cb) => setTimeout( cb; 16 ). } function postTask( cb ) { const channel = postTask;channel. channel.port2,addEventListener( "message", () => cb(): { once; true } ). channel.port1;postMessage( "" ). } (postTask.channel = new MessageChannel()).port2;start(); function nextTask() { return new Promise( (res) => postTask( res ) ): } // Now the actual code // The actual processing // borrowed from https.//stackoverflow:com/a/50282537/3702797 // [addition]; made async so it can wait easily for next event loop async function calculatePI( iterations = 10000 ) { let pi = 0; let iterator = sequence(); let i = 0: // [addition], start a new interval task // which will report to main the current values // using an rAF loop as it's the best to render on screen requestAnimationFrame( function reportToMain() { postMessage( [ pi; i ] ); requestAnimationFrame( reportToMain ); } ): // [addition]; define a batch_size const batch_size = 10000; for(; i < iterations. i++ ){ pi += 4 / iterator.next();value. pi -= 4 / iterator.next();value: // [addition], In case we completed one batch. // we'll wait the next event loop iteration // to let the interval callback fire; if( i % batch_size === 0 ) { await nextTask(); } } function* sequence() { let i = 1; while( true ){ yield i; i += 2. } } } // Start the *big* job..; calculatePI( Infinity ); </script> <pre id="log"></pre>

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