Difference between microtask and macrotask within an event loop context - javascript

I've just finished reading the Promises/A+ specification and stumbled upon the terms microtask and macrotask: see http://promisesaplus.com/#notes
I've never heard of these terms before, and now I'm curious what the difference could be?
I've already tried to find some information on the web, but all I've found is this post from the w3.org Archives (which does not explain the difference to me): http://lists.w3.org/Archives/Public/public-nextweb/2013Jul/0018.html
Additionally, I've found an npm module called "macrotask": https://www.npmjs.org/package/macrotask
Again, it is not clarified what the difference exactly is.
All I know is, that it has something to do with the event loop, as described in https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
and https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
I know I should theoretically be able to extract the differences myself, given this WHATWG specification. But I'm sure that others could benefit as well from a short explanation given by an expert.

One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification).
After this macrotask has finished, all available microtasks will be processed, namely within the same go-around cycle. While these microtasks are processed, they can queue even more microtasks, which will all be run one by one, until the microtask queue is exhausted.
What are the practical consequences of this?
If a microtask recursively queues other microtasks, it might take a long time until the next macrotask is processed. This means, you could end up with a blocked UI, or some finished I/O idling in your application.
However, at least concerning Node.js's process.nextTick function (which queues microtasks), there is an inbuilt protection against such blocking by means of process.maxTickDepth. This value is set to a default of 1000, cutting down further processing of microtasks after this limit is reached which allows the next macrotask to be processed)
So when to use what?
Basically, use microtasks when you need to do stuff asynchronously in a synchronous way (i.e. when you would say perform this (micro-)task in the most immediate future).
Otherwise, stick to macrotasks.
Examples
macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
microtasks: process.nextTick, Promises, queueMicrotask, MutationObserver

Basic concepts in spec:
An event loop has one or more task queues.(task queue is macrotask queue)
Each event loop has a microtask queue.
task queue = macrotask queue != microtask queue
a task may be pushed into macrotask queue,or microtask queue
when a task is pushed into a queue(micro/macro),we mean preparing work is finished,so the task can be executed now.
And the event loop process model is as follows:
when call stack is empty,do the steps-
select the oldest task(task A) in task queues
if task A is null(means task queues is empty),jump to step 6
set "currently running task" to "task A"
run "task A"(means run the callback function)
set "currently running task" to null,remove "task A"
perform microtask queue
(a).select the oldest task(task x) in microtask queue
(b).if task x is null(means microtask queues is empty),jump to step (g)
(c).set "currently running task" to "task x"
(d).run "task x"
(e).set "currently running task" to null,remove "task x"
(f).select next oldest task in microtask queue,jump to step(b)
(g).finish microtask queue;
jump to step 1.
a simplified process model is as follows:
run the oldest task in macrotask queue,then remove it.
run all available tasks in microtask queue,then remove them.
next round:run next task in macrotask queue(jump step 2)
something to remember:
when a task (in macrotask queue) is running,new events may be registered.So new tasks may be created.Below are two new created tasks:
promiseA.then()'s callback is a task
promiseA is resolved/rejected: the task will be pushed into microtask queue in current round of event loop.
promiseA is pending: the task will be pushed into microtask queue in the future round of event loop(may be next round)
setTimeout(callback,n)'s callback is a task,and will be pushed into macrotask queue,even n is 0;
task in microtask queue will be run in the current round,while task in macrotask queue has to wait for next round of event loop.
we all know callback of "click","scroll","ajax","setTimeout"... are tasks,however we should also remember js codes as a whole in script tag is a task(a macrotask) too.

