[英]Is there a faster way than a for loop for thresholding an image in 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
上,那是因為以這樣的速率調度這么多任務是有成本的。
在 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.