Web Worker 的消息有多快? [英] How fast are Web Worker's messages?

查看:23
本文介绍了Web Worker 的消息有多快?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想知道与网络工作者之间的传输是否会成为瓶颈.我们应该在触发任何类型的事件时发布消息,还是应该小心并尽量限制两者之间的通信?

I wondered if transmission to or from a web worker can be a bottleneck. Should we post message just as we trigger any kind of events, or should we take care and try to limit as much as possible the communication between the two ?

让我们举个例子.如果我有一个动态构造的巨大数组(例如来自 mousemovetouchmove 的用于手势识别器的接触点数组),传输数据是否更有效迭代地——即在我们收到每个元素后立即发送并让工作人员将它们存储在其一侧——或者最好将它们存储在主线程上并在最后一次发送所有数据,特别是当一个人无法使用时一个可转移的对象?

Let's have an example. If I have a huge array that is dynamically constructed (e.g. an array of contact points coming from mousemove or touchmove for a gesture recogniser), is it more efficient to transfer the data iteratively – i.e. send each element as soon as we receive it and let the worker store them on its side – or is it better to store them on the main thread and send all the data at once at the end, in particular when one cannot use a transferable object?

推荐答案

好吧,你可以缓冲 Uint16Array1 中的数据.然后您可以做一个小技巧并移动数据而不是复制.在 MDN 上查看 这个演示 介绍.

Well you can buffer the data in Uint16Array1. You can then do a little trick and move the data instead of copying. See this demo on MDN for an introduction.

1:对于小于 16x16 米且像素密度为每毫米 0.25 像素的屏幕应该足够了,我相信这是世界上大多数屏幕

1: should be enough for screens smaller than 16x16 meters at pixel density 0.25 pixels per milimeter, which I believe is most screens on the world

首先,让我们测试一下网络工作者的速度.

First to your question, let's test the web workers speed.

我创建了这个测试片段,试图测量工人的实际速度.但是尝试在这里很重要.我确实发现只有可靠的时间测量方法才能影响时间,就像我们在现代物理学理论中所经历的那样.

I created this test snippet that attempts to measure actual speed of workers. But attempts is important here. Truly I figured out that only reliable way of measuring the time will affect the time, much like what we experience in modern physic theories.

代码绝对可以告诉我们的是,缓冲是一个好主意.第一个文本框设置要发送的数据量.第二个设置用于划分数据的样本数.您很快就会发现样本的开销是显着的.复选框允许您选择是否传输数据.正如预期的那样,随着数据量的增加,这开始变得重要.

What the code definitely can tell us is that buffering is a good idea. First textbox sets the total amount of data to be sent. Second sets the number of samples to divide the data in. You'll soon find out that overhead with samples is notable. Checkbox allows you to chose whether to transfer data or not. This starts to matter with bigger amount of data, just as anticipated.

请原谅凌乱的代码,在编写令人兴奋的测试片段时,我不能强迫自己行为.我创建了这个 tjes

Please forgive the messy code, I can't force myself to behave when writing exciting test snippets. I created this tjes

function WorkerFN() {
  console.log('WORKER: Worker ready for data.');
  // Amount of data expected
  var expectedData = 0;
  // Amount of data received
  var receivedData = 0;
  self.onmessage = function(e) {
      var type = e.data.type;
      if(type=="data") {
          receivedData+=e.data.data.byteLength;
          self.postMessage({type: "timeResponse", timeStart: e.data.time, timeHere: performance.now(), bytes: e.data.data.byteLength, all:expectedData<=receivedData});
      }
      else if(type=="expectData") {
          if(receivedData>0 && receivedData<expectedData) {
              console.warn("There is transmission in progress already!");  
          }
          console.log("Expecting ", e.data.bytes, " bytes of data.");
          expectedData = e.data.bytes;
          receivedData = 0;
      }
  }
}

var worker = new Worker(URL.createObjectURL(new Blob(["("+WorkerFN.toString()+")()"], {type: 'text/javascript'})));

