1

An event loop has one or more task queues..

According to what is said later in this specification:

Tasks encapsulate algorithms that are responsible for such work as:

Events: ...

Parsing: ...

Callbacks: ...

Using a resource: ...

Reacting to DOM manipulation: ...

Formally, a task is a struct which has:

Steps: ...

A source: ....

Can I think that every event loop has one or more taskqueues, then author has many operations(Events, Callbacks...), these operations will be put into corresponding task queue.

Like following picture.

enter image description here

Per its source field, each task is defined as coming from a specific task source. For each event loop, every task source must be associated with a specific task queue.

For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.

enter image description here

23
  • I'm not sure if my understanding is correct. : ) Commented Mar 16, 2023 at 2:28
  • @MisterJojo Thank you for pointing that out., because the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.. What I am thinking about is whether my task structure is correct. Commented Mar 16, 2023 at 2:32
  • @MisterJojo Maybe my expression is not clear.. Every browser page has an event loop. Commented Mar 16, 2023 at 2:38
  • @MisterJojo I'm curious about whether the task queue of Event loop will have the above structure. One of the task sources, used to group and serialize related tasks. Commented Mar 16, 2023 at 2:41
  • ok; Every browser page has an event loop, but every page can't comunicate with the other, so, whch interest ? Commented Mar 16, 2023 at 2:43

2 Answers 2

2

A task, as defined by the HTML specs is a specs construct. It allows to execute algorithms asynchronously. These algorithm might involve calling some JS script, but that's not a given. Also, all JS isn't executed from a task, for instance all the callbacks in the update the rendering aren't called from a task. So seeing a 1-to-1 relationship between tasks and the JS runtime is often misleading. And this graph you shown seems completely wrong. UI event are sent by the OS to one of the browser's processes, which will communicate to the event-loop's process through IPC and queue the various tasks there. The event-loop will then pick one such task and yet again delegate to various other processes and sometime wait for these to do their job, like it does for the process where the JS runtime is ran.

As per your quote, the HTML specs do not require that there are multiple event-queues. What they do require however, is that there are multiple task-sources. This is to ensure that two tasks, queued on the same task-source will execute in the queued order.

During the event-loop processing, the browser will start by choosing one task-source from which it will execute the oldest available task.
This is critical since this allows to have a prioritization system among all the task-sources. This is what your quote is talking about when they say that there could be one event-queue for UI events, and one for all the rest, because it is generally preferable to treat UI events as being "user-visible", since they triggered the event they expect a fast visible response to it. Whereas, e.g. for network tasks, a lot of other actors were involved, having a small delay is generally more acceptable.

And indeed, most implementations actually have at least one event-queue dedicated for just the UI task-source. But they generally don't have a single task-queue for all the other task-sources. This would mean that a network task queued before a message task would be executed before. Most browsers will have yet another task-queue just for network tasks, and another for timer tasks, and some could even have one per MessagePort instance, since each MessagePort has its own task-source. Firefox even has one event-queue for timers that are scheduled during the page load, and another queue for the ones scheduled afterwards, so that the former can have a lower priority (and match the old Chrome's behavior with their 1ms minimal timeout).

In the near future I even expect this wording about the possibility to have a single event-queue to be removed, because we'll soon have access to the Prioritized Task Scheduling API (it already there in Chrome), which does require multiple task-queues since the priority is enforced there.

So, to recap, yes, you can see the various event-queues as being ordered lists of task to be executed, and yes different kinds of task may be queued in different queues. But unless you're using the Prioritized Task Scheduling API, you shouldn't expect any particular behavior as this is currently all let to implementors to decide which behavior they deem the best to their users.

Sign up to request clarification or add additional context in comments.

Comments

-2

The idea of a queue is just conceptual to allow humans to describe the architecture in a way familiar to ordinary people. In code, there is no actual "queue". Because the tasks don't actually queue, they're listed.

In code what you actually have is a task group or set or collection (implemented as an array of tasks or a linked list of tasks.. basically a database of tasks). What actually do the queuing are the events.

The basic program flow is actually fairly simple to understand. The pseudocode looks something like this:

while(still_waiting_for(events)) {

  execute_script()

  current_event = wait_for(events)
  current_task = search_related_task(tasks, current_event)
  call_function(current_task.callback)

}

In the code above, there's no variable or data structure that represents the event queue. Instead we just have a variable that stores the list of all events we're listening to/waiting for. The actual event "queue" is normally managed by the OS and is invisible in our code.

The tasks variable is all the tasks that are waiting for their respective events. Like I previously mention above, this is normally a set or collection of tasks. We call this the task queue but as you can see the tasks are not queuing (lining up) for the events. Instead when an event happens we search the for the corresponding task in tasks.

Now, the data structure for different types of tasks may be slightly different (for example, timer tasks are usually handled very differently). So because C/C++ is a typed language one valid way to handle this fact is to have multiple task lists (task queues). Another reason might be that you may want certain type of tasks to be evaluated before waiting for any event. The obvious one being callbacks to setImmediate(). In which case you have a separate task queue for setImmediate callbacks to be processed before everything else:

