Javascript Workers - how can I improve the handling of worker messages and address any delays?

I have a situation where a Worker is sharing a SharedArrayBuffer with the main thread. In order for everything to function properly, it's crucial that the worker has access to the SAB before the main thread tries to access it. (EDIT: The code creating the worker must be in a separate function which returns an array pointing to the SAB.) (Perhaps this is not even possible, you might say).

The initial code snippet appears as follows:

function init() {
  var code = `onmessage = function(event) {
      console.log('starting');
      var buffer=event.data;
      var arr = new Uint32Array(buffer);// I need to have this done before accessing the buffer again from the main
      //some other code, manipulating the array
  }`
  var buffer = new SharedArrayBuffer(BUFFER_ELEMENT_SIZE);
  var blob = new Blob([code], { "type": 'application/javascript' });
  var url = window.URL || window.webkitURL;
  var blobUrl = url.createObjectURL(blob);
  var counter = new Worker(blobUrl);
  counter.postMessage(buffer);
  let res = new Uint32Array(buffer);
  return res;
}

function test (){
  let array = init();
  console.log('main');
  //accessing the SAB again
};

The worker code always executes after test(), displaying main first, then starting.

Using setTimeout does not seem to resolve the issue. Consider the following modification to test:

function test(){
  let array = [];
  console.log('main'); 
  setTimeout(function(){
    array = initSAB();
  },0);
  setTimeout(function(){
    console.log('main');
   //accessing the SAB again
  },0);
  console.log('end');
};

Now, the console logs end initially, followed by main, and finally starting.

Interestingly, assigning the buffer to a global array outside of the test() function seems to solve the problem without requiring timeouts.

Here are my questions:

  • Why doesn't the worker start immediately after the message is sent or received? Given that workers have their own event queue, shouldn't they operate independently of the main stack?
  • Is there a detailed specification on when a worker begins working after receiving a message?
  • Is there a way to ensure the worker has initiated before accessing the SAB again without relying on global variables? Using busy waiting could be an option, but it comes with risks. Is there any alternative method available?

Edit

To clarify further:

  • In a completely parallel scenario, ideally the Worker should process the message promptly after it is posted. However, this does not seem to be the case.
  • Most Browser APIs, including Workers, utilize a callback queue to handle API calls. If this were true, the message would have been handled prior to the timeout callbacks being executed.
  • Another observation: If I attempt busy waiting postMessage by continuously checking the SAB until it changes value, the program will become stuck indefinitely. This implies that the browser does not post the message until the call stack is clear. This behavior, to my knowledge, is undocumented and puzzling.

In summary: I am interested in understanding how the browser determines when to post and handle messages by the worker, particularly if the call to postMessage is within a function. Although I found a workaround using global variables, I am curious about the underlying mechanics. If someone can demonstrate a functioning example, I would greatly appreciate it.

EDIT 2:

The code utilizing a global variable (the solution that works effectively) resembles the following:

function init() {
//Unchanged
}

var array = init(); //global

function test (){
  console.log('main');
  //accessing the SAB again
};

This sequence prints starting, followed by main on the console.

Furthermore, it's worth noting that when debugging the code with Firefox (not tested on Chrome), the desired outcome is achieved even without the global variable (starting preceding main). Can anyone provide an explanation for this discrepancy?

Answer №1

What is the reason why the worker does not immediately start after the message has been received? I believe that workers have their own event queue, so they should not depend on the main stack emptying?

Initially, even though your Worker object is accessible synchronously in the main thread, there are several tasks to be completed in the actual worker thread before it can handle your message:

  • The worker must perform a network request to fetch the script content, which is an asynchronous operation even with a blobURI.
  • It needs to initialize the entire JavaScript context, adding to parallel execution time even if the network request is quick.
  • It must await the next event loop frame following the execution of the main script to process your message, resulting in some waiting time regardless of how fast the initialization was.

Under normal circumstances, it is unlikely that your Worker will execute your code precisely when you need the data.

You also mentioned blocking the main thread.

If I attempt to wait indefinitely after postMessage by reading from the SAB until it changes one value, the program will be blocked infinitely.

During the initialization of your Worker, messages are temporarily stored on the main thread in what is referred to as the "outside port." Only after fetching the script is complete is this outside port entwined with the inside port, allowing messages to transition to the parallel thread. Therefore, if you block the main thread before the ports have been entangled, the message cannot pass to the worker's thread.