/** SPEED CALCULATION IN THIS BLOCK **/
var results = {
  transfered: 0,
  timeIntegral: 0 //Total time between sending data and receiving confirmation
}
// I just love getters and setters. They are so irresistably confusing :)
// ... little bit like women. You think you're just changing a value and whoops - a function triggers
Object.defineProperty(results, "speed", {get: function() {
  if(this.timeIntegral>0)
    return (this.transfered/this.timeIntegral)*1000;
  else
    return this.transfered==0?0:Infinity;
}
});
// Worker sends times he received the messages with data, we can compare them with sent time
worker.addEventListener("message", function(e) {
  var type = e.data.type;
  if(type=="timeResponse") {
    results.transfered+=e.data.bytes;
    results.timeIntegral+=e.data.timeHere-e.data.timeStart;
    // Display finish message if allowed
    if(e.data.all) {
        status("Done. Approx speed: "+humanFileSize(Math.round(results.speed/100)/10, true)+"/s"); 
        addRecentResult();
    }
  }
});

/** GUI CRAP HERE **/
// Firefox caches disabled values after page reload, which makes testing a pain
$(".disableIfWorking").attr("disabled", false);
$("#start_measure").click(startMeasure);
$("#bytes").on("input", function() {
  $("#readableBytes").text(humanFileSize(this.value, true));
});
$("#readableBytes").text(humanFileSize($("#bytes").val()*1||0, true));

function addRecentResult() {
  var bytes = $("#bytes").val()*1;
  var chunks = $("#chunks").val()*1;
  var bpch = Math.ceil(bytes/chunks);
  var string = '<tr><td class="transfer '+($("#transfer")[0].checked)+'">    </td><td class="speed">'+humanFileSize(results.speed, true)+'/s</td><td class="bytes">'+humanFileSize(bytes, true)+'</td><td class="bpch">'+humanFileSize(bpch, true)+'</td><td class="time">'+results.timeIntegral+'</td></tr>';
  if($("#results td.transfer").length==0)
    $("#results").append(string);
  else
    $(string).insertBefore($($("#results td.transfer")[0].parentNode));
}
function status(text, className) {
  $("#status_value").text(text);
  if(typeof className=="string")
    $("#status")[0].className = className;
  else
    $("#status")[0].className = "";
}
window.addEventListener("error",function(e) {
  status(e.message, "error");
  // Enable buttons again
  $(".disableIfWorking").attr("disabled", false);
});
function startMeasure() {
  if(Number.isNaN(1*$("#bytes").val()) || Number.isNaN(1*$("#chunks").val()))
    return status("Fill the damn fields!", "error");
  $(".disableIfWorking").attr("disabled", "disabled");
  DataFabricator(1*$("#bytes").val(), 1*$("#chunks").val(), sendData);
}

