Deno internals: Event loop source

Mayank C
Tech Tonic

--

Introduction

JavaScript is a single-threaded programming language. This means that JavaScript can do only one thing at a single point in time. In that case, how does it run concurrent requests?

JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. This model is quite different from models in other languages like C and Java. In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external “event provider” (that generally blocks the request until an event has arrived), then calls the relevant event handler (“dispatches the event”). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.

Node.js’s single threaded design embraced the concept of event loop and made it famous in the world. Node.js’s name is synonymous to the famous event loop. The event loop is often viewed as a magical entity that helps in achieving high performance without the need of dedicated threads. The event loop does 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 has the attention it deserves.

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.

Deno, too, has the same event loop that enables high performance through a single thread. The famous mysterious event loop spark curiosity among the developers who are interested in learning the internals of the runtime. In this article, we’ll take a loop at the source code of Deno’s event loop.

A theoretical introduction to Deno’s event loop can be read here.

Source of Deno’s event loop

As Deno is written in Rust, so is its event loop. The event loop is the heart of the runtime, therefore it carries out a number of critical tasks. Here is a brief list:

  • Poll pending ops
  • Process async op responses
  • Prepare dynamic imports
  • Poll pending dynamic imports
  • Evaluate dynamic imports
  • Evaluate pending modules
  • Check for inspector sessions
  • Check promise exceptions
  • etc.

If any task is pending, the event loop runs again in the next tick. If nothing is pending, the event loop ends, and so is the program.

As mentioned above, Deno is written in Rust, and so is its event loop. Here is the code of Deno’s event loop (as per Deno v1.24):

This is the real event loop code!

run_event_loop / poll_event_loop
--------------------------------
let _ = self.inspector().poll_unpin(cx);let state_rc = Self::state(self.v8_isolate());
let module_map_rc = Self::module_map(self.v8_isolate());
{
let state = state_rc.borrow();
state.waker.register(cx.waker());
}
self.pump_v8_message_loop()?;// Ops
{
self.resolve_async_ops(cx)?;
self.drain_nexttick()?;
self.drain_macrotasks()?;
self.check_promise_exceptions()?;
}
// Dynamic module loading - ie. modules loaded using "import()"
{
loop {
let poll_imports = self.prepare_dyn_imports(cx)?;
assert!(poll_imports.is_ready());
let poll_imports = self.poll_dyn_imports(cx)?;
assert!(poll_imports.is_ready());
if !self.evaluate_dyn_imports() {
break;
}
}
self.check_promise_exceptions()?;
}
// Event loop middlewares
let mut maybe_scheduling = false;
{
let op_state = state_rc.borrow().op_state.clone();
for f in &self.event_loop_middlewares {
if f(op_state.clone(), cx) {
maybe_scheduling = true;
}
}
}
// Top level module
self.evaluate_pending_module();
let pending_state = Self::event_loop_pending_state(self.v8_isolate());
let inspector_has_active_sessions = self
.inspector
.as_ref()
.map(|i| i.has_active_sessions())
.unwrap_or(false);
if !pending_state.is_pending() && !maybe_scheduling {
if wait_for_inspector && inspector_has_active_sessions {
return Poll::Pending;
}
return Poll::Ready(Ok(()));
}
let mut state = state_rc.borrow_mut();
let module_map = module_map_rc.borrow();
if state.have_unpolled_ops
|| pending_state.has_pending_background_tasks
|| pending_state.has_tick_scheduled
|| maybe_scheduling
{
state.waker.wake();
}
if pending_state.has_pending_module_evaluation {
if pending_state.has_pending_refed_ops
|| pending_state.has_pending_dyn_imports
|| pending_state.has_pending_dyn_module_evaluation
|| pending_state.has_pending_background_tasks
|| pending_state.has_tick_scheduled
|| maybe_scheduling
{
// pass, will be polled again
} else {
let msg = "Module evaluation is still pending but there are no pending ops or dynamic imports. This situation is often caused by unresolved promises.";
return Poll::Ready(Err(generic_error(msg)));
}
}
if pending_state.has_pending_dyn_module_evaluation {
if pending_state.has_pending_refed_ops
|| pending_state.has_pending_dyn_imports
|| pending_state.has_pending_background_tasks
|| pending_state.has_tick_scheduled
{
// pass, will be polled again
} else if state.dyn_module_evaluate_idle_counter >= 1 {
let mut msg = "Dynamically imported module evaluation is still pending but there are no pending ops. This situation is often caused by unresolved promises.
Pending dynamic modules:\n".to_string();
for pending_evaluate in &state.pending_dyn_mod_evaluate {
let module_info = module_map
.get_info_by_id(&pending_evaluate.module_id)
.unwrap();
msg.push_str("- ");
msg.push_str(module_info.name.as_str());
}
return Poll::Ready(Err(generic_error(msg)));
} else {
state.dyn_module_evaluate_idle_counter += 1;
state.waker.wake();
}
}
Poll::Pending

The code can be seen here too:

The event loop code itself isn’t as huge as we’d expected. This is because, the event loop code depends on a number of helper functions. Going through all the helper functions can’t be done in this article. All the helpers functions are present in the same file:

--

--