簡體   English   中英

有沒有比 setTimeout(0) 更快地屈服於 Javascript 事件循環的方法?

[英]Is there a faster way to yield to Javascript event loop than setTimeout(0)?

我正在嘗試編寫一個執行可中斷計算的 web 工作程序。 我知道的唯一方法(除了Worker.terminate() )是定期屈服於消息循環,以便它可以檢查是否有任何新消息。 例如,這個 web worker 計算從 0 到data的整數之和,但是如果你在計算過程中向它發送一條新消息,它將取消計算並開始一個新消息。

let currentTask = {
  cancelled: false,
}

onmessage = event => {
  // Cancel the current task if there is one.
  currentTask.cancelled = true;

  // Make a new task (this takes advantage of objects being references in Javascript).
  currentTask = {
    cancelled: false,
  };
  performComputation(currentTask, event.data);
}

// Wait for setTimeout(0) to complete, so that the event loop can receive any pending messages.
function yieldToMacrotasks() {
  return new Promise((resolve) => setTimeout(resolve));
}

async function performComputation(task, data) {
  let total = 0;

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    // Yield to the event loop.
    await yieldToMacrotasks();

    // Check if this task has been superceded by another one.
    if (task.cancelled) {
      return;
    }
  }

  // Return the result.
  postMessage(total);
}

這行得通,但速度慢得驚人。 在我的機器上, while循環的每次迭代平均需要 4 毫秒。 如果您想快速取消,這是一個相當大的開銷。

為什么這么慢? 有沒有更快的方法來做到這一點?

是的,消息隊列將比超時具有更高的重要性,因此會以更高的頻率觸發。

您可以使用MessageChannel API輕松綁定到該隊列:

 let i = 0; let j = 0; const channel = new MessageChannel(); channel.port1.onmessage = messageLoop; function messageLoop() { i++; // loop channel.port2.postMessage(""); } function timeoutLoop() { j++; setTimeout( timeoutLoop ); } messageLoop(); timeoutLoop(); // just to log requestAnimationFrame( display ); function display() { log.textContent = "message: " + i + '\n' + "timeout: " + j; requestAnimationFrame( display ); }
 <pre id="log"></pre>

現在,您可能還希望在每個事件循環中批處理幾輪相同的操作。

以下是此方法有效的幾個原因:

  • 根據規范,在第 5 級調用后,即 OP 循環的第 5 次迭代后, setTimeout將被限制為至少 4 毫秒。
    消息事件不受此限制。

  • 在某些情況下,某些瀏覽器會使setTimeout發起的任務具有較低的優先級。
    Firefox 在頁面加載時執行此操作,因此此時調用setTimeout的腳本不會阻塞其他事件; 他們甚至為此創建了一個任務隊列。
    即使仍未指定,似乎至少在 Chrome 中,消息事件具有“用戶可見” 優先級,這意味着某些 UI 事件可能首先出現,但僅此而已。 (使用 Chrome 中即將推出的scheduler.postTask() API 對此進行了測試)

  • 大多數現代瀏覽器會在頁面不可見時限制默認超時,這甚至可能適用於 Workers
    消息事件不受此限制。

  • 正如 OP 所發現的Chrome 確實為前 5 次調用設置了至少 1 毫秒。


但是請記住,如果所有這些限制都放在setTimeout上,那是因為以這樣的速率調度這么多任務是有成本的。

僅在 Worker 線程中使用它!

在 Window 上下文中執行此操作將限制瀏覽器必須處理的所有正常任務,但他們會認為這些任務不太重要,例如網絡請求、垃圾收集等。
此外,發布新任務意味着事件循環必須以高頻率運行並且永遠不會空閑,這意味着更多的能量消耗。

為什么這么慢?

Chrome (Blink) 實際上將最小超時設置為 4 毫秒

// Chromium uses a minimum timer interval of 4ms. We'd like to go
// lower; however, there are poorly coded websites out there which do
// create CPU-spinning loops.  Using 4ms prevents the CPU from
// spinning too busily and provides a balance between CPU spinning and
// the smallest possible interval timer.
static constexpr base::TimeDelta kMinimumInterval =
    base::TimeDelta::FromMilliseconds(4);

編輯:如果您在代碼中進一步閱讀,則僅在嵌套級別超過 5 時才使用該最小值,但在所有情況下它仍將最小值設置為 1 毫秒:

  base::TimeDelta interval_milliseconds =
      std::max(base::TimeDelta::FromMilliseconds(1), interval);
  if (interval_milliseconds < kMinimumInterval &&
      nesting_level_ >= kMaxTimerNestingLevel)
    interval_milliseconds = kMinimumInterval;

顯然,WHATWG 和 W3C 規范對於是否應始終應用至少 4 ms 或僅適用於某個嵌套級別以上存在分歧,但 WHATWG 規范對 HTML 很重要,而且 Chrome 似乎已經實現了這一點。

我不確定為什么我的測量結果表明它仍然需要 4 毫秒。


有沒有更快的方法來做到這一點?

基於 Kaiido 使用另一個消息通道的好主意,您可以執行以下操作:


let currentTask = {
  cancelled: false,
}

onmessage = event => {
  currentTask.cancelled = true;
  currentTask = {
    cancelled: false,
  };
  performComputation(currentTask, event.data);
}

async function performComputation(task, data) {
  let total = 0;

  let promiseResolver;

  const channel = new MessageChannel();
  channel.port2.onmessage = event => {
    promiseResolver();
  };

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    // Yield to the event loop.
    const promise = new Promise(resolve => {
      promiseResolver = resolve;
    });
    channel.port1.postMessage(null);
    await promise;

    // Check if this task has been superceded by another one.
    if (task.cancelled) {
      return;
    }
  }

  // Return the result.
  postMessage(total);
}

