Получение прогресса от веб-воркера, выполняющего интенсивные вычисления - PullRequest
2 голосов
/ 13 июля 2020

У меня WebWorker выполняет ресурсоемкие рекурсивные вычисления, длящиеся несколько секунд. Я хотел бы публиковать сообщение с прогрессом в родительский поток (главное окно), скажем, каждые 500 миллисекунд.

Я пытался использовать setInterval для этого. Но поскольку поток заблокирован основным вычислением, setInterval вообще не выполнялся в течение этого времени.

Код веб-воркера:

    // 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 код окна

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

См. пример jsFiddle - https://jsfiddle.net/m3geaxbo/36/

Конечно, я мог бы добавить код для расчета прошедшего времени в функцию fibonacci и отправьте сообщение оттуда. Но мне это не нравится, потому что он загрязняет функцию нерелевантным кодом.

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

Есть ли предпочтительный способ, как получить прогресс интенсивного вычисления веб-воркера, не загрязняя код, выполняющий вычисления?

1 Ответ

0 голосов
/ 16 июля 2020

Невозможно позволить вашему алгоритму работать синхронно без интеграции какого-либо урожая внутри. Вам нужно будет адаптировать свой алгоритм, чтобы вы могли приостановить его и проверить, прошло ли достаточно времени, или даже позволить событию-l oop на самом деле l oop.

Позволить событию l oop выполнение других задач - мой личный фаворит, поскольку он также позволяет основному потоку взаимодействовать с Worker, однако, если вы действительно хотите, чтобы он подробно описывал текущий прогресс, простая и синхронная проверка времени вполне подойдет.

Обратите внимание, что рекурсивные функции по самой своей природе не могут использоваться в таком случае, потому что значения, которые функция будет генерировать на 5-м уровне вложенности, не будут отражать значение, которое вы получили бы, вызвав основную функцию. с 5 в качестве входных данных.

Таким образом, получение промежуточных значений с помощью рекурсивной функции очень утомительно.

Однако калькулятор Фибоначчи можно очень легко переписать в строке:

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

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

Отсюда очень легко добавить проверку прошедшего времени и довольно просто переписать ее так, чтобы мы могли приостановить это в середине:

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

Чтобы приостановить функцию посередине, очень удобен синтаксис async/await, так как он позволяет нам писать линейный код вместо нескольких сложных рекурсивных обратных вызовов. Лучшее, что вы можете использовать, чтобы разрешить event-l oop to l oop, - , как показано в этом ответе , использовать MessageChannel в качестве планировщика следующей задачи.

Теперь вы можете позволить предпочтительному методу планирования вставать между этими паузами и отправлять сообщения на основной порт или прослушивать обновления из основного потока.

Но встраивание вашей функции также улучшает производительность настолько, что вы можете рассчитать полную последовательность до Infinity менее чем за несколько мс ... (fibonacci( 1476 ) действительно возвращает Infinity).

Итак, Фибоначчи не лучший кандидат для демонстрации этой проблемы, давайте лучше вычислим π.

Я заимствую функцию для вычисления PI из этого ответа , не оценивая, насколько она эффективна или нет, это просто ради демонстрации того, как разрешить рабочему потоку приостанавливать длительную работу функции.

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

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

...