Is there a specification indicating when a worker begins working after sending a message?

Indeed, the port message queue is activated at step 26 and the Event loop commences at step 29 according to the specification outlined here.

Is there a method to ensure the worker has started before accessing the SAB again without utilizing global variables?

A simple solution is to have your Worker send a message to the main thread once it has commenced its operations.

// Some precautions because all browsers haven't re-enabled SharedArrayBuffers
const has_shared_array_buffer = window.SharedArrayBuffer;

function init() {
  // Since our worker will perform only a single operation, we can promisify it.
  return new Promise((resolve, reject) => {
    const code = `
    onmessage = function(event) {
      console.log('hi');
      var buffer= event.data;
      var arr = new Uint32Array(buffer);
      arr.fill(255);
      if(self.SharedArrayBuffer) {
        postMessage("done");
      }
      else {
        postMessage(buffer, [buffer]);
      }
    }`
    let buffer = has_shared_array_buffer ? new SharedArrayBuffer(16) : new ArrayBuffer(16);
    const blob = new Blob([code], { "type": 'application/javascript' });
    const blobUrl = URL.createObjectURL(blob);
    const counter = new Worker(blobUrl);
    counter.onmessage = e => {
      if(!has_shared_array_buffer) {
        buffer = e.data;
      }
      const res = new Uint32Array(buffer);
      resolve(res);
    };
    counter.onerror = reject;
    if(has_shared_array_buffer) {
      counter.postMessage(buffer);
    }
    else {
      counter.postMessage(buffer, [buffer]);
    }
  });
};

async function test (){
  let array = await init();
  // Accessing the SAB again
  console.log(array);
};
test().catch(console.error);

Answer №2

As outlined by MDN:

The data exchanged between the main page and workers is duplicated, not shared. Objects are serialized as they're passed to the worker and then de-serialized on the receiving end. Since the page and worker operate independently, a copy is generated on each side. This functionality is commonly known as structured cloning in most browsers.

To learn more about transferring data to and from workers

Here is an example code snippet that involves sharing a buffer with a worker. It initializes an array with even values (i*2) and transmits it to the worker. The process utilizes Atomic operations to manipulate the buffer values.

In order to confirm the worker's initiation, you can utilize distinct messages.

var code = document.querySelector('[type="javascript/worker"]').textContent;

var blob = new Blob([code], { "type": 'application/javascript' });
var blobUrl = URL.createObjectURL(blob);
var counter = new Worker(blobUrl);

var sab;

var initBuffer = function (msg) {
  sab = new SharedArrayBuffer(16);
  counter.postMessage({
    init: true, 
    msg: msg, 
    buffer: sab
  });
};

var editArray = function () {
  var res = new Int32Array(sab);
  for (let i = 0; i < 4; i++) {
    Atomics.store(res, i, i*2);
  }
  console.log('Array edited', res);
};

initBuffer('Init buffer and start worker');

counter.onmessage = function(event) {
  console.log(event.data.msg);
  if (event.data.edit) {
    editArray();
    // share new buffer with worker
    counter.postMessage({buffer: sab});
    // end worker
    counter.postMessage({end: true});
  }
};
<script type="javascript/worker">
  var sab;
  self['onmessage'] = function(event) {
    if (event.data.init) {
      postMessage({msg: event.data.msg, edit: true});
    }
    if (event.data.buffer) {
      sab = event.data.buffer;
      var sharedArray = new Int32Array(sab);
      postMessage({msg: 'Shared Array: '+sharedArray});
    }
    if (event.data.end) {
      postMessage({msg: 'Time to rest'});
    }
  };
</script>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Exclude previous dates from the front end of ACF date picker

I am using Advanced Custom Fields to post job ads, and I have incorporated a date picker field for the end date of each job ad. <?php if (have_rows('jobs', 'option')): ?> <?php $now = time(); ?> <div i ...

Enhance the functionality of NodeJS core applications

I recently attempted to modify some custom functions in the FS module of NodeJS, which is an integral part of NodeJS' core modules. The specific file I targeted was fs.js, located in /usr/lib/nodejs. However, despite making changes to the code, I noti ...

The properly grouped image failed to load

I have a React component designed to showcase an image, with Webpack handling the bundling process. It's important to mention that I am utilizing ReactJS.NET in this scenario. Even though the webpack bundle is successfully generated and the .jpg fil ...

