498 views
Jan 6
LILily Chen
Understanding the JavaScript Event Loop with the help of the Chrome Profiler

Understanding the JavaScript Event Loop with the help of the Chrome Profiler

Event loop clearly explained

Image Description

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

Image Description

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:

Image Description

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

Image Description

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:

Image Description

The timeout callbacks appear separately from 

App
, even though they’re registered within 
App
.

Here are actual screenshots from Chrome DevTools:

Image Description

Followed by:

Image Description

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.

Image Description

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.

Image Description

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.

Image Description

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:

Image Description

Followed by:

Image Description

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.

Image Description

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.

Image Description

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 profilin

Originally published at medium.com.

More Articles