Skip to content

Commit

Permalink
feat: support for v8 code cache (denoland#655)
Browse files Browse the repository at this point in the history
This PR adds V8 code cache support for ES modules loaded through
`ModuleLoader`, and for scripts evaluated through `evalContext` (used by
`require` in Deno). Embedders have full control over whether code
caching is enabled, and how code cache is retrieved/stored.

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
  • Loading branch information
Igor Zinkovsky and bartlomieju authored Mar 14, 2024
1 parent c11bc3d commit c7d9b2e
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 13 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ deno_core = { version = "0.270.0", path = "./core" }
deno_ops = { version = "0.146.0", path = "./ops" }
serde_v8 = { version = "0.179.0", path = "./serde_v8" }

v8 = { version = "0.85.0", default-features = false }
v8 = { version = "0.86.0", default-features = false }
deno_ast = { version = "=0.32.0", features = ["transpiling"] }
deno_unsync = "0.3.2"
deno_core_icudata = "0.0.73"
Expand Down
1 change: 1 addition & 0 deletions core/examples/ts_module_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ impl ModuleLoader for TypescriptModuleLoader {
module_type,
ModuleSourceCode::String(code.into()),
module_specifier,
None,
))
}

Expand Down
16 changes: 16 additions & 0 deletions core/modules/loaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ pub trait ModuleLoader {
) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
async { Ok(()) }.boxed_local()
}

/// Called when new v8 code cache is available for this module. Implementors
/// can store the provided code cache for future executions of the same module.
///
/// It's not required to implement this method.
fn code_cache_ready(
&self,
_module_specifier: &ModuleSpecifier,
_code_cache: &[u8],
) -> Pin<Box<dyn Future<Output = ()>>> {
async {}.boxed_local()
}
}

/// Placeholder structure used when creating
Expand Down Expand Up @@ -186,6 +198,7 @@ impl ModuleLoader for ExtModuleLoader {
ModuleType::JavaScript,
ModuleSourceCode::String(source),
specifier,
None,
)))
}

Expand Down Expand Up @@ -240,6 +253,7 @@ impl ModuleLoader for LazyEsmModuleLoader {
ModuleType::JavaScript,
ModuleSourceCode::String(source),
specifier,
None,
)))
}

