Skip to content

Commit

Permalink
feat(ops): implement fast lazy async ops (denoland#16579)
Browse files Browse the repository at this point in the history
Implements fast scheduling of deferred op futures. 

```rs
#[op(fast)]
async fn op_read(
  state: Rc<RefCell<OpState>>,
  rid: ResourceId,
  buf: &mut [u8],
) -> Result<u32, Error> {
  // ...
}
```

The future is scheduled via a fast API call and polled by the event loop
after being woken up by its waker.
  • Loading branch information
littledivy authored Nov 11, 2022
1 parent ff92feb commit 5b9620d
Show file tree
Hide file tree
Showing 24 changed files with 352 additions and 53 deletions.
2 changes: 2 additions & 0 deletions core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ pub mod _ops {
pub use super::error_codes::get_error_code;
pub use super::ops::to_op_result;
pub use super::ops::OpCtx;
pub use super::ops::OpResult;
pub use super::runtime::queue_async_op;
pub use super::runtime::queue_fast_async_op;
pub use super::runtime::V8_WRAPPER_OBJECT_INDEX;
pub use super::runtime::V8_WRAPPER_TYPE_INDEX;
}
Expand Down
16 changes: 16 additions & 0 deletions core/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,22 @@ impl JsRealm {
// TODO(andreubotella): `mod_evaluate`, `load_main_module`, `load_side_module`
}

#[inline]
pub fn queue_fast_async_op(
ctx: &OpCtx,
op: impl Future<Output = (PromiseId, OpId, OpResult)> + 'static,
) {
let runtime_state = match ctx.runtime_state.upgrade() {
Some(rc_state) => rc_state,
// atleast 1 Rc is held by the JsRuntime.
None => unreachable!(),
};

let mut state = runtime_state.borrow_mut();
state.pending_ops.push(OpCall::lazy(op));
state.have_unpolled_ops = true;
}

#[inline]
pub fn queue_async_op(
ctx: &OpCtx,
Expand Down
111 changes: 91 additions & 20 deletions ops/fast_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ pub(crate) fn generate(

// TODO(@littledivy): Use `let..else` on 1.65.0
let output_ty = match &optimizer.fast_result {
// Assert that the optimizer did not set a return type.
//
// @littledivy: This *could* potentially be used to optimize resolving
// promises but knowing the return type at compile time instead of
// serde_v8 serialization.
Some(_) if optimizer.is_async => &FastValue::Void,
Some(ty) => ty,
None if optimizer.is_async => &FastValue::Void,
None => {
return FastImplItems {
impl_and_fn: TokenStream::new(),
Expand Down Expand Up @@ -131,17 +138,31 @@ pub(crate) fn generate(
.collect::<Punctuated<_, Comma>>();

// Apply *hard* optimizer hints.
if optimizer.has_fast_callback_option || optimizer.needs_opstate() {
if optimizer.has_fast_callback_option
|| optimizer.needs_opstate()
|| optimizer.is_async
{
fast_fn_inputs.push(parse_quote! {
fast_api_callback_options: *mut #core::v8::fast_api::FastApiCallbackOptions
});

input_variants.push(q!({ CallbackOptions }));
}

// (recv, p_id, ...)
//
// Optimizer has already set it in the fast parameter variant list.
if optimizer.is_async {
if fast_fn_inputs.is_empty() {
fast_fn_inputs.push(parse_quote! { __promise_id: i32 });
} else {
fast_fn_inputs.insert(0, parse_quote! { __promise_id: i32 });
}
}

let mut output_transforms = q!({});

if optimizer.needs_opstate() {
if optimizer.needs_opstate() || optimizer.is_async {
// Grab the op_state identifier, the first one. ¯\_(ツ)_/¯
let op_state = match idents.first() {
Some(ident) if optimizer.has_opstate_in_parameters() => ident.clone(),
Expand All @@ -155,24 +176,36 @@ pub(crate) fn generate(
// - `data` union is always initialized as the `v8::Local<v8::Value>` variant.
// - deno_core guarantees that `data` is a v8 External pointing to an OpCtx for the
// isolate's lifetime.
let prelude = q!(
Vars {
op_state: &op_state
},
{
let __opts: &mut v8::fast_api::FastApiCallbackOptions =
unsafe { &mut *fast_api_callback_options };
let __ctx = unsafe {
&*(v8::Local::<v8::External>::cast(unsafe { __opts.data.data })
.value() as *const _ops::OpCtx)
};
let op_state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state);
}
);
let prelude = q!({
let __opts: &mut v8::fast_api::FastApiCallbackOptions =
unsafe { &mut *fast_api_callback_options };
let __ctx = unsafe {
&*(v8::Local::<v8::External>::cast(unsafe { __opts.data.data }).value()
as *const _ops::OpCtx)
};
});

pre_transforms.push_tokens(&prelude);
pre_transforms.push_tokens(&match optimizer.is_async {
false => q!(
Vars {
op_state: &op_state
},
{
let op_state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state);
}
),
true => q!(
Vars {
op_state: &op_state
},
{
let op_state = __ctx.state.clone();
}
),
});

if optimizer.returns_result {
if optimizer.returns_result && !optimizer.is_async {
// Magic fallback 🪄
//
// If Result<T, E> is Ok(T), return T as fast value.
Expand All @@ -196,9 +229,42 @@ pub(crate) fn generate(
}
}

if optimizer.is_async {
// Referenced variables are declared in parent block.
let track_async = q!({
let __op_id = __ctx.id;
let __state = ::std::cell::RefCell::borrow(&__ctx.state);
__state.tracker.track_async(__op_id);
});

output_transforms.push_tokens(&track_async);

let queue_future = if optimizer.returns_result {
q!({
let __get_class = __state.get_error_class_fn;
let result = _ops::queue_fast_async_op(__ctx, async move {
let result = result.await;
(
__promise_id,
__op_id,
_ops::to_op_result(__get_class, result),
)
});
})
} else {
q!({
let result = _ops::queue_fast_async_op(__ctx, async move {
let result = result.await;
(__promise_id, __op_id, _ops::OpResult::Ok(result.into()))
});
})
};

output_transforms.push_tokens(&queue_future);
}

if !optimizer.returns_result {
let default_output = q!({ result });

output_transforms.push_tokens(&default_output);
}

Expand Down Expand Up @@ -360,7 +426,7 @@ fn exclude_lifetime_params(
#[cfg(test)]
mod tests {
use super::*;
use crate::Op;
use crate::{Attributes, Op};
use std::path::PathBuf;

#[testing_macros::fixture("optimizer_tests/**/*.rs")]
Expand All @@ -371,8 +437,13 @@ mod tests {
let source =
std::fs::read_to_string(&input).expect("Failed to read test file");

let mut attrs = Attributes::default();
if source.contains("// @test-attr:fast") {
attrs.must_be_fast = true;
}

let item = syn::parse_str(&source).expect("Failed to parse test file");
let mut op = Op::new(item, Default::default());
let mut op = Op::new(item, attrs);
let mut optimizer = Optimizer::new();
if optimizer.analyze(&mut op).is_err() {
// Tested by optimizer::test tests.
Expand Down
1 change: 0 additions & 1 deletion ops/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ impl Op {
Err(BailoutReason::FastUnsupportedParamType) => {
optimizer.fast_compatible = false;
}
Err(err) => return quote!(compile_error!(#err);),
};

let Self {
Expand Down
61 changes: 37 additions & 24 deletions ops/optimizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
use crate::Op;
use pmutil::{q, Quote};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use std::collections::HashMap;
use std::fmt::Debug;
use std::fmt::Formatter;
Expand All @@ -18,22 +17,6 @@ pub(crate) enum BailoutReason {
// Recoverable errors
MustBeSingleSegment,
FastUnsupportedParamType,

FastAsync,
}

impl ToTokens for BailoutReason {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
BailoutReason::FastAsync => {
tokens.extend(quote! { "fast async calls are not supported" });
}
BailoutReason::MustBeSingleSegment
| BailoutReason::FastUnsupportedParamType => {
unreachable!("error not recovered");
}
}
}
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -197,6 +180,8 @@ pub(crate) struct Optimizer {

pub(crate) transforms: HashMap<usize, Transform>,
pub(crate) fast_compatible: bool,

pub(crate) is_async: bool,
}

impl Debug for Optimizer {
Expand All @@ -213,6 +198,8 @@ impl Debug for Optimizer {
writeln!(f, "fast_result: {:?}", self.fast_result)?;
writeln!(f, "fast_parameters: {:?}", self.fast_parameters)?;
writeln!(f, "transforms: {:?}", self.transforms)?;
writeln!(f, "is_async: {}", self.is_async)?;
writeln!(f, "fast_compatible: {}", self.fast_compatible)?;
Ok(())
}
}
Expand All @@ -231,16 +218,18 @@ impl Optimizer {
}

pub(crate) fn analyze(&mut self, op: &mut Op) -> Result<(), BailoutReason> {
if op.is_async && op.attrs.must_be_fast {
// Fast async ops are opt-in as they have a lazy polling behavior.
if op.is_async && !op.attrs.must_be_fast {
self.fast_compatible = false;
return Err(BailoutReason::FastAsync);
return Ok(());
}

if op.attrs.is_v8 || op.is_async {
if op.attrs.is_v8 {
self.fast_compatible = false;
return Ok(());
}

self.is_async = op.is_async;
self.fast_compatible = true;
let sig = &op.item.sig;

Expand All @@ -253,12 +242,29 @@ impl Optimizer {
Signature {
output: ReturnType::Type(_, ty),
..
} => self.analyze_return_type(ty)?,
} if !self.is_async => self.analyze_return_type(ty)?,

// No need to error on the return type for async ops, its OK if
// it's not a fast value.
Signature {
output: ReturnType::Type(_, ty),
..
} => {
let _ = self.analyze_return_type(ty);
// Recover.
self.fast_result = None;
self.fast_compatible = true;
}
};

// The reciever, which we don't actually care about.
self.fast_parameters.push(FastValue::V8Value);

if self.is_async {
// The promise ID.
self.fast_parameters.push(FastValue::I32);
}

// Analyze parameters
for (index, param) in sig.inputs.iter().enumerate() {
self.analyze_param_type(index, param)?;
Expand Down Expand Up @@ -406,7 +412,9 @@ impl Optimizer {
let segment = single_segment(segments)?;
match segment {
// -> Rc<RefCell<T>>
PathSegment { ident, .. } if ident == "RefCell" => {
PathSegment {
ident, arguments, ..
} if ident == "RefCell" => {
if let PathArguments::AngleBracketed(
AngleBracketedGenericArguments { args, .. },
) = arguments
Expand Down Expand Up @@ -543,7 +551,7 @@ fn double_segment(
#[cfg(test)]
mod tests {
use super::*;
use crate::Op;
use crate::{Attributes, Op};
use std::path::PathBuf;
use syn::parse_quote;

Expand Down Expand Up @@ -573,8 +581,13 @@ mod tests {
let expected = std::fs::read_to_string(input.with_extension("expected"))
.expect("Failed to read expected file");

let mut attrs = Attributes::default();
if source.contains("// @test-attr:fast") {
attrs.must_be_fast = true;
}

let item = syn::parse_str(&source).expect("Failed to parse test file");
let mut op = Op::new(item, Default::default());
let mut op = Op::new(item, attrs);
let mut optimizer = Optimizer::new();
if let Err(e) = optimizer.analyze(&mut op) {
let e_str = format!("{:?}", e);
Expand Down
10 changes: 10 additions & 0 deletions ops/optimizer_tests/async_nop.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
=== Optimizer Dump ===
returns_result: false
has_ref_opstate: false
has_rc_opstate: false
has_fast_callback_option: false
fast_result: Some(Void)
fast_parameters: [V8Value, I32]
transforms: {}
is_async: true
fast_compatible: true
Loading

0 comments on commit 5b9620d

Please sign in to comment.