Skip to content

Commit

Permalink
feat: optional code cache for extension sources (denoland#1071)
Browse files Browse the repository at this point in the history
Wires up the code cache through the `ExtModuleLoader` and extension
initialization, so that embedders can use the V8 code cache on extension
sources.
  • Loading branch information
nathanwhit authored Feb 10, 2025
1 parent 490079f commit 1927f2c
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 19 deletions.
6 changes: 3 additions & 3 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 @@ -29,7 +29,7 @@ deno_ast = { version = "=0.44.0", features = ["transpiling"] }
deno_core_icudata = "0.74.0"
deno_error = { version = "0.5.5", features = ["serde_json", "serde", "url", "tokio"] }
deno_unsync = "0.4.1"
v8 = { version = "130.0.8", default-features = false }
v8 = { version = "130.0.7", default-features = false }

anyhow = "1"
bencher = "0.1"
Expand Down
6 changes: 5 additions & 1 deletion core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub enum CoreError {
Module(ModuleConcreteError),
#[error(transparent)]
DataError(DataError),
#[error("Unable to get code cache from unbound module script for {0}")]
CreateCodeCache(String),
}

impl CoreError {
Expand Down Expand Up @@ -174,6 +176,7 @@ impl JsErrorClass for CoreError {
| CoreError::MissingFromModuleMap(_)
| CoreError::ExecutionTerminated
| CoreError::PendingPromiseResolution
| CoreError::CreateCodeCache(_)
| CoreError::EvaluateDynamicImportedModule => {
Cow::Borrowed(GENERIC_ERROR)
}
Expand Down Expand Up @@ -206,7 +209,8 @@ impl JsErrorClass for CoreError {
| CoreError::FutureCanceled(_)
| CoreError::ExecutionTerminated
| CoreError::PendingPromiseResolution
| CoreError::EvaluateDynamicImportedModule => self.to_string().into(),
| CoreError::EvaluateDynamicImportedModule
| CoreError::CreateCodeCache(_) => self.to_string().into(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion core/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ impl JsRuntimeInspector {
let stack_trace = message.get_stack_trace(scope);
let mut v8_inspector_ref = self.v8_inspector.borrow_mut();
let v8_inspector = v8_inspector_ref.as_mut().unwrap();
let stack_trace = v8_inspector.create_stack_trace(stack_trace);
let stack_trace = v8_inspector.create_stack_trace(stack_trace.unwrap());
v8_inspector.exception_thrown(
context,
if in_promise {
Expand Down
1 change: 1 addition & 0 deletions core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub use crate::module_specifier::resolve_url;
pub use crate::module_specifier::ModuleResolutionError;
pub use crate::module_specifier::ModuleSpecifier;
pub use crate::modules::CustomModuleEvaluationKind;
pub use crate::modules::ExtCodeCache;
pub use crate::modules::FsModuleLoader;
pub use crate::modules::ModuleCodeBytes;
pub use crate::modules::ModuleCodeString;
Expand Down
51 changes: 46 additions & 5 deletions core/modules/loaders.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2018-2025 the Deno authors. MIT license.

use crate::error::CoreError;
use crate::module_specifier::ModuleSpecifier;
use crate::modules::IntoModuleCodeString;
use crate::modules::ModuleCodeString;
Expand All @@ -13,7 +14,6 @@ use crate::resolve_import;
use crate::ModuleSourceCode;
use deno_error::JsErrorBox;

use deno_core::error::CoreError;
use futures::future::FutureExt;
use std::borrow::Cow;
use std::cell::RefCell;
Expand All @@ -22,6 +22,8 @@ use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;

use super::SourceCodeCacheInfo;

#[derive(Debug, thiserror::Error, deno_error::JsError)]
#[class(generic)]
pub enum ModuleLoaderError {
Expand Down Expand Up @@ -220,23 +222,45 @@ impl ModuleLoader for NoopModuleLoader {
}
}

pub trait ExtCodeCache {
fn get_code_cache_info(
&self,
specifier: &ModuleSpecifier,
code: &ModuleSourceCode,
esm: bool,
) -> SourceCodeCacheInfo;

fn code_cache_ready(
&self,
specifier: ModuleSpecifier,
hash: u64,
code_cache: &[u8],
esm: bool,
);
}

pub(crate) struct ExtModuleLoader {
sources: RefCell<HashMap<ModuleName, ModuleCodeString>>,
ext_code_cache: Option<Rc<dyn ExtCodeCache>>,
}

impl ExtModuleLoader {
pub fn new(loaded_sources: Vec<(ModuleName, ModuleCodeString)>) -> Self {
pub fn new(
loaded_sources: Vec<(ModuleName, ModuleCodeString)>,
ext_code_cache: Option<Rc<dyn ExtCodeCache>>,
) -> Self {
// Guesstimate a length
let mut sources = HashMap::with_capacity(loaded_sources.len());
for source in loaded_sources {
sources.insert(source.0, source.1);
}
ExtModuleLoader {
sources: RefCell::new(sources),
ext_code_cache,
}
}

pub fn finalize(self) -> Result<(), CoreError> {
pub fn finalize(&self) -> Result<(), CoreError> {
let sources = self.sources.take();
let unused_modules: Vec<_> = sources.iter().collect();

Expand Down Expand Up @@ -296,11 +320,16 @@ impl ModuleLoader for ExtModuleLoader {
))
}
};
let code = ModuleSourceCode::String(source);
let code_cache = self
.ext_code_cache
.as_ref()
.map(|cache| cache.get_code_cache_info(specifier, &code, true));
ModuleLoadResponse::Sync(Ok(ModuleSource::new(
ModuleType::JavaScript,
ModuleSourceCode::String(source),
code,
specifier,
None,
code_cache,
)))
}

Expand All @@ -312,6 +341,18 @@ impl ModuleLoader for ExtModuleLoader {
) -> Pin<Box<dyn Future<Output = Result<(), ModuleLoaderError>>>> {
async { Ok(()) }.boxed_local()
}

fn code_cache_ready(
&self,
module_specifier: ModuleSpecifier,
hash: u64,
code_cache: &[u8],
) -> Pin<Box<dyn Future<Output = ()>>> {
if let Some(ext_code_cache) = &self.ext_code_cache {
ext_code_cache.code_cache_ready(module_specifier, hash, code_cache, true);
}
std::future::ready(()).boxed_local()
}
}

/// A loader that is used in `op_lazy_load_esm` to load and execute
Expand Down
1 change: 1 addition & 0 deletions core/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod recursive_load;
#[cfg(all(test, not(miri)))]
mod tests;

pub use loaders::ExtCodeCache;
pub(crate) use loaders::ExtModuleLoader;
pub use loaders::FsModuleLoader;
pub(crate) use loaders::LazyEsmModuleLoader;
Expand Down
2 changes: 1 addition & 1 deletion core/modules/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1946,7 +1946,7 @@ fn test_load_with_code_cache() {

#[test]
fn ext_module_loader_relative() {
let loader = ExtModuleLoader::new(vec![]);
let loader = ExtModuleLoader::new(vec![], None);
let cases = [
(
("../../foo.js", "ext:test/nested/mod/bar.js"),
Expand Down
89 changes: 89 additions & 0 deletions core/runtime/jsrealm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
use super::exception_state::ExceptionState;
#[cfg(test)]
use super::op_driver::OpDriver;
use crate::ModuleSourceCode;
use crate::SourceCodeCacheInfo;
use crate::_ops::OpMethodDecl;
use crate::cppgc::FunctionTemplateData;
use crate::error::exception_to_err_result;
Expand Down Expand Up @@ -352,6 +354,93 @@ impl JsRealm {
}
}

// TODO(nathanwhit): reduce duplication between this and `execute_script`, and
// try to factor out the code cache logic to share with `op_eval_context`
pub fn execute_script_with_cache(
&self,
isolate: &mut v8::Isolate,
name: ModuleSpecifier,
source_code: impl IntoModuleCodeString,
get_cache: &dyn Fn(
&ModuleSpecifier,
&ModuleSourceCode,
) -> SourceCodeCacheInfo,
cache_ready: &dyn Fn(ModuleSpecifier, u64, &[u8]),
) -> Result<v8::Global<v8::Value>, CoreError> {
let scope = &mut self.0.handle_scope(isolate);

let specifier = name.clone();
let code = source_code.into_module_code();
let source = ModuleSourceCode::String(code);
let code_cache = get_cache(&name, &source);
let ModuleSourceCode::String(source) = source else {
unreachable!()
};
let name = name.into_module_name().v8_string(scope).unwrap();
let source = source.v8_string(scope).unwrap();
let origin = script_origin(scope, name, false, None);
let tc_scope = &mut v8::TryCatch::new(scope);

let (maybe_script, maybe_code_cache_hash) =
if let Some(data) = &code_cache.data {
let mut source = v8::script_compiler::Source::new_with_cached_data(
source,
Some(&origin),
v8::CachedData::new(data),
);
let script = v8::script_compiler::compile(
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,
};
let maybe_code_cache_hash = if rejected {
Some(code_cache.hash) // recreate the cache
} else {
None
};
(Some(script), maybe_code_cache_hash)
} else {
(None, Some(code_cache.hash))
};

let script = maybe_script
.unwrap_or_else(|| v8::Script::compile(tc_scope, source, Some(&origin)));

let script = match script {
Some(script) => script,
None => {
let exception = tc_scope.exception().unwrap();
return exception_to_err_result(tc_scope, exception, false, false);
}
};

if let Some(code_cache_hash) = maybe_code_cache_hash {
let unbound_script = script.get_unbound_script(tc_scope);
let code_cache = unbound_script
.create_code_cache()
.ok_or_else(|| CoreError::CreateCodeCache(specifier.to_string()))?;
cache_ready(specifier, code_cache_hash, &code_cache);
}

match script.run(tc_scope) {
Some(value) => {
let value_handle = v8::Global::new(tc_scope, value);
Ok(value_handle)
}
None => {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
exception_to_err_result(tc_scope, exception, false, false)
}
}
}

/// Returns the namespace object of a module.
///
/// This is only available after module evaluation has completed.
Expand Down
Loading

0 comments on commit 1927f2c

Please sign in to comment.