The JS runtime serves as the central handler for executing JavaScript code within Deno. Instead of being a standalone JavaScript engine, it acts as a bridge between Deno and the v8 engine. The JS runtime leverages a component called "rusty_v8" to establish communication with the v8 engine. This communication facilitates various operations, ranging from loading and initializing modules to executing code and managing the well-known event loop. A substantial portion of code in the JS runtime is dedicated to interfacing with v8.
In the context of any user-level program, the JS runtime offers a "future" that reaches completion under three primary conditions:
Encountering an error during execution.
Successful evaluation of all modules.
Completion of all pending operations.
Should none of the above scenarios occur, the future remains in a continuous polling state within the event loop. To illustrate, consider our "hello world" example where the future concludes quickly due to the absence of waiting operations.
The JS runtime performs many tasks, making it intricate. Here, we'll provide a high-level overview of its functionality. As we explore script execution, we'll gain a deeper understanding of its inner workings. The JS runtime is a vital hub where various crucial actions occur, making it a critical component in Deno's ecosystem.
Initialization of JS runtime
The process of initializing the JavaScript runtime occurs through several sequential steps. Let's take a closer look at these steps to gain a better understanding.
v8_init
The initial step involves setting up v8, the core engine of Deno.
Begin by generating a fresh default platform within the v8 framework.
In the world of v8, we encounter the concept of "contexts." These contexts pertain to scripts or modules and play an important role. Yet, alongside them, there exists what we call the "global context." This special global context comes into existence as soon as v8 is initialized for operation. It serves a vital purpose in various aspects, including module loading, instantiation, and more.
The process of establishing this global context unfolds through a series of steps, with the initial stage involving the creation of what are known as "isolates." These isolates are integral components originating from v8 itself. If you're curious to delve deeper into the specifics of isolates, you can find valuable insights on the website v8.dev. Essentially, isolates are set up and initialized at this point in our discussion. This marks the foundation of the broader context in which code and modules operate within Deno.
Deno operates by maintaining multiple states to ensure its proper functioning. As we continue to explore running our code, we will discover some of these states. This concludes our discussion on initialization. Now, let's examine how the JsRuntime executes programs. For now, we'll maintain a high-level perspective to understand the overall process.
Execute
In Deno, the JsRuntime offers a handy tool called "execute_script()" for carrying out script execution within the V8 engine. It's crucial to keep in mind that Deno serves as a runtime environment, not merely a JavaScript execution engine. The actual running of scripts takes place within the V8 engine.
The purpose of the "execute_script()" function is to handle the execution of conventional JavaScript code, which refers to code without ES modules. If your code follows the traditional structure, this function is your go-to. However, if you're dealing with code organized into ES modules, fret not! Deno provides distinct functions tailored for such module-based JavaScript.
An interesting point to note is that the "execute_script()" function operates within the global context. This means that any code executed using this function will interact with the broader environment and variables. This global context approach might impact how your code behaves and communicates with other parts of your program.
In essence, Deno's JsRuntime simplifies script execution through the "execute_script()" function, emphasizing compatibility with traditional JavaScript while accommodating module-based code through separate functions. Understanding the distinction between these execution methods can significantly enhance your understanding of the internals of Deno.
The execution of a script in Deno involves several significant steps that work together to make things happen:
Obtaining the Global Context: At the outset, Deno fetches the global context, which provides the foundational environment for your script's execution.
Initializing the Script or File: Next, Deno takes the necessary actions to set up and prepare the script or file for execution, ensuring that everything is in place.
Setting Up v8's TryCatch Error Handler: Deno allocates v8's TryCatch error handler, a tool that helps manage and capture errors during the execution process, enhancing the reliability of the script.
Compiling the Script: The script is then compiled, which involves transforming the human-readable code into a form that the computer can understand and execute.
Running the Script: With all the groundwork laid, Deno finally runs the compiled script, bringing it to life and allowing it to perform its intended tasks.
This sequence of steps showcases the intricate process Deno follows to take your code from its initial form to actual execution. Each step plays a crucial role in ensuring that your script runs smoothly and successfully carries out its desired operations.
ES Modules
The JS runtime plays a vital role in managing ES modules, which have unique requirements compared to traditional JS code.
The JS runtime performs this task through various essential functions. The JS runtime has several functions dedicated to supporting ES modules. Let's take a brief look at some of these key functions:
Function
Use
new_es_module
Called during module loading or dynamic import loading
Creates a module
Compiles module in v8
Returns compiled module id
instantiate_module
Needs module id (this comes from new_es_module)
Instantiates an ES module with module id
mod_evaluate
Evaluate an instantiated ES module
This is equivalent to running a JS code
dyn_mod_evaluate
This is for dynamic imports (similar functionality as dyn_mod_evaluate)
load_main_module
Asynchronously load the main module and all of its dependencies
This is a recursive procedure
load_side_module
Used to load some module that is not part of the main module and it's dependencies
One crucial role of the JS runtime is to facilitate and manage ES modules, which have distinct requirements compared to traditional JS code. The JS runtime accomplishes this through a range of significant functions.
The JS runtime has various functions that support ES modules. Let's explore a few of these key functions.
Event loop
The JavaScript runtime also operates the renowned event loop, which has gained prominence within the universe of JavaScript, particularly through Node.js. This event loop can be considered as a kind of enchanting cycle that remains active until a program has completed its tasks.
In essence, the event loop represents a path to the future, persistently operating as long as there are tasks yet to be accomplished. Its fundamental code structure can be depicted as follows:
pubasyncfnrun_event_loop(&mut self, wait_for_inspector:bool, ) ->Result<(), Error> {poll_fn(|cx| self.poll_event_loop(cx, wait_for_inspector)).await}pubfnpoll_event_loop(&mut self, cx:&mutContext, wait_for_inspector:bool, ) ->Poll<Result<(), Error>> {let has_inspector:bool; {let state = self.inner.state.borrow(); has_inspector = state.inspector.is_some(); state.op_state.borrow().waker.register(cx.waker()); }if has_inspector {// We poll the inspector first.let _ = self.inspector().borrow().poll_sessions(Some(cx)).unwrap(); } self.pump_v8_message_loop()?;// Dynamic module loading - ie. modules loaded using "import()" {// Run in a loop so that dynamic imports that only depend on another// dynamic import can be resolved in this event loop iteration.//// For example, a dynamically imported module like the following can be// immediately resolved after `dependency.ts` is fully evaluated, but it// wouldn't if not for this loop.//// await delay(1000);// await import("./dependency.ts");// console.log("test")//// These dynamic import dependencies can be cross-realm://// await delay(1000);// await new ShadowRealm().importValue("./dependency.js", "default");//loop {letmut has_evaluated =false;let state = self.inner.state.borrow();if state.known_realms.len() ==1 {drop(state);// Try and resolve as many dynamic imports in each realm as possible// before moving to the next.let realm = self.inner.main_realm.as_ref().unwrap();loop {let poll_imports = realm.prepare_dyn_imports(&mut self.inner.v8_isolate, cx)?;assert!(poll_imports.is_ready());let poll_imports = realm.poll_dyn_imports(&mut self.inner.v8_isolate, cx)?;assert!(poll_imports.is_ready());if realm.evaluate_dyn_imports(&mut self.inner.v8_isolate) { has_evaluated =true; } else {break; } } } else {// TODO(bartlomieju|mmastrac): Remove cloning in the runtime looplet realms = state.known_realms.clone();drop(state);for inner_realm in realms {let realm =JsRealm::new(inner_realm);// Try and resolve as many dynamic imports in each realm as possible// before moving to the next.loop {let poll_imports = realm.prepare_dyn_imports(&mut self.inner.v8_isolate, cx)?;assert!(poll_imports.is_ready());let poll_imports = realm.poll_dyn_imports(&mut self.inner.v8_isolate, cx)?;assert!(poll_imports.is_ready());if realm.evaluate_dyn_imports(&mut self.inner.v8_isolate) { has_evaluated =true; } else {break; } } } }if!has_evaluated {break; } } }// Resolve async ops, run all next tick callbacks and macrotasks callbacks// and only then check for any promise exceptions (`unhandledrejection`// handlers are run in macrotasks callbacks so we need to let them run// first).let dispatched_ops = self.do_js_event_loop_tick(cx)?; self.check_promise_rejections()?;// Event loop middlewaresletmut maybe_scheduling =false; {let op_state = self.inner.state.borrow().op_state.clone();for f in&self.event_loop_middlewares {iff(op_state.clone(), cx) { maybe_scheduling =true; } } }// Top level module {let state = self.inner.state.borrow();if state.known_realms.len() ==1 {drop(state);let realm = self.inner.main_realm.as_ref().unwrap(); realm.evaluate_pending_module(&mut self.inner.v8_isolate); } else {// TODO(bartlomieju|mmastrac): Remove cloning in the runtime looplet realms = state.known_realms.clone();drop(state);for inner_realm in realms {let realm =JsRealm::new(inner_realm); realm.evaluate_pending_module(&mut self.inner.v8_isolate); } } }let pending_state = self.event_loop_pending_state();if!pending_state.is_pending() &&!maybe_scheduling {if has_inspector {let inspector = self.inspector();let has_active_sessions = inspector.borrow().has_active_sessions();let has_blocking_sessions = inspector.borrow().has_blocking_sessions();if wait_for_inspector && has_active_sessions {// If there are no blocking sessions (eg. REPL) we can now notify// debugger that the program has finished running and we're ready// to exit the process once debugger disconnects.if!has_blocking_sessions {let context = self.main_context();let scope =&mut self.handle_scope(); inspector.borrow_mut().context_destroyed(scope, context);println!("Program finished. Waiting for inspector to disconnect to exit the process..."); }returnPoll::Pending; } }returnPoll::Ready(Ok(())); }let state = self.inner.state.borrow();// Check if more async ops have been dispatched// during this turn of event loop.// If there are any pending background tasks, we also wake the runtime to// make sure we don't miss them.// TODO(andreubotella) The event loop will spin as long as there are pending// background tasks. We should look into having V8 notify us when a// background task is done.if pending_state.has_pending_background_tasks|| pending_state.has_tick_scheduled|| maybe_scheduling { state.op_state.borrow().waker.wake(); }// If ops were dispatched we may have progress on pending modules that we should re-checkif (pending_state.has_pending_module_evaluation|| pending_state.has_pending_dyn_module_evaluation)&& dispatched_ops { state.op_state.borrow().waker.wake(); }drop(state);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 known_realms =&self.inner.state.borrow().known_realms;returnPoll::Ready(Err(find_and_report_stalled_level_await_in_any_realm(&mut self.inner.v8_isolate, known_realms, ), )); } }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 } elseif self.inner.state.borrow().dyn_module_evaluate_idle_counter >=1 {let known_realms =&self.inner.state.borrow().known_realms;returnPoll::Ready(Err(find_and_report_stalled_level_await_in_any_realm(&mut self.inner.v8_isolate, known_realms, ), )); } else {letmut state = self.inner.state.borrow_mut();// Delay the above error by one spin of the event loop. A dynamic import// evaluation may complete during this, in which case the counter will// reset. state.dyn_module_evaluate_idle_counter +=1; state.op_state.borrow().waker.wake(); } }Poll::Pending }fnevent_loop_pending_state(&mut self) ->EventLoopPendingState {letmut scope = v8::HandleScope::new(self.inner.v8_isolate.as_mut());EventLoopPendingState::new(&mut scope, &mut self.inner.state.borrow_mut()) }}
Understanding the event loop can be challenging without exploring the code. We'll gain a better understanding of the event loop as we go through the main module evaluation process in the "Hello World" program. For now, let's get a high-level overview of the event loop's role.
The event loop mechanism can be best understood by examining the poll_event_loop function, which runs asynchronously until certain conditions are met:
An error is encountered, or
All tasks are completed, implying that there's nothing left to be done. This involves checking for various scenarios:
No pending operations are waiting to be executed.
No dynamic imports are in the queue, waiting for their turn.
All pending dynamic imports have been successfully evaluated.
All modules in the evaluation queue have been processed.
No active inspector sessions are ongoing.
By adhering to these conditions, the event loop ensures the orderly execution of tasks within the Deno runtime environment. As we proceed, we will dive into the intricacies of these concepts, shedding light on the orchestration of asynchronous operations and module evaluations, ultimately resulting in a well-functioning Deno application.
If there's nothing to be done, the event loop comes to a halt. In our illustration, the event loop would conclude fairly quickly. However, in real-world scenarios, this loop would hardly ever reach its end. Take, for instance, a web server—it perpetually remains waiting to receive new connections. Applications in production would generally only terminate when an error surfaces.
Let's look into some higher-level tasks that the poll event loop undertakes:
Polling Pending Operations: It scans for operations that are pending, waiting for them to become available.
Verifying Responses from Asynchronous Operations: The loop checks the responses from asynchronous operations, making sure they are ready.
Draining Macrotasks: It processes larger-scale tasks, often known as "macrotasks," from the queue.
Handling Promise Exceptions: The loop takes a look at promise-based operations to catch and handle any exceptions that might have occurred.
Prepping Dynamic Module Imports: It readies the environment for importing dynamic modules, which are modules that can be loaded on demand.
Polling Dynamic Imports: The loop keeps an eye out for dynamically imported modules, ensuring they're fetched when necessary.
Evaluating Dynamic Module Imports: It assesses and evaluates the dynamically imported modules.
Checking Promise Exceptions Again: The loop revisits promise-based operations to double-check for exceptions.
Assessing Pending Top-Level Modules: It evaluates and processes any pending top-level modules that are waiting to be executed.
Verifying for Exceptions: The loop examines the code for any potential exceptions before proceeding.
As we progress through our program in this and the following chapters, we will explore various aspects of the event loop. Understanding the event loop's intricacies can be difficult without a practical context or a concrete example.
Next steps
That concludes our discussion on the JS runtime. So far, we've explored the initialization process, which primarily focused on preparation. Now, we'll move on to the exciting phase of executing user-level code in our simple "hello world" program. Our focus shifts to the next segment, which covers the execution of the main module. In the following passages, we'll delve into the details of loading and running our code, uncovering the steps that bring our program to life.