Skip to content

Commit cc5557c

Browse files
authored
feat(ops): zero-cost-if-unused, pluggable metrics for op2 (denoland#244)
Introduces a basic metrics API for op2 that is zero cost when unused, pluggable-per-op and a flexible system that we can add more to in the future. As we remove op1 from the system, we will likely be able to simplify the async portion of this metrics system. Right now we're juggling the old and new op systems.
1 parent e58b23c commit cc5557c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2488
-244
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Use Unix line endings in all text files.
22
* text=auto eol=lf
33
*.png -text
4+
*.out linguist-generated=true

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ path = "examples/http_bench_json_ops/main.rs"
4949
cooked-waker.workspace = true
5050
deno_ast.workspace = true
5151
bencher.workspace = true
52+
pretty_assertions.workspace = true
5253

5354
[[bench]]
5455
name = "ops_sync"

core/extensions.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,14 @@ pub struct OpDecl {
9999
pub is_unstable: bool,
100100
pub is_v8: bool,
101101
pub arg_count: u8,
102-
pub(crate) v8_fn_ptr: OpFnRef,
102+
/// The slow dispatch call. If metrics are disabled, the `v8::Function` is created with this callback.
103+
pub(crate) slow_fn: OpFnRef,
104+
/// The slow dispatch call with metrics enabled. If metrics are enabled, the `v8::Function` is created with this callback.
105+
pub(crate) slow_fn_with_metrics: OpFnRef,
106+
/// The fast dispatch call. If metrics are disabled, the `v8::Function`'s fastcall is created with this callback.
103107
pub(crate) fast_fn: Option<FastFunction>,
108+
/// The fast dispatch call with metrics enabled. If metrics are enabled, the `v8::Function`'s fastcall is created with this callback.
109+
pub(crate) fast_fn_with_metrics: Option<FastFunction>,
104110
}
105111

106112
impl OpDecl {
@@ -112,7 +118,7 @@ impl OpDecl {
112118
is_unstable: bool,
113119
is_v8: bool,
114120
arg_count: u8,
115-
v8_fn_ptr: OpFnRef,
121+
slow_fn: OpFnRef,
116122
fast_fn: Option<FastFunction>,
117123
) -> Self {
118124
Self {
@@ -122,8 +128,35 @@ impl OpDecl {
122128
is_unstable,
123129
is_v8,
124130
arg_count,
125-
v8_fn_ptr,
131+
slow_fn,
132+
slow_fn_with_metrics: slow_fn,
126133
fast_fn,
134+
fast_fn_with_metrics: fast_fn,
135+
}
136+
}
137+
138+
/// For use by internal op implementation only.
139+
#[doc(hidden)]
140+
pub const fn new_internal_op2(
141+
name: &'static str,
142+
is_async: bool,
143+
arg_count: u8,
144+
slow_fn: OpFnRef,
145+
slow_fn_with_metrics: OpFnRef,
146+
fast_fn: Option<FastFunction>,
147+
fast_fn_with_metrics: Option<FastFunction>,
148+
) -> Self {
149+
Self {
150+
name,
151+
enabled: true,
152+
is_async,
153+
is_unstable: false,
154+
is_v8: false,
155+
arg_count,
156+
slow_fn,
157+
slow_fn_with_metrics,
158+
fast_fn,
159+
fast_fn_with_metrics,
127160
}
128161
}
129162

@@ -141,8 +174,10 @@ impl OpDecl {
141174
/// `OpDecl`.
142175
pub const fn with_implementation_from(self, from: &Self) -> Self {
143176
Self {
144-
v8_fn_ptr: from.v8_fn_ptr,
177+
slow_fn: from.slow_fn,
178+
slow_fn_with_metrics: from.slow_fn_with_metrics,
145179
fast_fn: from.fast_fn,
180+
fast_fn_with_metrics: from.fast_fn_with_metrics,
146181
..self
147182
}
148183
}

core/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ pub mod _ops {
140140
pub use super::extensions::OpDecl;
141141
pub use super::ops::to_op_result;
142142
pub use super::ops::OpCtx;
143+
pub use super::ops::OpMetricsEvent;
143144
pub use super::ops::OpResult;
144145
pub use super::runtime::ops::*;
145146
pub use super::runtime::ops_rust_to_v8::*;

core/ops.rs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use v8::Isolate;
2828

2929
pub type PromiseId = i32;
3030
pub type OpId = u16;
31+
pub struct PendingOp(pub PromiseId, pub OpId, pub OpResult, pub bool);
3132

3233
#[pin_project]
3334
pub struct OpCall<F: Future<Output = OpResult>> {
@@ -50,7 +51,7 @@ impl<F: Future<Output = OpResult>> OpCall<F> {
5051
}
5152

5253
impl<F: Future<Output = OpResult>> Future for OpCall<F> {
53-
type Output = (PromiseId, OpId, OpResult);
54+
type Output = PendingOp;
5455

5556
fn poll(
5657
self: std::pin::Pin<&mut Self>,
@@ -59,7 +60,9 @@ impl<F: Future<Output = OpResult>> Future for OpCall<F> {
5960
let promise_id = self.promise_id;
6061
let op_id = self.op_id;
6162
let fut = self.project().fut;
62-
fut.poll(cx).map(move |res| (promise_id, op_id, res))
63+
fut
64+
.poll(cx)
65+
.map(move |res| PendingOp(promise_id, op_id, res, false))
6366
}
6467
}
6568

@@ -120,6 +123,22 @@ pub fn to_op_result<R: Serialize + 'static>(
120123
}
121124
}
122125

126+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
127+
pub enum OpMetricsEvent {
128+
/// Entered an op.
129+
Dispatched,
130+
/// Left an op synchronously.
131+
Completed,
132+
/// Left an op asynchronously.
133+
CompletedAsync,
134+
/// Left an op synchronously with an exception.
135+
Error,
136+
/// Left an op asynchronously with an exception.
137+
ErrorAsync,
138+
}
139+
140+
pub type OpMetricsFn = Rc<dyn Fn(&OpDecl, OpMetricsEvent)>;
141+
123142
/// Per-op context.
124143
///
125144
// Note: We don't worry too much about the size of this struct because it's allocated once per realm, and is
@@ -133,12 +152,14 @@ pub struct OpCtx {
133152
pub decl: Rc<OpDecl>,
134153
pub fast_fn_c_info: Option<NonNull<v8::fast_api::CFunctionInfo>>,
135154
pub runtime_state: Weak<RefCell<JsRuntimeState>>,
155+
pub(crate) metrics_fn: Option<OpMetricsFn>,
136156
pub(crate) context_state: Rc<RefCell<ContextState>>,
137157
/// If the last fast op failed, stores the error to be picked up by the slow op.
138158
pub(crate) last_fast_error: UnsafeCell<Option<AnyError>>,
139159
}
140160

141161
impl OpCtx {
162+
#[allow(clippy::too_many_arguments)]
142163
pub(crate) fn new(
143164
id: OpId,
144165
isolate: *mut Isolate,
@@ -147,10 +168,21 @@ impl OpCtx {
147168
state: Rc<RefCell<OpState>>,
148169
runtime_state: Weak<RefCell<JsRuntimeState>>,
149170
get_error_class_fn: GetErrorClassFn,
171+
metrics_fn: Option<OpMetricsFn>,
150172
) -> Self {
151173
let mut fast_fn_c_info = None;
152174

153-
if let Some(fast_fn) = &decl.fast_fn {
175+
// If we want metrics for this function, create the fastcall `CFunctionInfo` from the metrics
176+
// `FastFunction`. For some extremely fast ops, the parameter list may change for the metrics
177+
// version and require a slightly different set of arguments (for example, it may need the fastcall
178+
// callback information to get the `OpCtx`).
179+
let fast_fn = if metrics_fn.is_some() {
180+
&decl.fast_fn
181+
} else {
182+
&decl.fast_fn_with_metrics
183+
};
184+
185+
if let Some(fast_fn) = fast_fn {
154186
let args = CTypeInfo::new_from_slice(fast_fn.args);
155187
let ret = CTypeInfo::new(fast_fn.return_type);
156188

@@ -169,7 +201,7 @@ impl OpCtx {
169201
fast_fn_c_info = Some(c_fn);
170202
}
171203

172-
OpCtx {
204+
Self {
173205
id,
174206
state,
175207
get_error_class_fn,
@@ -179,9 +211,14 @@ impl OpCtx {
179211
fast_fn_c_info,
180212
last_fast_error: UnsafeCell::new(None),
181213
isolate,
214+
metrics_fn,
182215
}
183216
}
184217

218+
pub fn metrics_enabled(&self) -> bool {
219+
self.metrics_fn.is_some()
220+
}
221+
185222
/// This takes the last error from an [`OpCtx`], assuming that no other code anywhere
186223
/// can hold a `&mut` to the last_fast_error field.
187224
///

core/runtime/bindings.rs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,34 @@ pub(crate) fn external_references(
4343
});
4444

4545
for ctx in ops {
46-
let ctx_ptr = ctx as *const OpCtx as _;
47-
references.push(v8::ExternalReference { pointer: ctx_ptr });
48-
references.push(v8::ExternalReference {
49-
function: ctx.decl.v8_fn_ptr,
50-
});
51-
if let Some(fast_fn) = &ctx.decl.fast_fn {
46+
if ctx.metrics_enabled() {
47+
let ctx_ptr = ctx as *const OpCtx as _;
48+
references.push(v8::ExternalReference { pointer: ctx_ptr });
5249
references.push(v8::ExternalReference {
53-
pointer: fast_fn.function as _,
50+
function: ctx.decl.slow_fn_with_metrics,
5451
});
52+
if let Some(fast_fn) = &ctx.decl.fast_fn_with_metrics {
53+
references.push(v8::ExternalReference {
54+
pointer: fast_fn.function as _,
55+
});
56+
references.push(v8::ExternalReference {
57+
pointer: ctx.fast_fn_c_info.unwrap().as_ptr() as _,
58+
});
59+
}
60+
} else {
61+
let ctx_ptr = ctx as *const OpCtx as _;
62+
references.push(v8::ExternalReference { pointer: ctx_ptr });
5563
references.push(v8::ExternalReference {
56-
pointer: ctx.fast_fn_c_info.unwrap().as_ptr() as _,
64+
function: ctx.decl.slow_fn,
5765
});
66+
if let Some(fast_fn) = &ctx.decl.fast_fn {
67+
references.push(v8::ExternalReference {
68+
pointer: fast_fn.function as _,
69+
});
70+
references.push(v8::ExternalReference {
71+
pointer: ctx.fast_fn_c_info.unwrap().as_ptr() as _,
72+
});
73+
}
5874
}
5975
}
6076

@@ -190,12 +206,22 @@ fn op_ctx_function<'s>(
190206
let v8name =
191207
v8::String::new_external_onebyte_static(scope, op_ctx.decl.name.as_bytes())
192208
.unwrap();
209+
210+
let (slow_fn, fast_fn) = if op_ctx.metrics_enabled() {
211+
(
212+
op_ctx.decl.slow_fn_with_metrics,
213+
op_ctx.decl.fast_fn_with_metrics,
214+
)
215+
} else {
216+
(op_ctx.decl.slow_fn, op_ctx.decl.fast_fn)
217+
};
218+
193219
let builder: v8::FunctionBuilder<v8::FunctionTemplate> =
194-
v8::FunctionTemplate::builder_raw(op_ctx.decl.v8_fn_ptr)
220+
v8::FunctionTemplate::builder_raw(slow_fn)
195221
.data(external.into())
196222
.length(op_ctx.decl.arg_count as i32);
197223

198-
let template = if let Some(fast_function) = &op_ctx.decl.fast_fn {
224+
let template = if let Some(fast_function) = &fast_fn {
199225
builder.build_fast(
200226
scope,
201227
fast_function,

core/runtime/jsrealm.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ use crate::modules::ModuleId;
1010
use crate::modules::ModuleLoadId;
1111
use crate::modules::ModuleMap;
1212
use crate::ops::OpCtx;
13+
use crate::ops::PendingOp;
1314
use crate::runtime::JsRuntimeState;
1415
use crate::JsRuntime;
15-
use crate::OpId;
16-
use crate::OpResult;
17-
use crate::PromiseId;
1816
use anyhow::Error;
1917
use deno_unsync::JoinSet;
2018
use futures::channel::oneshot;
@@ -76,7 +74,7 @@ pub(crate) struct ContextState {
7674
pending_dyn_mod_evaluate: Vec<DynImportModEvaluate>,
7775
pub(crate) pending_mod_evaluate: Option<ModEvaluate>,
7876
pub(crate) unrefed_ops: HashSet<i32, BuildHasherDefault<IdentityHasher>>,
79-
pub(crate) pending_ops: JoinSet<(PromiseId, OpId, OpResult)>,
77+
pub(crate) pending_ops: JoinSet<PendingOp>,
8078
// We don't explicitly re-read this prop but need the slice to live alongside
8179
// the context
8280
pub(crate) op_ctxs: Box<[OpCtx]>,

core/runtime/jsruntime.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use super::bindings;
44
use super::jsrealm::JsRealmInner;
5+
use super::ops::dispatch_metrics_async;
56
use super::snapshot_util;
67
use crate::error::exception_to_err_result;
78
use crate::error::generic_error;
@@ -361,6 +362,8 @@ fn v8_init(
361362
v8::V8::initialize();
362363
}
363364

365+
pub type OpMetricsFactoryFn = Box<dyn Fn(&OpDecl) -> Option<OpMetricsFn>>;
366+
364367
#[derive(Default)]
365368
pub struct RuntimeOptions {
366369
/// Source map reference for errors.
@@ -377,6 +380,10 @@ pub struct RuntimeOptions {
377380
/// executed tries to load modules.
378381
pub module_loader: Option<Rc<dyn ModuleLoader>>,
379382

383+
/// Provide a function that may optionally provide a metrics collector
384+
/// for a given op.
385+
pub op_metrics_fn: Option<OpMetricsFactoryFn>,
386+
380387
/// JsRuntime extensions, not to be confused with ES modules.
381388
/// Only ops registered by extensions will be initialized. If you need
382389
/// to execute JS code from extensions, pass source files in `js` or `esm`
@@ -594,6 +601,8 @@ impl JsRuntime {
594601
.into_iter()
595602
.enumerate()
596603
.map(|(id, decl)| {
604+
let metrics_fn =
605+
options.op_metrics_fn.as_ref().and_then(|f| (f)(&decl));
597606
OpCtx::new(
598607
id as u16,
599608
std::ptr::null_mut(),
@@ -602,6 +611,7 @@ impl JsRuntime {
602611
op_state.clone(),
603612
weak.clone(),
604613
options.get_error_class_fn.unwrap_or(&|_| "Error"),
614+
metrics_fn,
605615
)
606616
})
607617
.collect::<Vec<_>>()
@@ -832,6 +842,7 @@ impl JsRuntime {
832842
op_ctx.state.clone(),
833843
op_ctx.runtime_state.clone(),
834844
op_ctx.get_error_class_fn,
845+
op_ctx.metrics_fn.clone(),
835846
)
836847
})
837848
.collect();
@@ -1965,7 +1976,7 @@ impl JsRuntime {
19651976
break;
19661977
};
19671978
// TODO(mmastrac): If this task is really errored, things could be pretty bad
1968-
let (promise_id, op_id, resp) = item.unwrap();
1979+
let PendingOp(promise_id, op_id, resp, metrics_event) = item.unwrap();
19691980
state
19701981
.borrow()
19711982
.op_state
@@ -1975,7 +1986,22 @@ impl JsRuntime {
19751986
context_state.unrefed_ops.remove(&promise_id);
19761987
dispatched_ops |= true;
19771988
args.push(v8::Integer::new(scope, promise_id).into());
1978-
args.push(match resp.to_v8(scope) {
1989+
let was_error = matches!(resp, OpResult::Err(_));
1990+
let res = resp.to_v8(scope);
1991+
if metrics_event {
1992+
if res.is_ok() && !was_error {
1993+
dispatch_metrics_async(
1994+
&context_state.op_ctxs[op_id as usize],
1995+
OpMetricsEvent::CompletedAsync,
1996+
);
1997+
} else {
1998+
dispatch_metrics_async(
1999+
&context_state.op_ctxs[op_id as usize],
2000+
OpMetricsEvent::ErrorAsync,
2001+
);
2002+
}
2003+
}
2004+
args.push(match res {
19792005
Ok(v) => v,
19802006
Err(e) => OpResult::Err(OpError::new(&|_| "TypeError", e.into()))
19812007
.to_v8(scope)

0 commit comments

Comments
 (0)