Skip to content

Commit 125afab

Browse files
committed
perf(core): export only the ops something imports from ext:core/ops
1 parent 0cf86a4 commit 125afab

8 files changed

Lines changed: 947 additions & 4 deletions

File tree

libs/core/runtime/bindings.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use crate::ops::OpCtx;
4444
use crate::runtime::ContextState;
4545
use crate::runtime::InitMode;
4646
use crate::runtime::JsRealm;
47+
use crate::runtime::ops_exports::OpsExportFilter;
4748

4849
pub(crate) fn create_external_references(
4950
ops: &[OpCtx],
@@ -2096,12 +2097,16 @@ where
20962097

20972098
/// This function generates a list of tuples, that are a mapping of `<op_name>`
20982099
/// to a JavaScript function that executes and op.
2100+
///
2101+
/// Only names `filter` allows get an export cell; see
2102+
/// [`crate::runtime::ops_exports`] for how that set is derived.
20992103
pub fn create_exports_for_ops_virtual_module<'s, 'i>(
21002104
op_ctxs: &[OpCtx],
21012105
op_method_decls: &[OpMethodDecl],
21022106
methods_ctx_offset: usize,
21032107
scope: &mut v8::PinScope<'s, 'i>,
21042108
global: v8::Local<'s, v8::Object>,
2109+
filter: &OpsExportFilter,
21052110
) -> Vec<(FastStaticString, v8::Local<'s, v8::Value>)> {
21062111
let mut exports = Vec::with_capacity(op_ctxs.len());
21072112

@@ -2120,15 +2125,25 @@ pub fn create_exports_for_ops_virtual_module<'s, 'i>(
21202125
index += 1;
21212126
}
21222127

2128+
index += decl.methods.len() + decl.static_methods.len();
2129+
2130+
// Reading the name off `Deno.core.ops` goes through the lazy-ops
2131+
// interceptor and *materializes* the function -- which is then held alive
2132+
// by the export cell, and lands in the snapshot blob. Skipping the names
2133+
// nothing imports is the whole point of this filter.
21232134
let name = decl.name.1;
2135+
if !filter.allows(decl.name.0) {
2136+
continue;
2137+
}
21242138
let op_fn = get(scope, ops_obj, name, "op");
21252139
exports.push((name, op_fn));
2126-
2127-
index += decl.methods.len() + decl.static_methods.len();
21282140
}
21292141

21302142
let op_ctxs = &op_ctxs[index..];
21312143
for op_ctx in op_ctxs {
2144+
if !filter.allows(op_ctx.decl().name) {
2145+
continue;
2146+
}
21322147
let op_fn = get(scope, ops_obj, op_ctx.decl().name_fast, "op");
21332148
exports.push((op_ctx.decl().name_fast, op_fn));
21342149
}

libs/core/runtime/jsruntime.rs

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ use super::bindings::watch_promise;
7171
use super::exception_state::ExceptionState;
7272
use super::jsrealm::JsRealmInner;
7373
use super::op_driver::OpDriver;
74+
use super::ops_exports;
7475
use super::setup;
7576
use super::snapshot;
7677
use super::stats::RuntimeActivityStatsFactory;
@@ -598,6 +599,28 @@ pub struct RuntimeOptions {
598599
/// Should op registration be skipped?
599600
pub skip_op_registration: bool,
600601

602+
/// Make the `ext:core/ops` synthetic module export **every** registered op,
603+
/// the way it did before the export list was shrunk to the set of names that
604+
/// extension sources actually import.
605+
///
606+
/// By default deno_core scans the extension sources it is given — including
607+
/// `lazy_loaded_*` and residual sources — for
608+
/// `import { … } from "ext:core/ops"` clauses and exports only those names.
609+
/// Anything the scan cannot prove to be a plain named import (a namespace
610+
/// import, an `export * from`, a dynamic `import("ext:core/ops")`) already
611+
/// switches that runtime back to exporting everything automatically, so this
612+
/// flag is only needed when the importing module is **not** among the sources
613+
/// deno_core sees — e.g. a module served by the embedder's own
614+
/// `ModuleLoader` that imports ops by name.
615+
///
616+
/// Turning this on restores the pre-shrink blob cost: one export cell, and
617+
/// hence one materialized op function, per registered op (~270 bytes of
618+
/// snapshot each). Prefer letting the scanner see the source.
619+
///
620+
/// Only meaningful when the runtime builds the module itself, i.e. when there
621+
/// is no startup snapshot or the snapshot is being created.
622+
pub export_all_ops_from_virtual_module: bool,
623+
601624
/// Isolate creation parameters.
602625
pub create_params: Option<v8::CreateParams>,
603626

@@ -849,6 +872,69 @@ impl JsRuntime {
849872
)?;
850873
startup_phase_end(_phase, "into_sources_and_source_maps");
851874

875+
// Work out which names `ext:core/ops` has to export, before the sources are
876+
// externalized. Only the runtime that *builds* the module pays for this;
877+
// when the module comes out of a snapshot it is already shaped.
878+
let ops_export_filter = if init_mode == InitMode::New {
879+
let _phase = startup_phase_begin();
880+
let mut scan = ops_exports::OpsImportScan::default();
881+
if options.export_all_ops_from_virtual_module {
882+
scan.force_export_all();
883+
}
884+
for source in sources
885+
.js
886+
.iter()
887+
.chain(sources.esm.iter())
888+
.chain(sources.lazy_esm.iter())
889+
.chain(sources.lazy_js.iter())
890+
{
891+
scan.add_source(source.code.as_str());
892+
}
893+
// Residual `lazy_loaded_*` sources are instantiated *after* the snapshot
894+
// is deserialized, but they are known here, so their imports go into the
895+
// same union rather than exploding at load time.
896+
for (_, source) in options
897+
.residual_lazy_js_sources
898+
.iter()
899+
.chain(options.residual_lazy_esm_sources.iter())
900+
{
901+
scan.add_source(source);
902+
}
903+
// deno_core's own built-ins are executed after the synthetic module is
904+
// created, so they have to be in the union too.
905+
for source in CONTEXT_SETUP_SOURCES.iter().chain(BUILTIN_SOURCES.iter()) {
906+
scan.add_source(source.source.as_str());
907+
}
908+
for source in &BUILTIN_ES_MODULES {
909+
scan.add_source(source.load()?.as_str());
910+
}
911+
let filter = scan.finish();
912+
startup_phase_end(_phase, "scan_ops_module_imports");
913+
if startup_phases_enabled() {
914+
#[allow(clippy::print_stderr, reason = "diagnostic")]
915+
{
916+
// The fail-safe direction of this analysis is "export everything",
917+
// which costs the whole win silently. Make it visible next to the
918+
// other startup diagnostics.
919+
match &filter {
920+
ops_exports::OpsExportFilter::All => eprintln!(
921+
"[startup] {:>32} ALL (export-all requested, or a source uses \
922+
ext:core/ops in a way the scanner cannot analyse)",
923+
"ext:core/ops exports"
924+
),
925+
ops_exports::OpsExportFilter::Only(names) => eprintln!(
926+
"[startup] {:>32} {} names",
927+
"ext:core/ops exports",
928+
names.len()
929+
),
930+
}
931+
}
932+
}
933+
filter
934+
} else {
935+
ops_exports::OpsExportFilter::All
936+
};
937+
852938
for loaded_source in sources
853939
.js
854940
.iter()
@@ -1257,8 +1343,11 @@ impl JsRuntime {
12571343
// ) {
12581344
if init_mode == InitMode::New {
12591345
let _phase = startup_phase_begin();
1260-
js_runtime
1261-
.execute_virtual_ops_module(context_global, module_map.clone());
1346+
js_runtime.execute_virtual_ops_module(
1347+
context_global,
1348+
module_map.clone(),
1349+
&ops_export_filter,
1350+
);
12621351
startup_phase_end(_phase, "execute_virtual_ops_module");
12631352
}
12641353

@@ -1471,6 +1560,7 @@ impl JsRuntime {
14711560
&mut self,
14721561
context_global: &v8::Global<v8::Context>,
14731562
module_map: Rc<ModuleMap>,
1563+
filter: &ops_exports::OpsExportFilter,
14741564
) {
14751565
scope!(scope, self);
14761566
let context_local = v8::Local::new(scope, context_global);
@@ -1482,6 +1572,7 @@ impl JsRuntime {
14821572
context_state.methods_ctx_offset,
14831573
scope,
14841574
global,
1575+
filter,
14851576
);
14861577
let mod_id = module_map.new_synthetic_module(
14871578
scope,

libs/core/runtime/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod jsruntime;
88
pub mod op_driver;
99
#[doc(hidden)]
1010
pub mod ops;
11+
mod ops_exports;
1112
pub mod ops_rust_to_v8;
1213
mod setup;
1314
mod snapshot;

0 commit comments

Comments
 (0)