while(still_waiting_for(events)) {

  execute_script()

  for (i=0; i<immediate_tasks.length; i++) {
    immediate = immediate_tasks[i];
    call_function(immediate.callback)
  }

  current_event = wait_for(events)
  current_task = search_related_task(tasks, current_event)
  call_function(current_task.callback)

}

The actual coding to implement this in C/C++ may be a bit confusing to a typical C programmer but should be fairly easy to understand for someone used to thinking asynchronously in javascript. The low level function for handling asynchronous events vary depending on what OS you're using (eg, poll/epoll on Linux, overlapped I/O on Windows, kqueue for BSD/Mac OS) and different javascript interpreters handle them in different ways (eg, libuv in Node.js which itself uses epoll or kqueue or overlapped I/O depending on which OS you compile Node.js on) but the basic way they work are similar.

I'm going to use the cross-platform (POSIX) select() function to demonstrate how the wait_for(events) part actually work in real code:

while(1) {
  retval = select(maxNumberOfIO, readableIOList, writableIOList, NULL, timeout);

  if (retval == -1) {
    perror("Invalid select()");
  }
  else if (retval) {
    //  Find which file descriptor triggered the event
    for (i=0; i<maxNumberOfIO; i++) {
      if (FD_ISSET(i, readableIOList) && readable_task_queue[i]) {
        call_js_callback(readable_task_queue[i]);
      }
      if (FD_ISSET(i, writableIOList) && writable_task_queue[i]) {
        call_js_callback(writable_task_queue[i]);
      }
    }
  }
}

For the select() system call, if you pass in 0 as the value of timeout it will wait forever for an event to happen. This allows you to set a time limit for waiting in case you need to execute other code (eg. waiting for keyboard or mouse event which may not use the select() system call to communicate with your process). This timeout also allows us a simple way to implement timer events like setTimeout and setInterval:

while(1) {
  // Calculate value of timeout:
  nearest_timer = find_nearest_timer(timer_task_queue);
  
  gettimeofday(&current_time, NULL);

  now_millisecs = current_time.tv_sec*1000 + current_time.tv_usec/1000;

  next_timeout_millisecs = nearest_timer.timeout - now_millisecs;
  if (next_timeout_millisecs < 0) {
    next_timeout_millisecs = 1; // remember, zero means wait forever
  }

  timeout.time_t = next_timeout_millisecs/1000;
  timeout.suseconds_t = (next_timeout_millisecs%1000)*1000;

  // Wait for events:
  retval = select(maxNumberOfIO, readableIOList, writableIOList, NULL, timeout);

  // Process event:
  if (retval == -1) {
    perror("Invalid select()");
  }
  else if (retval) {
    //  Find which file descriptor triggered the event
    for (i=0; i<maxNumberOfIO; i++) {
      if (FD_ISSET(i, readableIOList) && readable_task_queue[i]) {
        call_js_callback(readable_task_queue[i]);
      }
      if (FD_ISSET(i, writableIOList) && writable_task_queue[i]) {
        call_js_callback(writable_task_queue[i]);
      }
    }
  }
  else {
    // If we reach here it means we've timed out.
    // So call the timer callback:

    call_js_callback(nearest_timer);
  }
}

As you can see, because timers work differently from normal I/O events we use a separate timer event queue.

To get better familiarity with the logic flow you can try implementing a simple single-threaded server in C/C++ yourself using the select() system call. I've done it several times. The first time as a homework assignment in college and one time when I was tasked with adding asynchronous I/O to the Ferite programming language.

5 Comments

The term "task" in the html spec refers to a handler that is executed (or: becomes ready to execute) when an even occurs. Your answer however uses the meaning where a "task" is something ongoing that emits the events (like completion). This makes it a bit confusing.
Are you certain that JS runtimes do not continue polling the OS while executing the callbacks in parallel? Sure, it can be done on only a single thread in lockstep, without an actual queue, but using one (or multiple) queues seems like a natural extension for a multithreaded implementation.
@Bergi I'm not sure how you're reading the code but it's obvious that the thing that's ongoing is the OS itself and the thing that's waiting is the select() function. I'm using tasks to mean a data structure that stores the callback (eg. task.callback or call_js_function(task)) which is how it's defined in the spec.
As for how js runtimes implement the wait, it depends. In Node.js they use the OS async I/O feature (eg. select()) to wait for network events but they use a thread pool to manage disk event. It's not impossible to use async I/O programming for disk events (as is done in the Tcl programming language) but is kind of messy when it comes to doing it on Windows. Thus it's simpler to do traditional blocking I/O and then use IPC to asynchronously update the main thread.
@Bergi Basically Node.js uses pipes as the asynchronous primitive for managing disk I/O which internally they handle using blocking reads and writes in threads.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.