5.7 Main Worker

Overview

MainWorker is the main/central piece of Deno. This is also like an orchestrator. The main worker is where the program prepares itself and eventually runs. The job of the main worker ranges from the initialization of the JS runtime to module loading to execution, etc.
One of the major service providers for MainWorker is the JS runtime, which itself is huge enough to be discussed separately. While discussing MainWorker, we'll keep JS runtime as a black box. We'll go over the JS runtime in the next section.
In the run command code, we can see that the main worker is created using the following code:
let mut worker =
create_main_worker(&program_state, main_module.clone(), permissions);
create_main_worker creates a CLIModuleLoader (very useful in loading modules) before creating a worker. The parameters to the main worker creator are:
  • Program state
  • Main module
  • permissions
  • CLI module loader
JS runtime is created and initialized in the main worker, so doesn't get passed to the main worker allocator. In a way, JS runtime is a part of the main worker.

Functionality

The main worker isn't complicated by itself. The main worker is more like an orchestrator to execute code. Here is the main functionality provided by the main worker:
  • Bootstrap
    • Load and instantiate the core functionality of Deno.
  • Initialize ops
    • Initialize and register external ops with JsRuntime
  • Create JsRuntime
    • Create a v8 isolate and a runtime to execute JS code
  • Load module
    • Loads and instantiates a module
  • Execute module
    • Execute or evaluate a module
  • Create inspector session
    • For debugging purposes

Steps to create a worker

The creation of a worker happens in a number of steps. The process starts with creating a CLI module loader and ends with bootstrapping the runtime. Let's see all the steps in detail.
pub fn create_main_worker(
program_state: &Arc<ProgramState>,
main_module: ModuleSpecifier,
permissions: Permissions,
) -> MainWorker {
let module_loader = CliModuleLoader::new(program_state.clone());
/// CODE SUPPRESSED
let create_web_worker_cb = create_web_worker_callback(program_state.clone());
let options = WorkerOptions {
apply_source_maps: true,
args: program_state.flags.argv.clone(),
debug_flag: program_state
.flags
.log_level
.map_or(false, |l| l == log::Level::Debug),
unstable: program_state.flags.unstable,
ca_filepath: program_state.flags.ca_file.clone(),
user_agent: http_util::get_user_agent(),
seed: program_state.flags.seed,
js_error_create_fn: Some(js_error_create_fn),
create_web_worker_cb,
attach_inspector,
maybe_inspector_server,
should_break_on_first_statement,
module_loader,
runtime_version: version::deno(),
ts_version: version::TYPESCRIPT.to_string(),
no_color: !colors::use_color(),
get_error_class_fn: Some(&crate::errors::get_error_class_name),
};
let mut worker = MainWorker::from_options(main_module, permissions, &options);
// This block registers additional ops and state that
// are only available in the CLI
{
let js_runtime = &mut worker.js_runtime;
js_runtime
.op_state()
.borrow_mut()
.put::<Arc<ProgramState>>(program_state.clone());
// Applies source maps - works in conjuction with `js_error_create_fn`
// above
ops::errors::init(js_runtime);
ops::runtime_compiler::init(js_runtime);
}
worker.bootstrap(&options);
worker
}
In addition to the above function, from_options() is used to create the actual worker. The code for from_options() is quite big, so will be shown abbreviated:
pub fn from_options(
main_module: ModuleSpecifier,
permissions: Permissions,
options: &WorkerOptions,
) -> Self {
let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(options.module_loader.clone()),
startup_snapshot: Some(js::deno_isolate_init()),
js_error_create_fn: options.js_error_create_fn.clone(),
get_error_class_fn: options.get_error_class_fn,
..Default::default()
});
// -- CODE OMITTED --
let mut worker = Self {
inspector,
js_runtime,
should_break_on_first_statement,
};
let js_runtime = &mut worker.js_runtime;
{
// All ops registered in this function depend on these
{
let op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
op_state.put::<Metrics>(Default::default());
op_state.put::<Permissions>(permissions);
op_state.put::<ops::UnstableChecker>(ops::UnstableChecker {
unstable: options.unstable,
});
}
ops::runtime::init(js_runtime, main_module);
ops::fetch::init(
js_runtime,
options.user_agent.clone(),
options.ca_filepath.as_deref(),
);
ops::timers::init(js_runtime);
ops::worker_host::init(
js_runtime,
None,
options.create_web_worker_cb.clone(),
);
ops::crypto::init(js_runtime, options.seed);
ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close);
ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources);
ops::reg_json_sync(
js_runtime,
"op_domain_to_ascii",
deno_web::op_domain_to_ascii,
);
ops::fs_events::init(js_runtime);
ops::fs::init(js_runtime);
ops::io::init(js_runtime);
ops::net::init(js_runtime);
ops::os::init(js_runtime);
ops::permissions::init(js_runtime);
ops::plugin::init(js_runtime);
ops::process::init(js_runtime);
ops::signal::init(js_runtime);
ops::tls::init(js_runtime);
ops::tty::init(js_runtime);
ops::websocket::init(
js_runtime,
options.ca_filepath.as_deref(),
options.user_agent.clone(),
);
}
{
let op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
let t = &mut op_state.resource_table;
let (stdin, stdout, stderr) = ops::io::get_stdio();
if let Some(stream) = stdin {
t.add(stream);
}
if let Some(stream) = stdout {
t.add(stream);
}
if let Some(stream) = stderr {
t.add(stream);
}
}
worker
}

