Web Worker的消息有多快?

Que*_*Roy 13 javascript web-worker

我想知道传入或传出Web工作者是否会成为瓶颈.我们应该在触发任何类型的事件时发布消息,还是应该注意并尽可能地限制两者之间的通信?

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

Tom*_*ica 15

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: 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

1. How fast?

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.

定义definitelly可以告诉我们的是缓冲是一个好主意.第一个文本框设置要发送的数据总量.第二个设置用于划分数据的样本数.您很快就会发现样本的开销值得注意.复选框允许您选择是否传输数据.正如预期的那样,这开始与更大量的数据相关.

请原谅凌乱的代码,我不能强迫自己在编写令人兴奋的测试片段时表现得很好.我创造了这个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];
}
Run Code Online (Sandbox Code Playgroud)
* {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;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

2.缓冲

我会坚持使用鼠标指针示例,因为它很容易模拟.我们将使用web worker创建一个计算鼠标指针路径距离的程序.

我们要做的是真正的,旧学校的缓冲.我们制作一个固定大小的数组(只允许转移给工人)并填写它,同时记住我们填写的最后一点.当我们结束时,我们可以发送数组并创建另一个数组.

// Creating a buffer
this.buffer = new Uint16Array(256);
this.bufferOffset = 0;
Run Code Online (Sandbox Code Playgroud)

我们可以轻松保存坐标,只要我们不让bufferOffset溢出buffer:

if(this.bufferOffset>=this.buffer.length)
    this.sendAndResetBuffer();
this.buffer[this.bufferOffset++] = X;
this.buffer[this.bufferOffset++] = Y;
Run Code Online (Sandbox Code Playgroud)

3.传输数据

您已经在MDN上看过这个例子(正确......?),所以只需快速概括一下:

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

4.缓冲区伪类

这是我带来的缓冲和发送数据.创建具有所需最大缓冲区长度的类.然后它存储数据(在这种情况下指针位置)并分派给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;
}
Run Code Online (Sandbox Code Playgroud)

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;
});
Run Code Online (Sandbox Code Playgroud)
* {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;
}
Run Code Online (Sandbox Code Playgroud)
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.
Run Code Online (Sandbox Code Playgroud)

4.1演示中的相关行

在行110上初始化类,因此您可以更改缓冲区长度:

var buffer = new MousePointerBuffer(worker, 400);
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)