1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-18 03:44:05 -05:00

feat: ES module snapshotting (#17460)

This commit adds support for snapshotting ES modules. This is done by
adding an ability to serialize and deserialize a "ModuleMap" and attach
it
to the snapshot, using "add_context_data" API.

This has been tested with 400 modules and seems to not have a limit on
the number of modules that might be snapshotted.

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Leo Kettmeir 2023-01-19 10:00:35 +01:00 committed by GitHub
parent 18e8ce4ca5
commit 8dbf7d7866
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 379 additions and 23 deletions

View file

@ -14,6 +14,8 @@ use futures::stream::Stream;
use futures::stream::StreamFuture;
use futures::stream::TryStreamExt;
use log::debug;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
@ -157,7 +159,7 @@ fn json_module_evaluation_steps<'a>(
/// how to interpret the module; it is only used to validate
/// the module against an import assertion (if one is present
/// in the import statement).
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum ModuleType {
JavaScript,
Json,
@ -720,7 +722,7 @@ impl Stream for RecursiveModuleLoad {
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub(crate) enum AssertedModuleType {
JavaScriptOrWasm,
Json,
@ -748,12 +750,13 @@ impl std::fmt::Display for AssertedModuleType {
/// Usually executable (`JavaScriptOrWasm`) is used, except when an
/// import assertions explicitly constrains an import to JSON, in
/// which case this will have a `AssertedModuleType::Json`.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub(crate) struct ModuleRequest {
pub specifier: ModuleSpecifier,
pub asserted_module_type: AssertedModuleType,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct ModuleInfo {
#[allow(unused)]
pub id: ModuleId,
@ -765,7 +768,8 @@ pub(crate) struct ModuleInfo {
}
/// A symbolic module entity.
enum SymbolicModule {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub(crate) enum SymbolicModule {
/// This module is an alias to another module.
/// This is useful such that multiple names could point to
/// the same underlying module (particularly due to redirects).
@ -783,12 +787,12 @@ pub(crate) enum ModuleError {
/// A collection of JS modules.
pub(crate) struct ModuleMap {
// Handling of specifiers and v8 objects
ids_by_handle: HashMap<v8::Global<v8::Module>, ModuleId>,
pub(crate) ids_by_handle: HashMap<v8::Global<v8::Module>, ModuleId>,
pub handles_by_id: HashMap<ModuleId, v8::Global<v8::Module>>,
pub info: HashMap<ModuleId, ModuleInfo>,
by_name: HashMap<(String, AssertedModuleType), SymbolicModule>,
next_module_id: ModuleId,
next_load_id: ModuleLoadId,
pub(crate) by_name: HashMap<(String, AssertedModuleType), SymbolicModule>,
pub(crate) next_module_id: ModuleId,
pub(crate) next_load_id: ModuleLoadId,
// Handling of futures for loading module sources
pub loader: Rc<dyn ModuleLoader>,
@ -806,6 +810,131 @@ pub(crate) struct ModuleMap {
}
impl ModuleMap {
pub fn serialize_for_snapshotting(
&self,
scope: &mut v8::HandleScope,
) -> (v8::Global<v8::Object>, Vec<v8::Global<v8::Module>>) {
let obj = v8::Object::new(scope);
let next_module_id_str = v8::String::new(scope, "next_module_id").unwrap();
let next_module_id = v8::Integer::new(scope, self.next_module_id);
obj.set(scope, next_module_id_str.into(), next_module_id.into());
let next_load_id_str = v8::String::new(scope, "next_load_id").unwrap();
let next_load_id = v8::Integer::new(scope, self.next_load_id);
obj.set(scope, next_load_id_str.into(), next_load_id.into());
let info_obj = v8::Object::new(scope);
for (key, value) in self.info.clone().into_iter() {
let key_val = v8::Integer::new(scope, key);
let module_info = serde_v8::to_v8(scope, value).unwrap();
info_obj.set(scope, key_val.into(), module_info);
}
let info_str = v8::String::new(scope, "info").unwrap();
obj.set(scope, info_str.into(), info_obj.into());
let by_name_triples: Vec<(String, AssertedModuleType, SymbolicModule)> =
self
.by_name
.clone()
.into_iter()
.map(|el| (el.0 .0, el.0 .1, el.1))
.collect();
let by_name_array = serde_v8::to_v8(scope, by_name_triples).unwrap();
let by_name_str = v8::String::new(scope, "by_name").unwrap();
obj.set(scope, by_name_str.into(), by_name_array);
let obj_global = v8::Global::new(scope, obj);
let mut handles_and_ids: Vec<(ModuleId, v8::Global<v8::Module>)> =
self.handles_by_id.clone().into_iter().collect();
handles_and_ids.sort_by_key(|(id, _)| *id);
let handles: Vec<v8::Global<v8::Module>> = handles_and_ids
.into_iter()
.map(|(_, handle)| handle)
.collect();
(obj_global, handles)
}
pub fn update_with_snapshot_data(
&mut self,
scope: &mut v8::HandleScope,
data: v8::Global<v8::Object>,
module_handles: Vec<v8::Global<v8::Module>>,
) {
let local_data: v8::Local<v8::Object> = v8::Local::new(scope, data);
{
let next_module_id_str =
v8::String::new(scope, "next_module_id").unwrap();
let next_module_id =
local_data.get(scope, next_module_id_str.into()).unwrap();
assert!(next_module_id.is_int32());
let integer = next_module_id.to_integer(scope).unwrap();
let val = integer.int32_value(scope).unwrap();
self.next_module_id = val;
}
{
let next_load_id_str = v8::String::new(scope, "next_load_id").unwrap();
let next_load_id =
local_data.get(scope, next_load_id_str.into()).unwrap();
assert!(next_load_id.is_int32());
let integer = next_load_id.to_integer(scope).unwrap();
let val = integer.int32_value(scope).unwrap();
self.next_load_id = val;
}
{
let mut info = HashMap::new();
let info_str = v8::String::new(scope, "info").unwrap();
let info_data: v8::Local<v8::Object> = local_data
.get(scope, info_str.into())
.unwrap()
.try_into()
.unwrap();
let keys = info_data
.get_own_property_names(scope, v8::GetPropertyNamesArgs::default())
.unwrap();
let keys_len = keys.length();
for i in 0..keys_len {
let key = keys.get_index(scope, i).unwrap();
let key_val = key.to_integer(scope).unwrap();
let key_int = key_val.int32_value(scope).unwrap();
let value = info_data.get(scope, key).unwrap();
let module_info = serde_v8::from_v8(scope, value).unwrap();
info.insert(key_int, module_info);
}
self.info = info;
}
{
let by_name_str = v8::String::new(scope, "by_name").unwrap();
let by_name_data = local_data.get(scope, by_name_str.into()).unwrap();
let by_name_deser: Vec<(String, AssertedModuleType, SymbolicModule)> =
serde_v8::from_v8(scope, by_name_data).unwrap();
self.by_name = by_name_deser
.into_iter()
.map(|(name, module_type, symbolic_module)| {
((name, module_type), symbolic_module)
})
.collect();
}
self.ids_by_handle = module_handles
.iter()
.enumerate()
.map(|(index, handle)| (handle.clone(), (index + 1) as i32))
.collect();
self.handles_by_id = module_handles
.iter()
.enumerate()
.map(|(index, handle)| ((index + 1) as i32, handle.clone()))
.collect();
}
pub(crate) fn new(
loader: Rc<dyn ModuleLoader>,
op_state: Rc<RefCell<OpState>>,

View file

@ -351,7 +351,8 @@ impl JsRuntime {
DENO_INIT.call_once(move || v8_init(v8_platform, options.will_snapshot));
// Add builtins extension
if options.startup_snapshot.is_none() {
let has_startup_snapshot = options.startup_snapshot.is_some();
if !has_startup_snapshot {
options
.extensions_with_js
.insert(0, crate::ops_builtin::init_builtins());
@ -423,6 +424,62 @@ impl JsRuntime {
// V8 takes ownership of external_references.
let refs: &'static v8::ExternalReferences = Box::leak(Box::new(refs));
let global_context;
let mut module_map_data = None;
let mut module_handles = vec![];
fn get_context_data(
scope: &mut v8::HandleScope<()>,
context: v8::Local<v8::Context>,
) -> (Vec<v8::Global<v8::Module>>, v8::Global<v8::Object>) {
fn data_error_to_panic(err: v8::DataError) -> ! {
match err {
v8::DataError::BadType { actual, expected } => {
panic!(
"Invalid type for snapshot data: expected {}, got {}",
expected, actual
);
}
v8::DataError::NoData { expected } => {
panic!("No data for snapshot data: expected {}", expected);
}
}
}
let mut module_handles = vec![];
let mut scope = v8::ContextScope::new(scope, context);
// The 0th element is the module map itself, followed by X number of module
// handles. We need to deserialize the "next_module_id" field from the
// map to see how many module handles we expect.
match scope.get_context_data_from_snapshot_once::<v8::Object>(0) {
Ok(val) => {
let next_module_id = {
let next_module_id_str =
v8::String::new(&mut scope, "next_module_id").unwrap();
let next_module_id =
val.get(&mut scope, next_module_id_str.into()).unwrap();
assert!(next_module_id.is_int32());
let integer = next_module_id.to_integer(&mut scope).unwrap();
integer.int32_value(&mut scope).unwrap()
};
let no_of_modules = next_module_id - 1;
for i in 1..=no_of_modules {
match scope
.get_context_data_from_snapshot_once::<v8::Module>(i as usize)
{
Ok(val) => {
let module_global = v8::Global::new(&mut scope, val);
module_handles.push(module_global);
}
Err(err) => data_error_to_panic(err),
}
}
(module_handles, v8::Global::new(&mut scope, val))
}
Err(err) => data_error_to_panic(err),
}
}
let (mut isolate, snapshot_options) = if options.will_snapshot {
let (snapshot_creator, snapshot_loaded) =
@ -468,6 +525,14 @@ impl JsRuntime {
let scope = &mut v8::HandleScope::new(&mut isolate);
let context =
bindings::initialize_context(scope, &op_ctxs, snapshot_options);
// Get module map data from the snapshot
if has_startup_snapshot {
let context_data = get_context_data(scope, context);
module_handles = context_data.0;
module_map_data = Some(context_data.1);
}
global_context = v8::Global::new(scope, context);
scope.set_default_context(context);
}
@ -510,6 +575,13 @@ impl JsRuntime {
let context =
bindings::initialize_context(scope, &op_ctxs, snapshot_options);
// Get module map data from the snapshot
if has_startup_snapshot {
let context_data = get_context_data(scope, context);
module_handles = context_data.0;
module_map_data = Some(context_data.1);
}
global_context = v8::Global::new(scope, context);
}
@ -544,7 +616,7 @@ impl JsRuntime {
state.inspector = inspector;
state
.known_realms
.push(v8::Weak::new(&mut isolate, global_context));
.push(v8::Weak::new(&mut isolate, &global_context));
}
isolate.set_data(
Self::STATE_DATA_OFFSET,
@ -552,6 +624,16 @@ impl JsRuntime {
);
let module_map_rc = Rc::new(RefCell::new(ModuleMap::new(loader, op_state)));
if let Some(module_map_data) = module_map_data {
let scope =
&mut v8::HandleScope::with_context(&mut isolate, global_context);
let mut module_map = module_map_rc.borrow_mut();
module_map.update_with_snapshot_data(
scope,
module_map_data,
module_handles,
);
}
isolate.set_data(
Self::MODULE_MAP_DATA_OFFSET,
Rc::into_raw(module_map_rc.clone()) as *mut c_void,
@ -911,15 +993,37 @@ impl JsRuntime {
}
}
self.state.borrow_mut().global_realm.take();
self.state.borrow_mut().inspector.take();
// Serialize the module map and store its data in the snapshot.
{
let module_map_rc = self.module_map.take().unwrap();
let module_map = module_map_rc.borrow();
let (module_map_data, module_handles) =
module_map.serialize_for_snapshotting(&mut self.handle_scope());
let context = self.global_context();
let mut scope = self.handle_scope();
let local_context = v8::Local::new(&mut scope, context);
let local_data = v8::Local::new(&mut scope, module_map_data);
let offset = scope.add_context_data(local_context, local_data);
assert_eq!(offset, 0);
for (index, handle) in module_handles.into_iter().enumerate() {
let module_handle = v8::Local::new(&mut scope, handle);
let offset = scope.add_context_data(local_context, module_handle);
assert_eq!(offset, index + 1);
}
}
// Drop existing ModuleMap to drop v8::Global handles
{
self.module_map.take();
let v8_isolate = self.v8_isolate();
Self::drop_state_and_module_map(v8_isolate);
}
self.state.borrow_mut().global_realm.take();
// Drop other v8::Global handles before snapshotting
{
for weak_context in &self.state.clone().borrow().known_realms {
@ -2605,10 +2709,13 @@ pub mod tests {
use super::*;
use crate::error::custom_error;
use crate::error::AnyError;
use crate::modules::AssertedModuleType;
use crate::modules::ModuleInfo;
use crate::modules::ModuleSource;
use crate::modules::ModuleSourceFuture;
use crate::modules::ModuleType;
use crate::modules::ResolutionKind;
use crate::modules::SymbolicModule;
use crate::ZeroCopyBuf;
use deno_ops::op;
use futures::future::lazy;
@ -2618,6 +2725,7 @@ pub mod tests {
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
// deno_ops macros generate code assuming deno_core in scope.
mod deno_core {
pub use crate::*;
@ -3422,8 +3530,6 @@ pub mod tests {
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, Error> {
assert_eq!(specifier, "file:///main.js");
assert_eq!(referrer, ".");
let s = crate::resolve_import(specifier, referrer).unwrap();
Ok(s)
}
@ -3434,29 +3540,150 @@ pub mod tests {
_maybe_referrer: Option<ModuleSpecifier>,
_is_dyn_import: bool,
) -> Pin<Box<ModuleSourceFuture>> {
eprintln!("load() should not be called");
unreachable!()
}
}
let loader = std::rc::Rc::new(ModsLoader::default());
fn create_module(
runtime: &mut JsRuntime,
i: usize,
main: bool,
) -> ModuleInfo {
let specifier = crate::resolve_url(&format!("file:///{i}.js")).unwrap();
let prev = i - 1;
let source_code = format!(
r#"
import {{ f{prev} }} from "file:///{prev}.js";
export function f{i}() {{ return f{prev}() }}
"#
);
let id = if main {
futures::executor::block_on(
runtime.load_main_module(&specifier, Some(source_code)),
)
.unwrap()
} else {
futures::executor::block_on(
runtime.load_side_module(&specifier, Some(source_code)),
)
.unwrap()
};
assert_eq!(i + 1, id as usize);
let _ = runtime.mod_evaluate(id);
futures::executor::block_on(runtime.run_event_loop(false)).unwrap();
ModuleInfo {
id,
main,
name: specifier.to_string(),
requests: vec![crate::modules::ModuleRequest {
specifier: crate::resolve_url(&format!("file:///{prev}.js")).unwrap(),
asserted_module_type: AssertedModuleType::JavaScriptOrWasm,
}],
module_type: ModuleType::JavaScript,
}
}
fn assert_module_map(runtime: &mut JsRuntime, modules: &Vec<ModuleInfo>) {
let module_map_rc = runtime.get_module_map();
let module_map = module_map_rc.borrow();
assert_eq!(module_map.ids_by_handle.len(), modules.len());
assert_eq!(module_map.handles_by_id.len(), modules.len());
assert_eq!(module_map.info.len(), modules.len());
assert_eq!(module_map.by_name.len(), modules.len());
assert_eq!(module_map.next_module_id, (modules.len() + 1) as ModuleId);
assert_eq!(module_map.next_load_id, (modules.len() + 1) as ModuleId);
let ids_by_handle = module_map.ids_by_handle.values().collect::<Vec<_>>();
for info in modules {
assert!(ids_by_handle.contains(&&info.id));
assert!(module_map.handles_by_id.contains_key(&info.id));
assert_eq!(module_map.info.get(&info.id).unwrap(), info);
assert_eq!(
module_map
.by_name
.get(&(info.name.clone(), AssertedModuleType::JavaScriptOrWasm))
.unwrap(),
&SymbolicModule::Mod(info.id)
);
}
}
let loader = Rc::new(ModsLoader::default());
let mut runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(loader),
module_loader: Some(loader.clone()),
will_snapshot: true,
..Default::default()
});
let specifier = crate::resolve_url("file:///main.js").unwrap();
let source_code = "Deno.core.print('hello\\n')".to_string();
let module_id = futures::executor::block_on(
runtime.load_main_module(&specifier, Some(source_code)),
let specifier = crate::resolve_url("file:///0.js").unwrap();
let source_code =
r#"export function f0() { return "hello world" }"#.to_string();
let id = futures::executor::block_on(
runtime.load_side_module(&specifier, Some(source_code)),
)
.unwrap();
let _ = runtime.mod_evaluate(module_id);
let _ = runtime.mod_evaluate(id);
futures::executor::block_on(runtime.run_event_loop(false)).unwrap();
let _snapshot = runtime.snapshot();
let mut modules = vec![];
modules.push(ModuleInfo {
id,
main: false,
name: specifier.to_string(),
requests: vec![],
module_type: ModuleType::JavaScript,
});
modules.extend((1..200).map(|i| create_module(&mut runtime, i, false)));
assert_module_map(&mut runtime, &modules);
let snapshot = runtime.snapshot();
let mut runtime2 = JsRuntime::new(RuntimeOptions {
module_loader: Some(loader.clone()),
will_snapshot: true,
startup_snapshot: Some(Snapshot::JustCreated(snapshot)),
..Default::default()
});
assert_module_map(&mut runtime2, &modules);
modules.extend((200..400).map(|i| create_module(&mut runtime2, i, false)));
modules.push(create_module(&mut runtime2, 400, true));
assert_module_map(&mut runtime2, &modules);
let snapshot2 = runtime2.snapshot();
let mut runtime3 = JsRuntime::new(RuntimeOptions {
module_loader: Some(loader),
startup_snapshot: Some(Snapshot::JustCreated(snapshot2)),
..Default::default()
});
assert_module_map(&mut runtime3, &modules);
let source_code = r#"(async () => {
const mod = await import("file:///400.js");
return mod.f400();
})();"#
.to_string();
let val = runtime3.execute_script(".", &source_code).unwrap();
let val = futures::executor::block_on(runtime3.resolve_value(val)).unwrap();
{
let scope = &mut runtime3.handle_scope();
let value = v8::Local::new(scope, val);
let str_ = value.to_string(scope).unwrap().to_rust_string_lossy(scope);
assert_eq!(str_, "hello world");
}
}
#[test]