/** SENDING DATA HERE **/
function sendData(dataArray, bytes, bytesPerChunk, transfer, currentOffset) {
  // Initialisation before async recursion
  if(typeof currentOffset!="number") {
    worker.postMessage({type:"expectData", bytes: bytesPerChunk*dataArray.length});
    // Reset results
    results.timeIntegral = 0;
    results.transfered = 0;
    results.finish = false;
    setTimeout(sendData, 500, dataArray, bytes, bytesPerChunk, $("#transfer")[0].checked, 0);
  }
  else {
    var param1 = {
         type:"data",
         time: performance.now(),
         data: dataArray[currentOffset]
    };
    // I decided it's optimal to write code twice and use if
    if(transfer)
      worker.postMessage(param1, [dataArray[currentOffset]]);
    else 
      worker.postMessage(param1);
    // Allow GC
    dataArray[currentOffset] = undefined;
    // Increment offset
    currentOffset++; 
    // Continue or re-enable controls
    if(currentOffset<dataArray.length) {
    // Update status
      status("Sending data... "+Math.round((currentOffset/dataArray.length)*100)+"% at "+humanFileSize(Math.round(results.speed/100)/10, true)+"/s");
      setTimeout(sendData, 100, dataArray, bytes, bytesPerChunk, transfer, currentOffset);
    }
    else {
      //status("Done. Approx speed: "+humanFileSize(Math.round(results.speed/100)/10, true)+"/s");
      $(".disableIfWorking").attr("disabled", false);
      results.finish = true;
    }
  }
}
/** CREATING DATA HERE **/
function DataFabricator(bytes, chunks, callback) {
  var loop;

  var args = [
      chunks, // How many chunks to create
      bytes,  // How many bytes to transfer total
      Math.ceil(bytes/chunks), // How many bytes per chunk, byt min 1 byte per chunk
      0,      // Which offset of current chunk are we filling
      [],     // Array of existing chunks
      null,   // Currently created chunk
  ];
  // Yeah this is so damn evil it randomly turns bytes in your memory to 666
  //                                                     ... yes I said BYTES
  (loop=function(chunks, bytes, bytesPerChunk, chunkOffset, chunkArray, currentChunk) {
    var time = performance.now();
    // Runs for max 40ms
    while(performance.now()-time<40) {
      if(currentChunk==null) {
        currentChunk = new Uint8Array(bytesPerChunk);
        chunkOffset = 0;
        chunkArray.push(currentChunk.buffer);
      }
      if(chunkOffset>=currentChunk.length) {
        // This means the array is full
        if(chunkArray.length>=chunks)
          break;
        else {
          currentChunk = null;
          // Back to the top
          continue;
        }
      }
      currentChunk[chunkOffset] = Math.floor(Math.random()*256);
      // No need to change every value in array
      chunkOffset+=Math.floor(bytesPerChunk/5)||1;
    }
    // Calculate progress in bytes
    var progress = (chunkArray.length-1)*bytesPerChunk+chunkOffset;
    status("Generating data - "+(Math.round((progress/(bytesPerChunk*chunks))*1000)/10)+"%");
    
    if(chunkArray.length<chunks || chunkOffset<currentChunk.length) {
      // NOTE: MODIFYING arguments IS PERFORMANCE KILLER!
      Array.prototype.unshift.call(arguments, loop, 5);
      setTimeout.apply(null, arguments);
    }
    else {
      callback(chunkArray, bytes, bytesPerChunk);
      Array.splice.call(arguments, 0);
    }
  }).apply(this, args);
}
/** HELPER FUNCTIONS **/
// Thanks: http://stackoverflow.com/a/14919494/607407
function humanFileSize(bytes, si) {
    var thresh = si ? 1000 : 1024;
    if(Math.abs(bytes) < thresh) {
        return bytes + ' B';
    }
    var units = si
        ? ['kB','MB','GB','TB','PB','EB','ZB','YB']
        : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
    var u = -1;
    do {
        bytes /= thresh;
        ++u;
    } while(Math.abs(bytes) >= thresh && u < units.length - 1);
    return bytes.toFixed(1)+' '+units[u];
}

* {margin:0;padding:0}
#start_measure {
   border: 1px solid black;
   background-color:orange;
}
button#start_measure[disabled] {
   border: 1px solid #333;
   font-style: italic;
   background-color:#AAA;
   width: 100%;
}
.buttontd {
  text-align: center;
}
#status {
  margin-top: 3px;
  border: 1px solid black;
}
#status.error {
  color: yellow;
  font-weight: bold;
  background-color: #FF3214;
}
#status.error div.status_text {
  text-decoration: underline;
  background-color: red;
}
#status_value {
  display: inline-block;
  border-left: 1px dotted black;
  padding-left: 1em;
}
div.status_text {
  display: inline-block;
  background-color: #EEE;
}
#results {
  width: 100%
}
#results th {
  padding: 3px;
  border-top:1px solid black;
}
#results td, #results th {
  border-right: 1px dotted black;
}
#results td::first-child, #results th::first-child {
  border-left: 1px dotted black;
}
#results td.transfer.false {
  background-color: red;
}
#results td.transfer.true {
  background-color: green;
}

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table>
<tr><td>Bytes to send total: </td><td><input class="disableIfWorking" id="bytes" type="text" pattern="\d*" placeholder="1024"/></td><td id="readableBytes"></td></tr>
<tr><td>Divide in chunks: </td><td><input class="disableIfWorking" id="chunks" type="text" pattern="\d*" placeholder="number of chunks"/></td><td></td></tr>
<tr><td>Use transfer: </td><td>    <input class="disableIfWorking" id="transfer" type="checkbox" checked /></td><td></td></tr>
<tr><td colspan="2" class="buttontd"><button id="start_measure" class="disableIfWorking">Start measuring speed</button></td><td></td></tr>
</table>