Is it possible for two overlapping Javascript divs to both be draggable at the same time?

I have multiple stacked div elements. The top one needs to be draggable, while the one beneath should remain clickable. An illustration is provided below for better understanding: The green div elements are contained within cells. Clicking on a cell trigg ...

Utilizing a single component for various purposes with diverse tags in react

I am faced with the challenge of creating two almost identical components, Component A: const ClaimedLabel = ():React.Element => ( <div tw=""> <p tw="">Some Text here</p> <div tw=""> <Icon ...

Remove items from an array using JavaScript

My array, results = [duplicate, otherdup], holds a list of duplicates. Within my regular array, original_array = [duplicate, duplicate, duplicate, otherdup, otherdup, unique, unique2, unique_etc], how can I iterate through the results array and remove dup ...

Vue - Seamlessly Editing and Creating Content Together

When using Laravel, I am attempting to pass data to a Vue component. Depending on whether the user enters the component through an edit URL or a create URL, we send either an array of data or null. Here is an example of how this process works: Blade view ...

Dynamic importing fails to locate file without .js extension

I created a small TS app recently. Inside the project, there is a file named en.js with the following content: export default { name: "test" } However, when I attempt to import it, the import does not work as expected: await import("./e ...

Creating images or PDFs from HTML with CSS filters: a guide

Looking for someone who has experience creating images or PDFs from HTML code. The HTML contains images with CSS filters such as sepia and grayscale. If you have worked on this type of project before, I would love to hear about your experience. <img cl ...

What is the best way to change a byte array into an image using JavaScript?

I need assistance converting a byte array to an image using Javascript for frontend display. I have saved an image into a MySQL database as a blob, and it was first converted to a byte array before storage. When retrieving all table values using a JSON ar ...

AppProps in Next.js - Ensure that you have the correct loader set up to handle this specific file type as there are currently no loaders configured for processing it

I've encountered an issue while working on a Next.JS 13.5.6 application in development mode. When I try to connect to the site, I receive an error message. However, everything works fine when I switch to production mode after building and starting the ...

Picture goes missing from slideshow

I am currently using a CSS + Javascript slideshow on my website, inspired by an example from the W3Schools website. Here is the code I have adapted for my web application: $(document).ready(function() { var slideIndex = 1; function showSlides(n) { ...

Expanding the content of a single page by clicking on a separate tab

I have a link on Page A. Page B has two tabs, with the content of tabs 1 and tab 2 currently set to "display:none". I want clicking the hyperlink on page A to automatically open or activate the second tab on page B. I am seeking a solution using JavaScri ...

Using React.js to create a search filter for users

When using useEffect with fetch(api) to set [search], I encounter an issue where "loading..." appears each time I enter something in the input box. To continue typing, I have to click on the box after every word or number. I am seeking advice on how to pr ...

Navigating through pages using Nuxt UI-pagination

Having some trouble with setting up the pagination feature from Nuxt UI. Despite trying to research through documentation and videos, I can't seem to figure out how to connect my data to the component. <template> <div> ...

Navigating through props outside a class component in React

I'm struggling to grasp how I can access props that are defined outside of a React class component. In the code snippet below, all props are clearly outlined except for this.props.checkboxArray, which is currently throwing an error "cannot read prope ...

The three.js pointLight has been updated from version 67 to version 68

There appears to be a change in the interaction between a pointlight and a plane from version r.67 to r.68. I am currently studying three.js by following along with a book that is a year old. I have simplified the tutorial example to include just a plane, ...

Mobile Drag and Drop with JavaScript

While experimenting with a user interface I created, I utilized jQuery UI's draggable, droppable, and sortable features. However, I observed that the drag and drop functionality does not work in mobile browsers. It seems like events are triggered diff ...

Sending dynamic data through AJAX to a CodeIgniter controller is a common task that allows for seamless

Can anyone help me with retrieving data from a looping form in CodeIgniter? The form works fine, but I'm struggling to fetch the looping data in the controller. Here's my view (form): <form action="#" id="ap_data"> <div class="table-r ...

AngularJS: Issue with inputMask functionality within an angularJS table

Within my ng-repeat table, I have two date fields. Here is the code snippet: <tr ng-repeat="ol in orderLines"> <td> <input class="audioStartTime" type="text" ng-model="ol.AudioStartTime"/> </td> <td> ...