Repository: denoland/deno_core
Pull request #269 (2023-10-15 05:53:46)

feat(ops): fast async(lazy) and async(deferred)
This doesn't move the needle much on async ops that are submitted and immediately awaited upon, but it does offer slighly better latency when taking a promise from an async op and then waiting on it after performing other work.

One of the big refactorings in this PR is ensuring that async and sync fast ops share the same code path. The same refactoring can be applied to slow ops, but that would be slightly too much to tackle in this PR.

```
running 14 tests
test baseline                             ... bench:       1,215 ns/iter (+/- 55)
test baseline_promise                     ... bench:      49,198 ns/iter (+/- 1,829)
test baseline_sync                        ... bench:      59,163 ns/iter (+/- 2,600)
test bench_op_async_void                  ... bench:     144,511 ns/iter (+/- 4,112)
test bench_op_async_void_deferred         ... bench:   2,069,403 ns/iter (+/- 51,417)
test bench_op_async_void_deferred_nofast  ... bench:   2,074,620 ns/iter (+/- 94,785)
test bench_op_async_void_deferred_return  ... bench:   2,100,433 ns/iter (+/- 72,988)
test bench_op_async_void_lazy             ... bench:   1,972,197 ns/iter (+/- 73,343)
test bench_op_async_void_lazy_nofast      ... bench:   1,946,581 ns/iter (+/- 57,475)
test bench_op_async_yield                 ... bench:   2,035,129 ns/iter (+/- 71,406)
test bench_op_async_yield_deferred        ... bench:   2,064,232 ns/iter (+/- 523,829)
test bench_op_async_yield_deferred_nofast ... bench:   2,040,228 ns/iter (+/- 81,427)
test bench_op_async_yield_lazy            ... bench:   3,083,485 ns/iter (+/- 99,416)
test bench_op_async_yield_lazy_nofast     ... bench:   3,015,587 ns/iter (+/- 68,670)
```



Patch:
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 1bc39ce..a57a205 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -56,3 +56,9 @@ name = "ops_sync"
 path = "benches/ops/sync.rs"
 harness = false
 required-features = ["unsafe_runtime_options"]
+
+[[bench]]
+name = "ops_async"
+path = "benches/ops/async.rs"
+harness = false
+required-features = ["unsafe_runtime_options"]
diff --git a/core/benches/ops/async.rs b/core/benches/ops/async.rs
new file mode 100644
index 0000000..da75239
--- /dev/null
+++ b/core/benches/ops/async.rs
@@ -0,0 +1,223 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use bencher::*;
+use deno_core::error::generic_error;
+use deno_core::*;
+use std::ffi::c_void;
+
+deno_core::extension!(
+  testing,
+  ops = [
+    op_void,
+    op_make_external,
+    op_async_void,
+    op_async_void_lazy,
+    op_async_void_lazy_nofast,
+    op_async_void_deferred,
+    op_async_void_deferred_nofast,
+    op_async_void_deferred_return,
+    op_async_yield,
+    op_async_yield_lazy,
+    op_async_yield_lazy_nofast,
+    op_async_yield_deferred,
+    op_async_yield_deferred_nofast,
+  ],
+);
+
+#[op2(fast)]
+pub fn op_void() {}
+
+#[op2(fast)]
+pub fn op_make_external() -> *const c_void {
+  std::ptr::null()
+}
+
+#[op2(async)]
+pub async fn op_async_void() {}
+
+#[op2(async)]
+pub async fn op_async_yield() {
+  tokio::task::yield_now().await
+}
+
+#[op2(async(lazy), fast)]
+pub async fn op_async_yield_lazy() {
+  tokio::task::yield_now().await
+}
+
+#[op2(async(lazy), nofast)]
+pub async fn op_async_yield_lazy_nofast() {
+  tokio::task::yield_now().await
+}
+
+#[op2(async(deferred), fast)]
+pub async fn op_async_yield_deferred() {
+  tokio::task::yield_now().await
+}
+
+#[op2(async(deferred), nofast)]
+pub async fn op_async_yield_deferred_nofast() {
+  tokio::task::yield_now().await
+}
+
+#[op2(async(lazy), fast)]
+pub async fn op_async_void_lazy() {}
+
+#[op2(async(lazy), nofast)]
+pub async fn op_async_void_lazy_nofast() {}
+
+#[op2(async(deferred), fast)]
+pub async fn op_async_void_deferred_return() -> u32 {
+  1
+}
+
+#[op2(async(deferred), fast)]
+pub async fn op_async_void_deferred() {}
+
+#[op2(async(deferred), nofast)]
+pub async fn op_async_void_deferred_nofast() {}
+
+fn bench_op(
+  b: &mut Bencher,
+  count: usize,
+  op: &str,
+  arg_count: usize,
+  call: &str,
+) {
+  #[cfg(not(feature = "unsafe_runtime_options"))]
+  unreachable!(
+    "This benchmark must be run with --features=unsafe_runtime_options"
+  );
+
+  let tokio = tokio::runtime::Builder::new_current_thread()
+    .build()
+    .unwrap();
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    extensions: vec![testing::init_ops_and_esm()],
+    // We need to feature gate this here to prevent IDE errors
+    #[cfg(feature = "unsafe_runtime_options")]
+    unsafe_expose_natives_and_gc: true,
+    ..Default::default()
+  });
+  let err_mapper =
+    |err| generic_error(format!("{op} test failed ({call}): {err:?}"));
+
+  let args = (0..arg_count)
+    .map(|n| format!("arg{n}"))
+    .collect::<Vec<_>>()
+    .join(", ");
+
+  let mut harness = include_str!("async_harness.js").to_owned();
+  for (key, value) in [
+    ("PERCENT", "%"),
+    ("CALL", call),
+    ("COUNT", &format!("{count}")),
+    ("ARGS", &args.to_string()),
+    ("OP", op),
+  ] {
+    harness = harness.replace(&format!("__{key}__"), value);
+  }
+
+  // Prime the optimizer
+  runtime
+    .execute_script("", FastString::Owned(harness.into()))
+    .map_err(err_mapper)
+    .unwrap();
+  let guard = tokio.enter();
+  let run = runtime.execute_script("", ascii_str!("run()")).unwrap();
+  let bench = tokio.block_on(runtime.resolve_value(run)).unwrap();
+  let mut scope = runtime.handle_scope();
+  let bench: v8::Local<v8::Function> =
+    v8::Local::new(&mut scope, bench).try_into().unwrap();
+  let bench = v8::Global::new(&mut scope, bench);
+  drop(scope);
+  drop(guard);
+  b.iter(move || {
+    tokio.block_on(async {
+      let guard = tokio.enter();
+      runtime.call_and_await(&bench).await.unwrap();
+      drop(guard);
+    });
+  });
+}
+
+const BENCH_COUNT: usize = 1000;
+
+/// Tests the overhead of execute_script.
+fn baseline(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_async_void", 0, "accum += __index__;");
+}
+
+/// Tests the overhead of execute_script with a promise.
+fn baseline_promise(b: &mut Bencher) {
+  bench_op(
+    b,
+    BENCH_COUNT,
+    "op_async_void",
+    0,
+    "await Promise.resolve(null);",
+  );
+}
+
+/// Tests the overhead of execute_script, but also returns a value so we can make sure things are
+/// working.
+fn bench_op_async_void_deferred_return(b: &mut Bencher) {
+  bench_op(
+    b,
+    BENCH_COUNT,
+    "op_async_void_deferred_return",
+    0,
+    "accum += await op_async_void_deferred_return();",
+  );
+}
+
+macro_rules! bench_void {
+  ($bench:ident, $op:ident) => {
+    fn $bench(b: &mut Bencher) {
+      bench_op(
+        b,
+        BENCH_COUNT,
+        stringify!($op),
+        0,
+        concat!("await ", stringify!($op), "()"),
+      );
+    }
+  };
+}
+
+bench_void!(baseline_sync, op_void);
+bench_void!(bench_op_async_yield, op_async_yield);
+bench_void!(bench_op_async_yield_lazy, op_async_yield_lazy);
+bench_void!(bench_op_async_yield_lazy_nofast, op_async_yield_lazy_nofast);
+bench_void!(bench_op_async_yield_deferred, op_async_yield_deferred);
+bench_void!(
+  bench_op_async_yield_deferred_nofast,
+  op_async_yield_deferred_nofast
+);
+bench_void!(bench_op_async_void, op_async_void);
+bench_void!(bench_op_async_void_lazy, op_async_void_lazy);
+bench_void!(bench_op_async_void_lazy_nofast, op_async_void_lazy_nofast);
+bench_void!(bench_op_async_void_deferred, op_async_void_deferred);
+bench_void!(
+  bench_op_async_void_deferred_nofast,
+  op_async_void_deferred_nofast
+);
+
+benchmark_group!(
+  benches,
+  baseline,
+  baseline_promise,
+  baseline_sync,
+  bench_op_async_yield,
+  bench_op_async_yield_lazy,
+  bench_op_async_yield_lazy_nofast,
+  bench_op_async_yield_deferred,
+  bench_op_async_yield_deferred_nofast,
+  bench_op_async_void,
+  bench_op_async_void_lazy,
+  bench_op_async_void_lazy_nofast,
+  bench_op_async_void_deferred,
+  bench_op_async_void_deferred_nofast,
+  bench_op_async_void_deferred_return,
+);
+
+benchmark_main!(benches);
diff --git a/core/benches/ops/async_harness.js b/core/benches/ops/async_harness.js
new file mode 100644
index 0000000..daf1ce5
--- /dev/null
+++ b/core/benches/ops/async_harness.js
@@ -0,0 +1,41 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// deno-lint-ignore-file no-unused-vars, prefer-const, require-await
+
+// This harness is dynamically generated for each individual bench run.
+async function run() {
+  const LARGE_STRING_1000000 = "*".repeat(1000000);
+  const LARGE_STRING_1000 = "*".repeat(1000);
+  const LARGE_STRING_UTF8_1000000 = "\u1000".repeat(1000000);
+  const LARGE_STRING_UTF8_1000 = "\u1000".repeat(1000);
+  const BUFFER = new Uint8Array(1024);
+  const ARRAYBUFFER = new ArrayBuffer(1024);
+  const { __OP__: op } = Deno.core.ensureFastOps();
+  const { op_make_external } = Deno.core.ensureFastOps();
+  const EXTERNAL = op_make_external();
+
+  // TODO(mmastrac): Because of current v8 limitations, these ops are not always fast unless we do this.
+  // The reason is not entirely clear.
+  function __OP__(__ARGS__) {
+    return op(__ARGS__);
+  }
+
+  let accum = 0;
+  let __index__ = 0;
+  __PERCENT__PrepareFunctionForOptimization(__OP__);
+  __CALL__;
+  __PERCENT__OptimizeFunctionOnNextCall(__OP__);
+  __CALL__;
+
+  async function bench() {
+    let accum = 0;
+    for (let __index__ = 0; __index__ < __COUNT__; __index__++) __CALL__;
+    return accum;
+  }
+
+  __PERCENT__PrepareFunctionForOptimization(bench);
+  await bench();
+  __PERCENT__OptimizeFunctionOnNextCall(bench);
+  await bench();
+
+  return bench;
+}
diff --git a/core/benches/ops/sync.rs b/core/benches/ops/sync.rs
index 87be491..14d8e3a 100644
--- a/core/benches/ops/sync.rs
+++ b/core/benches/ops/sync.rs
@@ -1,14 +1,15 @@
-use std::borrow::Cow;
-
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use bencher::*;
 use deno_core::error::generic_error;
 use deno_core::*;
