【问题标题】:How to make the onmessage phase of a webworker asynchronous?如何使 webworker 的 onmessage 阶段异步?
【发布时间】:2021-03-01 14:13:48
【问题描述】:

我正在使用网络工作者来计算属于这些地方的坐标和值。计算完美地发生在后台,保持 DOM 响应。但是,当我将数据从 webworker 发送回主线程时,DOM 在部分传输时间内变得无响应。

我的网络工作者(发送部分):

//calculates happen before; this is the final step to give the calculated data back to the mainthread.
var processProgressGEO = {'cmd':'geoReport', 'name': 'starting transfer to main', 'current': c, 'total': polys}
postMessage(processProgressGEO);
postMessage({
  'cmd':'heatmapCompleted',
  'heatdata': rehashedMap,
  'heatdatacount': p,
  'current': c,
  'total': polys,
  'heatmapPeak': peakHM,
});
self.close();

上面代码sn-p中的变量rehashedMap是一个带有数字键的对象。每个键包含一个array 和另一个object

我的主线程(仅相关部分:)

var heatMaxforAuto = 1000000;  //maximum amount of datapoints allowed in the texdata. This takes into account the spread of a singel datapoint.
async function fetchHeatData(){
  return new Promise((resolve, reject) => {
    var numbercruncher = new Worker('calculator.js');
    console.log("Performing Second XHR request:");
    var url2 = 'backend.php?datarequest=geodata'
    $.ajax({
      type: "GET",
      url: url2,
    }).then(async function(RAWGEOdata) {
      data.georaw = RAWGEOdata;
      numbercruncher.onmessage = async function(e){
        var w = (e.data.current/e.data.total)*100+'%';
        if (e.data.cmd === 'geoReport'){
          console.log("HEAT: ", e.data.name, end(),'Sec.' );
        }else if (e.data.cmd === 'heatmapCompleted') {
          console.log("received Full heatmap data: "+end());
          data.heatmap = e.data.heatdata;
          console.log("heatData transfered", end());
          data.heatmapMaxValue = e.data.heatmapPeak;
          data.pointsInHeatmap = e.data.heatdatacount;
          console.log("killing worker");
          numbercruncher.terminate();
          resolve(1);
        }else{
          throw "Unexpected command received by worker: "+ e.data.cmd;
        }
      }
      console.log('send to worker')
      numbercruncher.postMessage({'mode':'geo', 'data':data});
    }).catch(function(error) {
      reject(0);
      throw error;
    })
  });
}

async function makemap(){
  let heatDone = false;
      if (data.texdatapoints<= heatMaxforAuto){
      heatDone = await fetchHeatData();
    }else{
      var manualHeatMapFetcher = document.createElement("BUTTON");
      var manualHeatMapFetcherText = document.createTextNode('Fetch records');
      manualHeatMapFetcher.appendChild(manualHeatMapFetcherText);
      manualHeatMapFetcher.id='manualHeatTriggerButton';
      manualHeatMapFetcher.addEventListener("click", async function(){
        $(this).toggleClass('hidden');
        heatDone = await fetchHeatData();
        console.log(heatDone, 'allIsDone', end());
      });
      document.getElementById("toggleIDheatmap").appendChild(manualHeatMapFetcher);
    }


}

makemap();

需要调用 end() 函数来计算自 webworker 启动以来的秒数。它返回全局设置的开始时间和调用时间之间的差异。

我的控制台中显示的内容:

HEAT:  starting transfer to main 35 Sec.   animator.js:44:19
received Full heatmap data: 51             animator.js:47:19
heatData transfered 51                     animator.js:49:19
killing worker                             animator.js:52:19

1 allIsDone 51

问题: 我的 DOM 在数据传输开始和收到完整热图数据后的消息之间冻结。这是我的控制台中第一条消息和第二条消息之间的阶段。传输需要 16 秒,但 DOM 仅在其中一部分时间内无响应。 Webworkers 无法与主线程共享数据,因此需要传输。