CLIModuleLoader

let module_loader = CliModuleLoader::new(program_state.clone());
CLIModuleLoader is a wrapper for module loading. Given a ModuleSpecifier, this module loader can load and compile modules. There are three main functions provided by this module loader:
  • resolve
    • Returns a module specifier for an ES module
  • prepare load
    • Prepares and loads a module
  • load
    • loads a compiled module
We'll revisit this in detail when we will discuss module loading.

JS Runtime

MainWorker::from_options creates the main worker, and the process of creation of the main worker starts with js runtime. Js runtime needs the following things to initialize:
  • Module loader (or CLI module loader)
  • V8 Isolate

Initialize isolate

Isolate is one of the core concepts of Google's V8 engine. Function deno_isoloate_init() initializes static v8 isolate. Static isolates are used for faster loading. For more information on v8 isolates, visit v8.dev.
pub fn deno_isolate_init() -> Snapshot {
let data = CLI_SNAPSHOT;
Snapshot::Static(data)
}
These are static snapshots. These come bundled with Deno. Static snapshots are very useful in faster initialization of the v8 engine. CLI_SNAPSHOT is built from a .bin file:
pub static CLI_SNAPSHOT: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin"));

Initialize runtime

JS runtime is a big component that enables running javascript programs through V8. JS runtime has a lot of v8 interfacing code. A separate JS runtime is allocated for every worker as JS runtime isn't shared between workers. More about JS runtime will be covered later.
let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(options.module_loader.clone()),
startup_snapshot: Some(js::deno_isolate_init()),
js_error_create_fn: options.js_error_create_fn.clone(),
get_error_class_fn: options.get_error_class_fn,
..Default::default()
});

Ops initialization

Ops are the low-level operations that are implemented in Rust to support high-level Deno functionality. Let's take an example to understand this better.
Consider the functionality of reading a file from the disk. Here is how it goes in the chain:
  • There is a high-level function called Deno.copyFileSync() to copy one file to another
  • To support the high-level function, there is a low-level op called op_copy_file_sync() which will implement this operation in rust
  • That's the lowest level it goes in Deno.
Check this code to understand the distinction:
function copyFileSync(
fromPath,
toPath,
) {
core.jsonOpSync("op_copy_file_sync", {
from: pathFromURL(fromPath),
to: pathFromURL(toPath),
});
}
Ops requires crossing boundary from v8 to Deno code. That's because copyFileSync() would run in v8, however, the corresponding op would run in Deno. To make this happen, the initialization of ops is required.
The main work in the initialization of ops is to register ops with js runtime as external references. At the runtime, v8 will make a call to these external references. These are the functions that aren't implemented by v8 as these aren't the core javascript functions. V8 strictly implements what is present in the ECMAScript specification.
Here are the categories of ops that gets initialized in the scope of worker:
  • runtime
  • fetch
  • timers
  • worker_host
  • crypto
  • errors
  • fs
  • fs events
  • io
  • net
  • os
  • permissions
  • plugin
  • process
  • signal
  • tls
  • tty
  • websocket
It is not possible to go through in detail each op category. We'll just pick an easy one for a quick discussion: process.
Process op category supports three core ops:
  • op_run
  • op_run_status
  • op_kill
