Skip to content

Commit 336da42

Browse files
authored
perf(core): make extension op tables const in static memory (#36696)
1 parent 2b5912c commit 336da42

2 files changed

Lines changed: 189 additions & 10 deletions

File tree

libs/core/extensions.rs

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,10 @@ macro_rules! or {
429429
/// The following options are available for the [`extension`] macro:
430430
///
431431
/// * deps: a comma-separated list of module dependencies, eg: `deps = [ my_other_extension ]`
432-
/// * parameters: a comma-separated list of parameters and base traits, eg: `parameters = [ P: MyTrait ]`
432+
/// * parameters: a comma-separated list of parameters and base traits, eg: `parameters = [ P: MyTrait ]`.
433+
/// A generic extension's op table is a `const` in static memory just like a non-generic one's, but
434+
/// there is one such table per monomorphization, so instantiating the same extension with many
435+
/// different parameters multiplies the table in the binary.
433436
/// * bounds: a comma-separated list of additional type bounds, eg: `bounds = [ P::MyAssociatedType: MyTrait ]`
434437
/// * ops: a comma-separated list of [`OpDecl`]s to provide, eg: `ops = [ op_foo, op_bar ]`
435438
/// * esm: a comma-separated list of ESM module filenames (see [`include_js_files`]), eg: `esm = [ dir "dir", "my_file.js" ]`
@@ -530,10 +533,19 @@ macro_rules! extension {
530533
const V: ::std::option::Option<&'static ::std::primitive::str> = $crate::or!($(::std::option::Option::Some($esm_entry_point))?, ::std::option::Option::None);
531534
V
532535
},
533-
ops: ::std::borrow::Cow::Owned(vec![$($({
534-
$( #[ $m ] )*
535-
$( $op )::+ $( :: < $($op_param),* > )? ()
536-
}),+)?]),
536+
// The op declaration constructors are `const fn`, so the whole table
537+
// is a compile-time constant living in static memory (see the
538+
// `! __ops_table__` arms below). A generic extension gets one such
539+
// table per monomorphization, via an associated const on a generic
540+
// helper type.
541+
ops: $crate::extension!(
542+
! __ops_table__
543+
[ $( $( $param : $type )+ )? ]
544+
[ $($({
545+
$( #[ $m ] )*
546+
$( $op )::+ $( :: < $($op_param),* > )? ()
547+
}),+)? ]
548+
),
537549
objects: ::std::borrow::Cow::Borrowed(&[$($({
538550
$( $object )::+::DECL
539551
}),+)?]),
@@ -661,6 +673,40 @@ macro_rules! extension {
661673
$( $args.op_state_fn = ::std::option::Option::Some(::std::boxed::Box::new($state_fn)); )?
662674
};
663675

676+
// No type parameters: the op declarations are constant expressions, so the
677+
// table is a `const` item in static memory and `Extension::ops` borrows it.
678+
// `init_ops` then hands it to the realm as an `OpDeclStorage::Static` run,
679+
// i.e. the realm keeps no copy of the decls at all.
680+
(! __ops_table__ [] [ $( $decl:expr ),* $(,)? ]) => {
681+
{
682+
const OPS: &'static [$crate::OpDecl] = &[ $( $decl ),* ];
683+
::std::borrow::Cow::Borrowed(OPS)
684+
}
685+
};
686+
687+
// With type parameters: a plain `const` item inside `fn ext<P: ..>()` cannot
688+
// name `P`, but an *associated* const on a generic type can name that type's
689+
// own parameters. So the table becomes `__OpsTable::<P>::OPS`, which the
690+
// compiler const-evaluates once per monomorphization and places in static
691+
// memory — one static table per `P`, with `Extension::ops` borrowing the one
692+
// that belongs to this instantiation. `init_ops` then hands it to the realm
693+
// as an `OpDeclStorage::Static` run, exactly like the non-generic case.
694+
(! __ops_table__ [ $( $param:ident : $type:ident )+ ] [ $( $decl:expr ),* $(,)? ]) => {
695+
{
696+
#[doc(hidden)]
697+
#[allow(non_camel_case_types, dead_code, reason = "generated by macro")]
698+
struct __OpsTable< $( $param : $type + 'static ),+ >(
699+
::std::marker::PhantomData<( $( $param, )+ )>,
700+
);
701+
702+
impl< $( $param : $type + 'static ),+ > __OpsTable< $( $param ),+ > {
703+
const OPS: &'static [$crate::OpDecl] = &[ $( $decl ),* ];
704+
}
705+
706+
::std::borrow::Cow::Borrowed(<__OpsTable< $( $param ),+ >>::OPS)
707+
}
708+
};
709+
664710
(! __ops__ $ext:ident __eot__) => {
665711
};
666712

@@ -1082,4 +1128,135 @@ mod tests {
10821128
);
10831129
assert_eq!("a", files[0].specifier);
10841130
}
1131+
1132+
mod op_tables {
1133+
use std::borrow::Cow;
1134+
1135+
use crate::extension_set::init_ops;
1136+
use crate::op2;
1137+
use crate::ops::OpDeclStorage;
1138+
use crate::ops_builtin::op_void_async;
1139+
use crate::ops_builtin::op_void_sync;
1140+
1141+
pub trait TestParam {
1142+
const ID: u32;
1143+
}
1144+
1145+
/// Resembles the generic ops in `deno_node`/`deno_http`: the parameter is
1146+
/// named in the op's turbofish and reached through the op body.
1147+
#[op2(fast)]
1148+
fn op_generic_probe<P: TestParam + 'static>() -> u32 {
1149+
P::ID
1150+
}
1151+
1152+
crate::extension!(const_table_ext, ops = [op_void_sync, op_void_async],);
1153+
1154+
crate::extension!(
1155+
generic_table_ext,
1156+
parameters = [P: TestParam],
1157+
ops = [op_generic_probe<P>, op_void_sync],
1158+
);
1159+
1160+
/// Mirrors `deno_node`'s shape: several parameters, only some of which any
1161+
/// given op names, plus a parameter used by the options/state function.
1162+
#[op2(fast)]
1163+
fn op_generic_multi<A: TestParam + 'static, B: TestParam + 'static>() -> u32
1164+
{
1165+
A::ID + B::ID
1166+
}
1167+
1168+
crate::extension!(
1169+
generic_multi_ext,
1170+
parameters = [A: TestParam, B: TestParam],
1171+
ops = [
1172+
op_generic_multi<A, B>,
1173+
op_generic_probe<B>,
1174+
op_void_async,
1175+
],
1176+
options = { marker: u32 },
1177+
state = |state, options| {
1178+
state.put(options.marker + A::ID + B::ID);
1179+
},
1180+
);
1181+
1182+
struct P1;
1183+
impl TestParam for P1 {
1184+
const ID: u32 = 1;
1185+
}
1186+
1187+
struct P2;
1188+
impl TestParam for P2 {
1189+
const ID: u32 = 2;
1190+
}
1191+
1192+
/// A non-generic extension's op table is a `const` in static memory, so
1193+
/// `Extension::ops` borrows it and the realm never copies the decls.
1194+
#[test]
1195+
fn non_generic_extension_table_is_static() {
1196+
let ext = const_table_ext::init();
1197+
assert!(matches!(ext.ops, Cow::Borrowed(_)));
1198+
assert_eq!(ext.ops.len(), 2);
1199+
1200+
// Two calls hand out the very same static allocation.
1201+
let a = const_table_ext::init();
1202+
let b = const_table_ext::init();
1203+
assert_eq!(a.ops.as_ptr(), b.ops.as_ptr());
1204+
}
1205+
1206+
/// ... and it reaches the realm as an `OpDeclStorage::Static` run.
1207+
#[test]
1208+
fn non_generic_extension_reaches_realm_as_static() {
1209+
let mut extensions = vec![const_table_ext::init()];
1210+
let (decls, _methods) = init_ops(&[], &mut extensions);
1211+
let ext_decls = decls
1212+
.iter()
1213+
.find(|d| d.len() == 2)
1214+
.expect("extension ops missing");
1215+
assert!(matches!(ext_decls, OpDeclStorage::Static(_)));
1216+
}
1217+
1218+
/// A generic extension's table is an associated const on a generic helper
1219+
/// type, so it is also a `const` in static memory — one per
1220+
/// monomorphization.
1221+
#[test]
1222+
fn generic_extension_table_is_static() {
1223+
let ext = generic_table_ext::init::<P1>();
1224+
assert!(matches!(ext.ops, Cow::Borrowed(_)));
1225+
assert_eq!(ext.ops.len(), 2);
1226+
1227+
// Two inits of the *same* monomorphization share one static allocation.
1228+
let a = generic_table_ext::init::<P1>();
1229+
let b = generic_table_ext::init::<P1>();
1230+
assert_eq!(a.ops.as_ptr(), b.ops.as_ptr());
1231+
}
1232+
1233+
/// Each monomorphization must get its own table: if they collapsed into
1234+
/// one, `generic_table_ext::init::<P2>()` would dispatch to `P1`'s ops.
1235+
#[test]
1236+
fn generic_extension_table_is_per_monomorphization() {
1237+
let p1 = generic_table_ext::init::<P1>();
1238+
let p2 = generic_table_ext::init::<P2>();
1239+
assert_ne!(p1.ops.as_ptr(), p2.ops.as_ptr());
1240+
1241+
let m11 = generic_multi_ext::init::<P1, P1>(0);
1242+
let m12 = generic_multi_ext::init::<P1, P2>(0);
1243+
let m12b = generic_multi_ext::init::<P1, P2>(0);
1244+
assert!(matches!(m11.ops, Cow::Borrowed(_)));
1245+
assert_ne!(m11.ops.as_ptr(), m12.ops.as_ptr());
1246+
assert_eq!(m12.ops.as_ptr(), m12b.ops.as_ptr());
1247+
}
1248+
1249+
/// ... and it reaches the realm as an `OpDeclStorage::Static` run, just
1250+
/// like a non-generic extension's table.
1251+
#[test]
1252+
fn generic_extension_reaches_realm_as_static() {
1253+
let mut extensions = vec![generic_table_ext::init::<P1>()];
1254+
let (decls, _methods) = init_ops(&[], &mut extensions);
1255+
let ext_decls = decls
1256+
.iter()
1257+
.find(|d| d.len() == 2)
1258+
.expect("extension ops missing");
1259+
assert!(matches!(ext_decls, OpDeclStorage::Static(_)));
1260+
}
1261+
}
10851262
}

libs/core/ops.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,13 @@ pub struct OpCtxs {
280280
///
281281
/// Declarations that already live in static memory (deno_core's `BUILTIN_OPS`,
282282
/// an extension's method tables, and any extension whose op table is
283-
/// `Cow::Borrowed`) are borrowed rather than copied into the realm. The owned
284-
/// variant covers the cases where a declaration genuinely cannot be static:
285-
/// extension middleware (`Extension::middleware`) rewrites decls at startup,
286-
/// `extension!` builds its op table with `Cow::Owned` at runtime, and method
287-
/// constructors get their name patched from the enclosing `OpMethodDecl`.
283+
/// `Cow::Borrowed` — which `extension!` emits for every extension, generic or
284+
/// not, a generic one getting one static table per monomorphization) are
285+
/// borrowed rather than copied into the realm. The owned variant covers the
286+
/// cases where a declaration genuinely cannot be static: extension middleware
287+
/// (`Extension::middleware`) rewrites decls at startup, `ops_fn` appends to
288+
/// the table, and method constructors get their name patched from the
289+
/// enclosing `OpMethodDecl`.
288290
pub enum OpDeclStorage {
289291
Static(&'static [OpDecl]),
290292
Owned(Vec<OpDecl>),

0 commit comments

Comments
 (0)