<div id="status"><div class="status_text">Status </div><span id="status_value">idle</span></div>

<h2>Recent results:</h2>
<table id="results" cellpading="0" cellspacing="0">
<tr><th>transfer</th><th>Speed</th><th>Volume</th><th>Per chunk</th><th>Time (only transfer)</th></tr>

</table>

我将坚持使用鼠标指针示例,因为它很容易模拟.我们将编写一个使用 Web Worker 计算鼠标指针路径距离的程序.

I'll stick to the mouse pointer example, because it's easy to simulate. We'll make a program that calculates mouse pointer path distance using web worker.

我们要做的是真实的、老式的缓冲.我们制作了一个固定大小的数组(只有那些允许转移到工人)并在记住我们填充的最后一点的同时填充它.当我们结束时,我们可以发送数组并创建另一个.

What we're gonna do is real, old school buffering. We make a fixed size array (only those allow transferring to workers) and fill it while remembering last point we filled. When we're at the end, we can send the array and create another.

// Creating a buffer
this.buffer = new Uint16Array(256);
this.bufferOffset = 0;

然后我们可以很容易地保存坐标,只要我们不让bufferOffset溢出buffer:

We can save coordinates easily then, as long as we do not let bufferOffset overflow the buffer:

if(this.bufferOffset>=this.buffer.length)
    this.sendAndResetBuffer();
this.buffer[this.bufferOffset++] = X;
this.buffer[this.bufferOffset++] = Y;

3.传输数据

你已经在 MDN 上看到过这个例子(对吧......?)所以简单回顾一下:

3. Transfering the data

You've already seen the example on MDN (right...?) so just a quick recapitulation:

worker.postMessage(myTypedArray.buffer, [myTypedArray.buffer]);
// The buffer must be empty now!
console.assert(myTypedArray.buffer.byteLength==0)

4.缓冲区伪类

这是我用于缓冲和发送数据的内容.该类是使用所需的最大缓冲区长度创建的.然后它存储数据(在这种情况下是指针位置)并分派给 Worker.

4. The buffer pseudo class

Here's what I came with for the buffering and sending data. The class is created with desired max buffer length. It then stores data (pointer locations in this case) and dispatches to the Worker.

/** MousePointerBuffer saves mouse locations and when it's buffer is full,
    sends them as array to the web worker.
  * worker - valid worker object ready to accept messages
  * buffer_size - size of the buffer, in BYTES, not numbers or points
**/
function MousePointerBuffer(worker, buffer_size) {
    this.worker = worker;
    if(buffer_size%4!=0)
        throw new Error("MousePointerBuffer requires complement of 4 bytes number, because 1 mouse point is 2 shorts which is 4 bytes!");
    this.buffer_size = buffer_size/2;
    // Make buffer lazy
    this.buffer = null;
    this.bufferOffset = 0;
    // This will print the aproximate time taken to send data + all of the overheads
    worker.addEventListener("message", function(e) {
        if(e.data.type=="timer")
            console.log("Approximate time: ", e.data.time-this.lastSentTime);
    }.bind(this));
}
MousePointerBuffer.prototype.makeBuffer = function() {
    if(this.buffer!=null) {
        // Buffer created and not full
        if(this.bufferOffset<this.buffer_size)
            return;
        // Buffer full, send it then re-create
        else
            this.sendBuffer();
    }
    this.buffer = new Uint16Array(this.buffer_size);
    this.bufferOffset = 0;
}
/** Sends current buffer, even if not full. Data is sent as array
    [ArrayBuffer buffer, Number bufferLength] where buffer length means
    occupied bytes. **/