问题: 首先,如何防止 DOM 在 webworker 的onmessage 阶段冻结?其次,更多的是出于好奇:这种冻结怎么可能只发生在该阶段的一部分,因为这些是由两个连续步骤触发的,中间没有任何事情发生?

到目前为止我尝试了什么:

  1. 对 rehashedMap 执行 for 循环并逐个键返回。这仍然会触发 DOM 冻结;更短,但不止一次。在极少数情况下,它会拉下标签。
  2. 寻找缓冲onmessage阶段的方法;但是,与 postMessage 阶段 (https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) 相比,文档 (https://developer.mozilla.org/en-US/docs/Web/API/Worker/onmessage) 中没有指定这样的选项。我在这里遗漏了什么吗?
  3. 作为测试,我将 rehashedMap 替换为一个空对象;这不会导致 DOM 冻结。当然,这让我无法访问计算数据。
  4. 我在 SO:Javascript WebWorker - Async/Await 上查看了这个帖子,但我不确定如何将该上下文与我的上下文进行比较。

【问题讨论】:

  • 您在上面的几个地方使用了术语“JSON 对象”,我确定您只是指“对象”。 JSON 是一种用于数据交换的文本符号(More here.) 如果您正在处理 JavaScript 源代码,而不是处理 string,那么您就不是在处理 JSON。
  • 我现在可以理解在解释问题时这是多么令人困惑。感谢您指出。我会在需要的地方进行更改。
  • 你在不同的浏览器上体验过吗?你在这个数据中有循环引用吗?这个物体有多深?您的开发工具的性能选项卡说的是瓶颈。无论如何,如果你要从同一个脚本开始一个新的工作人员,不要杀死你的工作人员:启动一个工作人员对浏览器来说是一项巨大的工作,让它等待一个事件是什么。
  • 请将您在以下答案中提供的信息作为edit 包含在您的问题中,并按照要求包含minimal reproducible example。另外,你真的需要在你的前端线程上拥有所有这些数据吗?我想你不能同时显示所有这些,所以有没有办法拆分这个请求,以便你的工作人员只生成真正需要的内容? (如果你能展示一些你的 visu 脚本也有帮助的话)。

标签: javascript asynchronous web-worker


【解决方案1】:

选项

你应该把它和 web worker 联系起来是可以理解的,但它可能与它没有任何关系。 我错了,它确实如此。我看到问题的两个可能原因:

  1. (我们知道这对于 OP 而言并非如此,但可能仍然与其他人相关。) 问题可能在于,一旦您收到热图。如果您在一个严密的循环中执行此操作,并且不会让主线程执行任何其他操作,那么页面将在此期间无响应。

    如果发生了这种情况,您必须找到一种方法来更快地进行 DOM 操作(有时可能,有时则不然),或者找到一种方法将其分割成块并分别处理每个块,然后返回块之间的浏览器,以便浏览器可以处理任何待处理的 UI 工作(包括渲染新元素)。

    您没有将 DOM 工作包含在热图上,因此实际上不可能给您 代码 来解决问题,但“分割”将通过处理数据的子集,然后使用setTimeout(fn, 0)(可能与requestAnimationFrame结合以确保已发生重绘)安排在短暂屈服于浏览器后继续工作(使用fn)。

  2. 如果确实是在工作线程和主线程之间传输数据所花费的时间,您也许可以将transferable object 用于您的热图数据而不是您当前的对象,尽管这样做所以可能需要显着改变你的数据结构。使用可转移对象,您可以避免将数据从工作线程复制到主线程;相反,worker 将实际内存转移到主线程(worker 失去了对可转移对象的访问权,而主线程获得了对它的访问权——所有这些都没有复制它)。例如,类型化数组(Int32Array 等)使用的 ArrayBuffer 是可转移的。

  3. 如果确实是从工作人员那里接收数据所花费的时间(从您的实验中听起来确实如此),并且使用可转移不是一种选择(例如,因为您需要数据采用某种格式与可转移的不兼容),我能看到的唯一剩下的选择是让工作人员向主脚本发送较小的数据块,这些数据块的间距足以让主线程保持响应。 (甚至可能在数据可用时发送数据。)

仔细看看#3

您已经描述了一个包含 1,600 个条目的数组,其中每个条目是一个包含 0 到“超过 7,000 个”对象的数组,每个对象具有三个属性(带有数字值)。超过 5.6 百万 个对象。克隆这些数据需要相当长的时间也就不足为奇了。

这是您描述的问题的一个示例:

const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
worker.addEventListener("message", ({data}) => {
    if ((data && data.action) === "data") {
        console.log(Date.now(), `Received ${data.array.length} rows`);
        if (data.done) {
            stopSpinning();
        }
    }
});
document.getElementById("btn-go").addEventListener("click", () => {
    console.log(Date.now(), "requesting data");
    startSpinning();
    worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
    if (spinner) {
        cancelAnimationFrame(spinHandle);
        maxDelay = 0;
        queueUpdate();
    }
}
function queueUpdate() {
    intervalStart = Date.now();
    spinHandle = requestAnimationFrame(() => {
        updateMax();
        spinner.textContent = states[stateIndex];
        stateIndex = (stateIndex + 1) % states.length;
        if (spinHandle) {
            queueUpdate();
        }
    });
}
function stopSpinning() {
    updateMax();
    cancelAnimationFrame(spinHandle);
    spinHandle = 0;
    if (spinner) {
        spinner.textContent = "Done";
        console.log(`Max delay between frames: ${maxDelay}ms`);
    }
}
function updateMax() {
    if (intervalStart !== 0) {
        const elapsed = Date.now() - intervalStart;
        if (elapsed > maxDelay) {
            maxDelay = elapsed;
        }
    }
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
    if ((data && data.action) === "go") {
        console.log(Date.now(), "building data");
        const array = Array.from({length: 1600}, () =>
            Array.from({length: Math.floor(r() * 7000)}, () => ({lat: r(), lng: r(), value: r()}))
        );
        console.log(Date.now(), "data built");
        console.log(Date.now(), "sending data");
        postMessage({
            action: "data",
            array,
            done: true
        });
        console.log(Date.now(), "data sent");
    }
});
</script>

这是一个工人以尽可能快的速度以单独的消息形式发送数据的示例。它在接收数据时使页面响应(尽管仍然紧张):

const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
let array = null;
let clockTimeStart = 0;
worker.addEventListener("message", ({data}) => {
    if ((data && data.action) === "data") {
        if (clockTimeStart === 0) {
            clockTimeStart = Date.now();
            console.log(Date.now(), "Receiving data");
        }
        array.push(...data.array);
        if (data.done) {
            console.log(Date.now(), `Received ${array.length} row(s) in total, clock time to receive data: ${Date.now() - clockTimeStart}ms`);
            stopSpinning();
        }
    }
});
document.getElementById("btn-go").addEventListener("click", () => {
    console.log(Date.now(), "requesting data");
    array = [];
    clockTimeStart = 0;
    startSpinning();
    worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
    if (spinner) {
        cancelAnimationFrame(spinHandle);
        maxDelay = 0;
        queueUpdate();
    }
}
function queueUpdate() {
    intervalStart = Date.now();
    spinHandle = requestAnimationFrame(() => {
        updateMax();
        spinner.textContent = states[stateIndex];
        stateIndex = (stateIndex + 1) % states.length;
        if (spinHandle) {
            queueUpdate();
        }
    });
}
function stopSpinning() {
    updateMax();
    cancelAnimationFrame(spinHandle);
    spinHandle = 0;
    if (spinner) {
        spinner.textContent = "Done";
        console.log(`Max delay between frames: ${maxDelay}ms`);
    }
}
function updateMax() {
    if (intervalStart !== 0) {
        const elapsed = Date.now() - intervalStart;
        if (elapsed > maxDelay) {
            maxDelay = elapsed;
        }
    }
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
    if ((data && data.action) === "go") {
        console.log(Date.now(), "building data");
        const array = Array.from({length: 1600}, () =>
            Array.from({length: Math.floor(r() * 7000)}, () => ({lat: r(), lng: r(), value: r()}))
        );
        console.log(Date.now(), "data built");
        const total = 1600;
        const chunks = 100;
        const perChunk = total / chunks;
        if (perChunk !== Math.floor(perChunk)) {
            throw new Error(`total = ${total}, chunks = ${chunks}, total / chunks has remainder`);
        }
        for (let n = 0; n < chunks; ++n) {
            postMessage({
                action: "data",
                array: array.slice(n * perChunk, (n + 1) * perChunk),
                done: n === chunks - 1
            });
        }
    }
});
</script>

这自然是一种权衡。接收数据所花费的总时钟时间越长,块越小;块越小,页面的抖动就越小。这是非常小的块(分别发送 1,600 个数组中的每一个):

const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
let array = null;
let clockTimeStart = 0;
worker.addEventListener("message", ({data}) => {
    if ((data && data.action) === "data") {
        if (clockTimeStart === 0) {
            clockTimeStart = Date.now();
        }
        array.push(data.array);
        if (data.done) {
            console.log(`Received ${array.length} row(s) in total, clock time to receive data: ${Date.now() - clockTimeStart}ms`);
            stopSpinning();
        }
    }
});
document.getElementById("btn-go").addEventListener("click", () => {
    console.log(Date.now(), "requesting data");
    array = [];
    clockTimeStart = 0;
    startSpinning();
    worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
    if (spinner) {
        cancelAnimationFrame(spinHandle);
        maxDelay = 0;
        queueUpdate();
    }
}
function queueUpdate() {
    intervalStart = Date.now();
    spinHandle = requestAnimationFrame(() => {
        updateMax();
        spinner.textContent = states[stateIndex];
        stateIndex = (stateIndex + 1) % states.length;
        if (spinHandle) {
            queueUpdate();
        }
    });
}
function stopSpinning() {
    updateMax();
    cancelAnimationFrame(spinHandle);
    spinHandle = 0;
    if (spinner) {
        spinner.textContent = "Done";
        console.log(`Max delay between frames: ${maxDelay}ms`);
    }
}
function updateMax() {
    if (intervalStart !== 0) {
        const elapsed = Date.now() - intervalStart;
        if (elapsed > maxDelay) {
            maxDelay = elapsed;
        }
    }
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
    if ((data && data.action) === "go") {
        console.log(Date.now(), "building data");
        const array = Array.from({length: 1600}, () =>
            Array.from({length: Math.floor(r() * 7000)}, () => ({lat: r(), lng: r(), value: r()}))
        );
        console.log(Date.now(), "data built");
        array.forEach((chunk, index) => {
            postMessage({
                action: "data",
                array: chunk,
                done: index === array.length - 1
            });
        });
    }
});
</script>

这是构建所有数据然后发送它,但是如果构建数据时间,穿插构建和发送它可能会使页面响应更流畅,特别是如果您可以发送更小的内部数组(甚至发送 ~7,000对象仍然会导致抖动,正如我们在上面的最后一个示例中所见)。

结合#2和#3

主数组中的每个条目都是具有三个数字属性的对象数组。我们可以改为发送 Float64Arrays 和 lat/lng/value 订单中的这些值,因为它们是可转移的:

const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
let array = null;
let clockTimeStart = 0;
worker.addEventListener("message", ({data}) => {
    if ((data && data.action) === "data") {
        if (clockTimeStart === 0) {
            clockTimeStart = Date.now();
        }
        const nums = data.array;
        let n = 0;
        const entry = [];
        while (n < nums.length) {
            entry.push({
                lat: nums[n++],
                lng: nums[n++],
                value: nums[n++]
            });
        }
        array.push(entry);
        if (data.done) {
            console.log(Date.now(), `Received ${array.length} row(s) in total, clock time to receive data: ${Date.now() - clockTimeStart}ms`);
            stopSpinning();
        }
    }
});
document.getElementById("btn-go").addEventListener("click", () => {
    console.log(Date.now(), "requesting data");
    array = [];
    clockTimeStart = 0;
    startSpinning();
    worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
    if (spinner) {
        cancelAnimationFrame(spinHandle);
        maxDelay = 0;
        queueUpdate();
    }
}
function queueUpdate() {
    intervalStart = Date.now();
    spinHandle = requestAnimationFrame(() => {
        updateMax();
        spinner.textContent = states[stateIndex];
        stateIndex = (stateIndex + 1) % states.length;
        if (spinHandle) {
            queueUpdate();
        }
    });
}
function stopSpinning() {
    updateMax();
    cancelAnimationFrame(spinHandle);
    spinHandle = 0;
    if (spinner) {
        spinner.textContent = "Done";
        console.log(`Max delay between frames: ${maxDelay}ms`);
    }
}
function updateMax() {
    if (intervalStart !== 0) {
        const elapsed = Date.now() - intervalStart;
        if (elapsed > maxDelay) {
            maxDelay = elapsed;
        }
    }
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
    if ((data && data.action) === "go") {
        for (let n = 0; n < 1600; ++n) {
            const nums = Float64Array.from(
                {length: Math.floor(r() * 7000) * 3},
                () => r()
            );
            postMessage({
                action: "data",
                array: nums,
                done: n === 1600 - 1
            }, [nums.buffer]);
        }
    }
});
</script>

这极大地减少了接收数据的时钟时间,同时保持 UI 相当灵敏。

【讨论】:

  • 你好@T.J.克劳德;感谢您与我一起思考。我已禁用所有 DOM 操作(进度条和统计报告)。我还禁用了热图图层的创建和运行它的整个传单画布。在另一个测试中,我将热图作为onmessage 调用的一部分发送到主线程;但没有通过注释将响应分配给主要的data 对象:data.heatmap = e.data.heatdata;。在这两个测试中,我的问题仍然存在,即使在清除现金之后也是如此。猜猜我必须弄清楚那个可转移的物体;感谢您指出该选项。
  • @Clueless_captain - 听到这个我真的很惊讶。但是,是的,在这种情况下,一个可转移的应该这样做,尽管重构您的数据会有点痛苦。
  • @Kaiido - 谢谢。有时间我得看看。现在急着做点别的。但可能是克隆Float64Array 比克隆包含一堆对象的数组快得多,这可以解释它......以及为什么我认为这取决于使用转移。这将为我们提供 OP 的前进方向:使用易于克隆(或真正转移)的原语。
  • @T.J.Crowder 我根据您的第二个和第三个解决方案实现了一些东西。从我的第一个工作人员那里,我发送了带有键的 heatmap 对象,但是这 1600 个数组的全部内容都是字符串。完成后,用户会收到通知,并在接受将该数据合并到原始数据后,会出现一个微调器,并且第二个工作人员开始将字符串解析为对象。我用整个数据集(现在已经超过 2000 万个对象)对它进行了测试,它看起来就像一个魅力。
  • @Kaiido 感谢您与我们一起思考,我认为这与对象的深度有关。请参阅我上面的评论。从第一个工作人员那里转移初始 heatmap 而不将其转换为字符串需要很长时间。如果我在完整数据集上运行它,它会阻塞 UI 四分之一小时或更长时间。按照 T.J. 的建议,将 heatmap 的处理分成块。克劳德显着改善了这一点。即使加上 JSON.parse()JSON.stringify 所需的时间,整个数据集也需要大约 3 分钟的时间来处理,只有几秒钟会阻塞 UI(使用微调器)。
猜你喜欢
  • 2021-01-18
  • 2023-03-12
  • 2021-11-01
  • 1970-01-01
  • 2019-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多