我對這段代碼並不完全滿意,但它似乎確實有效並且速度更快 在我的機器上,每個循環大約需要 0.04 毫秒。

查看我的另一個答案中的反對意見,我試圖用我的新知識來挑戰這個答案中的代碼setTimeout(..., 0)有大約 4ms 的強制延遲(至少在 Chromium 上)。 我在每個循環中放置了 100 毫秒的工作負載,並在工作負載之前安排了setTimeout() ,這樣setTimeout()的 4 毫秒就已經過去了。 為了公平起見,我對postMessage()做了同樣的事情。 我還更改了日志記錄。

結果令人驚訝:在觀察計數器時,消息方法在開始時比 timeout 方法獲得了 0-1 次迭代,但即使達到 3000 次迭代,它也保持不變 – 這證明了setTimeout()與並發postMessage()可以保持其份額(在 Chromium 中)。

從 scope 中滾動 iframe 會改變結果:與基於超時的工作負載相比,處理的消息觸發工作負載幾乎是 10 倍。 這可能與瀏覽器打算將更少的資源交給 JS 的視圖或另一個選項卡等有關。

在 Firefox 上,我看到一個工作負載處理,其中包含 7:1 消息以防止超時。 觀看它或讓它在另一個選項卡上運行似乎並不重要。

現在我將(稍作修改的)代碼移到了一個Worker 上。 事實證明,通過超時調度處理的迭代與基於消息的調度完全相同 在 Firefox 和 Chromium 上,我得到了相同的結果。

 let i = 0; let j = 0; const channel = new MessageChannel(); channel.port1.onmessage = messageLoop; timer = performance.now.bind(performance); function workload() { const start = timer(); while (timer() - start < 100); } function messageLoop() { i++; channel.port2.postMessage(""); workload(); } function timeoutLoop() { j++; setTimeout( timeoutLoop ); workload(); } setInterval(() => log.textContent = `message: ${i}\ntimeout: ${j}`, 300); timeoutLoop(); messageLoop();
 <pre id="log"></pre>

我可以確認setTimeout(..., 0)的 4ms 往返時間,但不一致。 我使用了以下工作人員(以let w = new Worker('url/to/this/code.js' ,以w.terminate()停止)。

在前兩輪中,暫停時間低於 1 毫秒,然后我得到一個在 8 毫秒范圍內的暫停,然后每次進一步迭代都保持在 4 毫秒左右。

為了減少等待,我將yieldPromise執行器移到了工作負載前面。 這樣setTimeout()可以保持它的最小延遲,而不會暫停工作循環超過必要的時間。 我想工作量必須超過 4ms 才能有效。 這應該不是問題,除非捕獲取消消息是工作量...... ;-)

結果:僅約 0.4 毫秒延遲。 即至少減少 10 倍。 1

'use strict';
const timer = performance.now.bind(performance);

async function work() {
    while (true) {
        const yieldPromise = new Promise(resolve => setTimeout(resolve, 0));
        const start = timer();
        while (timer() - start < 500) {
            // work here
        }
        const end = timer();
        // const yieldPromise = new Promise(resolve => setTimeout(resolve, 0));
        await yieldPromise;
        console.log('Took this time to come back working:', timer() - end);
    }
}
work();


1瀏覽器不是將計時器分辨率限制在該范圍內嗎? 那么沒有辦法衡量進一步的改進......

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM