The moment we've been waiting for has finally arrived! It's the culmination of all our efforts. Now, it's time to bring our code to life. In the v8 engine, this process is called module evaluation. Let's explore how our simple "hello world" program is evaluated, executed, and brought to life.
Overview
Once all the necessary modules have been fetched, cached, loaded, transpiled, compiled, and prepared for use, the next step is to initiate the execution of the application. Similar to how a C, C++, or Java program begins with the main program, a JavaScript code commences its execution from the main module or what is often referred to as the root module.
Immediately after the loading process is completed, the worker's run function triggers the evaluate_module function. This function is responsible for initiating the evaluation or execution of the root module. To achieve this, the evaluate_module function requires the module ID of the root module as its input.
The evaluate_module function undertakes two crucial tasks:
It invokes the mod_evaluate function of the JavaScript runtime, thereby initiating the module evaluation within the V8 engine.
It facilitates the operation of the well-known event loop, ensuring that the system awaits the completion of the module evaluation process and its resultant output. This step is fundamental for coordinating asynchronous operations and managing the flow of the program.
pubasyncfnevaluate_module(&mut self, id:ModuleId, ) ->Result<(), AnyError> { self.wait_for_inspector_session();letmut receiver = self.js_runtime.mod_evaluate(id); tokio::select! {// Not using biased mode leads to non-determinism for relatively simple// programs. biased; maybe_result =&mut receiver => {debug!("received module evaluate {:#?}", maybe_result); maybe_result.expect("Module evaluation result not provided.") } event_loop_result = self.run_event_loop(false) => { event_loop_result?;let maybe_result = receiver.await; maybe_result.expect("Module evaluation result not provided.") } } }
The mod_evaluate function initiates the execution process within the V8 engine. After this, the program enters an event loop phase that continually checks for various outcomes such as evaluation results, resolutions of promises, operational (OP) results, handling of dynamic imports, and identification of errors. Let's dive deeper into module evaluation and explore its intricacies. Next, we'll examine the event loop's role in managing asynchronous tasks and their outcomes, as it plays a vital part in this process.
Module evaluation
The initial module evaluation function is called mod_evaluate and it goes through these steps:
It sends the main module for evaluation.
It looks out for any instant:
Errors
Rejections of Promises
It's important to note that not all errors are brought up as soon as the evaluation begins. Nonetheless, certain errors are indeed raised right away in the process.
The function called mod_evaluate is somewhat larger. The next illustration demonstrates the sequence of actions that this function carries out:
Here is the source of mod_evaluate():
pubfnmod_evaluate(&self, isolate:&mut v8::Isolate, id:ModuleId, ) -> oneshot::Receiver<Result<(), Error>> {let state_rc = self.0.state();let module_map_rc = self.0.module_map();let scope =&mut self.handle_scope(isolate);let tc_scope =&mut v8::TryCatch::new(scope);let module = module_map_rc.borrow().get_handle(id).map(|handle| v8::Local::new(tc_scope, handle)).expect("ModuleInfo not found");letmut status = module.get_status();assert_eq!( status, v8::ModuleStatus::Instantiated,"{} {} ({})",if status == v8::ModuleStatus::Evaluated {"Module already evaluated. Perhaps you've re-provided a module or extension that was already included in the snapshot?" } else {"Module not instantiated" }, module_map_rc.borrow().get_info_by_id(id).unwrap().name.as_str(), id, );let (sender, receiver) = oneshot::channel(); {letmut state = state_rc.borrow_mut();assert!( state.pending_mod_evaluate.is_none(),"There is already pending top level module evaluation" ); state.pending_mod_evaluate =Some(ModEvaluate { promise:None, has_evaluated:false, handled_promise_rejections:vec![], sender, }); }let maybe_value = module.evaluate(tc_scope); {letmut state = state_rc.borrow_mut();let pending_mod_evaluate = state.pending_mod_evaluate.as_mut().unwrap(); pending_mod_evaluate.has_evaluated =true; }// Update status after evaluating. status = module.get_status();let has_dispatched_exception = self.0.runtime_state.borrow_mut().dispatched_exception.is_some();if has_dispatched_exception {// This will be overridden in `exception_to_err_result()`.let exception = v8::undefined(tc_scope).into();let pending_mod_evaluate = {letmut state = state_rc.borrow_mut(); state.pending_mod_evaluate.take().unwrap() }; pending_mod_evaluate.sender.send(exception_to_err_result(tc_scope, exception, false)).expect("Failed to send module evaluation error."); } elseifletSome(value) = maybe_value {assert!( status == v8::ModuleStatus::Evaluated|| status == v8::ModuleStatus::Errored );let promise = v8::Local::<v8::Promise>::try_from(value).expect("Expected to get promise as module evaluation result");let promise_global = v8::Global::new(tc_scope, promise);letmut state = state_rc.borrow_mut(); {let pending_mod_evaluate = state.pending_mod_evaluate.as_ref().unwrap();let pending_rejection_was_already_handled = pending_mod_evaluate.handled_promise_rejections.contains(&promise_global);if!pending_rejection_was_already_handled { state.pending_promise_rejections.retain(|(key, _)| key !=&promise_global); } }let promise_global = v8::Global::new(tc_scope, promise); state.pending_mod_evaluate.as_mut().unwrap().promise =Some(promise_global); tc_scope.perform_microtask_checkpoint(); } elseif tc_scope.has_terminated() || tc_scope.is_execution_terminating() {let pending_mod_evaluate = {letmut state = state_rc.borrow_mut(); state.pending_mod_evaluate.take().unwrap() }; pending_mod_evaluate.sender.send(Err(generic_error("Cannot evaluate module, because JavaScript execution has been terminated.") )).expect("Failed to send module evaluation error."); } else {assert!(status == v8::ModuleStatus::Errored); } receiver }
Let's delve into the intricacies of the process, breaking it down into distinct steps:
Obtaining the Module Handle: To initiate the process, the first step involves acquiring the module handle using the assigned module identifier. This handle serves as a way to interact with the specific module under consideration.
Invoking v8's Module Evaluate Function: The subsequent action entails invoking the module evaluate function provided by v8. This function plays a pivotal role in evaluating and processing the module's contents.
Verifying Immediate Evaluation Errors: After the evaluation function is called, a crucial check is performed to identify any immediate errors that might have arisen during the evaluation process. This preliminary assessment ensures that the module is evaluated correctly and any errors are promptly identified.
Fetching a Global Promise for Module's Scope: As the evaluation progresses, a global promise is acquired to establish the scope of the current module. This promise encapsulates the asynchronous execution and encapsulation of the module's functionality.
Storing the Global Promise: The acquired global promise is then stored within a designated container known as "pending_mod_evaluate." This repository serves as a means to manage and keep track of promises associated with module evaluations.
Providing a Return Path for Async Events: Finally, to ensure a smooth flow of asynchronous events and facilitate the retrieval of asynchronous updates later, the process concludes by returning the receiver. This return path acts as a means to capture and process any events that may occur asynchronously during the module's evaluation.
After submitting the module for evaluation, it's time to delve into the renowned event loop.
event_loop_result = self.run_event_loop(false) => { event_loop_result?;let maybe_result = receiver.await; maybe_result.expect("Module evaluation result not provided.")}
Event loop
Once you've sent the module for assessment, the JavaScript runtime steps into an asynchronous event loop. Think of this event loop as a busy worker that's responsible for managing different tasks. One of its important tasks is to keep an eye on the main promise, which is like a commitment made when you first introduced your code for evaluation.
This event loop is made up of various components, each with its own role, but going into all the details right now might be a bit overwhelming. For our current focus, which is the simple "hello world" program, there are a couple of parts within the event loop that matter:
Checking pending module evaluation: This involves making sure that the module you submitted is being properly evaluated. The event loop ensures that the evaluation process is happening smoothly and without any hiccups. It's like a supervisor making sure the work is being done.
Checking promise rejections: In the world of programming, promises are used to represent actions that might take some time to complete. If something goes wrong during these actions, promises can be rejected. The event loop keeps a watchful eye on these promises to catch any rejections and handle them appropriately. It's like having someone who catches errors and helps fix them.
So, you can think of the event loop as a diligent manager overseeing your code's progress, making sure evaluations are happening correctly and problems are taken care of. It's like the backstage crew of a show, ensuring everything runs smoothly for the main performance.
The aspect of the event loop's functioning that involves other tasks remains mysterious at this point. This is primarily due to the simplicity of our current example. In this scenario, there are no tasks that involve asynchronous calls, intricate dynamic imports, operations (referred to as "ops"), event listeners, and similar complexities. As a result, the hello world program commences and concludes on its own. It doesn't rely on any external dependencies to maintain its execution.
The global promise that is linked with the main module encapsulates the outcome of evaluating the hello world program. It's worth highlighting that the program's outcome isn't equivalent to its actual output. The visible output of the program is directed to a distinct location. Conversely, the assessment's outcome is fed back into the event loop via the global promise.
The following is the code of a single tick of the event loop:
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()) }}
In the Deno runtime, during each cycle of the event loop, numerous tasks are carried out. As we discussed earlier, in the context of the simple "hello world" program, the crucial aspect to note is the execution of the function call known as "evaluate_pending_module."
When a module is being evaluated, there's a possibility of encountering errors. These evaluation errors manifest asynchronously, typically as promise rejections. These errors adhere to the standard JavaScript error types, such as ReferenceError or TypeError, among others.
To manage this, in every iteration of the event loop, the global promise pool is examined until it transitions from a pending state. This transition might involve moving to either a resolved or a rejected state, depending on the outcome of the asynchronous operations associated with the module evaluations.
Here is the code snippet responsible for the process of monitoring promises linked to pending evaluations:
pub(incrate::runtime) fnevaluate_pending_module(&self, isolate:&mut v8::Isolate, ) {let maybe_module_evaluation = self.0.context_state.borrow_mut().pending_mod_evaluate.take();if maybe_module_evaluation.is_none() {return; }letmut module_evaluation = maybe_module_evaluation.unwrap();let state_rc = self.0.state();let scope =&mut self.handle_scope(isolate);let promise_global = module_evaluation.promise.clone().unwrap();let promise = promise_global.open(scope);let promise_state = promise.state();match promise_state { v8::PromiseState::Pending=> {// NOTE: `poll_event_loop` will decide if// runtime would be woken soon state_rc.borrow_mut().pending_mod_evaluate =Some(module_evaluation); } v8::PromiseState::Fulfilled=> { scope.perform_microtask_checkpoint();// Receiver end might have been already dropped, ignore the resultlet _ = module_evaluation.sender.send(Ok(())); module_evaluation.handled_promise_rejections.clear(); } v8::PromiseState::Rejected=> {let exception = promise.result(scope); scope.perform_microtask_checkpoint();// Receiver end might have been already dropped, ignore the resultif module_evaluation.handled_promise_rejections.contains(&promise_global) {let _ = module_evaluation.sender.send(Ok(())); module_evaluation.handled_promise_rejections.clear(); } else {let _ = module_evaluation.sender.send(exception_to_err_result(scope, exception, false)); } } } }
Within Deno, the global promise goes through three distinct states:
Pending
At this stage, the promise is in a waiting state.
It's like a question that hasn't been answered yet.
We'll come back and see if there's an answer in the next cycle.
Fulfilled
When a promise reaches this state, it means the task assigned to it has been successfully completed.
Think of it as getting a response to your question, and that response is positive.
If you were waiting for some information, now you have it, and you can share it with others.
Rejected
Unfortunately, not all promises end up fulfilled. Some end up in the rejected state.
This is like getting an answer, but it's not the answer you wanted. Something went wrong.
Just as sometimes things don't go as planned, the task associated with the promise couldn't be accomplished successfully.
In this case, you get an error message instead of the expected result.
After the global promise has been either fulfilled or rejected, the evaluation of the module finishes. The event loop comes to an end when the following conditions are satisfied:
There are no pending operations left.
There are no pending dynamic imports.
There are no dynamic imports awaiting evaluation.
There are no top-level modules waiting to be evaluated.
This signifies that the entire process of evaluating the module and handling asynchronous operations has been completed. The event loop, which is responsible for managing the execution flow, can gracefully conclude its operations once all these requirements are met.
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 {// Finish eventloop }}
In brief, when all tasks have been completed, the event loop concludes its operations, leading to the program's termination. In essence, if no further tasks remain, both the event loop and the program itself come to an end.
Evaluation of hello world
Let's take a closer look at how the evaluation process functions in our uncomplicated "hello world" illustration. Just to refresh our memory, here's the JavaScript code we're working with:
"use strict";
function printNumber(input) {
console.log(input);
}
function printString(input) {
console.log(input);
}
printNumber(1);
printString('One');
The current example we're looking at involves a situation where no external modules are being imported. In this case, we're dealing with a singular module located at the root or main level. This main module takes center stage for evaluation; no other modules are considered. Additionally, there are no asynchronous function calls or any other activities that would cause it to continue running.
Let's get into the sequence of events when the process of evaluating our main module, which we'll refer to as "mod_evaluate," takes place. This process unfolds as follows:
The V8 engine, which powers Deno, initiates the execution of the main module.
Within the V8 engine, the instruction to run console.log is executed.
V8, in turn, triggers an external reference named "op_print" to display the number 1 on the standard output (stdout).
Moving on, V8 performs another console.log operation.
Once again, V8 employs the external reference "op_print," this time to exhibit the word "One" on the standard output.
As the program lacks any further instructions, it reaches its conclusion.
The evaluation of the module by V8 comes to an end.
V8 designates the global promise of the module as fulfilled, indicating successful completion.
Given that there are no pending tasks or activities, the subsequent tick of the event loop transpires.
The global promise, having met its fulfillment, wraps up.
Consequently, the asynchronous polling concludes, culminating in the completion of the module's execution.
This step-by-step breakdown illustrates how the main module's journey unfolds during the evaluation process within the Deno environment.
Output:
1
One
After the module's execution has been completed, there remain additional tasks that the worker must attend to. Once the execute_main_module function wraps up its operations, it proceeds to the subsequent stages within the implementation of the run command:
If there are any listeners, the window load event is sent out.
The program enters the event loop.
However, it's important to note that the main module has already been executed.
The event loop closes immediately because there are no other tasks to perform or wait for.
The window unload event is sent out.
This marks the completion of the program.
Deno then shuts down.
Here's what remains to be done:
The tasks that are left will be completed right away since the primary module has already been executed. The program will come to an end once these four functions are finished. Even the process of running the event loop will conclude promptly, as there are no more tasks remaining.
--
That's fantastic! We have successfully executed our inaugural program.