All the low-level ops need to be registered with js runtime as either async or sync ops. Here is the relevant code:
pub fn init(rt: &mut deno_core::JsRuntime) {
super::reg_json_sync(rt, "op_run", op_run);
super::reg_json_async(rt, "op_run_status", op_run_status);
super::reg_json_sync(rt, "op_kill", op_kill);
}
As can be seen above, op_run and op_kill are sync ops, while op_run_status is an async op.
Let's see one more simple example: signal. This also supports three ops:
pub fn init(rt: &mut deno_core::JsRuntime) {
super::reg_json_sync(rt, "op_signal_bind", op_signal_bind);
super::reg_json_sync(rt, "op_signal_unbind", op_signal_unbind);
super::reg_json_async(rt, "op_signal_poll", op_signal_poll);
}
pub fn reg_json_async<F, R>(rt: &mut JsRuntime, name: &'static str, op_fn: F)
where
F: Fn(Rc<RefCell<OpState>>, Value, BufVec) -> R + 'static,
R: Future<Output = Result<Value, AnyError>> + 'static,
{
rt.register_op(name, metrics_op(json_op_async(op_fn)));
}
pub fn reg_json_sync<F>(rt: &mut JsRuntime, name: &'static str, op_fn: F)
where
F: Fn(&mut OpState, Value, &mut [ZeroCopyBuf]) -> Result<Value, AnyError>
+ 'static,
{
rt.register_op(name, metrics_op(json_op_sync(op_fn)));
}
That's all in ops initialization. We'll see how ops get called in detail later.

Add streams

The second last step in worker initialization is to add standard streams to the resource table:
{
let op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
let t = &mut op_state.resource_table;
let (stdin, stdout, stderr) = ops::io::get_stdio();
if let Some(stream) = stdin {
t.add(stream);
}
if let Some(stream) = stdout {
t.add(stream);
}
if let Some(stream) = stderr {
t.add(stream);
}
}

Bootstrap

At this point, the worker is ready with the JS runtime. However, it's still not ready to process the user program. Bootstrapping makes Deno ready to start executing the user program. Here is the call to the bootstrap function which is present at the very end of the create_main_worker function.
The final step in worker creation is the execution of a small script:
worker.bootstrap(&options);
pub fn bootstrap(&mut self, options: &WorkerOptions) {
let runtime_options = json!({
"args": options.args,
"applySourceMaps": options.apply_source_maps,
"debugFlag": options.debug_flag,
"denoVersion": options.runtime_version,
"noColor": options.no_color,
"pid": std::process::id(),
"ppid": ops::runtime::ppid(),
"target": env!("TARGET"),
"tsVersion": options.ts_version,
"unstableFlag": options.unstable,
"v8Version": deno_core::v8_version(),
});
let script = format!(
"bootstrap.mainRuntime({})",
serde_json::to_string_pretty(&runtime_options).unwrap()
);
self
.execute(&script)
.expect("Failed to execute bootstrap script");
}
The worker's bootstrap function creates some options and then executes a small script bootstrap.mainRuntime({}) in just initialized JS runtime.
Execute function of the worker is used to execute a javascript code. We'll go over script execution in detail shortly. For now, we'll move forward and look at the JS bootstrap function. bootstrap.mainRuntime() is a javascript code that wants to make a call to function mainRuntime in the bootstrap namespace.
Object.defineProperties(globalThis, {
bootstrap: {
value: {
mainRuntime: bootstrapMainRuntime,
workerRuntime: bootstrapWorkerRuntime,
},
configurable: true,
},
});
Here is the implementation of bootstrapMainRuntime():
function bootstrapMainRuntime() {
if (hasBootstrapped) {
throw new Error("Worker runtime already bootstrapped");
}
// Remove bootstrapping data from the global scope
delete globalThis.__bootstrap;
delete globalThis.bootstrap;
util.log("bootstrapMainRuntime");
hasBootstrapped = true;
Object.defineProperties(globalThis, windowOrWorkerGlobalScope);
Object.defineProperties(globalThis, mainRuntimeGlobalProperties);
Object.setPrototypeOf(globalThis, Window.prototype);
eventTarget.setEventTargetData(globalThis);
// Registers the handler for window.onload function.
globalThis.addEventListener("load", (e) => {
const { onload } = globalThis;
if (typeof onload === "function") {
onload(e);
}
});
// Registers the handler for window.onunload function.
globalThis.addEventListener("unload", (e) => {
const { onunload } = globalThis;
if (typeof onunload === "function") {
onunload(e);
}
});
const { args, noColor, pid, ppid, unstableFlag } = runtimeStart();
registerErrors();
const finalDenoNs = {
core,
internal: internalSymbol,
[internalSymbol]: internalObject,
resources: core.resources,
close: core.close,
...denoNs,
};
Object.defineProperties(finalDenoNs, {
pid: util.readOnly(pid),
ppid: util.readOnly(ppid),
noColor: util.readOnly(noColor),
args: util.readOnly(Object.freeze(args)),
mainModule: util.getterOnly(opMainModule),
});
if (unstableFlag) {
Object.assign(finalDenoNs, denoNsUnstable);
}
// Setup `Deno` global - we're actually overriding already
// existing global `Deno` with `Deno` namespace from "./deno.ts".
util.immutableDefine(globalThis, "Deno", finalDenoNs);
Object.freeze(globalThis.Deno);
Object.freeze(globalThis.Deno.core);
Object.freeze(globalThis.Deno.core.sharedQueue);
signals.setSignals();
util.log("args", args);
}
Basically, bootStrapMainRuntime does the following:
  • Registers handlers for load and unload events
  • Prepare deno namespace
  • Freeze core objects like Deno, Deno.core, Deno.core.sharedQueue
Once bootstrapping is done, the main worker is ready to process the main module or the user program.
--
That was all about the worker. Before moving to execute_module, we'll go over the functionality of the JS runtime in detail.