+use std::borrow::Cow;
 use std::ffi::c_void;
 
 deno_core::extension!(
   testing,
   ops = [
     op_void,
+    op_void_nofast,
     op_u32,
     op_option_u32,
     op_string,
@@ -28,6 +29,7 @@ deno_core::extension!(
     op_bigint_return,
     op_external,
     op_external_nofast,
+    op_buffer_old,
     op_buffer,
     op_buffer_jsbuffer,
     op_buffer_nofast,
@@ -42,6 +44,9 @@ deno_core::extension!(
 #[op2(fast)]
 pub fn op_void() {}
 
+#[op2(nofast)]
+pub fn op_void_nofast() {}
+
 #[op2(fast)]
 pub fn op_u32() -> u32 {
   1
@@ -123,6 +128,9 @@ pub fn op_external(_input: *const c_void) {}
 #[op2(nofast)]
 pub fn op_external_nofast(_input: *const c_void) {}
 
+#[op]
+pub fn op_buffer_old(_buffer: &[u8]) {}
+
 #[op2(fast)]
 pub fn op_buffer(#[buffer] _buffer: &[u8]) {}
 
@@ -162,46 +170,21 @@ fn bench_op(
     .collect::<Vec<_>>()
     .join(", ");
 
+  let mut harness = include_str!("sync_harness.js").to_owned();
+  for (key, value) in [
+    ("PERCENT", "%"),
+    ("CALL", call),
+    ("COUNT", &format!("{count}")),
+    ("ARGS", &args.to_string()),
+    ("OP", op),
+    ("INIT", if op.contains("bigint") { "0n" } else { "0" }),
+  ] {
+    harness = harness.replace(&format!("__{key}__"), value);
+  }
+
   // Prime the optimizer
   runtime
-    .execute_script(
-      "",
-      FastString::Owned(
-        format!(
-          r"
-const LARGE_STRING_1000000 = '*'.repeat(1000000);
-const LARGE_STRING_1000 = '*'.repeat(1000);
-const LARGE_STRING_UTF8_1000000 = '\u1000'.repeat(1000000);
-const LARGE_STRING_UTF8_1000 = '\u1000'.repeat(1000);
-const BUFFER = new Uint8Array(1024);
-const ARRAYBUFFER = new ArrayBuffer(1024);
-const {{ {op}: op }} = Deno.core.ensureFastOps();
-const {{ op_make_external }} = Deno.core.ensureFastOps();
-const EXTERNAL = op_make_external();
-function {op}({args}) {{
-  op({args});
-}}
-let accum = 0;
-let __index__ = 0;
-%PrepareFunctionForOptimization({op});
-{call};
-%OptimizeFunctionOnNextCall({op});
-{call};
-
-function bench() {{
-  let accum = 0;
-  for (let __index__ = 0; __index__ < {count}; __index__++) {{ {call} }}
-  return accum;
-}}
-%PrepareFunctionForOptimization(bench);
-bench();
-%OptimizeFunctionOnNextCall(bench);
-bench();
-        "
-        )
-        .into(),
-      ),
-    )
+    .execute_script("", FastString::Owned(harness.into()))
     .map_err(err_mapper)
     .unwrap();
   let bench = runtime.execute_script("", ascii_str!("bench")).unwrap();
@@ -227,6 +210,16 @@ fn bench_op_void(b: &mut Bencher) {
   bench_op(b, BENCH_COUNT, "op_void", 0, "op_void()");
 }
 
+/// A void function with no return value.
+fn bench_op_void_2x(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_void", 0, "op_void(); op_void()");
+}
+
+/// A void function with no return value.
+fn bench_op_void_nofast(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_void_nofast", 0, "op_void_nofast();");
+}
+
 /// A function with a numeric return value.
 fn bench_op_u32(b: &mut Bencher) {
   bench_op(b, BENCH_COUNT, "op_u32", 0, "accum += op_u32();");
@@ -447,6 +440,10 @@ fn bench_op_external_nofast(b: &mut Bencher) {
   );
 }
 
+fn bench_op_buffer_old(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_buffer_old", 1, "op_buffer_old(BUFFER)");
+}
+
 fn bench_op_buffer(b: &mut Bencher) {
   bench_op(b, BENCH_COUNT, "op_buffer", 1, "op_buffer(BUFFER)");
 }
@@ -485,6 +482,8 @@ benchmark_group!(
   benches,
   baseline,
   bench_op_void,
+  bench_op_void_2x,
+  bench_op_void_nofast,
   bench_op_u32,
   bench_op_option_u32,
   bench_op_string_bytestring,
@@ -513,6 +512,7 @@ benchmark_group!(
   bench_op_v8_isolate_nofast,
   bench_op_external,
   bench_op_external_nofast,
+  bench_op_buffer_old,
   bench_op_buffer,
   bench_op_buffer_jsbuffer,
   bench_op_buffer_nofast,
diff --git a/core/benches/ops/sync_harness.js b/core/benches/ops/sync_harness.js
new file mode 100644
index 0000000..b457c4b
--- /dev/null
+++ b/core/benches/ops/sync_harness.js
@@ -0,0 +1,36 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// deno-lint-ignore-file no-unused-vars, prefer-const
+
+// This harness is dynamically generated for each individual bench run.
+const LARGE_STRING_1000000 = "*".repeat(1000000);
+const LARGE_STRING_1000 = "*".repeat(1000);
+const LARGE_STRING_UTF8_1000000 = "\u1000".repeat(1000000);
+const LARGE_STRING_UTF8_1000 = "\u1000".repeat(1000);
+const BUFFER = new Uint8Array(1024);
+const ARRAYBUFFER = new ArrayBuffer(1024);
+const { __OP__: op } = Deno.core.ensureFastOps();
+const { op_make_external } = Deno.core.ensureFastOps();
+const EXTERNAL = op_make_external();
+
+// TODO(mmastrac): Because of current v8 limitations, these ops are not always fast unless we do this.
+// The reason is not entirely clear.
+function __OP__(__ARGS__) {
+  return op(__ARGS__);
+}
+
+let accum = __INIT__;
+let __index__ = 0;
+__PERCENT__PrepareFunctionForOptimization(__OP__);
+__CALL__;
+__PERCENT__OptimizeFunctionOnNextCall(__OP__);
+__CALL__;
+
+function bench() {
+  let accum = __INIT__;
+  for (let __index__ = 0; __index__ < __COUNT__; __index__++) __CALL__;
+  return accum;
+}
+__PERCENT__PrepareFunctionForOptimization(bench);
+bench();
+__PERCENT__OptimizeFunctionOnNextCall(bench);
+bench();
diff --git a/core/inspector.rs b/core/inspector.rs
index eaedd31..a9e5349 100644
--- a/core/inspector.rs
+++ b/core/inspector.rs
@@ -801,7 +801,7 @@ impl LocalInspectorSession {
     }
   }
 
-  async fn receive_from_v8_session(&mut self) {
+  pub async fn receive_from_v8_session(&mut self) {
     let inspector_msg = self.v8_session_rx.next().await.unwrap();
     if let InspectorMsgKind::Message(msg_id) = inspector_msg.kind {
       let message: serde_json::Value =
diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs
index fc4286b..bb46470 100644
--- a/core/ops_builtin.rs
+++ b/core/ops_builtin.rs
@@ -12,7 +12,6 @@ use crate::OpState;
 use crate::Resource;
 use anyhow::Error;
 use bytes::BytesMut;
-use deno_ops::op;
 use deno_ops::op2;
 use std::cell::RefCell;
 use std::io::stderr;
@@ -118,14 +117,12 @@ pub async fn op_error_async() -> Result<(), Error> {
   Err(Error::msg("error"))
 }
 
-// TODO(bartlomieju): migration to op2 blocked by deferred support
-#[op(deferred)]
+#[op2(async(deferred), core, fast)]
 pub async fn op_error_async_deferred() -> Result<(), Error> {
   Err(Error::msg("error"))
 }
 
-// TODO(bartlomieju): migration to op2 blocked by deferred support
-#[op(deferred)]
+#[op2(async(deferred), core, fast)]
 pub async fn op_void_async_deferred() {}
 
 /// Remove a resource from the resource table.
diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs
index 9e67297..8ae1e4e 100644
--- a/core/runtime/ops.rs
+++ b/core/runtime/ops.rs
@@ -141,6 +141,7 @@ pub fn queue_async_op<'s>(
 pub fn map_async_op_infallible<R: 'static>(
   ctx: &OpCtx,
   lazy: bool,
+  deferred: bool,
   promise_id: i32,
   op: impl Future<Output = R> + 'static,
   rv_map: for<'r> fn(
@@ -173,16 +174,22 @@ pub fn map_async_op_infallible<R: 'static>(
   // of ops. If we can figure out a way around this, we can remove this call to boxed_local and save a malloc per future.
   let mut pinned = op.map(move |res| (promise_id, id, res)).boxed_local();
 
-  match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
-    Poll::Pending => {}
-    Poll::Ready(res) => {
-      if ctx.metrics_enabled() {
-        dispatch_metrics_async(ctx, OpMetricsEvent::Completed);
+  let pinned =
+    match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
+      Poll::Pending => pinned,
+      Poll::Ready(res) => {
+        // TODO(mmastrac): optimize this so we don't double-allocate
+        if deferred {
+          ready(res).boxed_local()
+        } else {
+          if ctx.metrics_enabled() {
+            dispatch_metrics_async(ctx, OpMetricsEvent::Completed);
+          }
+          ctx.state.borrow_mut().tracker.track_async_completed(ctx.id);
+          return Some(res.2);
+        }
       }
-      ctx.state.borrow_mut().tracker.track_async_completed(ctx.id);
-      return Some(res.2);
-    }
-  }
+    };
 
   ctx
     .context_state
@@ -203,6 +210,7 @@ pub fn map_async_op_infallible<R: 'static>(
 pub fn map_async_op_fallible<R: 'static, E: Into<Error> + 'static>(
   ctx: &OpCtx,
   lazy: bool,
+  deferred: bool,
   promise_id: i32,
   op: impl Future<Output = Result<R, E>> + 'static,
   rv_map: for<'r> fn(
@@ -239,20 +247,26 @@ pub fn map_async_op_fallible<R: 'static, E: Into<Error> + 'static>(
   // of ops. If we can figure out a way around this, we can remove this call to boxed_local and save a malloc per future.
   let mut pinned = op.map(move |res| (promise_id, id, res)).boxed_local();
 
-  match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
-    Poll::Pending => {}
-    Poll::Ready(res) => {
-      if ctx.metrics_enabled() {
-        if res.2.is_err() {
-          dispatch_metrics_async(ctx, OpMetricsEvent::Error);
+  let pinned =
+    match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
+      Poll::Pending => pinned,
+      Poll::Ready(res) => {
+        // TODO(mmastrac): optimize this so we don't double-allocate
+        if deferred {
+          ready(res).boxed_local()
         } else {
-          dispatch_metrics_async(ctx, OpMetricsEvent::Completed);
+          if ctx.metrics_enabled() {
+            if res.2.is_err() {
+              dispatch_metrics_async(ctx, OpMetricsEvent::Error);
+            } else {
+              dispatch_metrics_async(ctx, OpMetricsEvent::Completed);
+            }
+          }
+          ctx.state.borrow_mut().tracker.track_async_completed(ctx.id);
+          return Some(res.2);
         }
       }
-      ctx.state.borrow_mut().tracker.track_async_completed(ctx.id);
-      return Some(res.2);
-    }
-  }
+    };
 
   let get_class = ctx.get_error_class_fn;
   ctx.context_state.borrow_mut().pending_ops.spawn(pinned.map(
@@ -808,6 +822,8 @@ mod tests {
       op_buffer_ptr,
       op_buffer_slice_32,
       op_buffer_ptr_32,
+      op_buffer_slice_f64,
+      op_buffer_ptr_f64,
       op_buffer_slice_unsafe_callback,
       op_buffer_copy,
       op_buffer_bytesmut,
@@ -828,7 +844,10 @@ mod tests {
       op_async_sleep,
       op_async_sleep_impl,
       op_async_sleep_error,
+      op_async_deferred_error,
+      op_async_deferred_success,
       op_async_lazy_error,
+      op_async_lazy_success,
       op_async_result_impl,
       op_async_state_rc,
       op_async_buffer,
@@ -1701,12 +1720,45 @@ mod tests {
     }
   }
 
+  #[op2(core, fast)]
+  pub fn op_buffer_slice_f64(
+    #[buffer] input: &[f64],
+    #[number] inlen: usize,
+    #[buffer] output: &mut [f64],
+    #[number] outlen: usize,
+  ) {
+    assert_eq!(inlen, input.len());
+    assert_eq!(outlen, output.len());
+    if inlen > 0 && outlen > 0 {
+      output[0] = input[0];
+    }
+  }
+
+  #[op2(core, fast)]
+  pub fn op_buffer_ptr_f64(
+    #[buffer] input: *const f64,
+    #[number] inlen: usize,
+    #[buffer] output: *mut f64,
+    #[number] outlen: usize,
+  ) {
+    if inlen > 0 && outlen > 0 {
+      // SAFETY: for test
+      unsafe { std::ptr::write(output, std::ptr::read(input)) }
+    }
+  }
+
   #[tokio::test]
   pub async fn test_op_buffer_slice() -> Result<(), Box<dyn std::error::Error>>
   {
     for (op, op_ptr, arr, size) in [
       ("op_buffer_slice", "op_buffer_ptr", "Uint8Array", 1),
       ("op_buffer_slice_32", "op_buffer_ptr_32", "Uint32Array", 4),
+      (
+        "op_buffer_slice_f64",
+        "op_buffer_ptr_f64",
+        "Float64Array",
+        8,
+      ),
     ] {
       // Zero-length buffers
       run_test2(
@@ -2215,16 +2267,54 @@ mod tests {
     Ok(())
   }
 
-  #[op2(async(lazy), core)]
+  #[op2(async(deferred), core, fast)]
+  pub async fn op_async_deferred_success() -> Result<u32, Error> {
+    Ok(42)
+  }
+
+  #[op2(async(deferred), core, fast)]
+  pub async fn op_async_deferred_error() -> Result<(), Error> {
+    bail!("whoops")
+  }
+
+  #[tokio::test]
+  pub async fn test_op_async_deferred() -> Result<(), Box<dyn std::error::Error>>
+  {
+    run_async_test(
+      1000,
+      "op_async_deferred_success",
+      "assert(await op_async_deferred_success() == 42)",
+    )
+    .await?;
+    run_async_test(
+      1000,
+      "op_async_deferred_error",
+      "try { await op_async_deferred_error(); assert(false) } catch (e) {{ assertErrorContains(e, 'whoops') }}",
+    )
+    .await?;
+    Ok(())
+  }
+
+  #[op2(async(lazy), core, fast)]
+  pub async fn op_async_lazy_success() -> Result<u32, Error> {
+    Ok(42)
+  }
+
+  #[op2(async(lazy), core, fast)]
   pub async fn op_async_lazy_error() -> Result<(), Error> {
     bail!("whoops")
   }
 
   #[tokio::test]
-  pub async fn test_op_async_lazy_error(
-  ) -> Result<(), Box<dyn std::error::Error>> {
+  pub async fn test_op_async_lazy() -> Result<(), Box<dyn std::error::Error>> {
     run_async_test(
-      5,
+      1000,
+      "op_async_lazy_success",
+      "assert(await op_async_lazy_success() == 42)",
+    )
+    .await?;
+    run_async_test(
+      1000,
       "op_async_lazy_error",
       "try { await op_async_lazy_error(); assert(false) } catch (e) {{ assertErrorContains(e, 'whoops') }}",
     )
diff --git a/ops/op2/README.md b/ops/op2/README.md
index c29ed1d..b10c3a4 100644
--- a/ops/op2/README.md
+++ b/ops/op2/README.md
@@ -39,6 +39,38 @@ op system.
 fn op_xyz(promise_id: i32, /* ... */) -> Option<X> {}
 ```
 
+### Eager `async` calls: `async`
+
+By default, `async` functions are eagerly polled, which reduces the latency of
+the call dramatically if the async function is ready to return a value
+immediately.
+
+### `async(lazy)`
+
+`async` calls may be marked as `lazy`, which allows the runtime to defer polling
+the op until a later time. The submission of an `async(lazy)` op might be
+faster, but the latency will be higher for ops that would have been ready on the
+first poll.
+
+**NOTE**: You _may_ need to use this to get the maximum performance out of a set
+of async tasks, but it should only be used alongside careful benchmarking. In
+some cases it will allow for higher throughput at the expense of latency.
+
+Lazy `async` calls _may_ be fastcalls, though the resolution will still happen
+on a slow path.
+
+### `async(deferred)`
+
+`async` calls may also be marked as `deferred`, which will allow the runtime to
+poll the op immediately, but any results that are ready are deferred until a
+later run of the event loop.
+
+**NOTE**: This is almost certainly not what you want to use and should only be
+used if you really know what you are doing.
+
+Lazy `async(deferred)` calls _may_ be fastcalls, though the resolution will
+still happen on a slow path.
+
 # Parameters
 
 <!-- START ARGS -->
diff --git a/ops/op2/config.rs b/ops/op2/config.rs
index ec14c76..44675e2 100644
--- a/ops/op2/config.rs
+++ b/ops/op2/config.rs
@@ -17,6 +17,8 @@ pub(crate) struct MacroConfig {
   pub r#async: bool,
   /// Marks an lazy async function (async must also be true)
   pub async_lazy: bool,
+  /// Marks an deferred async function (async must also be true)
+  pub async_deferred: bool,
 }
 
 impl MacroConfig {
@@ -65,6 +67,9 @@ impl MacroConfig {
       } else if flag == "async(lazy)" {
         config.r#async = true;
         config.async_lazy = true;
+      } else if flag == "async(deferred)" {
+        config.r#async = true;
+        config.async_deferred = true;
       } else {
         return Err(Op2Error::InvalidAttribute(flag));
       }
@@ -74,10 +79,14 @@ impl MacroConfig {
     if config.fast && config.nofast {
       return Err(Op2Error::InvalidAttributeCombination("fast", "nofast"));
     }
-    if config.fast && (config.r#async && !config.async_lazy) {
+    if config.fast
+      && (config.r#async && !config.async_lazy && !config.async_deferred)
+    {
       return Err(Op2Error::InvalidAttributeCombination("fast", "async"));
     }
-    if config.nofast && (config.r#async && !config.async_lazy) {
+    if config.nofast
+      && (config.r#async && !config.async_lazy && !config.async_deferred)
+    {
       return Err(Op2Error::InvalidAttributeCombination("nofast", "async"));
     }
 
diff --git a/ops/op2/dispatch_async.rs b/ops/op2/dispatch_async.rs
index 0b334af..c271d4e 100644
--- a/ops/op2/dispatch_async.rs
+++ b/ops/op2/dispatch_async.rs
@@ -1,10 +1,6 @@
-use super::V8MappingError;
-use super::V8SignatureMappingError;
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use super::config::MacroConfig;
-use super::dispatch_slow::call;
-use super::dispatch_slow::extract_arg;
-use super::dispatch_slow::from_arg;
+use super::dispatch_slow::generate_dispatch_slow_call;
 use super::dispatch_slow::return_value_infallible;
 use super::dispatch_slow::return_value_result;
 use super::dispatch_slow::return_value_v8_value;
@@ -18,22 +14,16 @@ use super::generator_state::gs_quote;
 use super::generator_state::GeneratorState;
 use super::signature::ParsedSignature;
 use super::signature::RetVal;
+use super::V8MappingError;
+use super::V8SignatureMappingError;
 use proc_macro2::TokenStream;
 use quote::quote;
 
-fn map_async_return_type(
+pub(crate) fn map_async_return_type(
   generator_state: &mut GeneratorState,
   ret_val: &RetVal,
 ) -> Result<(TokenStream, TokenStream, TokenStream), V8MappingError> {
-  let return_value = match ret_val {
-    RetVal::Future(r) | RetVal::ResultFuture(r) => {
-      return_value_v8_value(generator_state, r)?
-    }
-    RetVal::FutureResult(r) | RetVal::ResultFutureResult(r) => {
-      return_value_v8_value(generator_state, r)?
-    }
-    RetVal::Infallible(_) | RetVal::Result(_) => return Err("an async return"),
-  };
+  let return_value = return_value_v8_value(generator_state, ret_val.arg())?;
   let (mapper, return_value_immediate) = match ret_val {
     RetVal::Future(r) | RetVal::ResultFuture(r) => (
       quote!(map_async_op_infallible),
@@ -55,25 +45,8 @@ pub(crate) fn generate_dispatch_async(
 ) -> Result<TokenStream, V8SignatureMappingError> {
   let mut output = TokenStream::new();
 
-  // Collect virtual arguments in a deferred list that we compute at the very end. This allows us to borrow
-  // the scope/opstate in the intermediate stages.
-  let mut args = TokenStream::new();
-  let mut deferred = TokenStream::new();
-
-  // Promise ID is the first arg
-  let mut input_index = 1;
-
-  for (index, arg) in signature.args.iter().enumerate() {
-    let arg_mapped = from_arg(generator_state, index, arg)
-      .map_err(|s| V8SignatureMappingError::NoArgMapping(s, arg.clone()))?;
-    if arg.is_virtual() {
-      deferred.extend(arg_mapped);
-    } else {
-      args.extend(extract_arg(generator_state, index, input_index));
-      args.extend(arg_mapped);
-      input_index += 1;
-    }
-  }
+  // input_index = 1 as promise ID is the first arg
+  let args = generate_dispatch_slow_call(generator_state, signature, 1)?;
 
   // Always need context and args
   // TODO(mmastrac): Do we?
@@ -89,8 +62,6 @@ pub(crate) fn generate_dispatch_async(
       },
     )?;
 
-  args.extend(deferred);
-  args.extend(call(generator_state));
   output.extend(gs_quote!(generator_state(deno_core, result, opctx) => {
     if #opctx.metrics_enabled() {
       #deno_core::_ops::dispatch_metrics_async(&#opctx, #deno_core::_ops::OpMetricsEvent::Dispatched);
@@ -100,10 +71,8 @@ pub(crate) fn generate_dispatch_async(
     };
   }));
 
-  if matches!(
-    signature.ret_val,
-    RetVal::ResultFuture(_) | RetVal::ResultFutureResult(_)
-  ) {
+  // TODO(mmastrac): we should save this unwrapped result
+  if signature.ret_val.unwrap_result().is_some() {
     let exception = throw_exception(generator_state);
     output.extend(gs_quote!(generator_state(deno_core, opctx, result) => {
       let #result = match #result {
@@ -119,16 +88,27 @@ pub(crate) fn generate_dispatch_async(
     }));
   }
 
-  let lazy = config.async_lazy;
-  output.extend(gs_quote!(generator_state(promise_id, fn_args, result, opctx, scope, deno_core) => {
-    let #promise_id = #deno_core::_ops::to_i32_option(&#fn_args.get(0)).unwrap_or_default();
-    if let Some(#result) = #deno_core::_ops::#mapper(#opctx, #lazy, #promise_id, #result, |#scope, #result| {
-      #return_value
-    }) {
-      // Eager poll returned a value
-      #return_value_immediate
-    }
-  }));
+  if config.async_lazy || config.async_deferred {
+    let lazy = config.async_lazy;
+    let deferred = config.async_deferred;
+    output.extend(gs_quote!(generator_state(promise_id, fn_args, result, opctx, scope, deno_core) => {
+      let #promise_id = #deno_core::_ops::to_i32_option(&#fn_args.get(0)).unwrap_or_default();
+      // Lazy and deferred results will always return None
+      #deno_core::_ops::#mapper(#opctx, #lazy, #deferred, #promise_id, #result, |#scope, #result| {
+        #return_value
+      });
+    }));
+  } else {
+    output.extend(gs_quote!(generator_state(promise_id, fn_args, result, opctx, scope, deno_core) => {
+      let #promise_id = #deno_core::_ops::to_i32_option(&#fn_args.get(0)).unwrap_or_default();
+      if let Some(#result) = #deno_core::_ops::#mapper(#opctx, false, false, #promise_id, #result, |#scope, #result| {
+        #return_value
+      }) {
+        // Eager poll returned a value
+        #return_value_immediate
+      }
+    }));
+  }
 
   let with_scope = if generator_state.needs_scope {
     with_scope(generator_state)
diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs
index c4319bb..9ad9e93 100644
--- a/ops/op2/dispatch_fast.rs
+++ b/ops/op2/dispatch_fast.rs
@@ -1,4 +1,5 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use super::config::MacroConfig;
 use super::dispatch_shared::byte_slice_to_buffer;
 use super::dispatch_shared::fast_api_typed_array_to_buffer;
 use super::dispatch_shared::v8_intermediate_to_arg;
@@ -14,18 +15,153 @@ use super::signature::NumericArg;
 use super::signature::NumericFlag;
 use super::signature::ParsedSignature;
 use super::signature::RefType;
-use super::signature::RetVal;
 use super::signature::Special;
 use super::signature::Strings;
 use super::V8MappingError;
 use super::V8SignatureMappingError;
+use crate::op2::dispatch_async::map_async_return_type;
 use proc_macro2::Ident;
 use proc_macro2::TokenStream;
 use quote::format_ident;
 use quote::quote;
-use std::iter::zip;
 use syn::Type;
 
+#[derive(Clone)]
+pub(crate) enum FastArg {
+  /// The argument is virtual and only has an output name.
+  Virtual {
+    name_out: Ident,
+    arg: Arg,
+  },
+  Actual {
+    arg_type: V8FastCallType,
+    name_in: Ident,
+    name_out: Ident,
+    arg: Arg,
+  },
+  CallbackOptions,
+  PromiseId,
+}
+
+#[derive(Clone)]
+pub(crate) struct FastSignature {
+  // The parsed arguments
+  pub args: Vec<FastArg>,
+  // The parsed return value
+  pub ret_val: V8FastCallType,
+  has_fast_api_callback_options: bool,
+}
+
+impl FastSignature {
+  /// Collect the output of `quote_type` for all actual arguments, used to populate the fast function
+  /// definition struct.
+  pub(crate) fn input_types(&self) -> Vec<TokenStream> {
+    self
+      .args
+      .iter()
+      .filter_map(|arg| match arg {
+        FastArg::PromiseId => Some(V8FastCallType::I32.quote_type()),
+        FastArg::CallbackOptions => {
+          Some(V8FastCallType::CallbackOptions.quote_type())
+        }
+        FastArg::Actual { arg_type, .. } => Some(arg_type.quote_type()),
+        _ => None,
+      })
+      .collect()
+  }
+
+  pub(crate) fn input_args(
+    &self,
+    generator_state: &GeneratorState,
+  ) -> Vec<(Ident, TokenStream)> {
+    self
+      .args
+      .iter()
+      .filter_map(|arg| match arg {
+        FastArg::CallbackOptions => Some((
+          generator_state.fast_api_callback_options.clone(),
+          V8FastCallType::CallbackOptions
+            .quote_rust_type(&generator_state.deno_core),
+        )),
+        FastArg::PromiseId => Some((
+          generator_state.promise_id.clone(),
+          V8FastCallType::I32.quote_rust_type(&generator_state.deno_core),
+        )),
+        FastArg::Actual {
+          arg_type, name_in, ..
+        } => Some((
+          format_ident!("{name_in}"),
+          arg_type.quote_rust_type(&generator_state.deno_core),
+        )),
+        _ => None,
+      })
+      .collect()
+  }
+
+  pub(crate) fn call_args(
+    &self,
+    generator_state: &mut GeneratorState,
+  ) -> Result<Vec<TokenStream>, V8SignatureMappingError> {
+    let mut call_args = vec![];
+    for arg in &self.args {
+      match arg {
+        FastArg::Actual { arg, name_out, .. }
+        | FastArg::Virtual { name_out, arg } => call_args.push(
+          map_v8_fastcall_arg_to_arg(generator_state, name_out, arg).map_err(
+            |s| V8SignatureMappingError::NoArgMapping(s, arg.clone()),
+          )?,
+        ),
+        FastArg::CallbackOptions | FastArg::PromiseId => {}
+      }
+    }
+    Ok(call_args)
+  }
+
+  pub(crate) fn call_names(&self) -> Vec<Ident> {
+    let mut call_names = vec![];
+    for arg in &self.args {
+      match arg {
+        FastArg::Actual { name_out, .. }
+        | FastArg::Virtual { name_out, .. } => {
+          call_names.push(name_out.clone())
+        }
+        FastArg::CallbackOptions | FastArg::PromiseId => {}
+      }
+    }
+    call_names
+  }
+
+  pub(crate) fn get_fast_function_def(
+    &self,
+    generator_state: &mut GeneratorState,
+    fast_function: &Ident,
+  ) -> TokenStream {
+    let input_types = self.input_types();
+    let output_type = self.ret_val.quote_ctype();
+
+    gs_quote!(generator_state(deno_core) => {
+      use #deno_core::v8::fast_api::Type;
+      use #deno_core::v8::fast_api::CType;
+      #deno_core::v8::fast_api::FastFunction::new_with_bigint(
+        &[ Type::V8Value, #( #input_types ),* ],
+        #output_type,
+        Self::#fast_function as *const ::std::ffi::c_void
+      )
+    })
+  }
+
+  pub(crate) fn ensure_fast_api_callback_options(&mut self) {
+    if !self.has_fast_api_callback_options {
+      self.has_fast_api_callback_options = true;
+      self.args.push(FastArg::CallbackOptions);
+    }
+  }
+
+  fn insert_promise_id(&mut self) {
+    self.args.insert(0, FastArg::PromiseId)
+  }
+}
+
 #[allow(unused)]
 #[derive(Debug, Default, PartialEq, Clone)]
 pub(crate) enum V8FastCallType {
@@ -142,41 +278,39 @@ impl V8FastCallType {
   }
 }
 
-pub fn generate_dispatch_fast(
-  generator_state: &mut GeneratorState,
+// TODO(mmastrac): see note about index_in below
+#[allow(clippy::explicit_counter_loop)]
+pub(crate) fn get_fast_signature(
   signature: &ParsedSignature,
-) -> Result<
-  Option<(TokenStream, TokenStream, TokenStream)>,
-  V8SignatureMappingError,
-> {
-  enum Input<'a> {
-    Concrete(V8FastCallType),
-    Virtual(&'a Arg),
-  }
-
-  // Async not supported for fastcalls yet
-  if signature.ret_val.is_async() {
-    return Ok(None);
-  }
-
-  let mut inputs = vec![];
-  for arg in &signature.args {
-    let Some(fv) = map_arg_to_v8_fastcall_type(arg).map_err(|s| V8SignatureMappingError::NoArgMapping(s, arg.clone()))? else {
+) -> Result<Option<FastSignature>, V8SignatureMappingError> {
+  let mut args = vec![];
+  let mut index_in = 0;
+  for (index_out, arg) in signature.args.iter().cloned().enumerate() {
+    let Some(arg_type) = map_arg_to_v8_fastcall_type(&arg).map_err(|s| V8SignatureMappingError::NoArgMapping(s, arg.clone()))? else {
       return Ok(None);
     };
-    if fv == V8FastCallType::Virtual {
-      inputs.push(Input::Virtual(arg));
+    let name_out = format_ident!("arg{index_out}");
+    // TODO(mmastrac): this could be a valid arg, but we need to update has_fast_api_callback_options below
+    assert!(arg_type != V8FastCallType::CallbackOptions);
+    if arg_type == V8FastCallType::Virtual {
+      args.push(FastArg::Virtual { arg, name_out });
     } else {
-      inputs.push(Input::Concrete(fv));
+      args.push(FastArg::Actual {
+        arg,
+        arg_type,
+        name_in: format_ident!("arg{index_in}"),
+        name_out,
+      });
     }
+    // TODO(mmastrac): these fastcall indexes should not use the same index as the outparam
+    index_in += 1;
   }
 
-  let ret_val = match &signature.ret_val {
-    RetVal::Infallible(arg) => arg,
-    RetVal::Result(arg) => arg,
-    _ => todo!(),
+  let ret_val = if signature.ret_val.is_async() {
+    &Arg::Void
+  } else {
+    signature.ret_val.arg()
   };
-
   let output = match map_retval_to_v8_fastcall_type(ret_val).map_err(|s| {
     V8SignatureMappingError::NoRetValMapping(s, signature.ret_val.clone())
   })? {
@@ -184,75 +318,108 @@ pub fn generate_dispatch_fast(
     Some(rv) => rv,
   };
 
-  // Collect the names and types for the fastcall and the underlying op call
-  let mut fastcall_names = vec![];
-  let mut fastcall_types = vec![];
-  let mut input_types = vec![];
-  let mut call_names = vec![];
-  let mut call_args = vec![];
-  for (i, (input, arg)) in zip(inputs, &signature.args).enumerate() {
-    let name = format_ident!("arg{i}");
-    if let Input::Concrete(fv) = input {
-      fastcall_names.push(name.clone());
-      fastcall_types.push(fv.quote_rust_type(&generator_state.deno_core));
-      input_types.push(fv.quote_type());
-    }
-
-    call_names.push(name.clone());
-    call_args.push(
-      map_v8_fastcall_arg_to_arg(generator_state, &name, arg)
-        .map_err(|s| V8SignatureMappingError::NoArgMapping(s, arg.clone()))?,
-    )
+  Ok(Some(FastSignature {
+    args,
+    ret_val: output,
+    has_fast_api_callback_options: false,
+  }))
+}
+
+/// Sheds the error in a `Result<T, E>` as an early return, leaving just the `T` and requesting
+/// that v8 re-call the slow function to throw the error.
+pub(crate) fn generate_fast_result_early_exit(
+  generator_state: &mut GeneratorState,
+) -> TokenStream {
+  generator_state.needs_fast_api_callback_options = true;
+  generator_state.needs_fast_opctx = true;
+  gs_quote!(generator_state(fast_api_callback_options, opctx, result) => {
+    let #result = match #result {
+      Ok(#result) => #result,
+      Err(err) => {
+        // FASTCALL FALLBACK: This is where we set the errors for the slow-call error pickup path. There
+        // is no code running between this and the other FASTCALL FALLBACK comment, except some V8 code
+        // required to perform the fallback process. This is why the below call is safe.
+
+        // The reason we need to do this is because V8 does not allow exceptions to be thrown from the
+        // fast call. Instead, you are required to set the fallback flag, which indicates to V8 that it
+        // should re-call the slow version of the function. Technically the slow call should perform the
+        // same operation and then throw the same error (because it should be idempotent), but in our
+        // case we stash the error and pick it up on the slow path before doing any work.
+
+        // TODO(mmastrac): We should allow an #[op] flag to re-perform slow calls without the error path when
+        // the method is performance sensitive.
+
+        // SAFETY: We guarantee that OpCtx has no mutable references once ops are live and being called,
+        // allowing us to perform this one little bit of mutable magic.
+        unsafe { #opctx.unsafely_set_last_error_for_ops_only(err); }
+        #fast_api_callback_options.fallback = true;
+
+        // SAFETY: All fast return types have zero as a valid value
+        return unsafe { std::mem::zeroed() };
+      }
+    };
+  })
+}
+
+pub(crate) fn generate_dispatch_fast(
+  config: &MacroConfig,
+  generator_state: &mut GeneratorState,
+  signature: &ParsedSignature,
+) -> Result<
+  Option<(TokenStream, TokenStream, TokenStream)>,
+  V8SignatureMappingError,
+> {
+  // async(lazy) can be fast
+  if signature.ret_val.is_async()
+    && !config.async_lazy
+    && !config.async_deferred
+  {
+    return Ok(None);
   }
 
-  let GeneratorState {
-    result,
-    fast_function,
-    fast_function_metrics,
-    needs_fast_opctx,
-    needs_fast_api_callback_options,
-    needs_fast_js_runtime_state,
-    ..
-  } = generator_state;
+  let Some(mut fastsig) = get_fast_signature(signature)? else {
+    return Ok(None);
+  };
 
-  let handle_error = match signature.ret_val {
-    RetVal::Infallible(_) => quote!(),
-    RetVal::Result(_) => {
-      *needs_fast_api_callback_options = true;
-      *needs_fast_opctx = true;
-      gs_quote!(generator_state(fast_api_callback_options, opctx) => {
-        let #result = match #result {
-          Ok(#result) => #result,
-          Err(err) => {
-            // FASTCALL FALLBACK: This is where we set the errors for the slow-call error pickup path. There
-            // is no code running between this and the other FASTCALL FALLBACK comment, except some V8 code
-            // required to perform the fallback process. This is why the below call is safe.
-
-            // The reason we need to do this is because V8 does not allow exceptions to be thrown from the
-            // fast call. Instead, you are required to set the fallback flag, which indicates to V8 that it
-            // should re-call the slow version of the function. Technically the slow call should perform the
-            // same operation and then throw the same error (because it should be idempotent), but in our
-            // case we stash the error and pick it up on the slow path before doing any work.
-
-            // TODO(mmastrac): We should allow an #[op] flag to re-perform slow calls without the error path when
-            // the method is performance sensitive.
-
-            // SAFETY: We guarantee that OpCtx has no mutable references once ops are live and being called,
-            // allowing us to perform this one little bit of mutable magic.
-            unsafe { #opctx.unsafely_set_last_error_for_ops_only(err); }
-            #fast_api_callback_options.fallback = true;
-
-            // SAFETY: All fast return types have zero as a valid value
-            return unsafe { std::mem::zeroed() };
-          }
-        };
-      })
-    }
-    _ => todo!(),
+  // TODO(mmastrac): we should save this unwrapped result
+  let handle_error = match signature.ret_val.unwrap_result() {
+    Some(_) => generate_fast_result_early_exit(generator_state),
+    _ => quote!(),
   };
 
-  let with_js_runtime_state = if *needs_fast_js_runtime_state {
-    *needs_fast_opctx = true;
+  if signature.ret_val.is_async() {
+    fastsig.insert_promise_id();
+  }
+
+  // Note that this triggers needs_* values in generator_state
+  let call_args = fastsig.call_args(generator_state)?;
+
+  let handle_result = if signature.ret_val.is_async() {
+    generator_state.needs_fast_opctx = true;
+    let (return_value, mapper, _) =
+      map_async_return_type(generator_state, &signature.ret_val).map_err(
+        |s| {
+          V8SignatureMappingError::NoRetValMapping(s, signature.ret_val.clone())
+        },
+      )?;
+
+    let lazy = config.async_lazy;
+    let deferred = config.async_deferred;
+    gs_quote!(generator_state(promise_id, result, opctx, scope, deno_core) => {
+      // Lazy results will always return None
+      #deno_core::_ops::#mapper(#opctx, #lazy, #deferred, #promise_id, #result, |#scope, #result| {
+        #return_value
+      });
+    })
+  } else {
+    gs_quote!(generator_state(result) => {
+      // Result may need a simple cast (eg: SMI u32->i32)
+      #result as _
+    })
+  };
+
+  let with_js_runtime_state = if generator_state.needs_fast_js_runtime_state {
+    generator_state.needs_fast_opctx = true;
     gs_quote!(generator_state(js_runtime_state, opctx) => {
       let #js_runtime_state = std::rc::Weak::upgrade(&#opctx.runtime_state).unwrap();
     })
@@ -260,8 +427,8 @@ pub fn generate_dispatch_fast(
     quote!()
   };
 
-  let with_opctx = if *needs_fast_opctx {
-    *needs_fast_api_callback_options = true;
+  let with_opctx = if generator_state.needs_fast_opctx {
+    generator_state.needs_fast_api_callback_options = true;
     gs_quote!(generator_state(deno_core, opctx, fast_api_callback_options) => {
       let #opctx = unsafe {
         &*(#deno_core::v8::Local::<#deno_core::v8::External>::cast(unsafe { #fast_api_callback_options.data.data }).value()
@@ -272,17 +439,13 @@ pub fn generate_dispatch_fast(
     quote!()
   };
 
-  let mut fastcall_metrics_names = fastcall_names.clone();
-  let mut fastcall_metrics_types = fastcall_types.clone();
-  let mut input_types_metrics = input_types.clone();
-
-  let with_fast_api_callback_options = if *needs_fast_api_callback_options {
-    input_types.push(V8FastCallType::CallbackOptions.quote_type());
-    fastcall_types.push(
-      V8FastCallType::CallbackOptions
-        .quote_rust_type(&generator_state.deno_core),
-    );
-    fastcall_names.push(generator_state.fast_api_callback_options.clone());
+  let mut fastsig_metrics = fastsig.clone();
+  fastsig_metrics.ensure_fast_api_callback_options();
+
+  let with_fast_api_callback_options = if generator_state
+    .needs_fast_api_callback_options
+  {
+    fastsig.ensure_fast_api_callback_options();
     gs_quote!(generator_state(fast_api_callback_options) => {
       let #fast_api_callback_options = unsafe { &mut *#fast_api_callback_options };
     })
@@ -290,42 +453,26 @@ pub fn generate_dispatch_fast(
     quote!()
   };
 
-  input_types_metrics.push(V8FastCallType::CallbackOptions.quote_type());
-  fastcall_metrics_types.push(
-    V8FastCallType::CallbackOptions.quote_rust_type(&generator_state.deno_core),
-  );
-  fastcall_metrics_names
-    .push(generator_state.fast_api_callback_options.clone());
-
-  let output_type = output.quote_ctype();
-
-  let fast_definition = gs_quote!(generator_state(deno_core) => {
-    use #deno_core::v8::fast_api::Type;
-    use #deno_core::v8::fast_api::CType;
-    #deno_core::v8::fast_api::FastFunction::new_with_bigint(
-      &[ Type::V8Value, #( #input_types ),* ],
-      #output_type,
-      Self::#fast_function as *const ::std::ffi::c_void
-    )
-  });
-
-  let fast_definition_metrics = gs_quote!(generator_state(deno_core) => {
-    use #deno_core::v8::fast_api::Type;
-    use #deno_core::v8::fast_api::CType;
-    #deno_core::v8::fast_api::FastFunction::new_with_bigint(
-      &[ Type::V8Value, #( #input_types_metrics ),* ],
-      #output_type,
-      Self::#fast_function_metrics as *const ::std::ffi::c_void
-    )
-  });
+  let fast_function = generator_state.fast_function.clone();
+  let fast_definition =
+    fastsig.get_fast_function_def(generator_state, &fast_function);
+  let fast_function = generator_state.fast_function_metrics.clone();
+  let fast_definition_metrics =
+    fastsig_metrics.get_fast_function_def(generator_state, &fast_function);
 
-  // Ensure that we have the same types in the fast function definition as we do in the signature
-  debug_assert!(fastcall_types.len() == input_types.len());
+  let output_type = fastsig.ret_val.quote_rust_type(&generator_state.deno_core);
 
-  let output_type = output.quote_rust_type(&generator_state.deno_core);
   // We don't want clippy to trigger warnings on number of arguments of the fastcall
   // function -- these will still trigger on our normal call function, however.
-  let fast_fn = gs_quote!(generator_state(deno_core, fast_api_callback_options) => {
+  let call_names = fastsig.call_names();
+  let (fastcall_metrics_names, fastcall_metrics_types): (Vec<_>, Vec<_>) =
+    fastsig_metrics
+      .input_args(generator_state)
+      .into_iter()
+      .unzip();
+  let (fastcall_names, fastcall_types): (Vec<_>, Vec<_>) =
+    fastsig.input_args(generator_state).into_iter().unzip();
+  let fast_fn = gs_quote!(generator_state(deno_core, result, fast_api_callback_options, fast_function, fast_function_metrics) => {
     #[allow(clippy::too_many_arguments)]
     fn #fast_function_metrics(
       this: #deno_core::v8::Local<#deno_core::v8::Object>,
@@ -356,8 +503,7 @@ pub fn generate_dispatch_fast(
         Self::call(#(#call_names),*)
       };
       #handle_error
-      // Result may need a simple cast (eg: SMI u32->i32)
-      #result as _
+      #handle_result
     }
   });
 
@@ -587,6 +733,14 @@ fn map_arg_to_v8_fastcall_type(
       _,
       BufferSource::TypedArray,
     ) => V8FastCallType::Uint32Array,
+    Arg::Buffer(
+      BufferType::Slice(.., NumericArg::f64)
+      | BufferType::Ptr(.., NumericArg::f64)
+      | BufferType::Vec(.., NumericArg::f64)
+      | BufferType::BoxSlice(.., NumericArg::f64),
+      _,
+      BufferSource::TypedArray,
+    ) => V8FastCallType::Float64Array,
     Arg::Buffer(_, _, BufferSource::TypedArray) => V8FastCallType::Uint8Array,
     // Virtual OpState arguments
     Arg::RcRefCell(Special::OpState)
diff --git a/ops/op2/dispatch_shared.rs b/ops/op2/dispatch_shared.rs
index 06e704b..78c0495 100644
--- a/ops/op2/dispatch_shared.rs
+++ b/ops/op2/dispatch_shared.rs
@@ -87,22 +87,36 @@ pub fn v8slice_to_buffer(
     BufferType::V8Slice(..) => {
       quote!(let #arg_ident = #arg_ident;)
     }
-    BufferType::Slice(RefType::Ref, NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Slice(
+      RefType::Ref,
+      NumericArg::u8 | NumericArg::u32 | NumericArg::f64,
+    ) => {
       quote!(let #arg_ident = #v8slice.as_ref();)
     }
-    BufferType::Slice(RefType::Mut, NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Slice(
+      RefType::Mut,
+      NumericArg::u8 | NumericArg::u32 | NumericArg::f64,
+    ) => {
       quote!(let #arg_ident = #v8slice.as_mut();)
     }
-    BufferType::Ptr(RefType::Ref, NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Ptr(
+      RefType::Ref,
+      NumericArg::u8 | NumericArg::u32 | NumericArg::f64,
+    ) => {
       quote!(let #arg_ident = if #v8slice.len() == 0 { std::ptr::null() } else { #v8slice.as_ref().as_ptr() };)
     }
-    BufferType::Ptr(RefType::Mut, NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Ptr(
+      RefType::Mut,
+      NumericArg::u8 | NumericArg::u32 | NumericArg::f64,
+    ) => {
       quote!(let #arg_ident = if #v8slice.len() == 0 { std::ptr::null_mut() } else { #v8slice.as_mut().as_mut_ptr() };)
     }
-    BufferType::Vec(NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Vec(NumericArg::u8 | NumericArg::u32 | NumericArg::f64) => {
       quote!(let #arg_ident = #v8slice.to_vec();)
     }
-    BufferType::BoxSlice(NumericArg::u8 | NumericArg::u32) => {
+    BufferType::BoxSlice(
+      NumericArg::u8 | NumericArg::u32 | NumericArg::f64,
+    ) => {
       quote!(let #arg_ident = #v8slice.to_boxed_slice();)
     }
     BufferType::Bytes => {
@@ -123,16 +137,21 @@ pub fn byte_slice_to_buffer(
   buffer: BufferType,
 ) -> Result<TokenStream, V8MappingError> {
   let res = match buffer {
-    BufferType::Slice(_, NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Slice(
+      _,
+      NumericArg::u8 | NumericArg::u32 | NumericArg::f64,
+    ) => {
       quote!(let #arg_ident = #buf;)
     }
-    BufferType::Ptr(_, NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Ptr(_, NumericArg::u8 | NumericArg::u32 | NumericArg::f64) => {
       quote!(let #arg_ident = if #buf.len() == 0 { ::std::ptr::null_mut() } else { #buf.as_mut_ptr() as _ };)
     }
-    BufferType::Vec(NumericArg::u8 | NumericArg::u32) => {
+    BufferType::Vec(NumericArg::u8 | NumericArg::u32 | NumericArg::f64) => {
       quote!(let #arg_ident = #buf.to_vec();)
     }
-    BufferType::BoxSlice(NumericArg::u8 | NumericArg::u32) => {
+    BufferType::BoxSlice(
+      NumericArg::u8 | NumericArg::u32 | NumericArg::f64,
+    ) => {
       quote!(let #arg_ident = #buf.to_vec().into_boxed_slice();)
     }
     BufferType::Bytes => {
diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs
index 217b541..19c4857 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -29,6 +29,33 @@ use quote::format_ident;
 use quote::quote;
 use syn::Type;
 
+pub(crate) fn generate_dispatch_slow_call(
+  generator_state: &mut GeneratorState,
+  signature: &ParsedSignature,
+  mut input_index: usize,
+) -> Result<TokenStream, V8SignatureMappingError> {
+  // Collect virtual arguments in a deferred list that we compute at the very end. This allows us to borrow
+  // the scope/opstate in the intermediate stages.
+  let mut args = TokenStream::new();
+  let mut deferred = TokenStream::new();
+
+  for (index, arg) in signature.args.iter().enumerate() {
+    let arg_mapped = from_arg(generator_state, index, arg)
+      .map_err(|s| V8SignatureMappingError::NoArgMapping(s, arg.clone()))?;
+    if arg.is_virtual() {
+      deferred.extend(arg_mapped);
+    } else {
+      args.extend(extract_arg(generator_state, index, input_index));
+      args.extend(arg_mapped);
+      input_index += 1;
+    }
+  }
+
+  args.extend(deferred);
+  args.extend(call(generator_state));
+  Ok(args)
+}
+
 pub(crate) fn generate_dispatch_slow(
   config: &MacroConfig,
   generator_state: &mut GeneratorState,
@@ -54,26 +81,8 @@ pub(crate) fn generate_dispatch_slow(
     });
   }
 
-  // Collect virtual arguments in a deferred list that we compute at the very end. This allows us to borrow
-  // the scope/opstate in the intermediate stages.
-  let mut args = TokenStream::new();
-  let mut deferred = TokenStream::new();
-  let mut input_index = 0;
+  let args = generate_dispatch_slow_call(generator_state, signature, 0)?;
 
-  for (index, arg) in signature.args.iter().enumerate() {
-    let arg_mapped = from_arg(generator_state, index, arg)
-      .map_err(|s| V8SignatureMappingError::NoArgMapping(s, arg.clone()))?;
-    if arg.is_virtual() {
-      deferred.extend(arg_mapped);
-    } else {
-      args.extend(extract_arg(generator_state, index, input_index));
-      args.extend(arg_mapped);
-      input_index += 1;
-    }
-  }
-
-  args.extend(deferred);
-  args.extend(call(generator_state));
   output.extend(gs_quote!(generator_state(result) => (let #result = {
     #args
   };)));
diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs
index 8104434..5f4d299 100644
--- a/ops/op2/mod.rs
+++ b/ops/op2/mod.rs
@@ -200,7 +200,7 @@ fn generate_op2(
   }
 
   let (fast_definition, fast_definition_metrics, fast_fn) =
-    match generate_dispatch_fast(&mut generator_state, &signature)? {
+    match generate_dispatch_fast(&config, &mut generator_state, &signature)? {
       Some((fast_definition, fast_metrics_definition, fast_fn)) => {
         if !config.fast && !config.nofast {
           return Err(Op2Error::ShouldBeFast);
diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs
index 33a2be1..6e3e90c 100644
--- a/ops/op2/signature.rs
+++ b/ops/op2/signature.rs
@@ -8,6 +8,7 @@ use quote::quote;
 use quote::ToTokens;
 use quote::TokenStreamExt;
 use std::collections::BTreeMap;
+
 use strum::IntoEnumIterator;
 use strum::IntoStaticStr;
 use strum_macros::EnumIter;
@@ -626,6 +627,29 @@ impl RetVal {
       Future(..) | FutureResult(..) | ResultFuture(..) | ResultFutureResult(..)
     )
   }
+
+  /// If this function returns a `Result<T, E>` (including if `T` is a `Future`), return `Some(T)`.
+  pub fn unwrap_result(&self) -> Option<RetVal> {
+    use RetVal::*;
+    Some(match self {
+      Result(arg) => Infallible(arg.clone()),
+      ResultFuture(arg) => Future(arg.clone()),
+      ResultFutureResult(arg) => FutureResult(arg.clone()),
+      _ => return None,
+    })
+  }
+
+  pub fn arg(&self) -> &Arg {
+    use RetVal::*;
+    match self {
+      Infallible(arg)
+      | Result(arg)
+      | Future(arg)
+      | FutureResult(arg)
+      | ResultFuture(arg)
+      | ResultFutureResult(arg) => arg,
+    }
+  }
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
diff --git a/serde_v8/magic/v8slice.rs b/serde_v8/magic/v8slice.rs
index 78bdeb9..e56b4a6 100644
--- a/serde_v8/magic/v8slice.rs
+++ b/serde_v8/magic/v8slice.rs
@@ -46,6 +46,18 @@ impl V8Sliceable for u32 {
   }
 }
 
+impl V8Sliceable for f64 {
+  type V8 = v8::Float64Array;
+  fn new_buf<'s>(
+    scope: &mut v8::HandleScope<'s>,
+    buf: v8::Local<v8::ArrayBuffer>,
+    byte_offset: usize,
+    length: usize,
+  ) -> Option<v8::Local<'s, Self::V8>> {
+    v8::Float64Array::new(scope, buf, byte_offset, length)
+  }
+}
+
 /// A V8Slice encapsulates a slice that's been borrowed from a JavaScript
 /// ArrayBuffer object. JavaScript objects can normally be garbage collected,
 /// but the existence of a V8Slice inhibits this until it is dropped. It


Test patch:
diff --git a/core/runtime/tests/ops.rs b/core/runtime/tests/ops.rs
index 6631722..d8f7f93 100644
--- a/core/runtime/tests/ops.rs
+++ b/core/runtime/tests/ops.rs
@@ -593,15 +593,15 @@ pub async fn test_op_metrics() {
   }
 
   #[op2(async, core)]
-  pub async fn op_async_deferred() {
+  pub async fn op_async_yield() {
     tokio::task::yield_now().await;
-    println!("op_async_deferred!");
+    println!("op_async_yield!");
   }
 
   #[op2(async, core)]
-  pub async fn op_async_deferred_error() -> Result<(), AnyError> {
+  pub async fn op_async_yield_error() -> Result<(), AnyError> {
     tokio::task::yield_now().await;
-    println!("op_async_deferred_error!");
+    println!("op_async_yield_error!");
     bail!("dead");
   }
 
@@ -611,6 +611,16 @@ pub async fn test_op_metrics() {
     bail!("dead");
   }
 
+  #[op2(async(deferred), core, fast)]
+  pub async fn op_async_deferred() {
+    println!("op_async_deferred!");
+  }
+
+  #[op2(async(lazy), core, fast)]
+  pub async fn op_async_lazy() {
+    println!("op_async_lazy!");
+  }
+
   #[op2(core, fast)]
   pub fn op_sync() {
     println!("op_sync!");
@@ -621,8 +631,10 @@ pub async fn test_op_metrics() {
     ops = [
       op_async,
       op_async_error,
+      op_async_yield,
+      op_async_yield_error,
+      op_async_lazy,
       op_async_deferred,
-      op_async_deferred_error,
       op_sync
     ],
   );
@@ -650,13 +662,15 @@ pub async fn test_op_metrics() {
   .execute_script_static(
     "filename.js",
     r#"
-  const { op_sync, op_async, op_async_error, op_async_deferred, op_async_deferred_error } = Deno.core.ensureFastOps();
+  const { op_sync, op_async, op_async_error, op_async_yield, op_async_yield_error, op_async_deferred, op_async_lazy } = Deno.core.ensureFastOps();
     async function go() {
       op_sync();
       await op_async();
       try { await op_async_error() } catch {}
+      await op_async_yield();
+      try { await op_async_yield_error() } catch {}
       await op_async_deferred();
-      try { await op_async_deferred_error() } catch {}
+      await op_async_lazy();
     }
 
     go()
@@ -677,9 +691,13 @@ op_async Dispatched
 op_async Completed
 op_async_error Dispatched
 op_async_error Error
+op_async_yield Dispatched
+op_async_yield CompletedAsync
+op_async_yield_error Dispatched
+op_async_yield_error ErrorAsync
 op_async_deferred Dispatched
 op_async_deferred CompletedAsync
-op_async_deferred_error Dispatched
-op_async_deferred_error ErrorAsync"#
+op_async_lazy Dispatched
+op_async_lazy CompletedAsync"#
   );
 }
diff --git a/ops/op2/test_cases/async/async_arg_return.out b/ops/op2/test_cases/async/async_arg_return.out
index 6c3a117..9631de5 100644
--- a/ops/op2/test_cases/async/async_arg_return.out
+++ b/ops/op2/test_cases/async/async_arg_return.out
@@ -62,6 +62,7 @@ impl op_async {
         if let Some(result) = deno_core::_ops::map_async_op_infallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
diff --git a/ops/op2/test_cases/async/async_arg_return_result.out b/ops/op2/test_cases/async/async_arg_return_result.out
index ecbd4b6..598510b 100644
--- a/ops/op2/test_cases/async/async_arg_return_result.out
+++ b/ops/op2/test_cases/async/async_arg_return_result.out
@@ -62,6 +62,7 @@ impl op_async {
         if let Some(result) = deno_core::_ops::map_async_op_fallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
diff --git a/ops/op2/test_cases/async/async_deferred.out b/ops/op2/test_cases/async/async_deferred.out
new file mode 100644
index 0000000..c08c813
--- /dev/null
+++ b/ops/op2/test_cases/async/async_deferred.out
@@ -0,0 +1,125 @@
+#[allow(non_camel_case_types)]
+pub struct op_async_deferred {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_async_deferred {
+    const NAME: &'static str = stringify!(op_async_deferred);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal_op2(
+        stringify!(op_async_deferred),
+        true,
+        1usize as u8,
+        Self::v8_fn_ptr as _,
+        Self::v8_fn_ptr_metrics as _,
+        Some({
+            use deno_core::v8::fast_api::Type;
+            use deno_core::v8::fast_api::CType;
+            deno_core::v8::fast_api::FastFunction::new_with_bigint(
+                &[Type::V8Value, Type::Int32, Type::CallbackOptions],
+                CType::Void,
+                Self::v8_fn_ptr_fast as *const ::std::ffi::c_void,
+            )
+        }),
+        Some({
+            use deno_core::v8::fast_api::Type;
+            use deno_core::v8::fast_api::CType;
+            deno_core::v8::fast_api::FastFunction::new_with_bigint(
+                &[Type::V8Value, Type::Int32, Type::CallbackOptions],
+                CType::Void,
+                Self::v8_fn_ptr_fast_metrics as *const ::std::ffi::c_void,
+            )
+        }),
+    );
+}
+impl op_async_deferred {
+    pub const fn name() -> &'static str {
+        stringify!(op_async_deferred)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    #[allow(clippy::too_many_arguments)]
+    fn v8_fn_ptr_fast_metrics(
+        this: deno_core::v8::Local<deno_core::v8::Object>,
+        promise_id: i32,
+        fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions,
+    ) -> () {
+        let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<
+                deno_core::v8::External,
+            >::cast(unsafe { fast_api_callback_options.data.data })
+                .value() as *const deno_core::_ops::OpCtx)
+        };
+        deno_core::_ops::dispatch_metrics_fast(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Dispatched,
+        );
+        let res = Self::v8_fn_ptr_fast(this, promise_id, fast_api_callback_options);
+        deno_core::_ops::dispatch_metrics_fast(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Completed,
+        );
+        res
+    }
+    #[allow(clippy::too_many_arguments)]
+    fn v8_fn_ptr_fast(
+        _: deno_core::v8::Local<deno_core::v8::Object>,
+        promise_id: i32,
+        fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions,
+    ) -> () {
+        let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<
+                deno_core::v8::External,
+            >::cast(unsafe { fast_api_callback_options.data.data })
+                .value() as *const deno_core::_ops::OpCtx)
+        };
+        let result = { Self::call() };
+        deno_core::_ops::map_async_op_fallible(
+            opctx,
+            false,
+            true,
+            promise_id,
+            result,
+            |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
+        );
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        if opctx.metrics_enabled() {
+            deno_core::_ops::dispatch_metrics_async(
+                &opctx,
+                deno_core::_ops::OpMetricsEvent::Dispatched,
+            );
+        }
+        let result = { Self::call() };
+        let promise_id = deno_core::_ops::to_i32_option(&args.get(0))
+            .unwrap_or_default();
+        deno_core::_ops::map_async_op_fallible(
+            opctx,
+            false,
+            true,
+            promise_id,
+            result,
+            |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
+        );
+    }
+    extern "C" fn v8_fn_ptr_metrics(info: *const deno_core::v8::FunctionCallbackInfo) {
+        Self::v8_fn_ptr(info)
+    }
+    #[inline(always)]
+    pub async fn call() -> std::io::Result<i32> {
+        Ok(0)
+    }
+}
diff --git a/ops/op2/test_cases/async/async_deferred.rs b/ops/op2/test_cases/async/async_deferred.rs
new file mode 100644
index 0000000..95665b7
--- /dev/null
+++ b/ops/op2/test_cases/async/async_deferred.rs
@@ -0,0 +1,8 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+#[op2(async(deferred), fast)]
+pub async fn op_async_deferred() -> std::io::Result<i32> {
+    Ok(0)
+}
diff --git a/ops/op2/test_cases/async/async_jsbuffer.out b/ops/op2/test_cases/async/async_jsbuffer.out
index ec5e32c..834550a 100644
--- a/ops/op2/test_cases/async/async_jsbuffer.out
+++ b/ops/op2/test_cases/async/async_jsbuffer.out
@@ -66,6 +66,7 @@ impl op_async_v8_buffer {
         if let Some(result) = deno_core::_ops::map_async_op_infallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| {
diff --git a/ops/op2/test_cases/async/async_lazy.out b/ops/op2/test_cases/async/async_lazy.out
new file mode 100644
index 0000000..39e970f
--- /dev/null
+++ b/ops/op2/test_cases/async/async_lazy.out
@@ -0,0 +1,125 @@
+#[allow(non_camel_case_types)]
+pub struct op_async_lazy {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_async_lazy {
+    const NAME: &'static str = stringify!(op_async_lazy);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal_op2(
+        stringify!(op_async_lazy),
+        true,
+        1usize as u8,
+        Self::v8_fn_ptr as _,
+        Self::v8_fn_ptr_metrics as _,
+        Some({
+            use deno_core::v8::fast_api::Type;
+            use deno_core::v8::fast_api::CType;
+            deno_core::v8::fast_api::FastFunction::new_with_bigint(
+                &[Type::V8Value, Type::Int32, Type::CallbackOptions],
+                CType::Void,
+                Self::v8_fn_ptr_fast as *const ::std::ffi::c_void,
+            )
+        }),
+        Some({
+            use deno_core::v8::fast_api::Type;
+            use deno_core::v8::fast_api::CType;
+            deno_core::v8::fast_api::FastFunction::new_with_bigint(
+                &[Type::V8Value, Type::Int32, Type::CallbackOptions],
+                CType::Void,
+                Self::v8_fn_ptr_fast_metrics as *const ::std::ffi::c_void,
+            )
+        }),
+    );
+}
+impl op_async_lazy {
+    pub const fn name() -> &'static str {
+        stringify!(op_async_lazy)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    #[allow(clippy::too_many_arguments)]
+    fn v8_fn_ptr_fast_metrics(
+        this: deno_core::v8::Local<deno_core::v8::Object>,
+        promise_id: i32,
+        fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions,
+    ) -> () {
+        let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<
+                deno_core::v8::External,
+            >::cast(unsafe { fast_api_callback_options.data.data })
+                .value() as *const deno_core::_ops::OpCtx)
+        };
+        deno_core::_ops::dispatch_metrics_fast(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Dispatched,
+        );
+        let res = Self::v8_fn_ptr_fast(this, promise_id, fast_api_callback_options);
+        deno_core::_ops::dispatch_metrics_fast(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Completed,
+        );
+        res
+    }
+    #[allow(clippy::too_many_arguments)]
+    fn v8_fn_ptr_fast(
+        _: deno_core::v8::Local<deno_core::v8::Object>,
+        promise_id: i32,
+        fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions,
+    ) -> () {
+        let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<
+                deno_core::v8::External,
+            >::cast(unsafe { fast_api_callback_options.data.data })
+                .value() as *const deno_core::_ops::OpCtx)
+        };
+        let result = { Self::call() };
+        deno_core::_ops::map_async_op_fallible(
+            opctx,
+            true,
+            false,
+            promise_id,
+            result,
+            |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
+        );
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        if opctx.metrics_enabled() {
+            deno_core::_ops::dispatch_metrics_async(
+                &opctx,
+                deno_core::_ops::OpMetricsEvent::Dispatched,
+            );
+        }
+        let result = { Self::call() };
+        let promise_id = deno_core::_ops::to_i32_option(&args.get(0))
+            .unwrap_or_default();
+        deno_core::_ops::map_async_op_fallible(
+            opctx,
+            true,
+            false,
+            promise_id,
+            result,
+            |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
+        );
+    }
+    extern "C" fn v8_fn_ptr_metrics(info: *const deno_core::v8::FunctionCallbackInfo) {
+        Self::v8_fn_ptr(info)
+    }
+    #[inline(always)]
+    pub async fn call() -> std::io::Result<i32> {
+        Ok(0)
+    }
+}
diff --git a/ops/op2/test_cases/async/async_lazy.rs b/ops/op2/test_cases/async/async_lazy.rs
new file mode 100644
index 0000000..2a4cde7
--- /dev/null
+++ b/ops/op2/test_cases/async/async_lazy.rs
@@ -0,0 +1,8 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+#[op2(async(lazy), fast)]
+pub async fn op_async_lazy() -> std::io::Result<i32> {
+    Ok(0)
+}
diff --git a/ops/op2/test_cases/async/async_opstate.out b/ops/op2/test_cases/async/async_opstate.out
index 8318b78..a5c218a 100644
--- a/ops/op2/test_cases/async/async_opstate.out
+++ b/ops/op2/test_cases/async/async_opstate.out
@@ -50,6 +50,7 @@ impl op_async_opstate {
         if let Some(result) = deno_core::_ops::map_async_op_fallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
diff --git a/ops/op2/test_cases/async/async_result.out b/ops/op2/test_cases/async/async_result.out
index 7405fce..84df857 100644
--- a/ops/op2/test_cases/async/async_result.out
+++ b/ops/op2/test_cases/async/async_result.out
@@ -46,6 +46,7 @@ impl op_async {
         if let Some(result) = deno_core::_ops::map_async_op_fallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
diff --git a/ops/op2/test_cases/async/async_result_impl.out b/ops/op2/test_cases/async/async_result_impl.out
index c5286d2..e7675ce 100644
--- a/ops/op2/test_cases/async/async_result_impl.out
+++ b/ops/op2/test_cases/async/async_result_impl.out
@@ -81,6 +81,7 @@ impl op_async_result_impl {
         if let Some(result) = deno_core::_ops::map_async_op_fallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
diff --git a/ops/op2/test_cases/async/async_result_smi.out b/ops/op2/test_cases/async/async_result_smi.out
index 3e88b66..191e7e4 100644
--- a/ops/op2/test_cases/async/async_result_smi.out
+++ b/ops/op2/test_cases/async/async_result_smi.out
@@ -62,6 +62,7 @@ impl op_async {
         if let Some(result) = deno_core::_ops::map_async_op_fallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| {
diff --git a/ops/op2/test_cases/async/async_v8_global.out b/ops/op2/test_cases/async/async_v8_global.out
index 46c19e2..39d80cb 100644
--- a/ops/op2/test_cases/async/async_v8_global.out
+++ b/ops/op2/test_cases/async/async_v8_global.out
@@ -64,6 +64,7 @@ impl op_async_v8_global {
         if let Some(result) = deno_core::_ops::map_async_op_infallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
diff --git a/ops/op2/test_cases/async/async_void.out b/ops/op2/test_cases/async/async_void.out
index e567f0d..e0890a4 100644
--- a/ops/op2/test_cases/async/async_void.out
+++ b/ops/op2/test_cases/async/async_void.out
@@ -46,6 +46,7 @@ impl op_async {
         if let Some(result) = deno_core::_ops::map_async_op_infallible(
             opctx,
             false,
+            false,
             promise_id,
             result,
             |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
diff --git a/ops/op2/test_cases/sync/op_state_ref.out b/ops/op2/test_cases/sync/op_state_ref.out
index ef0800e..48dbba5 100644
--- a/ops/op2/test_cases/sync/op_state_ref.out
+++ b/ops/op2/test_cases/sync/op_state_ref.out
@@ -320,3 +320,149 @@ impl op_state_and_v8 {
     #[inline(always)]
     fn call(_state: &mut OpState, _callback: v8::Global<v8::Function>) {}
 }
+
+#[allow(non_camel_case_types)]
+struct op_state_and_v8_local {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_state_and_v8_local {
+    const NAME: &'static str = stringify!(op_state_and_v8_local);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal_op2(
+        stringify!(op_state_and_v8_local),
+        false,
+        2usize as u8,
+        Self::v8_fn_ptr as _,
+        Self::v8_fn_ptr_metrics as _,
+        Some({
+            use deno_core::v8::fast_api::Type;
+            use deno_core::v8::fast_api::CType;
+            deno_core::v8::fast_api::FastFunction::new_with_bigint(
+                &[Type::V8Value, Type::V8Value, Type::CallbackOptions],
+                CType::Void,
+                Self::v8_fn_ptr_fast as *const ::std::ffi::c_void,
+            )
+        }),
+        Some({
+            use deno_core::v8::fast_api::Type;
+            use deno_core::v8::fast_api::CType;
+            deno_core::v8::fast_api::FastFunction::new_with_bigint(
+                &[Type::V8Value, Type::V8Value, Type::CallbackOptions],
+                CType::Void,
+                Self::v8_fn_ptr_fast_metrics as *const ::std::ffi::c_void,
+            )
+        }),
+    );
+}
+impl op_state_and_v8_local {
+    pub const fn name() -> &'static str {
+        stringify!(op_state_and_v8_local)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    #[allow(clippy::too_many_arguments)]
+    fn v8_fn_ptr_fast_metrics(
+        this: deno_core::v8::Local<deno_core::v8::Object>,
+        arg1: deno_core::v8::Local<deno_core::v8::Value>,
+        fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions,
+    ) -> () {
+        let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<
+                deno_core::v8::External,
+            >::cast(unsafe { fast_api_callback_options.data.data })
+                .value() as *const deno_core::_ops::OpCtx)
+        };
+        deno_core::_ops::dispatch_metrics_fast(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Dispatched,
+        );
+        let res = Self::v8_fn_ptr_fast(this, arg1, fast_api_callback_options);
+        deno_core::_ops::dispatch_metrics_fast(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Completed,
+        );
+        res
+    }
+    #[allow(clippy::too_many_arguments)]
+    fn v8_fn_ptr_fast(
+        _: deno_core::v8::Local<deno_core::v8::Object>,
+        arg1: deno_core::v8::Local<deno_core::v8::Value>,
+        fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions,
+    ) -> () {
+        let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<
+                deno_core::v8::External,
+            >::cast(unsafe { fast_api_callback_options.data.data })
+                .value() as *const deno_core::_ops::OpCtx)
+        };
+        let result = {
+            let arg0 = &mut ::std::cell::RefCell::borrow_mut(&opctx.state);
+            let Ok(mut arg1) = deno_core::_ops::v8_try_convert::<
+                deno_core::v8::Function,
+            >(arg1) else {
+                fast_api_callback_options.fallback = true;
+                return unsafe { std::mem::zeroed() };
+            };
+            let arg1 = arg1;
+            Self::call(arg0, arg1)
+        };
+        result as _
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let opstate = &opctx.state;
+        let result = {
+            let arg1 = args.get(0usize as i32);
+            let Ok(mut arg1) = deno_core::_ops::v8_try_convert::<
+                deno_core::v8::Function,
+            >(arg1) else {
+                let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+                let msg = deno_core::v8::String::new_from_one_byte(
+                        &mut scope,
+                        "expected Function".as_bytes(),
+                        deno_core::v8::NewStringType::Normal,
+                    )
+                    .unwrap();
+                let exc = deno_core::v8::Exception::type_error(&mut scope, msg);
+                scope.throw_exception(exc);
+                return;
+            };
+            let arg1 = arg1;
+            let arg0 = &mut ::std::cell::RefCell::borrow_mut(&opstate);
+            Self::call(arg0, arg1)
+        };
+        deno_core::_ops::RustToV8RetVal::to_v8_rv(result, &mut rv)
+    }
+    extern "C" fn v8_fn_ptr_metrics(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        deno_core::_ops::dispatch_metrics_slow(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Dispatched,
+        );
+        Self::v8_fn_ptr(info);
+        deno_core::_ops::dispatch_metrics_slow(
+            &opctx,
+            deno_core::_ops::OpMetricsEvent::Completed,
+        );
+    }
+    #[inline(always)]
+    fn call(_state: &mut OpState, _callback: v8::Local<v8::Function>) {}
+}
diff --git a/ops/op2/test_cases/sync/op_state_ref.rs b/ops/op2/test_cases/sync/op_state_ref.rs
index 83d2632..08e4738 100644
--- a/ops/op2/test_cases/sync/op_state_ref.rs
+++ b/ops/op2/test_cases/sync/op_state_ref.rs
@@ -19,3 +19,6 @@ fn op_state_mut(_state: &mut OpState) {}
 
 #[op2]
 fn op_state_and_v8(_state: &mut OpState, #[global] _callback: v8::Global<v8::Function>) {}
+
+#[op2(fast)]
+fn op_state_and_v8_local(_state: &mut OpState, _callback: v8::Local<v8::Function>) {}