Event loop in Deno
Introduction
From the early days of Node.js, there was one thing that got a lot of attention:
The Event Loop
This is the magical, larger-than-life piece of code that glues everything together. It is also called the heart of the Node.js runtime. The event loop makes it possible to run asynchronous code in a single -threaded Node.js runtime (Node.js isn’t really single-threaded). The event loop did a lot of impossible looking work like running code, tracking and making callbacks, OS ops, V8 handling, etc. The event loop is indeed a complicated piece of code, and surely got the attention it deserved.
Deno is no different when it comes to the event loop. Deno runtime is quite similar to the Node.js runtime, therefore Deno also contains a similar event loop. Though Deno’s event loop hasn’t gotten as much attention as Node.js’s event loop got.
An event loop has the following properties:
- It runs forever: The event loop stops running only when the program is done.
- It polls: In each cycle of the event loop, there are a series of poll that it performs.
In this article, we’ll look at the Deno’s event loop. We’ll go over the parts (polls & series of checks) of the event loop.
Deno’s event loop
Deno’s event loop is part of Deno’s core module. This is because event loop is a core functionality of the runtime. Without event loop, it won’t be possible to run async code.
The event loop runs till there is nothing more to do in the application
In each run, it performs a round of polling. In most of the practical cases, the event loop would end only when the application shuts down.
Polling
In every run, the event loop polls a number of things. The poll logic of Deno’s event loop is roughly as follows:
- Poll pending ops
- Poll dynamic imports
- Poll module evaluation
If any of the above are pending, run another round of event loop.
In other words, if there is anything pending to do, the event loop would poll again in the next run.
Ending
The event loop ends when there are:
- no pending ops
- no pending dynamic imports
- no pending module evaluation
If nothing is pending, then the work of event loop is done. In other words, if there is nothing left to do, event loop can stop.
Example
Consider the following simple program:
console.log('Hello world');
That’s the only line in the program. It writes Hello world on the console.
The event loop of the above program would end as soon as the message is printed on the console. The message is printed through an op, and when that’s done, there is nothing else in the program to do. The event loop would end.
The complete event loop
So far, we’ve seen a high-level overview of the event loop. Now, let’s see everything the event loop does in a single run. At a relatively low-level, this is all the event loop does in a single run:
The event loop isn’t that complicated! All it does is poll, and check continuously. Let’s go over all the parts of the event loop in detail.
Parts of event loop
In this section, let’s go over the parts of the event loop in detail. We’ll see them in groups:
- ops
- dynamic imports
- top level evaluation
- checks
In each run of the event loop, it goes through all the four parts. The checks are done in the end. If none of the checks has anything pending, the event loop ends.
OPs
The first part of the event loop is to process pending async OPs. OPs are used to run the async functionality like reading from a file, waiting for a response, etc. Usually low-level functionality like console writing, file ops, timers, TCP, etc. are implemented in ops. The request for ops comes from the JS space, is executed in rust, and the response is sent back to the JS space.
The async OPs are processed in the following order:
Poll pending ops
The first step is to poll the pending ops. This means that, Deno’s event loop checks if any of the pending async ops have been responded to. All the responses are collected and saved in a buffer for further processing.
Process async responses
The second step is to process the collected responses. All the responses are processed one-by-one by calling the corresponding callbacks in V8. These are the ops that have been executed successfully.
Check promise exceptions
The third step is to check for pending promise exceptions. All the pending exceptions are processed by making an error callback to JS.
Dynamic imports
The imports that are performed using import keyword are handled at the startup i.e. in the loading phase. The static imports are not handled in the running phase. However, JavaScript also has an import function that is useful in getting imports in the running phase. These are called dynamic imports.
These imports are handled in a way similar to async OPs. Let’s see the steps:
Prepare dynamic imports
The first step is to prepare the new imports that have been called since the last run of the event loop. The preparation step initiates the loading of dynamic imports. It also adds the imports to the pending list.
Poll dynamic imports
The second step is to keep polling the pending dynamic imports. As soon dynamic imports are fetched, they are registered, loaded, and instantiated into v8.
Evaluate dynamic imports
The third step is to wait for evaluation of the dynamic import. Usually this would finish with a fulfilled promise. If promise is resolved, that means the module has been successfully evaluated, and is ready for use. If promise is rejected, that means the module evaluation has encountered errors.
Check promise exceptions
Before ending this block of event loop, a check is performed on all the pending promises. Those of them that got fulfilled, the result is reflected to the JS space.
Top-level module
A Deno program starts with the main module or the main program. This is also known as the root module. This is the starting point (like the main program in some languages). Simple apps would do something quick in few lines of code, and then finish (the main module is considered evaluated when there is nothing else left to do). But practical production apps are different. In production web apps, the evaluation of the top-level module would generally stay pending till the app shuts down. The top-level module’s evaluation promise would be fulfilled when all the dependents (imports) have been fulfilled.
In simple programs like console.log(1), the evaluation of the top-level module finishes as soon as the async op writing data to console is done. As there is nothing else to do, the event loop stops.
This step of event loop checks if main module evaluation is complete (i.e. promise is fulfilled).
Checks
This is the last step in a single run of the event loop. A bunch of checks are performed to find out if there is anything left to do. If there is nothing, the event loop would end.
The checks are:
If any of the following conditions is true, the event loop would run in the next tick:
- Are there any ops pending response?
- Are there any dynamic imports pending instantiation?
- Are there any dynamic imports pending evaluation?
- Is the top-level module pending evaluation?