diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/01_core.js | 88 | ||||
-rw-r--r-- | core/bindings.rs | 92 | ||||
-rw-r--r-- | core/examples/http_bench_json_ops.js | 4 | ||||
-rw-r--r-- | core/extensions.rs | 3 | ||||
-rw-r--r-- | core/runtime.rs | 57 |
5 files changed, 192 insertions, 52 deletions
diff --git a/core/01_core.js b/core/01_core.js index 7bee019d9..5df11c382 100644 --- a/core/01_core.js +++ b/core/01_core.js @@ -28,7 +28,7 @@ SymbolFor, setQueueMicrotask, } = window.__bootstrap.primordials; - const ops = window.Deno.core.ops; + const { ops } = window.Deno.core; const errorMap = {}; // Builtin v8 / JS errors @@ -159,21 +159,63 @@ return res; } - function opAsync(opName, ...args) { - const promiseId = nextPromiseId++; - let p = setPromise(promiseId); - try { - ops[opName](promiseId, ...args); - } catch (err) { - // Cleanup the just-created promise - getPromise(promiseId); - // Rethrow the error - throw err; + function rollPromiseId() { + return nextPromiseId++; + } + + // Generate async op wrappers. See core/bindings.rs + function initializeAsyncOps() { + function genAsyncOp(op, name, args) { + return new Function( + "setPromise", + "getPromise", + "promiseIdSymbol", + "rollPromiseId", + "handleOpCallTracing", + "op", + "unwrapOpResult", + "PromisePrototypeThen", + ` + return function ${name}(${args}) { + const id = rollPromiseId(); + let promise = PromisePrototypeThen(setPromise(id), unwrapOpResult); + try { + op(id, ${args}); + } catch (err) { + // Cleanup the just-created promise + getPromise(id); + // Rethrow the error + throw err; + } + handleOpCallTracing("${name}", id, promise); + promise[promiseIdSymbol] = id; + return promise; + } + `, + )( + setPromise, + getPromise, + promiseIdSymbol, + rollPromiseId, + handleOpCallTracing, + op, + unwrapOpResult, + PromisePrototypeThen, + ); } - p = PromisePrototypeThen(p, unwrapOpResult); + + // { <name>: <argc>, ... } + for (const ele of Object.entries(ops.asyncOpsInfo())) { + if (!ele) continue; + const [name, argc] = ele; + const op = ops[name]; + const args = Array.from({ length: argc }, (_, i) => `arg${i}`).join(", "); + ops[name] = genAsyncOp(op, name, args); + } + } + + function handleOpCallTracing(opName, promiseId, p) { if (opCallTracingEnabled) { - // Capture a stack trace by creating a new `Error` object. We remove the - // first 6 characters (the `Error\n` prefix) to get just the stack trace. const stack = StringPrototypeSlice(new Error().stack, 6); MapPrototypeSet(opCallTraces, promiseId, { opName, stack }); p = PromisePrototypeFinally( @@ -181,9 +223,10 @@ () => MapPrototypeDelete(opCallTraces, promiseId), ); } - // Save the id on the promise so it can later be ref'ed or unref'ed - p[promiseIdSymbol] = promiseId; - return p; + } + + function opAsync(opName, ...args) { + return ops[opName](...args); } function refOp(promiseId) { @@ -303,6 +346,7 @@ // Extra Deno.core.* exports const core = ObjectAssign(globalThis.Deno.core, { opAsync, + initializeAsyncOps, resources, metrics, registerErrorBuilder, @@ -322,11 +366,11 @@ setPromiseHooks, close: (rid) => ops.op_close(rid), tryClose: (rid) => ops.op_try_close(rid), - read: opAsync.bind(null, "op_read"), - readAll: opAsync.bind(null, "op_read_all"), - write: opAsync.bind(null, "op_write"), - writeAll: opAsync.bind(null, "op_write_all"), - shutdown: opAsync.bind(null, "op_shutdown"), + read: (rid, buffer) => ops.op_read(rid, buffer), + readAll: (rid) => ops.op_read_all(rid), + write: (rid, buffer) => ops.op_write(rid, buffer), + writeAll: (rid, buffer) => ops.op_write_all(rid, buffer), + shutdown: (rid) => ops.op_shutdown(rid), print: (msg, isErr) => ops.op_print(msg, isErr), setMacrotaskCallback: (fn) => ops.op_set_macrotask_callback(fn), setNextTickCallback: (fn) => ops.op_set_next_tick_callback(fn), diff --git a/core/bindings.rs b/core/bindings.rs index 1a869f3c4..b50c77df2 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -97,6 +97,7 @@ pub fn initialize_context<'s>( scope: &mut v8::HandleScope<'s, ()>, op_ctxs: &[OpCtx], snapshot_loaded: bool, + will_snapshot: bool, ) -> v8::Local<'s, v8::Context> { let scope = &mut v8::EscapableHandleScope::new(scope); @@ -112,7 +113,9 @@ pub fn initialize_context<'s>( let ops_obj = JsRuntime::grab_global::<v8::Object>(scope, "Deno.core.ops") .expect("Deno.core.ops to exist"); initialize_ops(scope, ops_obj, op_ctxs, snapshot_loaded); - + if !will_snapshot { + initialize_async_ops_info(scope, ops_obj, op_ctxs); + } return scope.escape(context); } @@ -124,8 +127,10 @@ pub fn initialize_context<'s>( // Bind functions to Deno.core.ops.* let ops_obj = JsRuntime::ensure_objs(scope, global, "Deno.core.ops").unwrap(); - - initialize_ops(scope, ops_obj, op_ctxs, snapshot_loaded); + if !will_snapshot { + initialize_async_ops_info(scope, ops_obj, op_ctxs); + } + initialize_ops(scope, ops_obj, op_ctxs, !will_snapshot); scope.escape(context) } @@ -586,3 +591,84 @@ pub fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef<str>) { let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); } + +struct AsyncOpsInfo { + ptr: *const OpCtx, + len: usize, +} + +impl<'s> IntoIterator for &'s AsyncOpsInfo { + type Item = &'s OpCtx; + type IntoIter = AsyncOpsInfoIterator<'s>; + + fn into_iter(self) -> Self::IntoIter { + AsyncOpsInfoIterator { + // SAFETY: OpCtx slice is valid for the lifetime of the Isolate + info: unsafe { std::slice::from_raw_parts(self.ptr, self.len) }, + index: 0, + } + } +} + +struct AsyncOpsInfoIterator<'s> { + info: &'s [OpCtx], + index: usize, +} + +impl<'s> Iterator for AsyncOpsInfoIterator<'s> { + type Item = &'s OpCtx; + + fn next(&mut self) -> Option<Self::Item> { + loop { + match self.info.get(self.index) { + Some(ctx) if ctx.decl.is_async => { + self.index += 1; + return Some(ctx); + } + Some(_) => { + self.index += 1; + } + None => return None, + } + } + } +} + +fn async_ops_info( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue, +) { + let async_op_names = v8::Object::new(scope); + let external: v8::Local<v8::External> = args.data().try_into().unwrap(); + let info: &AsyncOpsInfo = + // SAFETY: external is guaranteed to be a valid pointer to AsyncOpsInfo + unsafe { &*(external.value() as *const AsyncOpsInfo) }; + for ctx in info { + let name = v8::String::new(scope, ctx.decl.name).unwrap(); + let argc = v8::Integer::new(scope, ctx.decl.argc as i32); + async_op_names.set(scope, name.into(), argc.into()); + } + rv.set(async_op_names.into()); +} + +fn initialize_async_ops_info( + scope: &mut v8::HandleScope, + ops_obj: v8::Local<v8::Object>, + op_ctxs: &[OpCtx], +) { + let key = v8::String::new(scope, "asyncOpsInfo").unwrap(); + let external = v8::External::new( + scope, + Box::into_raw(Box::new(AsyncOpsInfo { + ptr: op_ctxs as *const [OpCtx] as _, + len: op_ctxs.len(), + })) as *mut c_void, + ); + let val = v8::Function::builder(async_ops_info) + .data(external.into()) + .build(scope) + .unwrap(); + val.set_name(key); + ops_obj.set(scope, key.into(), val.into()); +} diff --git a/core/examples/http_bench_json_ops.js b/core/examples/http_bench_json_ops.js index 883ef1752..261d91559 100644 --- a/core/examples/http_bench_json_ops.js +++ b/core/examples/http_bench_json_ops.js @@ -2,6 +2,8 @@ // This is not a real HTTP server. We read blindly one time into 'requestBuf', // then write this fixed 'responseBuf'. The point of this benchmark is to // exercise the event loop in a simple yet semi-realistic way. +Deno.core.initializeAsyncOps(); + const requestBuf = new Uint8Array(64 * 1024); const responseBuf = new Uint8Array( "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n" @@ -16,7 +18,7 @@ function listen() { /** Accepts a connection, returns rid. */ function accept(serverRid) { - return Deno.core.opAsync("op_accept", serverRid); + return Deno.core.ops.op_accept(serverRid); } async function serve(rid) { diff --git a/core/extensions.rs b/core/extensions.rs index 846770d1f..7b9ab5908 100644 --- a/core/extensions.rs +++ b/core/extensions.rs @@ -16,6 +16,9 @@ pub struct OpDecl { pub enabled: bool, pub is_async: bool, pub is_unstable: bool, + /// V8 argument count. Used as an optimization + /// hint by `core.initalizeAsyncOps`. + pub argc: usize, pub is_v8: bool, pub fast_fn: Option<Box<dyn FastFunction>>, } diff --git a/core/runtime.rs b/core/runtime.rs index e574bf9d3..e191596da 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -384,7 +384,8 @@ impl JsRuntime { isolate_ptr.read() }; let scope = &mut v8::HandleScope::new(&mut isolate); - let context = bindings::initialize_context(scope, &op_ctxs, false); + let context = + bindings::initialize_context(scope, &op_ctxs, false, true); global_context = v8::Global::new(scope, context); scope.set_default_context(context); } @@ -422,7 +423,7 @@ impl JsRuntime { }; let scope = &mut v8::HandleScope::new(&mut isolate); let context = - bindings::initialize_context(scope, &op_ctxs, snapshot_loaded); + bindings::initialize_context(scope, &op_ctxs, snapshot_loaded, false); global_context = v8::Global::new(scope, context); } @@ -550,6 +551,7 @@ impl JsRuntime { scope, &self.state.borrow().op_ctxs, self.built_from_snapshot, + false, ); JsRealm::new(v8::Global::new(scope, context)) }; @@ -2243,6 +2245,7 @@ pub mod tests { #[derive(Copy, Clone)] enum Mode { Async, + AsyncDeferred, AsyncZeroCopy(bool), } @@ -2251,20 +2254,28 @@ pub mod tests { dispatch_count: Arc<AtomicUsize>, } - #[op(deferred)] + #[op] async fn op_test( rc_op_state: Rc<RefCell<OpState>>, control: u8, buf: Option<ZeroCopyBuf>, ) -> Result<u8, AnyError> { + #![allow(clippy::await_holding_refcell_ref)] // False positive. let op_state_ = rc_op_state.borrow(); let test_state = op_state_.borrow::<TestState>(); test_state.dispatch_count.fetch_add(1, Ordering::Relaxed); - match test_state.mode { + let mode = test_state.mode; + drop(op_state_); + match mode { Mode::Async => { assert_eq!(control, 42); Ok(43) } + Mode::AsyncDeferred => { + tokio::task::yield_now().await; + assert_eq!(control, 42); + Ok(43) + } Mode::AsyncZeroCopy(has_buffer) => { assert_eq!(buf.is_some(), has_buffer); if let Some(buf) = buf { @@ -2314,14 +2325,15 @@ pub mod tests { #[test] fn test_ref_unref_ops() { - let (mut runtime, _dispatch_count) = setup(Mode::Async); + let (mut runtime, _dispatch_count) = setup(Mode::AsyncDeferred); runtime .execute_script( "filename.js", r#" + Deno.core.initializeAsyncOps(); var promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId"); - var p1 = Deno.core.opAsync("op_test", 42); - var p2 = Deno.core.opAsync("op_test", 42); + var p1 = Deno.core.ops.op_test(42); + var p2 = Deno.core.ops.op_test(42); "#, ) .unwrap(); @@ -2374,6 +2386,7 @@ pub mod tests { "filename.js", r#" let control = 42; + Deno.core.initializeAsyncOps(); Deno.core.opAsync("op_test", control); async function main() { Deno.core.opAsync("op_test", control); @@ -2392,6 +2405,7 @@ pub mod tests { .execute_script( "filename.js", r#" + Deno.core.initializeAsyncOps(); const p = Deno.core.opAsync("op_test", 42); if (p[Symbol.for("Deno.core.internalPromiseId")] == undefined) { throw new Error("missing id on returned promise"); @@ -2408,6 +2422,7 @@ pub mod tests { .execute_script( "filename.js", r#" + Deno.core.initializeAsyncOps(); Deno.core.opAsync("op_test"); "#, ) @@ -2422,6 +2437,7 @@ pub mod tests { .execute_script( "filename.js", r#" + Deno.core.initializeAsyncOps(); let zero_copy_a = new Uint8Array([0]); Deno.core.opAsync("op_test", null, zero_copy_a); "#, @@ -3021,7 +3037,6 @@ pub mod tests { function main() { console.log("asdf); } - main(); "#, ); @@ -3041,18 +3056,16 @@ function assert(cond) { throw Error("assert"); } } - function main() { assert(false); } - main(); "#, ); let expected_error = r#"Error: assert at assert (error_stack.js:4:11) - at main (error_stack.js:9:3) - at error_stack.js:12:1"#; + at main (error_stack.js:8:3) + at error_stack.js:10:1"#; assert_eq!(result.unwrap_err().to_string(), expected_error); } @@ -3070,7 +3083,6 @@ main(); throw new Error("async"); }); })(); - try { await p; } catch (error) { @@ -3083,7 +3095,7 @@ main(); let expected_error = r#"Error: async at error_async_stack.js:5:13 at async error_async_stack.js:4:5 - at async error_async_stack.js:10:5"#; + at async error_async_stack.js:9:5"#; match runtime.poll_event_loop(cx, false) { Poll::Ready(Err(e)) => { @@ -3138,6 +3150,7 @@ if (errMessage !== "higher-level sync error: original sync error") { .execute_script( "test_error_context_async.js", r#" +Deno.core.initializeAsyncOps(); (async () => { let errMessage; try { @@ -3176,7 +3189,6 @@ function assertEquals(a, b) { const sab = new SharedArrayBuffer(16); const i32a = new Int32Array(sab); globalThis.resolved = false; - (function() { const result = Atomics.waitAsync(i32a, 0, 0); result.value.then( @@ -3184,7 +3196,6 @@ globalThis.resolved = false; () => { assertUnreachable(); }); })(); - const notify_return_value = Atomics.notify(i32a, 0, 1); assertEquals(1, notify_return_value); "#, @@ -3294,7 +3305,7 @@ assertEquals(1, notify_return_value); runtime .execute_script( "op_async_borrow.js", - "Deno.core.opAsync('op_async_borrow')", + "Deno.core.initializeAsyncOps(); Deno.core.ops.op_async_borrow()", ) .unwrap(); runtime.run_event_loop(false).await.unwrap(); @@ -3368,7 +3379,8 @@ Deno.core.ops.op_sync_serialize_object_with_numbers_as_keys({ .execute_script( "op_async_serialize_object_with_numbers_as_keys.js", r#" -Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { +Deno.core.initializeAsyncOps(); +Deno.core.ops.op_async_serialize_object_with_numbers_as_keys({ lines: { 100: { unit: "m" @@ -3406,6 +3418,7 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { .execute_script( "macrotasks_and_nextticks.js", r#" + Deno.core.initializeAsyncOps(); (async function () { const results = []; Deno.core.ops.op_set_macrotask_callback(() => { @@ -3416,7 +3429,6 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { results.push("nextTick"); Deno.core.ops.op_set_has_tick_scheduled(false); }); - Deno.core.ops.op_set_has_tick_scheduled(true); await Deno.core.opAsync('op_async_sleep'); if (results[0] != "nextTick") { @@ -3627,7 +3639,6 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { Deno.core.ops.op_store_pending_promise_exception(promise); Deno.core.ops.op_promise_reject(); }); - new Promise((_, reject) => reject(Error("reject"))); "#, ) @@ -3645,7 +3656,6 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { prev(...args); }); } - new Promise((_, reject) => reject(Error("reject"))); "#, ) @@ -3695,7 +3705,6 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => { Deno.core.ops.op_promise_reject(); }); - throw new Error('top level throw'); "#; @@ -3826,8 +3835,6 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { const a1b = a1.subarray(0, 3); const a2 = new Uint8Array([5,10,15]); const a2b = a2.subarray(0, 3); - - if (!(a1.length > 0 && a1b.length > 0)) { throw new Error("a1 & a1b should have a length"); } @@ -3838,7 +3845,6 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { if (a1.length > 0 || a1b.length > 0) { throw new Error("expecting a1 & a1b to be detached"); } - const a3 = Deno.core.ops.op_boomerang(a2b); if (a3.byteLength != 3) { throw new Error(`Expected a3.byteLength === 3, got ${a3.byteLength}`); @@ -3849,7 +3855,6 @@ Deno.core.opAsync('op_async_serialize_object_with_numbers_as_keys', { if (a2.byteLength > 0 || a2b.byteLength > 0) { throw new Error("expecting a2 & a2b to be detached, a3 re-attached"); } - const wmem = new WebAssembly.Memory({ initial: 1, maximum: 2 }); const w32 = new Uint32Array(wmem.buffer); w32[0] = 1; w32[1] = 2; w32[2] = 3; |