Expand Down Expand Up @@ -316,6 +330,7 @@ impl ModuleLoader for FsModuleLoader {
module_type,
ModuleSourceCode::Bytes(code.into_boxed_slice().into()),
&module_specifier,
None,
);
Ok(module)
}
Expand Down Expand Up @@ -377,6 +392,7 @@ impl ModuleLoader for StaticModuleLoader {
ModuleType::JavaScript,
ModuleSourceCode::String(code.try_clone().unwrap()),
module_specifier,
None,
))
} else {
Err(generic_error("Module not found"))
Expand Down
155 changes: 152 additions & 3 deletions core/modules/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ use log::debug;
use v8::Function;
use v8::PromiseState;

use std::borrow::Cow;
use std::cell::Cell;
use std::cell::RefCell;
use std::collections::HashMap;
Expand All @@ -63,13 +64,22 @@ use super::RequestedModuleType;
type PrepareLoadFuture =
dyn Future<Output = (ModuleLoadId, Result<RecursiveModuleLoad, Error>)>;

type CodeCacheReadyFuture = dyn Future<Output = ()>;

use super::ImportMetaResolveCallback;

struct ModEvaluate {
module_map: Rc<ModuleMap>,
sender: Option<oneshot::Sender<Result<(), Error>>>,
}

type CodeCacheReadyCallback =
Box<dyn Fn(&[u8]) -> Pin<Box<dyn Future<Output = ()>>>>;
pub(crate) struct CodeCacheInfo {
code_cache: Option<Cow<'static, [u8]>>,
ready_callback: CodeCacheReadyCallback,
}

pub const BOM_CHAR: &[u8] = &[0xef, 0xbb, 0xbf];

/// Strips the byte order mark from the provided text if it exists.
Expand Down Expand Up @@ -107,8 +117,12 @@ pub(crate) struct ModuleMap {
pending_dyn_mod_evaluations: RefCell<Vec<DynImportModEvaluate>>,
pending_dyn_mod_evaluations_pending: Cell<bool>,
pending_mod_evaluation: Cell<bool>,
code_cache_ready_futs:
RefCell<FuturesUnordered<Pin<Box<CodeCacheReadyFuture>>>>,
pending_code_cache_ready: Cell<bool>,
module_waker: AtomicWaker,
data: RefCell<ModuleMapData>,
enable_code_cache: bool,

/// A counter used to delay our dynamic import deadlock detection by one spin
/// of the event loop.
Expand Down Expand Up @@ -162,6 +176,7 @@ impl ModuleMap {
loader: Rc<dyn ModuleLoader>,
exception_state: Rc<ExceptionState>,
import_meta_resolve_cb: ImportMetaResolveCallback,
enable_code_cache: bool,
) -> Self {
Self {
loader: loader.into(),
Expand All @@ -176,8 +191,11 @@ impl ModuleMap {
pending_dyn_mod_evaluations: Default::default(),
pending_dyn_mod_evaluations_pending: Default::default(),
pending_mod_evaluation: Default::default(),
code_cache_ready_futs: Default::default(),
pending_code_cache_ready: Default::default(),
module_waker: Default::default(),
data: Default::default(),
enable_code_cache,
}
}

Expand Down Expand Up @@ -271,6 +289,7 @@ impl ModuleMap {
module_type,
module_url_found,
module_url_specified,
code_cache,
} = module_source;

// Register the module in the module map unless it's already there. If the
Expand Down Expand Up @@ -310,13 +329,34 @@ impl ModuleMap {
let code =
ModuleSource::get_string_source(module_url_found.as_str(), code)
.map_err(ModuleError::Other)?;

let (code_cache_info, module_url_found) = if self.enable_code_cache {
let (module_url_found1, module_url_found2) =
module_url_found.into_cheap_copy();
let loader = self.loader.borrow().clone();
(
Some(CodeCacheInfo {
code_cache,
ready_callback: Box::new(move |cache| {
let specifier =
ModuleSpecifier::parse(module_url_found1.as_str()).unwrap();
loader.code_cache_ready(&specifier, cache)
}),
}),
module_url_found2,
)
} else {
(None, module_url_found)
};

self.new_module_from_js_source(
scope,
main,
ModuleType::JavaScript,
module_url_found,
code,
dynamic,
code_cache_info,
)?
}
ModuleType::Wasm => {
Expand Down Expand Up @@ -384,13 +424,33 @@ impl ModuleMap {
synthetic_module_type,
exports,
)?;

let (code_cache_info, url2) = if self.enable_code_cache {
let (url1, url2) = url2.into_cheap_copy();
let loader = self.loader.borrow().clone();
(
Some(CodeCacheInfo {
code_cache,
ready_callback: Box::new(move |cache| {
let specifier =
ModuleSpecifier::parse(url1.as_str()).unwrap();
loader.code_cache_ready(&specifier, cache)
}),
}),
url2,
)
} else {
(None, url2)
};

self.new_module_from_js_source(
scope,
main,
ModuleType::Other(module_type.clone()),
url2,
computed_src,
dynamic,
code_cache_info,
)?
}
}
Expand Down Expand Up @@ -464,6 +524,7 @@ impl ModuleMap {
name: impl IntoModuleName,
source: impl IntoModuleCodeString,
is_dynamic_import: bool,
code_cache_info: Option<CodeCacheInfo>,
) -> Result<ModuleId, ModuleError> {
let name = name.into_module_name();
self.new_module_from_js_source(
Expand All @@ -473,6 +534,7 @@ impl ModuleMap {
name.into_module_name(),
source.into_module_code(),
is_dynamic_import,
code_cache_info,
)
}

Expand All @@ -488,6 +550,7 @@ impl ModuleMap {
/// and attached to associated [`ModuleInfo`].
///
/// Returns an ID of newly created module.
#[allow(clippy::too_many_arguments)]
pub(crate) fn new_module_from_js_source(
&self,
scope: &mut v8::HandleScope,
Expand All @@ -496,6 +559,7 @@ impl ModuleMap {
name: ModuleName,
source: ModuleCodeString,
is_dynamic_import: bool,
mut code_cache_info: Option<CodeCacheInfo>,
) -> Result<ModuleId, ModuleError> {
if main {
let data = self.data.borrow();
Expand All @@ -513,11 +577,37 @@ impl ModuleMap {
let source_str = source.v8_string(scope);

let origin = module_origin(scope, name_str);
let source = v8::script_compiler::Source::new(source_str, Some(&origin));

let tc_scope = &mut v8::TryCatch::new(scope);

let maybe_module = v8::script_compiler::compile_module(tc_scope, source);
let (maybe_module, try_store_code_cache) = code_cache_info
.as_ref()
.and_then(|code_cache_info| {
code_cache_info.code_cache.as_ref().map(|cache| {
let mut source = v8::script_compiler::Source::new_with_cached_data(
source_str,
Some(&origin),
v8::CachedData::new(cache),
);
let maybe_module = v8::script_compiler::compile_module2(
tc_scope,
&mut source,
v8::script_compiler::CompileOptions::ConsumeCodeCache,
v8::script_compiler::NoCacheReason::NoReason,
);
// Check if the provided code cache is rejected by V8.
let rejected = match source.get_cached_data() {
Some(cached_data) => cached_data.rejected(),
_ => true,
};
(maybe_module, rejected)
})
})
.unwrap_or_else(|| {
let source =
v8::script_compiler::Source::new(source_str, Some(&origin));
(v8::script_compiler::compile_module(tc_scope, source), true)
});

if tc_scope.has_caught() {
assert!(maybe_module.is_none());
Expand All @@ -529,6 +619,23 @@ impl ModuleMap {

let module = maybe_module.unwrap();

if try_store_code_cache {
if let Some(code_cache_info) = code_cache_info.take() {
let unbound_module_script = module.get_unbound_module_script(tc_scope);
let code_cache =
unbound_module_script.create_code_cache().ok_or_else(|| {
ModuleError::Other(generic_error(
"Unable to get code cache from unbound module script",
))
})?;
let fut =
async move { (code_cache_info.ready_callback)(&code_cache).await }
.boxed_local();
self.code_cache_ready_futs.borrow_mut().push(fut);
self.pending_code_cache_ready.set(true);
}
}

// TODO(bartlomieju): maybe move to a helper function?
let module_requests = module.get_module_requests();
let requests_len = module_requests.length();
Expand Down Expand Up @@ -1228,6 +1335,9 @@ impl ModuleMap {
let poll_imports = self.poll_dyn_imports(cx, scope)?;
assert!(poll_imports.is_ready());

let poll_code_cache_ready = self.poll_code_cache_ready(cx)?;
assert!(poll_code_cache_ready.is_ready());

if self.evaluate_dyn_imports(scope) {
has_evaluated = true;
} else {
Expand Down Expand Up @@ -1366,6 +1476,26 @@ impl ModuleMap {
}
}

fn poll_code_cache_ready(&self, cx: &mut Context) -> Poll<Result<(), Error>> {
if !self.pending_code_cache_ready.get() {
return Poll::Ready(Ok(()));
}

loop {
let poll_result =
self.code_cache_ready_futs.borrow_mut().poll_next_unpin(cx);

if let Poll::Ready(Some(_)) = poll_result {
continue;
}

self
.pending_code_cache_ready
.set(!self.code_cache_ready_futs.borrow().is_empty());
return Poll::Ready(Ok(()));
}
}

/// Returns the namespace object of a module.
///
/// This is only available after module evaluation has completed.
Expand Down Expand Up @@ -1470,10 +1600,18 @@ impl ModuleMap {
scope: &mut v8::HandleScope,
module_specifier: &str,
source_code: ModuleCodeString,
code_cache_info: Option<CodeCacheInfo>,
) -> Result<v8::Global<v8::Value>, Error> {
let specifier = ModuleSpecifier::parse(module_specifier)?;
let mod_id = self
.new_es_module(scope, false, specifier, source_code, false)
.new_es_module(
scope,
false,
specifier.clone(),
source_code,
false,
code_cache_info,
)
.map_err(|e| e.into_any_error(scope, false, true))?;

self.instantiate_module(scope, mod_id).map_err(|e| {
Expand Down Expand Up @@ -1556,6 +1694,17 @@ impl ModuleMap {
scope,
module_specifier,
ModuleSource::get_string_source(specifier.as_str(), source.code)?,
if self.enable_code_cache {
let loader = self.loader.borrow().clone();
Some(CodeCacheInfo {
code_cache: source.code_cache,
ready_callback: Box::new(move |cache| {
loader.code_cache_ready(&specifier, cache)
}),
})
} else {
None
},
)
}
}
Expand Down
Loading

0 comments on commit c7d9b2e

Please sign in to comment.