MousePointerBuffer.prototype.sendBuffer = function() {
    this.lastSentTime = performance.now();
    console.log("Sending ",this.buffer.buffer.byteLength," bytes at: ",this.lastSentTime);
    this.worker.postMessage([this.buffer.buffer, this.bufferOffset]
                            , [this.buffer.buffer]  // Comment this line out to see
                                                    // How fast is it without transfer
    );
    // See? Bytes are gone.
    console.log("Bytes in buffer after sending: ",this.buffer.buffer.byteLength);
    this.buffer = null;
    this.bufferOffset = 0;
}
/* Creates event callback for mouse move events. Callback is stored in
   .listener property for later removal **/
MousePointerBuffer.prototype.startRecording = function() {
    // The || expression alows to use cached listener from the past
    this.listener = this.listener||this.recordPointerEvent.bind(this);   
    window.addEventListener("mousemove", this.listener);
}
/* Can be used to stop any time, doesn't send buffer though! **/
MousePointerBuffer.prototype.stopRecording = function() { 
    window.removeEventListener("mousemove", this.listener);
}
MousePointerBuffer.prototype.recordPointerEvent = function(event) {
    // This is probably not very efficient but makes code shorter
    // Of course 90% time that function call just returns immediatelly
    this.makeBuffer();
    // Save numbers - remember that ++ first returns then increments
    this.buffer[this.bufferOffset++] = event.clientX;
    this.buffer[this.bufferOffset++] = event.clientY;
}

4.活生生的例子

function WorkerFN() {
  console.log('WORKER: Worker ready for data.');
  // Variable to store mouse pointer path distance
  var dist = 0;
  // Last coordinates from last iteration - filled by first iteration
  var last_x = null,
      last_y = null;
  // Sums pythagorian distances between points
  function calcPath(array, lastPoint) {
      var i=0;
      // If first iteration, first point is the inital one
      if(last_x==null||last_y==null) {
          last_x = array[0];
          last_y = array[1];
          // So first point is already skipped
          i+=2;
      }
      // We're iterating by 2 so redyce final length by 1
      var l=lastPoint-1
      // Now loop trough points and calculate distances
      for(; i<l; i+=2) {
          console.log(dist,last_x, last_y);
          dist+=Math.sqrt((last_x-array[i]) * (last_x-array[i])+
                          (last_y-array[i+1])*(last_y-array[i+1])
          );
          last_x = array[i];
          last_y = array[i+1];
      }
      // Tell the browser about the distance
      self.postMessage({type:"dist", dist: dist});
  }
  self.onmessage = function(e) {
      if(e.data instanceof Array) {
          self.postMessage({type:'timer', time:performance.now()});
          setTimeout(calcPath, 0, new Uint16Array(e.data[0]), e.data[1]);
      }
      else if(e.data.type=="reset") {
          self.postMessage({type:"dist", dist: dist=0});
      }
  }
}

var worker = new Worker(URL.createObjectURL(new Blob(["("+WorkerFN.toString()+")()"], {type: 'text/javascript'})));