I think we can't discuss event loop in separation from the stack, so:
JS has three "stacks":
standard stack for all synchronous calls (one function calls another, etc)
microtask queue (or job queue or microtask stack) for all async operations with higher priority (process.nextTick, Promises, Object.observe, MutationObserver)
macrotask queue (or event queue, task queue, macrotask queue) for all async operations with lower priority (setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering)
|=======|
| macro |
| [...] |
| |
|=======|
| micro |
| [...] |
| |
|=======|
| stack |
| [...] |
| |
|=======|
And event loop works this way:
execute everything from bottom to top from the stack, and ONLY when the stack is empty, check what is going on in queues above
check micro stack and execute everything there (if required) with help of stack, one micro-task after another until the microtask queue is empty or don't require any execution and ONLY then check the macro stack
check macro stack and execute everything there (if required) with help of the stack
Micro stack won't be touched if the stack isn't empty. The macro stack won't be touched if the micro stack isn't empty OR does not require any execution.
To sum up: microtask queue is almost the same as macrotask queue but those tasks (process.nextTick, Promises, Object.observe, MutationObserver) have higher priority than macrotasks.
Micro is like macro but with higher priority.
Here you have "ultimate" code for understanding everything.
console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);
const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
setTimeout(() => {
console.log('stack [4]')
setTimeout(() => console.log("macro [5]"), 0);
p.then(() => console.log('micro [6]'));
}, 0);
console.log("stack [7]");
});
console.log("macro [8]");
/* Result:
stack [1]
macro [8]
stack [7], stack [7], stack [7]
macro [2]
macro [3]
stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]
macro [5], macro [5], macro [5]
--------------------
but in node in versions < 11 (older versions) you will get something different
stack [1]
macro [8]
stack [7], stack [7], stack [7]
macro [2]
macro [3]
stack [4], stack [4], stack [4]
micro [6], micro [6], micro [6]
macro [5], macro [5], macro [5]
more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
*/

Macro tasks include keyboard events, mouse events, timer events (setTimeout) , network events, Html parsing, changing Urletc. A macro task represents some discrete and independent work. micro task queue has higher priority so macro task will wait for all the micro tasks are executed first.
Microtasks, are smaller tasks that update the application state and should be executed before the browser continues with other assignments such as
re-rendering the UI. Microtasks include promise callbacks and DOM mutation changes. Microtasks enable us to execute certain actions before the UI is re-rendered, thereby avoiding unnecessary UI rendering that might show an inconsistent application state.
Separation of macro and microtask enables the
event loop to prioritize types of tasks; for example, giving priority to performance-sensitive tasks.
In a single loop iteration, one macro task at most is processed
(others are left waiting in the queue), whereas all microtasks are processed.
Both task queues are placed outside the event loop, to indicate that the act of adding tasks to their matching queues happens outside the
event loop. Otherwise, any events that occur while JavaScript code is
being executed would be ignored. The acts of detecting and adding
tasks are done separately from the event loop.
Both types of tasks are executed one at a time. When a task starts executing, it’s executed to its completion. Only the browser can stop
the execution of a task; for example, if the task takes up too much
time or memory.
All microtasks should be executed before the next rendering because their goal is to update the application state before rendering occurs.
The browser usually tries to render the page 60 times per second,
It's accepted that 60 frames per second are the rate at which
animations will appear smooth. if we want to achieve smooth-running
applications, a single task, and all microtasks generated by that task
should ideally complete within 16 ms. If a task gets executed for more
than a couple of seconds, the browser shows an “Unresponsive script”
message.
reference John Resig-secrets of JS Ninja

I created an event loop pseudocode following the 4 concepts:
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering are part of the macrotask queue. One macrotask item will be processed first.
process.nextTick, Promises, Object.observe, MutationObserver are part of the microtasks queue. The event loop will process all of the items in that queue including once that were processed during the current iteration.
There is another queue called animation queue which holds animation changes task items which will be processed next. All tasks which exists in this queue will be processed (not including new one which were added during the current iteration). It will be called if it is time for rendering
The rendering pipeline will try to render 60 times a second (every 16 ms)
while (true){
// 1. Get one macrotask (oldest) task item
task = macroTaskQueue.pop();
execute(task);
// 2. Go and execute microtasks while they have items in their queue (including those which were added during this iteration)
while (microtaskQueue.hasTasks()){
const microTask = microtaskQueue.pop();
execute(microTask);
}
// 3. If 16ms have elapsed since last time this condition was true
if (isPaintTime()){
// 4. Go and execute animationTasks while they have items in their queue (not including those which were added during this iteration)
const animationTasks = animationQueue.getTasks();
for (task in animationTasks){
execute(task);
}
repaint(); // render the page changes (via the render pipeline)
}
}

Related

JavaScript async callbacks - Promise and setTimeout [duplicate]

