How the Chrome Profiler helps you understand Javascript event loop

Event loop clearly explained

--

Javascript runtime

Profiling is commonly associated with performance optimization, but its applications extend far beyond that. For example, I had previously written on how profiling data can help estimate latency impact of infrastructure downsizing.

Increasingly, one of the things I’m starting to appreciate more about profiling is that it can help you understand how a language works.

As the title suggests, let’s look at Javascript. I have this block of code in a simple React app.

function App() {
setTimeout(planVacation, 0);
setTimeout(requestPTO, 0);

checkPTOBalance();
makePlans();

return (
...
);
}

Here, we put planVacation and requestPTO to be within setTimeout , to be executed after a 0ms delay. Note: There’s no guarantee the timer will run exactly on schedule, so the actual delay will be ≥ 0ms.

How do you think the Flame Chart for this code will look like?

First, a little background on profiling. The Chrome DevTools profiler is a wall time profiler. A wall time profiler samples the call stack at a set interval, e.g. every 1ms or 10ms.

If you have some code that looks like this:

const apple = () => {
banana();
beer();
}

const banana = () => {
carrot();
}

const beer = () => {
carrot();
}

The Flame Chart would look like

Flame Chart has time on the x-axis

Now, let’s go back to our slightly more complicated example.

function App() {
setTimeout(planVacation, 0);
setTimeout(requestPTO, 0);

checkPTOBalance();
makePlans();

return (
...
);
}

Here, the main App calls checkPTOBalance and makePlans, and it puts planVacation and requestPTO in setTimeouts to be executed after a ≥ 0ms delays.

If you’re not familiar with Javascript, you might think the Flame Chart would look like:

If you are familiar with Javascript, maybe you think it’d look like:

Javascript is asynchronous and it’s common knowledge that setTimeout registers its callback to be executed later, after the specified delay.

However, actually, the Flame Chart would look more like this:

The timeout callbacks appear separately from App, even though they’re registered within App.

Here are actual screenshots from Chrome DevTools:

Followed by:

Note: setTimeout may not always appear as a frame. In the screenshot below, it does appear. This is because setTimeout’s execution time is short, so the probability of hitting it while sampling is low.

What do these observations say about Javascript?

If you’ve worked with Javascript, you’re probably familiar with the phrase:

“Javascript uses an event-loop to handle asynchronous executions.”

Profiling Javascript code reveals precisely how this works and what this means.

Asynchronous

As mentioned already, asynchronous means the execution of some functions can be delayed. setTimeout is a way of delaying the execution of its callback. Hence, planVacation and requestPTO, the callbacks to setTimeout, show up after checkPTOBalance and makePlans in the Flame Chart.

Event loop

If the callbacks to setTimeouts can be delayed, it means they’re not immediately put onto the call stack. Thus, they must go somewhere else. This somewhere else is called the task queue.

setTimeout gets added to call stack and schedules planVacation to be added to the task queue. setTimeout then returns and leaves the call stack.

Here’s our example again:

function App() {
setTimeout(planVacation, 0);
setTimeout(requestPTO, 0);

checkPTOBalance();
makePlans();

return (
...
);
}

Notice how planVacation and requestPTO (callbacks to setTimeout) don’t appear below App in the Flame Chart even though they’re registered there and the timer is set with a 0ms delay.

This is due to how the event loop works.

An event loop is, as its name suggests, a loop. Very simplified, it’s like:

while (true) { // loop
const nextTask = taskQueue.pop()
nextTask.run()
}

The event loop dequeues a task from the task queue and runs it. When the task is running, the control of the program is given to the call stack. When the task itself returns, the control of the program is given back to the loop. The loop will then dequeue the next task and invoke it, and so on and so forth.*

*Note: this does not take into account the nuance of microtasks. I will come back to microtasks later in the article.

In our example, we first push App into the call stack. App calls setTimeout and setTimeout gets added to call stack. It schedules planVacation to be added to the task queue after the specified delay and then returns and leaves the call stack. Repeat for the second timeout. App then calls checkPTOBalance, and checkPTOBalance will get pushed onto the call stack. After checkPTOBalance returns, makePlans will get pushed onto the call stack.

When App eventually returns and is removed from the call stack, the control of the program is given to the event loop, and the event loop will dequeue planVacation and run it.

Javascript event loop

Thus, planVacation and requestPTO will never be part of the same call stack as App because the event loop will only dequeue these tasks when the existing call stack is empty.

Event loop with Promise

Let’s make our example slightly more complex and add in a Promise.

function App() {
setTimeout(requestPTO, 0);

prepareBudget().then(planVacation);

checkPTOBalance();
makePlans();

return (
...
);
}

const prepareBudget = () => {
return new Promise((resolve) => {
...
});
}

Will planVacation be executed before or after requestPTO? Maybe you’d think after, since the timeout has 0ms delay, and planVacation has to wait for prepareBudget to resolve.

In actuality though, we see:

Followed by:

Promise callback executed before setTimeout callback

The Promise callback planVacation gets executed before the timeout callback requestPTO. That’s unexpected?

If Promises are put into the same queue as the timeout callbacks, and if queues operate on a first-in-first-out principle, then we’d see the Promise execute after the timeout callbacks. This means Promises have to be put into a separate queue than the timeout callbacks.

In Javascript, Promises are put into the microtask queue. You might have noticed from the screenshot above the “Run Microtasks” frame.

Promises are put into microtask queue

Both the call stack and the microtask queue are part of the Javascript V8 engine. The event loop and the task queue are not. Tasks in the microtask queue will get executed before the control of the program is passed back to the event loop.

Javascript runtime

Once planVacation is done and the microtask queue is empty, the event loop gains control of the program back and will push requestPTO onto the call stack.

Profiling has many use cases other than performance optimization. Cost saving is one of them. As illustrated in this article, the Chrome Profiler reveals many insights into the internal workings of Javascript. Thus, understanding how a language works under the hood is another use case of profiling.

--

--