/** MousePointerBuffer saves mouse locations and when it's buffer is full,
    sends them as array to the web worker.
  * worker - valid worker object ready to accept messages
  * buffer_size - size of the buffer, in BYTES, not numbers or points
**/
function MousePointerBuffer(worker, buffer_size) {
    this.worker = worker;
    if(buffer_size%4!=0)
        throw new Error("MousePointerBuffer requires complement of 4 bytes number, because 1 mouse point is 2 shorts which is 4 bytes!");
    this.buffer_size = buffer_size/2;
    // Make buffer lazy
    this.buffer = null;
    this.bufferOffset = 0;
    // This will print the aproximate time taken to send data + all of the overheads
    worker.addEventListener("message", function(e) {
        if(e.data.type=="timer")
            console.log("Approximate time: ", e.data.time-this.lastSentTime);
    }.bind(this));
}
MousePointerBuffer.prototype.makeBuffer = function() {
    if(this.buffer!=null) {
        // Buffer created and not full
        if(this.bufferOffset<this.buffer_size)
            return;
        // Buffer full, send it then re-create
        else
            this.sendBuffer();
    }
    this.buffer = new Uint16Array(this.buffer_size);
    this.bufferOffset = 0;
}
/** Sends current buffer, even if not full. Data is sent as array
    [ArrayBuffer buffer, Number bufferLength] where buffer length means
    occupied bytes. **/
MousePointerBuffer.prototype.sendBuffer = function() {
    this.lastSentTime = performance.now();
    console.log("Sending ",this.buffer.buffer.byteLength," bytes at: ",this.lastSentTime);
    this.worker.postMessage([this.buffer.buffer, this.bufferOffset]
                            , [this.buffer.buffer]  // Comment this line out to see
                                                    // How fast is it without transfer
    );
    // See? Bytes are gone.
    console.log("Bytes in buffer after sending: ",this.buffer.buffer.byteLength);
    this.buffer = null;
    this.bufferOffset = 0;
}
/* Creates event callback for mouse move events. Callback is stored in
   .listener property for later removal **/
MousePointerBuffer.prototype.startRecording = function() {
    // The || expression alows to use cached listener from the past
    this.listener = this.listener||this.recordPointerEvent.bind(this);   
    window.addEventListener("mousemove", this.listener);
}
/* Can be used to stop any time, doesn't send buffer though! **/
MousePointerBuffer.prototype.stopRecording = function() { 
    window.removeEventListener("mousemove", this.listener);
}
MousePointerBuffer.prototype.recordPointerEvent = function(event) {
    // This is probably not very efficient but makes code shorter
    // Of course 90% time that function call just returns immediatelly
    this.makeBuffer();
    // Save numbers - remember that ++ first returns then increments
    this.buffer[this.bufferOffset++] = event.clientX;
    this.buffer[this.bufferOffset++] = event.clientY;
}
var buffer = new MousePointerBuffer(worker, 400);
buffer.startRecording();
// Cache text node reffernce here
var textNode = document.getElementById("px").childNodes[0];

worker.addEventListener("message", function(e) {
    if(e.data.type=="dist") {
        textNode.data=Math.round(e.data.dist);
    }
});
// The reset button
document.getElementById("reset").addEventListener("click", function() {
      worker.postMessage({type:"reset"});
      buffer.buffer = new Uint16Array(buffer.buffer_size);
      buffer.bufferOffset = 0;
});

* {margin:0;padding:0;}
#px {
    font-family: "Courier new", monospace;
    min-width:100px;
    display: inline-block;
    text-align: right;
}
#square {
    width: 200px;
    height: 200px;
    border: 1px dashed red;
    display:table-cell;
    text-align: center;
    vertical-align: middle;
}

Distance traveled: <span id="px">0</span> pixels<br />
<button id="reset">Reset</button>
Try this, if you hve steady hand, you will make it 800px around:
<div id="square">200x200 pixels</div>
This demo is printing into normal browser console, so take a look there.

在线110类被初始化,所以你可以改变缓冲区长度:

On line 110 class is initialized, so you can change buffer length:

var buffer = new MousePointerBuffer(worker, 400);

83 行,您可以注释掉 transfer 命令来模拟正常的复制操作.在我看来,在这种情况下,差异确实微不足道:

On line 83, you can comment out transfer command to simulate normal copy operation. It seems to me that the difference is really insignificant in this case:

, [this.buffer.buffer]  // Comment this line out to see
                        // How fast is it without transfer

这篇关于Web Worker 的消息有多快?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