This question already has answers here:
Promise vs setTimeout
(6 answers)
What is the relationship between event loop and Promise [duplicate]
(2 answers)
Closed 1 year ago.
In the following code:
setTimeout(() => console.log("hello"), 0);
Promise.resolve('Success!')
.then(console.log)
What should happen in my understanding:
setTimeout is called => print hello directly added to callback queue as time is 0
Promise.resolve => print Success! added to callback queue
If I am not wrong, the callback queue is FIFO.
But the code output is:
Success!
hello
What is the explanation?
There are 2 separate queues for handling of the callbacks. A macro and a micro queue. setTimeout enqueues an item in the macro queue, while promise resolution - to the micro queue. The currently executing macro task(the main script itself in this case) is executed synchronously, line by line until it is finished. The moment it is finished, the loop executes everything queued in the microtask queue before continuing with the next item from the macro queue(which in your case is the console.log("hello") queued from the setTimeout).
Basically, the flow looks like this:
Script starts executing.
MacrotaskQueue: [], MicrotaskQueue: [].
setTimeout(() => console.log("hello"), 0); is encountered which leads to pushing a new item in the macrotask queue.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].
Promise.resolve('Success!').then(console.log) is read. Promise resolves to Success! immediately and console.log callback gets enqueued to the microtask queue.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [console.log('Success!')].
The script finishes executing so it checks if there is something in the microtask queue before proceeding with the next task from the macro queue.
console.log('Success!') is pulled from the microtask queue and executed.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].
Script checks again if there is something else in the microtask queue. There is none, so it fetches the first available task from the macrotask queue and executes it, namely - console.log("hello").
MacrotaskQueue: [], MicrotaskQueue: [].
After the script finishes executing the console.log("hello"), it once again checks if there is anything in the microtask queue. It is empty, so it checks the macrotask queue. It is empty as well so everything queued is executed and the script finishes.
This is a simplified explanation, though, as it can get trickier. The microtask queue normally handles mainly promise callbacks, but you can enqueue code on it yourself. The newly added items in the microtask queue will still be executed before the next macrotask item. Also, microtasks can enqueue other microtasks, which can lead to an endless loop of processing microtasks.
Some useful reference resources:
The event loop
Microtasks
Using Microtasks
There are two different queues involved here: a Task queue and a Microtask queue.
Callback functions scheduled using setTimeout are added in the task queue whereas the callbacks scheduled using promises are added in the microtask queue or a job queue.
A microtask queue is processed:
after each callback as long as the call-stack is empty.
after each task.
Also note that if a microtask in a microtask queue queues another microtask, that will also be processed before processing anything in the task queue. In other words, microtask queue will be processed until its empty before processing the next task in the task queue.
The following code snippet shows an example:
setTimeout(() => console.log('hello'), 0);
Promise.resolve('first microtask')
.then(res => {
console.log(res);
return 'second microtask';
})
.then(console.log);
In your code, callback function of setTimeout is added to the task queue and the Promise.resolve queues a micro-task in a microtask queue. This queue is processed at the end of the script execution. That is why "success" is logged before "hello".
The following image shows a step-by-step execution of your code:
Resources for further reading:
Tasks, microtasks, queues and schedules
JavaScript job queue and microtasks
Even though the timeout is 0, the callback function will still be added to the web API (after being fetched from the call stack). Web APIs are threads that you can’t access; you can just make calls like Ajax, Timeout, and the DOM.
Promise.resolve schedules a microtask whereas setTimeout schedules a macrotask. Microtasks are executed before running the next macrotask.
So in your example, the
Promise.resolve('Success!').then(console.log);
will be executed before the setTimout since promises have better priority than the setTimeout callback function in the event loop stack.

What does the callstack look like with promises and async/await?

I recently watched this video: https://www.youtube.com/watch?v=8aGhZQkoFbQ and am trying to find a similar explanation for with Promises. Specifically in node.
I'm probably wrong but I'm under the impression that Promises are placed on a microtask queue, the microtask queue is only processed when the callstack is empty and once the event-loop starts processing the microtask queue, it processes it until the queue is empty.
const sleep = (ms) => new Promise((res) => {
setTimeout(res, ms)
});
const doTask1 = (workerId) => sleep(1000).then(() => console.log(`[${workerId}] Completed Task 1`));
const doTask2 = (workerId) => sleep(5000).then(() => console.log(`[${workerId}] Completed Task 2`));
const worker = async (workerId) => {
console.log(`[${workerId}] Starting work`)
await doTask1(workerId);
await doTask2(workerId);
console.log(`[${workerId}] Completed all tasks`);
};
worker('A');
worker('B');
When the snippet above is run, both worker A and B will start Task 1 and 2 at around the same time and complete at around the same time. I'm having trouble consolidating that behavior with what I've read/seen about Promises/microtasks and event-loops.
I imagine that when the code executes, the call stack looks like:
Callstack: [main.js]
Callstack: [main.js][worker('A')]
Callstack: [main.js][worker('A')][console.log()]
Callstack: [main.js][worker('A')][await doTask1()]
When we use await, what happens to the callstack? I suppose this is related to how generators work?
Callstack: ?? | Microtask Queue: [doTask1()]
What does it mean for the event-loop to "process" the microtask queue? Does processing doTask1() mean the event-loop is waiting 1 second? That doesn't sound right. Is the 1 second wait being done on a different thread (is that the right term?) as shown in the Youtube video's depiction of WebAPIs? Once the timeout is complete, the Promise's res goes onto the message queue where the event loop eventually picks it up and resolves the Promise? Does the callstack have to be empty for the callstack to execute res?
I'm stuck at this point and not sure what happens next in the callstack, the message queue, the microtask queue and how the event-loop decides what to do next.

