diff --git a/Cargo.lock b/Cargo.lock index b34f53e1b..782fe06f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,6 +669,7 @@ dependencies = [ "serde", "serde_json", "serde_v8", + "slab", "smallvec", "sourcemap", "static_assertions", @@ -2206,12 +2207,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" diff --git a/Cargo.toml b/Cargo.toml index 66d1886f3..ae50face3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ rstest = "0" serde = { version = "1", features = ["derive"] } serde_bytes = "0.11" serde_json = "1" +slab = "0.4.10" smallvec = "1.14" sourcemap = "9.1.2" static_assertions = "1" diff --git a/core/00_infra.js b/core/00_infra.js index c515e2955..324f672dd 100644 --- a/core/00_infra.js +++ b/core/00_infra.js @@ -3,25 +3,13 @@ ((window) => { const { - Array, - ArrayPrototypeFill, Error, ErrorCaptureStackTrace, - MapPrototypeDelete, - MapPrototypeGet, - MapPrototypeHas, - MapPrototypeSet, ObjectAssign, - ObjectDefineProperty, ObjectFreeze, - Promise, - PromiseReject, - PromiseResolve, - PromisePrototypeCatch, RangeError, ReferenceError, SafeArrayIterator, - SafeMap, StringPrototypeSplit, SymbolFor, SyntaxError, @@ -29,18 +17,11 @@ URIError, } = window.__bootstrap.primordials; - let nextPromiseId = 0; - const promiseMap = new SafeMap(); - const RING_SIZE = 4 * 1024; - const NO_PROMISE = null; // Alias to null is faster than plain nulls - const promiseRing = ArrayPrototypeFill(new Array(RING_SIZE), NO_PROMISE); // TODO(bartlomieju): in the future use `v8::Private` so it's not visible // to users. Currently missing bindings. - const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId"); + // const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId"); let isLeakTracingEnabled = false; - let submitLeakTrace; - let eventLoopTick; function __setLeakTracingEnabled(enabled) { isLeakTracingEnabled = enabled; @@ -50,11 +31,6 @@ return isLeakTracingEnabled; } - function __initializeCoreMethods(eventLoopTick_, submitLeakTrace_) { - eventLoopTick = eventLoopTick_; - submitLeakTrace = submitLeakTrace_; - } - const build = { target: "unknown", arch: "unknown", @@ -129,305 +105,6 @@ errorMap[className] = errorBuilder; } - function setPromise(promiseId) { - const idx = promiseId % RING_SIZE; - // Move old promise from ring to map - const oldPromise = promiseRing[idx]; - if (oldPromise !== NO_PROMISE) { - const oldPromiseId = promiseId - RING_SIZE; - MapPrototypeSet(promiseMap, oldPromiseId, oldPromise); - } - - const promise = new Promise((resolve, reject) => { - promiseRing[idx] = [resolve, reject]; - }); - const wrappedPromise = PromisePrototypeCatch( - promise, - (res) => { - // recreate the stacktrace and strip eventLoopTick() calls from stack trace - ErrorCaptureStackTrace(res, eventLoopTick); - throw res; - }, - ); - wrappedPromise[promiseIdSymbol] = promiseId; - return wrappedPromise; - } - - function __resolvePromise(promiseId, res, isOk) { - // Check if out of ring bounds, fallback to map - const outOfBounds = promiseId < nextPromiseId - RING_SIZE; - if (outOfBounds) { - const promise = MapPrototypeGet(promiseMap, promiseId); - if (!promise) { - throw "Missing promise in map @ " + promiseId; - } - MapPrototypeDelete(promiseMap, promiseId); - if (isOk) { - promise[0](res); - } else { - promise[1](res); - } - } else { - // Otherwise take from ring - const idx = promiseId % RING_SIZE; - const promise = promiseRing[idx]; - if (!promise) { - throw "Missing promise in ring @ " + promiseId; - } - promiseRing[idx] = NO_PROMISE; - if (isOk) { - promise[0](res); - } else { - promise[1](res); - } - } - } - - function hasPromise(promiseId) { - // Check if out of ring bounds, fallback to map - const outOfBounds = promiseId < nextPromiseId - RING_SIZE; - if (outOfBounds) { - return MapPrototypeHas(promiseMap, promiseId); - } - // Otherwise check it in ring - const idx = promiseId % RING_SIZE; - return promiseRing[idx] != NO_PROMISE; - } - - function setUpAsyncStub(opName, originalOp, maybeProto) { - let fn; - - // The body of this switch statement can be generated using the script above. - switch (originalOp.length - 1) { - /* BEGIN TEMPLATE setUpAsyncStub */ - /* DO NOT MODIFY: use rebuild_async_stubs.js to regenerate */ - case 0: - fn = function async_op_0() { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_0); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 1: - fn = function async_op_1(a) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_1); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 2: - fn = function async_op_2(a, b) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_2); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 3: - fn = function async_op_3(a, b, c) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b, c); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_3); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 4: - fn = function async_op_4(a, b, c, d) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b, c, d); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_4); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 5: - fn = function async_op_5(a, b, c, d, e) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b, c, d, e); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_5); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 6: - fn = function async_op_6(a, b, c, d, e, f) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b, c, d, e, f); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_6); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 7: - fn = function async_op_7(a, b, c, d, e, f, g) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b, c, d, e, f, g); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_7); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 8: - fn = function async_op_8(a, b, c, d, e, f, g, h) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b, c, d, e, f, g, h); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_8); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - case 9: - fn = function async_op_9(a, b, c, d, e, f, g, h, i) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = originalOp.call(this, id, a, b, c, d, e, f, g, h, i); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - ErrorCaptureStackTrace(err, async_op_9); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); - }; - break; - /* END TEMPLATE */ - - default: - throw new Error( - `Too many arguments for async op codegen (length of ${opName} was ${ - originalOp.length - 1 - })`, - ); - } - ObjectDefineProperty(fn, "name", { - value: opName, - configurable: false, - writable: false, - }); - - if (maybeProto) { - ObjectDefineProperty(fn, "prototype", { - value: maybeProto.prototype, - configurable: false, - writable: false, - }); - maybeProto.prototype[opName] = fn; - } - - return fn; - } - // Extra Deno.core.* exports const core = ObjectAssign(globalThis.Deno.core, { build, @@ -435,16 +112,11 @@ registerErrorBuilder, buildCustomError, registerErrorClass, - setUpAsyncStub, - hasPromise, - promiseIdSymbol, }); const infra = { - __resolvePromise, __setLeakTracingEnabled, __isLeakTracingEnabled, - __initializeCoreMethods, }; ObjectAssign(globalThis, { __infra: infra }); diff --git a/core/01_core.js b/core/01_core.js index 913237467..3bbced7fc 100755 --- a/core/01_core.js +++ b/core/01_core.js @@ -8,6 +8,7 @@ Error, ErrorCaptureStackTrace, FunctionPrototypeBind, + FunctionPrototypeCall, ObjectAssign, ObjectFreeze, ObjectFromEntries, @@ -28,15 +29,11 @@ } = window.__bootstrap.primordials; const { ops, - hasPromise, - promiseIdSymbol, registerErrorClass, } = window.Deno.core; const { __setLeakTracingEnabled, __isLeakTracingEnabled, - __initializeCoreMethods, - __resolvePromise, } = window.__infra; const { op_abort_wasm_streaming, @@ -60,6 +57,7 @@ op_print, op_queue_microtask, op_ref_op, + op_ref_op_promise, op_resources, op_run_microtasks, op_serialize, @@ -75,7 +73,9 @@ op_timer_queue_immediate, op_timer_ref, op_timer_unref, + op_unref_op_promise, op_unref_op, + op_promise_promise_id, op_cancel_handle, op_leak_tracing_enable, op_leak_tracing_submit, @@ -120,11 +120,6 @@ // core/infra collaborative code delete window.__infra; - __initializeCoreMethods( - eventLoopTick, - submitLeakTrace, - ); - function submitLeakTrace(id) { const error = new Error(); ErrorCaptureStackTrace(error, submitLeakTrace); @@ -141,122 +136,75 @@ } let unhandledPromiseRejectionHandler = () => false; - let timerDepth = 0; - let timersRunning = false; + let timerDepth = null; const cancelledTimers = new Set(); const macrotaskCallbacks = []; const nextTickCallbacks = []; - function setMacrotaskCallback(cb) { - ArrayPrototypePush(macrotaskCallbacks, cb); - } - - function setNextTickCallback(cb) { - ArrayPrototypePush(nextTickCallbacks, cb); - } - - // This function has variable number of arguments. The last argument describes - // if there's a "next tick" scheduled by the Node.js compat layer. Arguments - // before last are alternating integers and any values that describe the - // responses of async ops. - function eventLoopTick() { - // First respond to all pending ops. - for (let i = 0; i < arguments.length - 3; i += 3) { - const promiseId = arguments[i]; - const isOk = arguments[i + 1]; - const res = arguments[i + 2]; - - __resolvePromise(promiseId, res, isOk); - } - // Drain nextTick queue if there's a tick scheduled. - if (arguments[arguments.length - 1]) { - for (let i = 0; i < nextTickCallbacks.length; i++) { - nextTickCallbacks[i](); + const bindings = { + unhandledPromiseRejectionHandler(rejections) { + for (let i = 0; i < rejections.length; i += 2) { + const [promise, reason] = [rejections[i], rejections[i + 1]]; + const handled = unhandledPromiseRejectionHandler( + promise, + reason, + ); + if (!handled) op_dispatch_exception(reason, true); } - } else { - op_run_microtasks(); - } - - // Finally drain macrotask queue. - for (let i = 0; i < macrotaskCallbacks.length; i++) { - const cb = macrotaskCallbacks[i]; - while (true) { - const res = cb(); - - // If callback returned `undefined` then it has no work to do, we don't - // need to perform microtask checkpoint. - if (res === undefined) { - break; + }, + eventLoopTick(tick) { + if (tick) { // Drain nextTick queue if there's a tick scheduled. + for (let i = 0; i < nextTickCallbacks.length; i++) { + nextTickCallbacks[i](); } + } else op_run_microtasks(); + + // drain macrotask queue. + for (let i = 0; i < macrotaskCallbacks.length; i++) { + const cb = macrotaskCallbacks[i]; + while (true) { + const res = cb(); + + // If callback returned `undefined` then it has no work to do, we don't + // need to perform microtask checkpoint. + if (res === undefined) { + break; + } - op_run_microtasks(); - // If callback returned `true` then it has no more work to do, stop - // calling it then. - if (res === true) { - break; + op_run_microtasks(); + // If callback returned `true` then it has no more work to do, stop + // calling it then. + if (res === true) { + break; + } } } - } + }, + }; - const timers = arguments[arguments.length - 2]; - if (timers) { - timersRunning = true; - for (let i = 0; i < timers.length; i += 3) { - timerDepth = timers[i]; - const id = timers[i + 1]; - if (cancelledTimers.has(id)) { - continue; - } - try { - const f = timers[i + 2]; - f.call(window); - } catch (e) { - reportExceptionCallback(e); - } - op_run_microtasks(); - } - timersRunning = false; - timerDepth = 0; - cancelledTimers.clear(); - } + function setMacrotaskCallback(cb) { + ArrayPrototypePush(macrotaskCallbacks, cb); + } - // If we have any rejections for this tick, attempt to process them - const rejections = arguments[arguments.length - 3]; - if (rejections) { - for (let i = 0; i < rejections.length; i += 2) { - const handled = unhandledPromiseRejectionHandler( - rejections[i], - rejections[i + 1], - ); - if (!handled) { - const err = rejections[i + 1]; - op_dispatch_exception(err, true); - } - } - } + function setNextTickCallback(cb) { + ArrayPrototypePush(nextTickCallbacks, cb); } function refOp(promiseId) { - if (!hasPromise(promiseId)) { - return; - } op_ref_op(promiseId); } function unrefOp(promiseId) { - if (!hasPromise(promiseId)) { - return; - } op_unref_op(promiseId); } function refOpPromise(promise) { - refOp(promise[promiseIdSymbol]); + op_ref_op_promise(promise); } function unrefOpPromise(promise) { - unrefOp(promise[promiseIdSymbol]); + op_unref_op_promise(promise); } function resources() { @@ -652,14 +600,16 @@ internalRidSymbol: Symbol("Deno.internal.rid"), internalFdSymbol: Symbol("Deno.internal.fd"), resources, - eventLoopTick, + bindings, BadResource, BadResourcePrototype, Interrupted, InterruptedPrototype, NotCapable, NotCapablePrototype, + refOp, refOpPromise, + unrefOp, unrefOpPromise, setReportExceptionCallback, setPromiseHooks, @@ -686,7 +636,7 @@ return new SafeMap(traces); }, getLeakTraceForPromise: (promise) => - op_leak_tracing_get(0, promise[promiseIdSymbol]), + op_leak_tracing_get(0, op_promise_promise_id(promise)), setMacrotaskCallback, setNextTickCallback, runMicrotasks: () => op_run_microtasks(), @@ -788,7 +738,19 @@ reportUnhandledException: (e) => op_dispatch_exception(e, false), reportUnhandledPromiseRejection: (e) => op_dispatch_exception(e, true), queueUserTimer: (depth, repeat, timeout, task) => { - const id = op_timer_queue(depth, repeat, timeout, task); + const id = op_timer_queue(repeat, timeout, () => { + if (cancelledTimers.has(id)) { + cancelledTimers.delete(id); + return; + } + const oldTimerDepth = timerDepth; + timerDepth = depth; + try { + FunctionPrototypeCall(task, window); + } finally { + timerDepth = oldTimerDepth; + } + }); if (__isLeakTracingEnabled()) { submitTimerTrace(id); } @@ -799,14 +761,12 @@ op_timer_queue_system(repeat, timeout, task), queueImmediate: (task) => op_timer_queue_immediate(task), cancelTimer: (id) => { - if (timersRunning) { - cancelledTimers.add(id); - } + if (timerDepth) cancelledTimers.add(id); op_timer_cancel(id); }, refTimer: (id) => op_timer_ref(id), unrefTimer: (id) => op_timer_unref(id), - getTimerDepth: () => timerDepth, + getTimerDepth: () => timerDepth || 0, currentUserCallSite, wrapConsole, v8Console, diff --git a/core/Cargo.toml b/core/Cargo.toml index ef431cdf3..7f3d28011 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -46,6 +46,7 @@ pin-project.workspace = true serde.workspace = true serde_json = { workspace = true, features = ["float_roundtrip", "preserve_order"] } serde_v8.workspace = true +slab.workspace = true smallvec.workspace = true sourcemap.workspace = true static_assertions.workspace = true diff --git a/core/lib.rs b/core/lib.rs index 7e75e9201..4a9a25e4a 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -26,7 +26,7 @@ mod ops_builtin; mod ops_builtin_types; mod ops_builtin_v8; mod ops_metrics; -mod runtime; +pub mod runtime; mod source_map; mod tasks; mod web_timeout; @@ -130,6 +130,7 @@ pub use crate::ops::OpMetadata; pub use crate::ops::OpStackTraceCallback; pub use crate::ops::OpState; pub use crate::ops::PromiseId; +pub use crate::ops::PromiseResolver; pub use crate::ops_builtin::op_close; pub use crate::ops_builtin::op_print; pub use crate::ops_builtin::op_resources; @@ -225,9 +226,6 @@ macro_rules! located_script_name { #[cfg(all(test, not(miri)))] mod tests { - use std::process::Command; - use std::process::Stdio; - use super::*; #[test] @@ -242,31 +240,4 @@ mod tests { }; assert_eq!(&name[..expected.len()], expected); } - - // If the deno command is available, we ensure the async stubs are correctly rebuilt. - #[test] - fn test_rebuild_async_stubs() { - // Check for deno first - if let Err(e) = Command::new("deno") - .arg("--version") - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .status() - { - #[allow(clippy::print_stderr)] - { - eprintln!("Ignoring test because we couldn't find deno: {e:?}"); - } - } - let status = Command::new("deno") - .args(["run", "-A", "rebuild_async_stubs.js", "--check"]) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .status() - .unwrap(); - assert!( - status.success(), - "Async stubs were not updated, or 'rebuild_async_stubs.js' failed for some other reason" - ); - } } diff --git a/core/ops.rs b/core/ops.rs index e0faa3c10..295f61218 100644 --- a/core/ops.rs +++ b/core/ops.rs @@ -9,6 +9,7 @@ use crate::ops_metrics::OpMetricsFn; use crate::runtime::JsRuntimeState; use crate::runtime::OpDriverImpl; use crate::runtime::UnrefedOps; +use crate::runtime::op_driver::OpDriver; use futures::task::AtomicWaker; use std::cell::RefCell; use std::collections::HashSet; @@ -22,6 +23,7 @@ use v8::Isolate; use v8::fast_api::CFunction; pub type PromiseId = i32; +pub type PromiseResolver = v8::Global; pub type OpId = u16; #[cfg(debug_assertions)] @@ -198,6 +200,36 @@ impl OpCtx { &self.op_driver } + pub fn create_promise(&self, scope: &mut v8::HandleScope) -> PromiseId { + self.op_driver.create_promise(scope) + } + + pub fn get_promise<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + promise_id: PromiseId, + ) -> Option> { + self.op_driver.get_promise(scope, promise_id) + } + + pub fn resolve_promise( + &self, + scope: &mut v8::HandleScope, + promise_id: PromiseId, + value: v8::Local, + ) { + self.op_driver.resolve_promise(scope, promise_id, value) + } + + pub fn reject_promise( + &self, + scope: &mut v8::HandleScope, + promise_id: PromiseId, + reason: v8::Local, + ) { + self.op_driver.reject_promise(scope, promise_id, reason) + } + /// Get the [`JsRuntimeState`] for this op. pub(crate) fn runtime_state(&self) -> &JsRuntimeState { // SAFETY: JsRuntimeState outlives OpCtx diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs index b981ad8f1..264aad85b 100644 --- a/core/ops_builtin.rs +++ b/core/ops_builtin.rs @@ -104,7 +104,10 @@ builtin_ops! { ops_builtin_v8::op_timer_ref, ops_builtin_v8::op_timer_unref, ops_builtin_v8::op_ref_op, + ops_builtin_v8::op_ref_op_promise, ops_builtin_v8::op_unref_op, + ops_builtin_v8::op_unref_op_promise, + ops_builtin_v8::op_promise_promise_id, ops_builtin_v8::op_lazy_load_esm, ops_builtin_v8::op_run_microtasks, ops_builtin_v8::op_has_tick_scheduled, diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs index a5bb703e6..190f0408c 100644 --- a/core/ops_builtin_v8.rs +++ b/core/ops_builtin_v8.rs @@ -13,12 +13,14 @@ use crate::ops_builtin::WasmStreamingResource; use crate::resolve_url; use crate::runtime::JsRealm; use crate::runtime::JsRuntimeState; +use crate::runtime::op_driver::OpDriver; use crate::source_map::SourceMapApplication; use crate::stats::RuntimeActivityType; use deno_error::JsErrorBox; use serde::Deserialize; use serde::Serialize; use std::cell::RefCell; +use std::fmt::Write; use std::rc::Rc; use v8::ValueDeserializerHelper; use v8::ValueSerializerHelper; @@ -45,17 +47,64 @@ pub fn op_set_handled_promise_rejection_handler( } #[op2(fast)] -pub fn op_ref_op(scope: &mut v8::HandleScope, promise_id: i32) { +pub fn op_ref_op_promise( + scope: &mut v8::HandleScope, + promise: v8::Local, +) { let context_state = JsRealm::state_from_scope(scope); + let promise_id = match context_state + .pending_ops + .promise_id_from_promise(scope, promise) + { + Some(promise_id) => promise_id, + None => return, + }; context_state.unrefed_ops.borrow_mut().remove(&promise_id); } #[op2(fast)] -pub fn op_unref_op(scope: &mut v8::HandleScope, promise_id: i32) { +pub fn op_unref_op_promise( + scope: &mut v8::HandleScope, + promise: v8::Local, +) { let context_state = JsRealm::state_from_scope(scope); + let promise_id = match context_state + .pending_ops + .promise_id_from_promise(scope, promise) + { + Some(promise_id) => promise_id, + None => return, + }; context_state.unrefed_ops.borrow_mut().insert(promise_id); } +#[op2(fast)] +pub fn op_ref_op(scope: &mut v8::HandleScope, promise_id: i32) { + let context_state = JsRealm::state_from_scope(scope); + if context_state.pending_ops.has_promise(promise_id) { + context_state.unrefed_ops.borrow_mut().remove(&promise_id); + } +} + +#[op2(fast)] +pub fn op_unref_op(scope: &mut v8::HandleScope, promise_id: i32) { + let context_state = JsRealm::state_from_scope(scope); + if context_state.pending_ops.has_promise(promise_id) { + context_state.unrefed_ops.borrow_mut().insert(promise_id); + } +} + +#[op2] +pub fn op_promise_promise_id( + scope: &mut v8::HandleScope, + promise: v8::Local, +) -> Option { + let context_state = JsRealm::state_from_scope(scope); + context_state + .pending_ops + .promise_id_from_promise(scope, promise) +} + #[op2(fast)] pub fn op_leak_tracing_enable(scope: &mut v8::HandleScope, enabled: bool) { let context_state = JsRealm::state_from_scope(scope); @@ -67,13 +116,67 @@ pub fn op_leak_tracing_submit( scope: &mut v8::HandleScope, #[smi] kind: u8, #[smi] id: i32, - #[string] trace: &str, ) { let context_state = JsRealm::state_from_scope(scope); + let state = JsRuntime::state_from(scope); + + let mut source_mapper = state.source_mapper.borrow_mut(); + + let stack_trace = v8::StackTrace::current_stack_trace(scope, 10).unwrap(); + let frame_count = stack_trace.get_frame_count(); + let mut string = String::with_capacity((2 + 1 + 10) * frame_count); + for i in 0..frame_count { + let frame = stack_trace.get_frame(scope, i).unwrap(); + + let line_number = frame.get_line_number(); + let column_number = frame.get_column(); + + let (file_name, application) = match frame.get_script_name(scope) { + Some(name) => { + let file_name = name.to_rust_string_lossy(scope); + + let application = source_mapper.apply_source_map( + &file_name, + line_number as u32, + column_number as u32, + ); + (file_name, application) + } + None => { + if frame.is_eval() { + ("[eval]".to_string(), SourceMapApplication::Unchanged) + } else { + ("[unknown]".to_string(), SourceMapApplication::Unchanged) + } + } + }; + match application { + SourceMapApplication::Unchanged => { + writeln!(string, "{}:{}:{}", file_name, line_number, column_number) + .unwrap(); + } + SourceMapApplication::LineAndColumn { + line_number, + column_number, + } => { + writeln!(string, "{}:{}:{}", file_name, line_number, column_number) + .unwrap(); + } + SourceMapApplication::LineAndColumnAndFileName { + file_name, + line_number, + column_number, + } => { + writeln!(string, "{}:{}:{}", file_name, line_number, column_number) + .unwrap(); + } + }; + } + context_state.activity_traces.submit( RuntimeActivityType::from_u8(kind), id as _, - trace, + &*string, ); } @@ -123,7 +226,6 @@ pub fn op_leak_tracing_get<'s>( #[op2] pub fn op_timer_queue( scope: &mut v8::HandleScope, - depth: u32, repeat: bool, timeout_ms: f64, #[global] task: v8::Global, @@ -132,11 +234,9 @@ pub fn op_timer_queue( if repeat { context_state .timers - .queue_timer_repeat(timeout_ms as _, (task, depth)) as _ + .queue_timer_repeat(timeout_ms as _, task) as _ } else { - context_state - .timers - .queue_timer(timeout_ms as _, (task, depth)) as _ + context_state.timers.queue_timer(timeout_ms as _, task) as _ } } @@ -153,7 +253,7 @@ pub fn op_timer_queue_system( let context_state = JsRealm::state_from_scope(scope); context_state .timers - .queue_system_timer(repeat, timeout_ms as _, (task, 0)) as _ + .queue_system_timer(repeat, timeout_ms as _, task) as _ } /// Queue a timer. We return a "large integer" timer ID in an f64 which allows for up @@ -165,7 +265,7 @@ pub fn op_timer_queue_immediate( #[global] task: v8::Global, ) -> f64 { let context_state = JsRealm::state_from_scope(scope); - context_state.timers.queue_timer(0, (task, 0)) as _ + context_state.timers.queue_timer(0, task) as _ } #[op2(fast)] diff --git a/core/rebuild_async_stubs.js b/core/rebuild_async_stubs.js deleted file mode 100755 index e92d466e4..000000000 --- a/core/rebuild_async_stubs.js +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env deno run --allow-read --allow-write -// Copyright 2018-2025 the Deno authors. MIT license. - -const doNotModify = - "/* DO NOT MODIFY: use rebuild_async_stubs.js to regenerate */\n"; - -// The template function we build op_async_N functions from -function __TEMPLATE__(__ARGS_PARAM__) { - const id = nextPromiseId; - try { - // deno-fmt-ignore - const maybeResult = __OP__.call(this, __ARGS__); - if (maybeResult !== undefined) { - return PromiseResolve(maybeResult); - } - } catch (err) { - __ERR__; - ErrorCaptureStackTrace(err, __TEMPLATE__); - return PromiseReject(err); - } - if (isLeakTracingEnabled) { - submitLeakTrace(id); - } - nextPromiseId = (id + 1) & 0xffffffff; - return setPromise(id); -} - -const infraJsPath = new URL("00_infra.js", import.meta.url); -const infraJs = Deno.readTextFileSync(infraJsPath); - -const infraPristine = infraJs.replaceAll( - /\/\* BEGIN TEMPLATE ([^ ]+) \*\/.*?\/\* END TEMPLATE \*\//smg, - "TEMPLATE-$1", -); -const templateString = __TEMPLATE__.toString(); -let asyncStubCases = "/* BEGIN TEMPLATE setUpAsyncStub */\n"; -asyncStubCases += doNotModify; -const vars = "abcdefghijklm"; -for (let i = 0; i < 10; i++) { - let args = "id"; - for (let j = 0; j < i; j++) { - args += `, ${vars[j]}`; - } - const name = `async_op_${i}`; - // Replace the name and args, and add a two-space indent - const func = `fn = ${templateString}` - .replaceAll(/__TEMPLATE__/g, name) - .replaceAll(/__ARGS__/g, args) - .replaceAll(/__ARGS_PARAM__/g, args.replace(/id(, )?/, "")) - .replaceAll(/__OP__/g, "originalOp") - .replaceAll(/[\s]*__ERR__;/g, "") - .replaceAll(/^/gm, " "); - asyncStubCases += ` -case ${i}: -${func}; - break; - `.trim() + "\n"; -} -asyncStubCases += "/* END TEMPLATE */"; - -const asyncStubIndent = - infraPristine.match(/^([\t ]+)(?=TEMPLATE-setUpAsyncStub)/m)[0]; - -const infraOutput = infraPristine - .replace( - /[\t ]+TEMPLATE-setUpAsyncStub/, - asyncStubCases.replaceAll(/^/gm, asyncStubIndent), - ); - -if (Deno.args[0] === "--check") { - if (infraOutput !== infraJs) { - Deno.writeTextFileSync("/tmp/mismatch.txt", infraOutput); - throw new Error( - "Mismatch between pristine and updated source (wrote mismatch to /tmp/mismatch.txt)", - ); - } else { - console.log("✅ Templated sections would not change"); - } -} else { - Deno.writeTextFileSync(infraJsPath, infraOutput); -} diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs index 25db9edc5..60e3e4328 100644 --- a/core/runtime/bindings.rs +++ b/core/runtime/bindings.rs @@ -366,14 +366,6 @@ pub(crate) fn initialize_deno_core_ops_bindings<'s>( let deno_core_ops_obj: v8::Local = get(scope, deno_core_obj, OPS, "Deno.core.ops"); - let set_up_async_stub_fn: v8::Local = get( - scope, - deno_core_obj, - SET_UP_ASYNC_STUB, - "Deno.core.setUpAsyncStub", - ); - - let undefined = v8::undefined(scope); let mut index = 0; for decl in op_method_decls { @@ -407,14 +399,7 @@ pub(crate) fn initialize_deno_core_ops_bindings<'s>( continue; } - op_ctx_template_or_accessor( - &accessor_store, - set_up_async_stub_fn, - scope, - prototype, - tmpl, - method, - ); + op_ctx_template_or_accessor(&accessor_store, scope, prototype, method); } index += decl.methods.len(); @@ -433,14 +418,7 @@ pub(crate) fn initialize_deno_core_ops_bindings<'s>( // Register async methods at the end since we need to create the template instance. for method in method_ctxs.iter() { if method.decl.is_async { - op_ctx_template_or_accessor( - &accessor_store, - set_up_async_stub_fn, - scope, - prototype, - tmpl, - method, - ); + op_ctx_template_or_accessor(&accessor_store, scope, prototype, method); } } @@ -459,20 +437,9 @@ pub(crate) fn initialize_deno_core_ops_bindings<'s>( let op_ctxs = &op_ctxs[index..]; for op_ctx in op_ctxs { - let mut op_fn = - op_ctx_function(scope, op_ctx, v8::ConstructorBehavior::Allow); + let op_fn = op_ctx_function(scope, op_ctx, v8::ConstructorBehavior::Allow); let key = op_ctx.decl.name_fast.v8_string(scope).unwrap(); - // For async ops we need to set them up, by calling `Deno.core.setUpAsyncStub` - - // this call will generate an optimized function that binds to the provided - // op, while keeping track of promises and error remapping. - if op_ctx.decl.is_async { - let result = set_up_async_stub_fn - .call(scope, undefined.into(), &[key.into(), op_fn.into()]) - .unwrap(); - op_fn = result.try_into().unwrap() - } - deno_core_ops_obj.set(scope, key.into(), op_fn.into()); index += 1; @@ -481,31 +448,13 @@ pub(crate) fn initialize_deno_core_ops_bindings<'s>( fn op_ctx_template_or_accessor<'s>( accessor_store: &AccessorStore, - set_up_async_stub_fn: v8::Local, scope: &mut v8::HandleScope<'s>, tmpl: v8::Local<'s, v8::ObjectTemplate>, - constructor: v8::Local<'s, v8::FunctionTemplate>, op_ctx: &OpCtx, ) { if !op_ctx.decl.is_accessor() { let op_fn = op_ctx_template(scope, op_ctx, v8::ConstructorBehavior::Throw); let method_key = name_key(scope, &op_ctx.decl); - if op_ctx.decl.is_async { - let undefined = v8::undefined(scope); - let op_fn = op_fn.get_function(scope).unwrap(); - - let tmpl_fn = constructor.get_function(scope).unwrap(); - - let _result = set_up_async_stub_fn - .call( - scope, - undefined.into(), - &[method_key.into(), op_fn.into(), tmpl_fn.into()], - ) - .unwrap(); - - return; - } tmpl.set(method_key, op_fn.into()); diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs index c15fb5439..c1ffd8b5c 100644 --- a/core/runtime/jsrealm.rs +++ b/core/runtime/jsrealm.rs @@ -4,7 +4,6 @@ use super::exception_state::ExceptionState; #[cfg(test)] use super::op_driver::OpDriver; use crate::_ops::OpMethodDecl; -use crate::ModuleSourceCode; use crate::SourceCodeCacheInfo; use crate::cppgc::FunctionTemplateData; use crate::error::CoreError; @@ -22,6 +21,7 @@ use crate::ops::OpCtx; use crate::stats::RuntimeActivityTraces; use crate::tasks::V8TaskSpawnerFactory; use crate::web_timeout::WebTimers; +use crate::{ModuleSourceCode, PromiseId}; use futures::stream::StreamExt; use std::cell::Cell; use std::cell::RefCell; @@ -57,13 +57,18 @@ impl Hasher for IdentityHasher { pub(crate) type OpDriverImpl = super::op_driver::FuturesUnorderedDriver; pub(crate) type UnrefedOps = - Rc>>>; + Rc>>>; + +pub(crate) struct JsBindings { + pub(crate) unhandled_rejections_cb: v8::Global, + pub(crate) event_loop_tick_cb: v8::Global, +} pub struct ContextState { pub(crate) task_spawner_factory: Arc, - pub(crate) timers: WebTimers<(v8::Global, u32)>, - pub(crate) js_event_loop_tick_cb: RefCell>>, + pub(crate) timers: WebTimers>, pub(crate) js_wasm_streaming_cb: RefCell>>, + pub(crate) js_bindings: RefCell>, pub(crate) wasm_instance_fn: RefCell>>, pub(crate) unrefed_ops: UnrefedOps, pub(crate) activity_traces: RuntimeActivityTraces, @@ -94,8 +99,8 @@ impl ContextState { isolate: Some(isolate_ptr), exception_state: Default::default(), has_next_tick_scheduled: Default::default(), - js_event_loop_tick_cb: Default::default(), js_wasm_streaming_cb: Default::default(), + js_bindings: Default::default(), wasm_instance_fn: Default::default(), activity_traces: Default::default(), op_ctxs, @@ -202,7 +207,7 @@ impl JsRealmInner { let isolate = unsafe { raw_ptr.as_mut().unwrap() }; // These globals will prevent snapshots from completing, take them state.exception_state.prepare_to_destroy(); - std::mem::take(&mut *state.js_event_loop_tick_cb.borrow_mut()); + std::mem::take(&mut *state.js_bindings.borrow_mut()); std::mem::take(&mut *state.js_wasm_streaming_cb.borrow_mut()); { diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs index 95e743d05..f0a448664 100644 --- a/core/runtime/jsruntime.rs +++ b/core/runtime/jsruntime.rs @@ -6,7 +6,7 @@ use super::bindings; use super::bindings::create_exports_for_ops_virtual_module; use super::bindings::watch_promise; use super::exception_state::ExceptionState; -use super::jsrealm::JsRealmInner; +use super::jsrealm::{JsBindings, JsRealmInner}; use super::op_driver::OpDriver; use super::setup; use super::snapshot; @@ -1501,7 +1501,12 @@ impl JsRuntime { /// Grab and store JavaScript bindings to callbacks necessary for the /// JsRuntime to operate properly. fn store_js_callbacks(&mut self, realm: &JsRealm, will_snapshot: bool) { - let (event_loop_tick_cb, build_custom_error_cb, wasm_instance_fn) = { + let ( + event_loop_tick, + unhandled_promise_rejection, + build_custom_error_cb, + wasm_instance_fn, + ) = { let scope = &mut realm.handle_scope(self.v8_isolate()); let context = realm.context(); let context_local = v8::Local::new(scope, context); @@ -1512,13 +1517,22 @@ impl JsRuntime { bindings::get(scope, global, DENO, "Deno"); let core_obj: v8::Local = bindings::get(scope, deno_obj, CORE, "Deno.core"); + let bindings_obj: v8::Local = + bindings::get(scope, core_obj, BINDINGS, "Deno.core.bindings"); - let event_loop_tick_cb: v8::Local = bindings::get( + let event_loop_tick: v8::Local = bindings::get( scope, - core_obj, + bindings_obj, EVENT_LOOP_TICK, - "Deno.core.eventLoopTick", + "Deno.core.bindings.eventLoopTick", + ); + let unhandled_promise_rejection: v8::Local = bindings::get( + scope, + bindings_obj, + UNHANDLED_PROMISE_REJECTION, + "Deno.core.bindings.unhandledPromiseRejectionHandler", ); + let build_custom_error_cb: v8::Local = bindings::get( scope, core_obj, @@ -1546,7 +1560,8 @@ impl JsRuntime { } ( - v8::Global::new(scope, event_loop_tick_cb), + v8::Global::new(scope, event_loop_tick), + v8::Global::new(scope, unhandled_promise_rejection), v8::Global::new(scope, build_custom_error_cb), wasm_instance_fn.map(|f| v8::Global::new(scope, f)), ) @@ -1554,10 +1569,10 @@ impl JsRuntime { // Put global handles in the realm's ContextState let state_rc = realm.0.state(); - state_rc - .js_event_loop_tick_cb - .borrow_mut() - .replace(event_loop_tick_cb); + state_rc.js_bindings.borrow_mut().replace(JsBindings { + unhandled_rejections_cb: unhandled_promise_rejection, + event_loop_tick_cb: event_loop_tick, + }); state_rc .exception_state .js_build_custom_error_cb @@ -2616,27 +2631,7 @@ impl JsRuntime { } } - // We return async responses to JS in bounded batches. Note that because - // we're passing these to JS as arguments, it is possible to overflow the - // JS stack by just passing too many. - const MAX_VEC_SIZE_FOR_OPS: usize = 1024; - - // each batch is a flat vector of tuples: - // `[promise_id1, op_result1, promise_id2, op_result2, ...]` - // promise_id is a simple integer, op_result is an ops::OpResult - // which contains a value OR an error, encoded as a tuple. - // This batch is received in JS via the special `arguments` variable - // and then each tuple is used to resolve or reject promises - let mut args: SmallVec<[v8::Local; 32]> = - SmallVec::with_capacity(32); - loop { - if args.len() >= MAX_VEC_SIZE_FOR_OPS { - // We have too many, bail for now but re-wake the waker - cx.waker().wake_by_ref(); - break; - } - let Poll::Ready((promise_id, op_id, res)) = context_state.pending_ops.poll_ready(cx) else { @@ -2645,7 +2640,7 @@ impl JsRuntime { let res = res.unwrap(scope); - { + let op_driver = { let op_ctx = &context_state.op_ctxs[op_id as usize]; if op_ctx.metrics_enabled() { if res.is_ok() { @@ -2654,16 +2649,19 @@ impl JsRuntime { dispatch_metrics_async(op_ctx, OpMetricsEvent::ErrorAsync); } } - } + op_ctx.op_driver() + }; context_state.unrefed_ops.borrow_mut().remove(&promise_id); context_state .activity_traces .complete(RuntimeActivityType::AsyncOp, promise_id as _); + match res { + Ok(value) => op_driver.resolve_promise(scope, promise_id, value), + Err(reason) => op_driver.reject_promise(scope, promise_id, reason), + } + dispatched_ops |= true; - args.push(v8::Integer::new(scope, promise_id).into()); - args.push(v8::Boolean::new(scope, res.is_ok()).into()); - args.push(res.unwrap_or_else(std::convert::identity)); } let undefined: v8::Local = v8::undefined(scope).into(); @@ -2690,7 +2688,29 @@ impl JsRuntime { } } - let rejections = if !exception_state + let js_bindings = context_state.js_bindings.borrow(); + + let event_loop_tick_cb = &js_bindings.as_ref().unwrap().event_loop_tick_cb; + let event_loop_tick_cb = event_loop_tick_cb.open(scope); + let has_tick_scheduled_value = v8::Boolean::new(scope, has_tick_scheduled); + { + let tc_scope = &mut v8::TryCatch::new(scope); + event_loop_tick_cb.call( + tc_scope, + undefined, + &[has_tick_scheduled_value.into()], + ); + + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false, true); + } + if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Ok(false); + } + } + + // rejections + if !exception_state .pending_promise_rejections .borrow_mut() .is_empty() @@ -2698,7 +2718,7 @@ impl JsRuntime { // Avoid holding the pending rejection lock longer than necessary let mut pending_rejections = exception_state.pending_promise_rejections.borrow_mut(); - let mut rejections = VecDeque::default(); + let mut rejections = const { VecDeque::new() }; std::mem::swap(&mut *pending_rejections, &mut rejections); drop(pending_rejections); @@ -2712,58 +2732,49 @@ impl JsRuntime { arr.set_index(scope, index, value); index += 1; } - arr.into() - } else { - undefined - }; - - args.push(rejections); + let tc_scope = &mut v8::TryCatch::new(scope); + let js_unhandled_rejections_cb = + &js_bindings.as_ref().unwrap().unhandled_rejections_cb; + let js_unhandled_rejections_cb = + js_unhandled_rejections_cb.open(tc_scope); + js_unhandled_rejections_cb.call(tc_scope, undefined, &[arr.into()]); + + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false, true); + } + if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Ok(false); + } + } // TODO(mmastrac): timer dispatch should be done via direct function call, but we will have to start // storing the exception-reporting callback. - let timers = match context_state.timers.poll_timers(cx) { - Poll::Ready(timers) => { - let traces_enabled = context_state.activity_traces.is_enabled(); - let arr = v8::Array::new(scope, (timers.len() * 3) as _); - #[allow(clippy::needless_range_loop)] - for i in 0..timers.len() { - if traces_enabled { - // Timer and interval traces both use RuntimeActivityType::Timer - context_state - .activity_traces - .complete(RuntimeActivityType::Timer, timers[i].0 as _); + if let Poll::Ready(timers) = context_state.timers.poll_timers(cx) { + let traces_enabled = context_state.activity_traces.is_enabled(); + #[allow(clippy::needless_range_loop)] + for i in 0..timers.len() { + if traces_enabled { + // Timer and interval traces both use RuntimeActivityType::Timer + context_state + .activity_traces + .complete(RuntimeActivityType::Timer, timers[i].0 as _); + } + + let (_id, function): (_, v8::Local<'_, v8::Function>) = + (timers[i].0, v8::Local::new(scope, timers[i].1.clone())); + { + let tc_scope = &mut v8::TryCatch::new(scope); + function.call(tc_scope, undefined, &[]).unwrap(); + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false, true); + } + if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Ok(false); } - // depth, id, function - let value = v8::Integer::new(scope, timers[i].1.1 as _); - arr.set_index(scope, (i * 3) as _, value.into()); - let value = v8::Number::new(scope, timers[i].0 as _); - arr.set_index(scope, (i * 3 + 1) as _, value.into()); - let value = v8::Local::new(scope, timers[i].1.0.clone()); - arr.set_index(scope, (i * 3 + 2) as _, value.into()); } - arr.into() + scope.perform_microtask_checkpoint(); } - _ => undefined, }; - args.push(timers); - - let has_tick_scheduled = v8::Boolean::new(scope, has_tick_scheduled); - args.push(has_tick_scheduled.into()); - - let tc_scope = &mut v8::TryCatch::new(scope); - let js_event_loop_tick_cb = context_state.js_event_loop_tick_cb.borrow(); - let js_event_loop_tick_cb = - js_event_loop_tick_cb.as_ref().unwrap().open(tc_scope); - - js_event_loop_tick_cb.call(tc_scope, undefined, args.as_slice()); - - if let Some(exception) = tc_scope.exception() { - return exception_to_err_result(tc_scope, exception, false, true); - } - - if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - return Ok(false); - } Ok(dispatched_ops) } diff --git a/core/runtime/op_driver/futures_unordered_driver.rs b/core/runtime/op_driver/futures_unordered_driver.rs index 4c4a26d7d..e3f12a9bc 100644 --- a/core/runtime/op_driver/futures_unordered_driver.rs +++ b/core/runtime/op_driver/futures_unordered_driver.rs @@ -5,8 +5,8 @@ use super::OpInflightStats; use super::future_arena::FutureAllocation; use super::future_arena::FutureArena; use super::op_results::*; -use crate::OpId; use crate::PromiseId; +use crate::{OpId, PromiseResolver}; use bit_set::BitSet; use deno_error::JsErrorClass; use deno_unsync::JoinHandle; @@ -27,6 +27,7 @@ use std::task::Context; use std::task::Poll; use std::task::Waker; use std::task::ready; +use v8::{HandleScope, Local, Promise, Value}; async fn poll_task( mut results: SubmissionQueueResults< @@ -63,6 +64,7 @@ pub struct FuturesUnorderedDriver< completed_ops: Rc>>>, completed_waker: Rc, arena: FutureArena, PendingOpInfo>, + promises: RefCell>, } impl Drop for FuturesUnorderedDriver { @@ -91,6 +93,7 @@ impl Default for FuturesUnorderedDriver { queue, completed_waker, arena: Default::default(), + promises: Default::default(), } } } @@ -123,6 +126,56 @@ impl FuturesUnorderedDriver { } impl OpDriver for FuturesUnorderedDriver { + fn create_promise<'s>(&self, scope: &mut HandleScope) -> PromiseId { + let promise_resolver = v8::PromiseResolver::new(scope).unwrap(); + let promise_resolver: PromiseResolver = + v8::Global::new(scope, promise_resolver); + let promise_id = self.promises.borrow_mut().insert(promise_resolver); + promise_id.try_into().expect("promise id overflow") + } + + fn has_promise(&self, promise_id: PromiseId) -> bool { + self.promises.borrow().get(promise_id as usize).is_some() + } + + fn _get_promise<'s>( + &self, + scope: &mut HandleScope<'s>, + promise_id: PromiseId, + ) -> Option> { + self + .promises + .borrow() + .get(promise_id as usize) + .map(move |x| v8::Local::new(scope, x).get_promise(scope)) + } + + fn resolve_promise( + &self, + scope: &mut HandleScope<'_>, + promise_id: PromiseId, + value: Local, + ) { + if let Some(resolver) = + self.promises.borrow_mut().try_remove(promise_id as usize) + { + Local::new(scope, resolver).resolve(scope, value); + } + } + + fn reject_promise( + &self, + scope: &mut HandleScope<'_>, + promise_id: PromiseId, + reason: Local, + ) { + if let Some(resolver) = + self.promises.borrow_mut().try_remove(promise_id as usize) + { + Local::new(scope, resolver).reject(scope, reason); + } + } + fn submit_op_fallible< R: 'static, E: JsErrorClass + 'static, @@ -131,7 +184,7 @@ impl OpDriver for FuturesUnorderedDriver { >( &self, op_id: OpId, - promise_id: i32, + promise_id: PromiseId, op: impl Future> + 'static, rv_map: C::MappingFn, ) -> Option> { @@ -172,7 +225,7 @@ impl OpDriver for FuturesUnorderedDriver { >( &self, op_id: OpId, - promise_id: i32, + promise_id: PromiseId, op: impl Future + 'static, rv_map: C::MappingFn, ) -> Option { diff --git a/core/runtime/op_driver/mod.rs b/core/runtime/op_driver/mod.rs index f8d1c20d2..4a9eaa8e0 100644 --- a/core/runtime/op_driver/mod.rs +++ b/core/runtime/op_driver/mod.rs @@ -21,6 +21,7 @@ pub use self::op_results::OpResult; use self::op_results::PendingOpInfo; pub use self::op_results::V8OpMappingContext; pub use self::op_results::V8RetValMapper; +use crate::runtime::v8_static_strings::INTERNAL_PROMISE_ID; #[derive(Default)] /// Returns a set of stats on inflight ops. @@ -50,11 +51,70 @@ pub enum OpScheduling { pub(crate) trait OpDriver: Default { + fn get_private_promise_id_symbol<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + ) -> v8::Local<'s, v8::Private> { + let name = INTERNAL_PROMISE_ID.v8_string(scope).unwrap(); + v8::Private::for_api(scope, Some(name)) + } + + fn _get_promise<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + promise_id: PromiseId, + ) -> Option>; + + fn has_promise(&self, promise_id: PromiseId) -> bool; + + fn promise_id_from_promise( + &self, + scope: &mut v8::HandleScope<'_>, + promise: v8::Local, + ) -> Option { + let symbol = self.get_private_promise_id_symbol(scope); + let value = promise.get_private(scope, symbol); + value + .and_then(|x| TryInto::>::try_into(x).ok()) + .map(|x| x.int32_value(scope).unwrap()) + } + + /// Get the promise with the `promise_id`, set a private promiseIdSymbol to the promise id + fn get_promise<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + promise_id: PromiseId, + ) -> Option> { + let maybe_promise = self._get_promise(scope, promise_id); + maybe_promise.map(|promise| { + let id = v8::Integer::new(scope, promise_id); + let symbol = self.get_private_promise_id_symbol(scope); + promise.set_private(scope, symbol, id.into()); + promise + }) + } + + fn resolve_promise( + &self, + scope: &mut v8::HandleScope, + promise_id: PromiseId, + value: v8::Local, + ); + + fn reject_promise( + &self, + scope: &mut v8::HandleScope, + promise_id: PromiseId, + reason: v8::Local, + ); + + fn create_promise(&self, scope: &mut v8::HandleScope) -> PromiseId; + /// Submits an operation that is expected to complete successfully without errors. fn submit_op_infallible( &self, op_id: OpId, - promise_id: i32, + promise_id: PromiseId, op: impl Future + 'static, rv_map: C::MappingFn, ) -> Option; @@ -65,7 +125,7 @@ pub(crate) trait OpDriver: &self, scheduling: OpScheduling, op_id: OpId, - promise_id: i32, + promise_id: PromiseId, op: impl Future + 'static, rv_map: C::MappingFn, ) -> Option { @@ -91,7 +151,7 @@ pub(crate) trait OpDriver: >( &self, op_id: OpId, - promise_id: i32, + promise_id: PromiseId, op: impl Future> + 'static, rv_map: C::MappingFn, ) -> Option>; @@ -103,7 +163,7 @@ pub(crate) trait OpDriver: &self, scheduling: OpScheduling, op_id: OpId, - promise_id: i32, + promise_id: PromiseId, op: impl Future> + 'static, rv_map: C::MappingFn, ) -> Option> { diff --git a/core/runtime/stats.rs b/core/runtime/stats.rs index a80ea6ff5..b529bbc71 100644 --- a/core/runtime/stats.rs +++ b/core/runtime/stats.rs @@ -48,7 +48,7 @@ impl RuntimeActivityTraces { &self, activity_type: RuntimeActivityType, id: ActivityId, - trace: &str, + trace: impl Into>, ) { debug_assert_ne!( activity_type, diff --git a/core/runtime/v8_static_strings.rs b/core/runtime/v8_static_strings.rs index a826acc1b..f98ae1617 100644 --- a/core/runtime/v8_static_strings.rs +++ b/core/runtime/v8_static_strings.rs @@ -25,7 +25,6 @@ v8_static_strings!( DIRNAME = "dirname", ERR_MODULE_NOT_FOUND = "ERR_MODULE_NOT_FOUND", ERRORS = "errors", - EVENT_LOOP_TICK = "eventLoopTick", FILENAME = "filename", INSTANCE = "Instance", MAIN = "main", @@ -33,9 +32,12 @@ v8_static_strings!( NAME = "name", OPS = "ops", RESOLVE = "resolve", - SET_UP_ASYNC_STUB = "setUpAsyncStub", STACK = "stack", URL = "url", + BINDINGS = "bindings", + EVENT_LOOP_TICK = "eventLoopTick", + UNHANDLED_PROMISE_REJECTION = "unhandledPromiseRejectionHandler", + INTERNAL_PROMISE_ID = "Promise#Deno.core.internalPromiseId", WASM_INSTANCE = "WasmInstance", WEBASSEMBLY = "WebAssembly", ESMODULE = "__esModule", diff --git a/ops/op2/dispatch_async.rs b/ops/op2/dispatch_async.rs index 03599aa0a..12042e7c7 100644 --- a/ops/op2/dispatch_async.rs +++ b/ops/op2/dispatch_async.rs @@ -4,8 +4,6 @@ use super::V8MappingError; use super::V8SignatureMappingError; use super::config::MacroConfig; 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; use super::dispatch_slow::throw_exception; use super::dispatch_slow::with_fn_args; @@ -17,10 +15,185 @@ use super::dispatch_slow::with_self; use super::dispatch_slow::with_stack_trace; use super::generator_state::GeneratorState; use super::generator_state::gs_quote; -use super::signature::ParsedSignature; use super::signature::RetVal; +use super::signature::{Arg, ArgMarker, ArgSlowRetval, ParsedSignature}; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; + +pub fn resolve_value_infallible( + generator_state: &mut GeneratorState, + ret_type: &Arg, +) -> Result { + // In the future we may be able to make this false for void again + generator_state.needs_retval = true; + + let result = match ret_type.marker() { + ArgMarker::ArrayBuffer => { + gs_quote!(generator_state(result) => (deno_core::_ops::RustToV8Marker::::from(#result))) + } + ArgMarker::Serde => { + gs_quote!(generator_state(result) => (deno_core::_ops::RustToV8Marker::::from(#result))) + } + ArgMarker::Smi => { + gs_quote!(generator_state(result) => (deno_core::_ops::RustToV8Marker::::from(#result))) + } + ArgMarker::Number => { + gs_quote!(generator_state(result) => (deno_core::_ops::RustToV8Marker::::from(#result))) + } + ArgMarker::Cppgc if generator_state.use_this_cppgc => { + generator_state.needs_isolate = true; + let wrap_object = match ret_type { + Arg::CppGcProtochain(chain) => { + let wrap_object = format_ident!("wrap_object{}", chain.len()); + quote!(#wrap_object) + } + _ => { + if generator_state.use_proto_cppgc { + quote!(wrap_object1) + } else { + quote!(wrap_object) + } + } + }; + gs_quote!(generator_state(result, scope) => ( + Some(deno_core::cppgc::#wrap_object(&mut #scope, args.this(), #result)) + )) + } + ArgMarker::Cppgc if generator_state.use_proto_cppgc => { + let marker = quote!(deno_core::_ops::RustToV8Marker::::from); + if ret_type.is_option() { + gs_quote!(generator_state(result) => (#result.map(#marker))) + } else { + gs_quote!(generator_state(result) => (#marker(#result))) + } + } + ArgMarker::Cppgc => { + let marker = quote!(deno_core::_ops::RustToV8Marker::::from); + if ret_type.is_option() { + gs_quote!(generator_state(result) => (#result.map(#marker))) + } else { + gs_quote!(generator_state(result) => (#marker(#result))) + } + } + ArgMarker::ToV8 => { + gs_quote!(generator_state(result) => (deno_core::_ops::RustToV8Marker::::from(#result))) + } + ArgMarker::Undefined => { + gs_quote!(generator_state(scope) => (deno_core::v8::undefined(&mut #scope))) + } + ArgMarker::None => { + gs_quote!(generator_state(result) => (#result)) + } + }; + generator_state.needs_scope = true; + generator_state.needs_opctx = true; + let res = match ret_type.slow_retval() { + ArgSlowRetval::RetVal => { + gs_quote!(generator_state(scope, opctx, promise_id) => { + let value = deno_core::_ops::RustToV8::to_v8(#result, &mut #scope); + #opctx.resolve_promise(&mut #scope, #promise_id, value); + }) + } + ArgSlowRetval::RetValFallible => { + let err = format_ident!("{}_err", generator_state.retval); + + gs_quote!(generator_state(scope, opctx, promise_id) => (match deno_core::_ops::RustToV8Fallible::to_v8_fallible(#result, &mut #scope) { + Ok(v) => #opctx.resolve_promise(&mut #scope, #promise_id, v), + Err(#err) => { + let exception = deno_core::error::to_v8_error( + &mut #scope, + &#err, + ); + #opctx.reject_promise(&mut #scope, #promise_id, exception) + }, + })) + } + ArgSlowRetval::V8Local => { + gs_quote!(generator_state(scope, opctx, promise_id) => { + let value = deno_core::_ops::RustToV8::to_v8(#result, &mut #scope); + #opctx.resolve_promise(&mut #scope, #promise_id, value); + }) + } + ArgSlowRetval::V8LocalNoScope => { + gs_quote!(generator_state(scope, opctx, promise_id) => { + let value = deno_core::_ops::RustToV8NoScope::to_v8(#result); + #opctx.resolve_promise(&mut #scope, #promise_id, value); + }) + } + ArgSlowRetval::V8LocalFalliable => { + let err = format_ident!("{}_err", generator_state.retval); + + gs_quote!(generator_state(scope, opctx, promise_id) => (match deno_core::_ops::RustToV8Fallible::to_v8_fallible(#result, &mut #scope) { + Ok(v) => #opctx.resolve_promise(&mut #scope, #promise_id, v), + Err(#err) => { + let exception = deno_core::error::to_v8_error( + &mut #scope, + &#err, + ); + #opctx.reject_promise(&mut #scope, #promise_id, exception) + }, + })) + } + ArgSlowRetval::None => return Err("a slow return value"), + }; + + Ok(res) +} + +/// Generates code to reject an error, adding required additional dependencies as needed. +pub(crate) fn reject_error( + generator_state: &mut GeneratorState, +) -> TokenStream { + let maybe_scope = if generator_state.needs_scope { + quote!() + } else { + with_scope(generator_state) + }; + + let maybe_opctx = if generator_state.needs_opctx { + quote!() + } else { + with_opctx(generator_state) + }; + + let maybe_args = if generator_state.needs_args { + quote!() + } else { + with_fn_args(generator_state) + }; + + gs_quote!(generator_state(scope, opctx, promise_id) => { + #maybe_scope + #maybe_args + #maybe_opctx + let exception = deno_core::error::to_v8_error( + &mut #scope, + &err, + ); + #opctx.reject_promise(&mut #scope, #promise_id, exception); + return 1; + }) +} + +pub fn resolve_value_result( + generator_state: &mut GeneratorState, + ret_type: &Arg, +) -> Result { + let infallible = resolve_value_infallible(generator_state, ret_type)?; + let exception = reject_error(generator_state); + + let tokens = gs_quote!(generator_state(result) => ( + match #result { + Ok(#result) => { + #infallible + } + Err(err) => { + #exception + } + }; + )); + Ok(tokens) +} pub(crate) fn map_async_return_type( generator_state: &mut GeneratorState, @@ -30,16 +203,14 @@ pub(crate) fn map_async_return_type( let (mapper, return_value_immediate) = match ret_val { RetVal::Infallible(r, true) | RetVal::Future(r) - | RetVal::ResultFuture(r) => ( - quote!(map_async_op_infallible), - return_value_infallible(generator_state, r)?, - ), + | RetVal::ResultFuture(r) => (quote!(map_async_op_infallible), { + resolve_value_infallible(generator_state, r)? + }), RetVal::Result(r, true) | RetVal::FutureResult(r) - | RetVal::ResultFutureResult(r) => ( - quote!(map_async_op_fallible), - return_value_result(generator_state, r)?, - ), + | RetVal::ResultFutureResult(r) => (quote!(map_async_op_fallible), { + resolve_value_result(generator_state, r)? + }), RetVal::Infallible(_, false) | RetVal::Result(_, false) => { return Err("an async return"); } @@ -60,10 +231,8 @@ pub(crate) fn generate_dispatch_async( quote!() }; - // Set input_index = 1 when we don't want promise ID as the first arg. - let input_index = if config.promise_id { 0 } else { 1 }; - let args = - generate_dispatch_slow_call(generator_state, signature, input_index)?; + generator_state.needs_scope = true; + let args = generate_dispatch_slow_call(generator_state, signature, 0)?; // Always need context and args generator_state.needs_opctx = true; @@ -99,19 +268,24 @@ pub(crate) fn generate_dispatch_async( })); } + output.extend( + gs_quote!(generator_state(retval, opctx, scope, promise_id) => { + let #promise_id = #opctx.create_promise(&mut #scope); + #retval.set(#opctx.get_promise(&mut #scope, #promise_id).unwrap().into()); + }), + ); + 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) => { - let #promise_id = deno_core::_ops::to_i32_option(&#fn_args.get(0)).unwrap_or_default(); + output.extend(gs_quote!(generator_state(promise_id, result, opctx, scope) => { // 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) => { - let #promise_id = deno_core::_ops::to_i32_option(&#fn_args.get(0)).unwrap_or_default(); + output.extend(gs_quote!(generator_state(promise_id, result, opctx, scope) => { if let Some(#result) = deno_core::_ops::#mapper(#opctx, false, false, #promise_id, #result, |#scope, #result| { #return_value }) { diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs index 2d508d754..668f7dd1a 100644 --- a/ops/op2/dispatch_fast.rs +++ b/ops/op2/dispatch_fast.rs @@ -40,7 +40,6 @@ pub(crate) enum FastArg { arg: Arg, }, CallbackOptions, - PromiseId, } #[derive(Clone)] @@ -60,7 +59,6 @@ impl FastSignature { .args .iter() .filter_map(|arg| match arg { - FastArg::PromiseId => Some(V8FastCallType::I32.quote_ctype()), FastArg::CallbackOptions => { Some(V8FastCallType::CallbackOptions.quote_ctype()) } @@ -82,10 +80,6 @@ impl FastSignature { generator_state.fast_api_callback_options.clone(), V8FastCallType::CallbackOptions.quote_rust_type(), )), - FastArg::PromiseId => Some(( - generator_state.promise_id.clone(), - V8FastCallType::I32.quote_rust_type(), - )), FastArg::Actual { arg_type, name_in, .. } => Some((format_ident!("{name_in}"), arg_type.quote_rust_type())), @@ -114,7 +108,7 @@ impl FastSignature { generator_state.needs_scope = true; } } - FastArg::CallbackOptions | FastArg::PromiseId => {} + FastArg::CallbackOptions => {} } } Ok(call_args) @@ -132,7 +126,7 @@ impl FastSignature { call_names.push(quote!(#name_out)); } } - FastArg::CallbackOptions | FastArg::PromiseId => {} + FastArg::CallbackOptions => {} } } call_names @@ -166,10 +160,6 @@ impl FastSignature { self.args.push(FastArg::CallbackOptions); } } - - fn insert_promise_id(&mut self) { - self.args.insert(0, FastArg::PromiseId) - } } #[allow(unused)] @@ -403,10 +393,6 @@ pub(crate) fn generate_dispatch_fast( _ => quote!(), }; - 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)?; @@ -558,6 +544,14 @@ pub(crate) fn generate_dispatch_fast( let (fastcall_names, fastcall_types): (Vec<_>, Vec<_>) = fastsig.input_args(generator_state).into_iter().unzip(); + let maybe_promise = if config.r#async { + gs_quote!(generator_state(scope, opctx, promise_id) => ( + let #promise_id = #opctx.create_promise(&mut #scope); + )) + } else { + quote!() + }; + let fast_fn = gs_quote!(generator_state(result, fast_api_callback_options, fast_function, fast_function_metrics) => { #[allow(clippy::too_many_arguments)] extern "C" fn #fast_function_metrics<'s>( @@ -597,6 +591,7 @@ pub(crate) fn generate_dispatch_fast( #(#call_args)* #call (#(#call_names),*) }; + #maybe_promise #handle_error #handle_result } @@ -645,6 +640,7 @@ fn map_v8_fastcall_arg_to_arg( js_runtime_state, scope, needs_opctx, + needs_scope, needs_fast_api_callback_options, needs_fast_isolate, needs_js_runtime_state, @@ -722,6 +718,11 @@ fn map_v8_fastcall_arg_to_arg( let #arg_ident = #fast_api_callback_options.isolate; }) } + Arg::Special(Special::PromiseId) => { + *needs_opctx = true; + *needs_scope = true; + quote!(let #arg_ident = #opctx.create_promise(&mut #scope);) + } Arg::Ref(RefType::Ref, Special::OpState) => { *needs_opctx = true; quote!(let #arg_ident = &::std::cell::RefCell::borrow(&#opctx.state);) @@ -908,6 +909,7 @@ fn map_arg_to_v8_fastcall_type( | Arg::VarArgs | Arg::This | Arg::Special(Special::Isolate) + | Arg::Special(Special::PromiseId) | Arg::OptionState(..) => V8FastCallType::Virtual, // Other types + ref types are not handled Arg::OptionNumeric(..) diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs index 1920a9b5f..8df5906d1 100644 --- a/ops/op2/dispatch_slow.rs +++ b/ops/op2/dispatch_slow.rs @@ -588,6 +588,10 @@ pub fn from_arg( Arg::External(External::Ptr(_)) => { from_arg_option(generator_state, &arg_ident, "external") } + Arg::Special(Special::PromiseId) => { + *needs_opctx = true; + quote!(let #arg_ident = #opctx.create_promise(&mut #scope);) + } Arg::Special(Special::Isolate) => { *needs_opctx = true; quote!(let #arg_ident = #opctx.isolate;) diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs index dfbd5f5c8..f934d09dc 100644 --- a/ops/op2/signature.rs +++ b/ops/op2/signature.rs @@ -147,6 +147,7 @@ pub enum Special { JsRuntimeState, FastApiCallbackOptions, Isolate, + PromiseId, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -360,7 +361,8 @@ impl Arg { | Special::OpState | Special::JsRuntimeState | Special::HandleScope - | Special::Isolate, + | Special::Isolate + | Special::PromiseId, ) => true, Self::Ref( _, @@ -1444,6 +1446,7 @@ fn parse_type_path( } ( OpState ) => Ok(CBare(TSpecial(Special::OpState))), ( JsRuntimeState ) => Ok(CBare(TSpecial(Special::JsRuntimeState))), + ( PromiseId ) => Ok(CBare(TSpecial(Special::PromiseId))), ( v8 :: Isolate ) => Ok(CBare(TSpecial(Special::Isolate))), ( v8 :: HandleScope $( < $_scope:lifetime >)? ) => Ok(CBare(TSpecial(Special::HandleScope))), ( v8 :: FastApiCallbackOptions ) => Ok(CBare(TSpecial(Special::FastApiCallbackOptions))), @@ -1483,7 +1486,9 @@ fn parse_type_path( // the easiest way to work with the 'rules!' macro above. match res { // OpState and JsRuntimeState appears in both ways - CBare(TSpecial(Special::OpState | Special::JsRuntimeState)) => {} + CBare(TSpecial( + Special::OpState | Special::JsRuntimeState | Special::PromiseId, + )) => {} CBare( TString(Strings::RefStr) | TSpecial(Special::HandleScope) | TV8(_), ) => {