How fast are Web Worker's messages?

后端 未结 2 370
鱼传尺愫
鱼传尺愫 2020-12-16 00:36

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

相关标签:
2条回答
  • 2020-12-16 01:07

    They are as fast as the cpu core that's running it. Having that said, communication between processes always incurs some overhead so batching it will probably net you some additional performance. Personally I would probably use a timer to send the mouse location or location history every 25ms.

    The question you should ask yourself is: how often do you need the updates? Is 1 update per second enough? 100? 1000? At what point are you just burning cpu cycles for no added value.

    0 讨论(0)
  • 2020-12-16 01:08

    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.

    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.

    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>

    2. Buffering

    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;
    

    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. 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. 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. Live example

    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.

    4.1 Relevant lines in demo

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

    var buffer = new MousePointerBuffer(worker, 400);
    

    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
    
    0 讨论(0)
提交回复
热议问题