javascript promises, the event loop, and the job queue

Consider the following code:
function foo() {
console.log('foo');
new Promise(
function(resolve, reject) {
setTimeout(function() {
resolve('RESOLVING');
}, 5000);
}
)
.then(
function(value) {
console.log(value);
}
);
}
foo();
I am trying to understand what happens here correctly:
on executing new Promise the "executer function" is run directly and when setTimeout is called, an operation to add a new entry to the "event queue" is scheduled (for 5 seconds later)
because of the call to then an operation to add to a "job queue" a call to the passed function (which logs to the console) is organised to happen after the Promise is resolved
when the setTimeout callback is executed (on some tick of the event loop), the Promise is resolved and based on point 2, the function argument to the then call is added to a "job queue" and subsequently executed.
Notice I say [a "job queue"] because there is something I am not sure about; which "job queue is it?". The way I understand it, a "job queue" is linked to an entry on the "event queue". So would that be the setTimeout entry in above example?
Assuming no other events are added to the "event queue" before (and after) setTimeout's callback is added, wouldn't the entry for the main code (the call to foo) have been (usually) gone (run to completion) by that time and thus there would be no other entry than setTimeout's for the then's "job queue" entry to be linked to?
on executing new Promise the "executer function" is run directly and when setTimeout is called, an operation to add a new entry to the "event queue" is scheduled (for 5 seconds later)
Yes. More specifically, calling setTimeout schedules a timer with the browser's timer mechanism; roughly five seconds later, the timer mechanism adds a job to the main job queue that will call your callback.
because of the call to then an operation to add to a "job queue" a call to the passed function (which logs to the console) is organised to happen after the Promise is resolved
Right. then (with a single argument) adds a fulfillment handler to the promise (and creates another promise that it returns). When the promise resolves, a job to call the handler is added to the job queue (but it's a different job queue).
when the setTimeout callback is executed (on some tick of the event loop), the Promise is resolved and based on point 2, the function argument to the then call is added to a "job queue" and subsequently executed.
Yes, but it's not the same job queue. :-)
The main job queue is where things like event handlers and timer callbacks and such go. The primary event loop picks up a job from the queue, runs it to completion, and then picks up the next job, etc., idling if there are no jobs to run.
Once a job has been run to completion, another loop gets run, which is responsible for running any pending promise jobs that were scheduled during that main job.
In the JavaScript spec, the main job queue is called ScriptJobs, and the promise callback job queue is PromiseJobs. At the end of a ScriptJob, all PromiseJobs that have been queued are executed, before the next ScriptJob. (In the HTML spec, their names are "task" [or "macrotask"] and "microtask".)
And yes, this does mean that if Job A and Job B are both queued, and then Job A gets picked up and schedules a promise callback, that promise callback is run before Job B is run, even though Job B was queued (in the main queue) first.
Notice I say [a "job queue"] because there is something I am not sure about; which "job queue is it?"
Hopefully I covered that above. Basically:
Initial script execution, event handlers, timers, and requestAnimationFrame callbacks are queued to the ScriptJobs queue (the main one); they're "macrotasks" (or simply "tasks").
Promise callbacks are queued to the PromiseJobs queue, which is processed until empty at the end of a task. Thatis, promise callbacks are "microtasks."
The way I understand it, a "job queue" is linked to an entry on the "event queue".
Those are just different names for the same thing. The JavaScript spec uses the terms "job" and "job queue." The HTML spec uses "tasks" and "task queue" and "event loop". The event loop is what picks up jobs from the ScriptJobs queue.
So would that be the setTimeout entry in above example?
When the timer fires, the job is scheduled in the ScriptJobs queue.
Assuming no other events are added to the "event queue" before (and after) setTimeout's callback is added, wouldn't the entry for the main code (the call to foo) have been (usually) gone (run to completion) by that time and thus there would be no other entry than setTimeout's for the then's "job queue" entry to be linked to?
Basically yes. Let's run it down:
The browser loads the script and adds a job to ScriptJobs to run the script's top-level code.
The event loop picks up that job and runs it:
That code defines foo and calls it.
Within foo, you do the console.log and then create a promise.
The promise executor schedules a timer callback: that adds a timer to the browser's timer list. It doesn't queue a job yet.
then adds a fulfillment handler to the promise.
That job ends.
Roughly five seconds later, the browser adds a job to ScriptJobs to call your timer callback.
The event loop picks up that job and runs it:
The callback resolves the promise, which adds a promise fulfillment handler call to the PromiseJobs queue.
That job ends, but with entries in PromiseJobs, so the JavaScript engine loops through those in order:
It picks up the fulfillment handler callback job and runs it; that fulfillment handler does console.log
That job is completely done now.
More to explore:
Jobs and Job Queues in the JavaScript specification
Event Loops in the HTML5 specification

How long do resolved Promises take to enter the task queue?

I'm still trying to figure out the event loop and when exactly things tick/when the task queues are processed.
console.log('start');
Promise.resolve().then(function() {
console.log('promise');
});
console.log('end');
This outputs the following:
> "start"
> "end"
> "promise"
Shouldn't the promise resolve right away and thus the call stack would be empty right after (before "end" is printed) and thus the callback would be processed as a task before "end" is printed?
I'm clearly missing something here about either a) when resolved Promise callbacks are added to the task queue or b) when tasks are processed in the event loop.
Using the terminology you used. Task queues are executed when only platform code is running - that is when all synchronous code has finished executing. Your code is guaranteed to execute in exactly that order.
They are run before the event loop (they're "microtasks") which is why your then executes before any setTimeout for example.
The problem was that I was considering the stack to be clear after the immediately resolved Promise, but in reality the stack is not clear at that point. You could think of the entire block of code being executed as like the main() function of a C program to make a C analogy.
Thus, since the stack is not clear, even though an microtask in now in the callback queue waiting to be processed, it doesn't get processed until after all the synchronous JS code in the main thread is completed.

How to observe multiple task queues in JS event loop [duplicate]

This question already has answers here:
Which types of queues are in event loop?
(3 answers)
Closed 2 years ago.
I'm reading an article that explains the event loop and execution timings. In it, it's mention that there can be multiple task queues that the event loop can choose to from to execute tasks. My main question is when does the browser decide to create a new queue? I've tried to observe this happening, but so far haven't been able to.
Contrasted with this other article on the subject, there's no mention of multiple queues, so I'm either misunderstanding something or one of the two articles is incorrect somewhere.
I believe the two articles in question are just using different terminology.
In article 1:
An event loop has multiple task sources which guarantees execution order within that source...
In article 2
the event loop can have multiple task queues... tasks must be processed in insertion order in every queue.
To answer my own question about observing different queues, I think it's as simple as this:
function foo() {
console.log('Start of queue');
bar();
Promise.resolve().then(function() {
console.log('Promise resolved');
});
console.log('End of queue');
}
function bar() {
setTimeout(function() {
console.log('Start of next queue');
console.log('End of next queue');
}, 0);
}
foo();
//-> Start of queue
//-> End of queue
//-> Promise resolved
//-> Start of next queue
//-> End of next queue
The first task queue (or task source) is foo(). foo() calls bar(), and bar() calls a setTimeout() which sets up a new task queue (or task source). The way we can observe each task queue is to resolve a Promise. The Promise callback is inserted into the micro queue. All micro queue tasks are executed at the end of every task queue (or task source). Because we see Promise resolved between the End of queue and Start of next queue console logs, we can conclude that that we're observing different event queues.
https://www.youtube.com/watch?v=u1kqx6AenYw&feature=youtu.be This will help you understand I think. Skip to the 7min mark.
There can be multiple queues in the task queue, and then also micro task queue.
In HTML terms, the event loop for a page or set of pages from the same domain can have multiple task queues. Tasks from the same task source always go into the same queue, with the browser choosing which task queue to use next.

Categories