perf(core): async op pseudo-codegen and performance work (#18887)

Performance:

```
async_ops.js: 760k -> 1030k (!)
async_ops_deferred.js: 730k -> 770k
Deno.serve bench: 118k -> 124k
WS test w/ third_party/prebuilt/mac/load_test 100 localhost 8000 0 0: unchanged

Startup time: approx 0.5ms slower (13.7 -> 14.2ms)
```
This commit is contained in:
Matt Mastracci 2023-04-30 10:50:24 +02:00 committed by GitHub
parent 9c8ebce3dc
commit bb1f5e4262
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 737 additions and 237 deletions

View file

@ -17,4 +17,6 @@ async function bench(fun) {
}
const core = Deno[Deno.internal].core;
bench(() => core.opAsync("op_void_async"));
const ops = core.ops;
const opVoidAsync = ops.op_void_async;
bench(() => opVoidAsync());

View file

@ -17,4 +17,6 @@ async function bench(fun) {
}
const core = Deno[Deno.internal].core;
bench(() => core.opAsync("op_void_async_deferred"));
const ops = core.ops;
const opVoidAsyncDeferred = ops.op_void_async_deferred;
bench(() => opVoidAsyncDeferred());

View file

@ -80,12 +80,14 @@ Deno.test(function metricsForOpCrates() {
// Test that op_names == Objects.keys(Deno[Deno.internal].core.ops)
// since building the per-op metrics depends on op_names being complete
Deno.test(function opNamesMatch() {
// @ts-ignore: Deno[Deno.internal].core allowed
const ops = Object.keys(Deno[Deno.internal].core.ops);
// @ts-ignore: Deno[Deno.internal].core allowed
ops.concat(Object.keys(Deno[Deno.internal].core.asyncOps));
assertEquals(
// @ts-ignore: Deno[Deno.internal].core allowed
Deno[Deno.internal].core.opNames().sort(),
// @ts-ignore: Deno[Deno.internal].core allowed
Object.keys(Deno[Deno.internal].core.ops).sort().filter((name) =>
name !== "asyncOpsInfo"
),
ops.sort().filter((name) => name !== "asyncOpsInfo"),
);
});

View file

@ -1,20 +1,18 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "https://deno.land/std@v0.42.0/testing/asserts.ts";
import { assert, assertStringIncludes, unreachable } from "./test_util.ts";
Deno.test(async function sendAsyncStackTrace() {
const buf = new Uint8Array(10);
const rid = 10;
try {
await Deno.read(rid, buf);
await core.ops.op_error_async();
unreachable();
} catch (error) {
assert(error instanceof Error);
const s = error.stack?.toString();
assert(s);
console.log(s);
assertStringIncludes(s, "opcall_test.ts");
assertStringIncludes(s, "read");
assertStringIncludes(s, "sendAsyncStackTrace");
assert(
!s.includes("ext:core"),
"opcall stack traces should NOT include ext:core internals such as unwrapOpResult",
@ -22,6 +20,31 @@ Deno.test(async function sendAsyncStackTrace() {
}
});
Deno.test(async function sendAsyncStackTraceDeferred() {
try {
await core.ops.op_error_async_deferred();
unreachable();
} catch (error) {
assert(error instanceof Error);
const s = error.stack?.toString();
assert(s);
assertStringIncludes(s, "opcall_test.ts");
assertStringIncludes(s, "sendAsyncStackTraceDeferred");
assert(
!s.includes("ext:core"),
"opcall stack traces should NOT include ext:core internals such as unwrapOpResult",
);
}
});
Deno.test(function syncAdd() {
assertEquals(30, core.ops.op_add(10, 20));
});
Deno.test(async function asyncAdd() {
assertEquals(30, await core.ops.op_add_async(10, 20));
});
// @ts-ignore This is not publicly typed namespace, but it's there for sure.
const core = Deno[Deno.internal].core;

View file

@ -46,6 +46,8 @@ declare global {
encode(value: string): Uint8Array;
// deno-lint-ignore no-explicit-any
ops: Record<string, (...args: unknown[]) => any>;
// deno-lint-ignore no-explicit-any
asyncOps: Record<string, (...args: unknown[]) => any>;
print(msg: string, stderr: boolean): void;
registerErrorClass(
name: string,

View file

@ -16,11 +16,15 @@
ObjectAssign,
ObjectFreeze,
ObjectFromEntries,
ObjectKeys,
Promise,
PromiseReject,
PromiseResolve,
PromisePrototypeThen,
RangeError,
ReferenceError,
ReflectHas,
ReflectApply,
SafeArrayIterator,
SafeMap,
SafePromisePrototypeFinally,
@ -32,7 +36,7 @@
TypeError,
URIError,
} = window.__bootstrap.primordials;
const { ops } = window.Deno.core;
const { ops, asyncOps } = window.Deno.core;
const build = {
target: "unknown",
@ -85,6 +89,17 @@
return opCallTracingEnabled;
}
function movePromise(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);
}
return promiseRing[idx] = NO_PROMISE;
}
function setPromise(promiseId) {
const idx = promiseId % RING_SIZE;
// Move old promise from ring to map
@ -208,7 +223,29 @@
return error;
}
function unwrapOpResult(res) {
function unwrapOpError(hideFunction) {
return (res) => {
// .$err_class_name is a special key that should only exist on errors
const className = res?.$err_class_name;
if (!className) {
return res;
}
const errorBuilder = errorMap[className];
const err = errorBuilder ? errorBuilder(res.message) : new Error(
`Unregistered error class: "${className}"\n ${res.message}\n Classes of errors returned from ops should be registered via Deno.core.registerErrorClass().`,
);
// Set .code if error was a known OS error, see error_codes.rs
if (res.code) {
err.code = res.code;
}
// Strip unwrapOpResult() and errorBuilder() calls from stack trace
ErrorCaptureStackTrace(err, hideFunction);
throw err;
};
}
function unwrapOpResultNewPromise(id, res, hideFunction) {
// .$err_class_name is a special key that should only exist on errors
if (res?.$err_class_name) {
const className = res.$err_class_name;
@ -221,59 +258,359 @@
err.code = res.code;
}
// Strip unwrapOpResult() and errorBuilder() calls from stack trace
ErrorCaptureStackTrace(err, unwrapOpResult);
throw err;
ErrorCaptureStackTrace(err, hideFunction);
return PromiseReject(err);
}
return res;
const promise = PromiseResolve(res);
promise[promiseIdSymbol] = id;
return promise;
}
/*
Basic codegen.
TODO(mmastrac): automate this (handlebars?)
let s = "";
const vars = "abcdefghijklm";
for (let i = 0; i < 10; i++) {
let args = "";
for (let j = 0; j < i; j++) {
args += `${vars[j]},`;
}
s += `
case ${i}:
fn = function async_op_${i}(${args}) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, ${args});
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_${i});
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_${i});
return PromiseReject(err);
}
let promise = PromisePrototypeThen(setPromise(id), unwrapOpError(eventLoopTick));
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
`;
}
*/
// This function is called once per async stub
function asyncStub(opName, args) {
setUpAsyncStub(opName);
return ReflectApply(ops[opName], undefined, args);
}
function setUpAsyncStub(opName) {
const originalOp = asyncOps[opName];
let fn;
// The body of this switch statement can be generated using the script above.
switch (originalOp.length - 1) {
case 0:
fn = function async_op_0() {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_0);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_0);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 1:
fn = function async_op_1(a) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_1);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_1);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 2:
fn = function async_op_2(a, b) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_2);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_2);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 3:
fn = function async_op_3(a, b, c) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_3);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_3);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 4:
fn = function async_op_4(a, b, c, d) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_4);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_4);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 5:
fn = function async_op_5(a, b, c, d, e) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_5);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_5);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 6:
fn = function async_op_6(a, b, c, d, e, f) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_6);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_6);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 7:
fn = function async_op_7(a, b, c, d, e, f, g) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f, g);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_7);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_7);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 8:
fn = function async_op_8(a, b, c, d, e, f, g, h) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f, g, h);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_8);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_8);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 9:
fn = function async_op_9(a, b, c, d, e, f, g, h, i) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f, g, h, i);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_9);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_9);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
default:
throw new Error(
`Too many arguments for async op codegen (length of ${opName} was ${
originalOp.length - 1
})`,
);
}
return (ops[opName] = fn);
}
function opAsync2(name, arg0, arg1) {
const id = nextPromiseId++;
let promise = PromisePrototypeThen(setPromise(id), unwrapOpResult);
let maybeResult;
try {
maybeResult = ops[name](id, arg0, arg1);
} catch (err) {
// Cleanup the just-created promise
getPromise(id);
if (!ReflectHas(ops, name)) {
throw new TypeError(`${name} is not a registered op`);
const maybeResult = asyncOps[name](id, arg0, arg1);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, opAsync2);
}
// Rethrow the error
throw err;
} catch (err) {
movePromise(id);
if (!ReflectHas(asyncOps, name)) {
return PromiseReject(new TypeError(`${name} is not a registered op`));
}
ErrorCaptureStackTrace(err, opAsync2);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(name, id, promise);
promise[promiseIdSymbol] = id;
if (typeof maybeResult !== "undefined") {
const promise = getPromise(id);
promise.resolve(maybeResult);
}
return promise;
}
function opAsync(name, ...args) {
const id = nextPromiseId++;
let promise = PromisePrototypeThen(setPromise(id), unwrapOpResult);
let maybeResult;
try {
maybeResult = ops[name](id, ...new SafeArrayIterator(args));
} catch (err) {
// Cleanup the just-created promise
getPromise(id);
if (!ReflectHas(ops, name)) {
throw new TypeError(`${name} is not a registered op`);
const maybeResult = asyncOps[name](id, ...new SafeArrayIterator(args));
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, opAsync);
}
// Rethrow the error
throw err;
} catch (err) {
movePromise(id);
if (!ReflectHas(asyncOps, name)) {
return PromiseReject(new TypeError(`${name} is not a registered op`));
}
ErrorCaptureStackTrace(err, opAsync);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(name, id, promise);
promise[promiseIdSymbol] = id;
if (typeof maybeResult !== "undefined") {
const promise = getPromise(id);
promise.resolve(maybeResult);
}
return promise;
}
@ -439,8 +776,52 @@
);
}
// Eagerly initialize ops for snapshot purposes
for (const opName of new SafeArrayIterator(ObjectKeys(asyncOps))) {
setUpAsyncStub(opName);
}
function generateAsyncOpHandler(/* opNames... */) {
const fastOps = {};
for (const opName of new SafeArrayIterator(arguments)) {
if (ops[opName] === undefined) {
throw new Error(`Unknown or disabled op '${opName}'`);
}
if (asyncOps[opName] !== undefined) {
fastOps[opName] = setUpAsyncStub(opName);
} else {
fastOps[opName] = ops[opName];
}
}
return fastOps;
}
const {
op_close: close,
op_try_close: tryClose,
op_read: read,
op_read_all: readAll,
op_write: write,
op_write_all: writeAll,
op_read_sync: readSync,
op_write_sync: writeSync,
op_shutdown: shutdown,
} = generateAsyncOpHandler(
"op_close",
"op_try_close",
"op_read",
"op_read_all",
"op_write",
"op_write_all",
"op_read_sync",
"op_write_sync",
"op_shutdown",
);
// Extra Deno.core.* exports
const core = ObjectAssign(globalThis.Deno.core, {
asyncStub,
generateAsyncOpHandler,
opAsync,
opAsync2,
resources,
@ -460,15 +841,15 @@
unrefOp,
setReportExceptionCallback,
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"),
readSync: (rid, buffer) => ops.op_read_sync(rid, buffer),
writeSync: (rid, buffer) => ops.op_write_sync(rid, buffer),
shutdown: opAsync.bind(null, "op_shutdown"),
close,
tryClose,
read,
readAll,
write,
writeAll,
readSync,
writeSync,
shutdown,
print: (msg, isErr) => ops.op_print(msg, isErr),
setMacrotaskCallback,
setNextTickCallback,

49
core/bindings.js Normal file
View file

@ -0,0 +1,49 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
if (!globalThis.Deno) {
globalThis.Deno = {
core: {
ops: {},
asyncOps: {},
},
};
}
Deno.__op__console = function (callConsole, console) {
Deno.core.callConsole = callConsole;
Deno.core.console = console;
};
Deno.__op__registerOp = function (isAsync, op, opName) {
const core = Deno.core;
if (isAsync) {
if (core.ops[opName] !== undefined) {
return;
}
core.asyncOps[opName] = op;
core.ops[opName] = function (...args) {
if (this !== core.ops) {
// deno-lint-ignore prefer-primordials
throw new Error(
"An async stub cannot be separated from Deno.core.ops. Use ???",
);
}
return core.asyncStub(opName, args);
};
} else {
core.ops[opName] = op;
}
};
Deno.__op__unregisterOp = function (isAsync, opName) {
if (isAsync) {
delete Deno.core.asyncOps[opName];
}
delete Deno.core.ops[opName];
};
Deno.__op__cleanup = function () {
delete Deno.__op__console;
delete Deno.__op__registerOp;
delete Deno.__op__unregisterOp;
delete Deno.__op__cleanup;
};

View file

@ -1,9 +1,9 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use log::debug;
use std::fmt::Write;
use std::option::Option;
use std::os::raw::c_void;
use log::debug;
use v8::MapFnTo;
use crate::error::is_instance_of_error;
@ -98,6 +98,23 @@ pub fn module_origin<'a>(
)
}
fn get<'s, T>(
scope: &mut v8::HandleScope<'s>,
from: v8::Local<v8::Object>,
key: &'static [u8],
path: &'static str,
) -> T
where
v8::Local<'s, v8::Value>: TryInto<T>,
{
let key = v8::String::new_external_onebyte_static(scope, key).unwrap();
from
.get(scope, key.into())
.unwrap_or_else(|| panic!("{path} exists"))
.try_into()
.unwrap_or_else(|_| panic!("unable to convert"))
}
pub(crate) fn initialize_context<'s>(
scope: &mut v8::HandleScope<'s, ()>,
op_ctxs: &[OpCtx],
@ -108,135 +125,92 @@ pub(crate) fn initialize_context<'s>(
let scope = &mut v8::ContextScope::new(scope, context);
let deno_str =
v8::String::new_external_onebyte_static(scope, b"Deno").unwrap();
let core_str =
v8::String::new_external_onebyte_static(scope, b"core").unwrap();
let ops_str = v8::String::new_external_onebyte_static(scope, b"ops").unwrap();
let mut codegen = String::with_capacity(op_ctxs.len() * 200);
codegen.push_str(include_str!("bindings.js"));
_ = writeln!(
codegen,
"Deno.__op__ = function(opFns, callConsole, console) {{"
);
if !snapshot_options.loaded() {
_ = writeln!(codegen, "Deno.__op__console(callConsole, console);");
}
for op_ctx in op_ctxs {
if op_ctx.decl.enabled {
// If we're loading from a snapshot, we can skip registration for most ops
if matches!(snapshot_options, SnapshotOptions::Load)
&& !op_ctx.decl.force_registration
{
continue;
}
_ = writeln!(
codegen,
"Deno.__op__registerOp({}, opFns[{}], \"{}\");",
op_ctx.decl.is_async, op_ctx.id, op_ctx.decl.name
);
} else {
_ = writeln!(
codegen,
"Deno.__op__unregisterOp({}, \"{}\");",
op_ctx.decl.is_async, op_ctx.decl.name
);
}
}
codegen.push_str("Deno.__op__cleanup();");
_ = writeln!(codegen, "}}");
let ops_obj = if snapshot_options.loaded() {
// Snapshot already registered `Deno.core.ops` but
// extensions may provide ops that aren't part of the snapshot.
// Grab the Deno.core.ops object & init it
let deno_obj: v8::Local<v8::Object> = global
.get(scope, deno_str.into())
.unwrap()
.try_into()
.unwrap();
let core_obj: v8::Local<v8::Object> = deno_obj
.get(scope, core_str.into())
.unwrap()
.try_into()
.unwrap();
let ops_obj: v8::Local<v8::Object> = core_obj
.get(scope, ops_str.into())
.expect("Deno.core.ops to exist")
.try_into()
.unwrap();
ops_obj
let script = v8::String::new_from_one_byte(
scope,
codegen.as_bytes(),
v8::NewStringType::Normal,
)
.unwrap();
let script = v8::Script::compile(scope, script, None).unwrap();
script.run(scope);
let deno = get(scope, global, b"Deno", "Deno");
let op_fn: v8::Local<v8::Function> =
get(scope, deno, b"__op__", "Deno.__op__");
let recv = v8::undefined(scope);
let op_fns = v8::Array::new(scope, op_ctxs.len() as i32);
for op_ctx in op_ctxs {
let op_fn = op_ctx_function(scope, op_ctx);
op_fns.set_index(scope, op_ctx.id as u32, op_fn.into());
}
if snapshot_options.loaded() {
op_fn.call(scope, recv.into(), &[op_fns.into()]);
} else {
// globalThis.Deno = { core: { } };
let deno_obj = v8::Object::new(scope);
global.set(scope, deno_str.into(), deno_obj.into());
let core_obj = v8::Object::new(scope);
deno_obj.set(scope, core_str.into(), core_obj.into());
// Bind functions to Deno.core.*
set_func(scope, core_obj, "callConsole", call_console);
let call_console_fn = v8::Function::new(scope, call_console).unwrap();
// Bind v8 console object to Deno.core.console
let extra_binding_obj = context.get_extras_binding_object(scope);
let console_str =
v8::String::new_external_onebyte_static(scope, b"console").unwrap();
let console_obj = extra_binding_obj.get(scope, console_str.into()).unwrap();
core_obj.set(scope, console_str.into(), console_obj);
let console_obj: v8::Local<v8::Object> = get(
scope,
extra_binding_obj,
b"console",
"ExtrasBindingObject.console",
);
// Bind functions to Deno.core.ops.*
let ops_obj = v8::Object::new(scope);
core_obj.set(scope, ops_str.into(), ops_obj.into());
ops_obj
};
if matches!(snapshot_options, SnapshotOptions::Load) {
// Only register ops that have `force_registration` flag set to true,
// the remaining ones should already be in the snapshot. Ignore ops that
// are disabled.
for op_ctx in op_ctxs {
if op_ctx.decl.enabled {
if op_ctx.decl.force_registration {
add_op_to_deno_core_ops(scope, ops_obj, op_ctx);
}
} else {
delete_op_from_deno_core_ops(scope, ops_obj, op_ctx)
}
}
} else if matches!(snapshot_options, SnapshotOptions::CreateFromExisting) {
// Register all enabled ops, probing for which ones are already registered.
for op_ctx in op_ctxs {
let key = v8::String::new_external_onebyte_static(
scope,
op_ctx.decl.name.as_bytes(),
)
.unwrap();
if op_ctx.decl.enabled {
if ops_obj.get(scope, key.into()).is_some() {
continue;
}
add_op_to_deno_core_ops(scope, ops_obj, op_ctx);
} else {
delete_op_from_deno_core_ops(scope, ops_obj, op_ctx)
}
}
} else {
// In other cases register all ops enabled unconditionally.
for op_ctx in op_ctxs {
if op_ctx.decl.enabled {
add_op_to_deno_core_ops(scope, ops_obj, op_ctx);
}
}
op_fn.call(
scope,
recv.into(),
&[op_fns.into(), call_console_fn.into(), console_obj.into()],
);
}
context
}
fn set_func(
scope: &mut v8::HandleScope<'_>,
obj: v8::Local<v8::Object>,
name: &'static str,
callback: impl v8::MapFnTo<v8::FunctionCallback>,
) {
let key =
v8::String::new_external_onebyte_static(scope, name.as_bytes()).unwrap();
let val = v8::Function::new(scope, callback).unwrap();
val.set_name(key);
obj.set(scope, key.into(), val.into());
}
fn delete_op_from_deno_core_ops(
scope: &mut v8::HandleScope<'_>,
obj: v8::Local<v8::Object>,
fn op_ctx_function<'s>(
scope: &mut v8::HandleScope<'s>,
op_ctx: &OpCtx,
) {
let key =
v8::String::new_external_onebyte_static(scope, op_ctx.decl.name.as_bytes())
.unwrap();
obj.delete(scope, key.into());
}
fn add_op_to_deno_core_ops(
scope: &mut v8::HandleScope<'_>,
obj: v8::Local<v8::Object>,
op_ctx: &OpCtx,
) {
) -> v8::Local<'s, v8::Function> {
let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void;
let key =
v8::String::new_external_onebyte_static(scope, op_ctx.decl.name.as_bytes())
.unwrap();
let external = v8::External::new(scope, op_ctx_ptr as *mut c_void);
let builder = v8::FunctionTemplate::builder_raw(op_ctx.decl.v8_fn_ptr)
.data(external.into());
let builder: v8::FunctionBuilder<v8::FunctionTemplate> =
v8::FunctionTemplate::builder_raw(op_ctx.decl.v8_fn_ptr)
.data(external.into())
.length(op_ctx.decl.arg_count as i32);
let templ = if let Some(fast_function) = &op_ctx.decl.fast_fn {
builder.build_fast(
@ -249,9 +223,7 @@ fn add_op_to_deno_core_ops(
} else {
builder.build(scope)
};
let val = templ.get_function(scope).unwrap();
val.set_name(key);
obj.set(scope, key.into(), val.into());
templ.get_function(scope).unwrap()
}
pub extern "C" fn wasm_async_resolve_promise_callback(

View file

@ -73,6 +73,7 @@ pub struct OpDecl {
pub is_unstable: bool,
pub is_v8: bool,
pub force_registration: bool,
pub arg_count: u8,
pub fast_fn: Option<FastFunction>,
}

View file

@ -23,10 +23,16 @@ declare namespace Deno {
/**
* List of all registered ops, in the form of a map that maps op
* name to internal numerical op id.
* name to function.
*/
const ops: Record<string, (...args: unknown[]) => any>;
/**
* List of all registered async ops, in the form of a map that maps op
* name to function.
*/
const asyncOps: Record<string, (...args: unknown[]) => any>;
/**
* Retrieve a list of all open resources, in the form of a map that maps
* resource id to the resource name.

View file

@ -27,9 +27,12 @@ crate::extension!(
op_wasm_streaming_feed,
op_wasm_streaming_set_url,
op_void_sync,
op_error_async,
op_error_async_deferred,
op_void_async,
op_void_async_deferred,
op_add,
op_add_async,
// TODO(@AaronO): track IO metrics for builtin streams
op_read,
op_read_all,
@ -96,12 +99,27 @@ fn op_add(a: i32, b: i32) -> i32 {
a + b
}
#[op]
pub async fn op_add_async(a: i32, b: i32) -> i32 {
a + b
}
#[op(fast)]
pub fn op_void_sync() {}
#[op]
pub async fn op_void_async() {}
#[op]
pub async fn op_error_async() -> Result<(), Error> {
Err(Error::msg("error"))
}
#[op(deferred)]
pub async fn op_error_async_deferred() -> Result<(), Error> {
Err(Error::msg("error"))
}
#[op(deferred)]
pub async fn op_void_async_deferred() {}

View file

@ -3737,21 +3737,6 @@ assertEquals(1, notify_return_value);
})
}
#[test]
fn test_core_js_stack_frame() {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
// Call non-existent op so we get error from `core.js`
let error = runtime
.execute_script_static(
"core_js_stack_frame.js",
"Deno.core.opAsync('non_existent');",
)
.unwrap_err();
let error_string = error.to_string();
// Test that the script specifier is a URL: `ext:<repo-relative path>`.
assert!(error_string.contains("ext:core/01_core.js"));
}
#[test]
fn test_v8_platform() {
let options = RuntimeOptions {
@ -4721,21 +4706,6 @@ Deno.core.opAsync("op_async_serialize_object_with_numbers_as_keys", {
.is_ok());
}
#[test]
fn test_non_existent_async_op_error() {
// Verify that "resizable ArrayBuffer" is disabled
let mut runtime = JsRuntime::new(Default::default());
let err = runtime
.execute_script_static(
"test_rab.js",
r#"Deno.core.opAsync("this_op_doesnt_exist");"#,
)
.unwrap_err();
assert!(err
.to_string()
.contains("this_op_doesnt_exist is not a registered op"));
}
#[tokio::test]
async fn cant_load_internal_module_when_snapshot_is_loaded_and_not_snapshotting(
) {

View file

@ -1,4 +1,5 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file camelcase
const core = globalThis.Deno.core;
const primordials = globalThis.__bootstrap.primordials;
const internals = globalThis.__bootstrap.internals;
@ -46,6 +47,39 @@ const {
Uint8Array,
} = primordials;
const {
op_http_wait,
op_upgrade,
op_get_request_headers,
op_get_request_method_and_url,
op_read_request_body,
op_serve_http,
op_set_promise_complete,
op_set_response_body_bytes,
op_set_response_body_resource,
op_set_response_body_stream,
op_set_response_body_text,
op_set_response_header,
op_set_response_headers,
op_upgrade_raw,
op_ws_server_create,
} = Deno.core.generateAsyncOpHandler(
"op_http_wait",
"op_upgrade",
"op_get_request_headers",
"op_get_request_method_and_url",
"op_read_request_body",
"op_serve_http",
"op_set_promise_complete",
"op_set_response_body_bytes",
"op_set_response_body_resource",
"op_set_response_body_stream",
"op_set_response_body_text",
"op_set_response_header",
"op_set_response_headers",
"op_upgrade_raw",
"op_ws_server_create",
);
const _upgraded = Symbol("_upgraded");
function internalServerError() {
@ -143,7 +177,7 @@ class InnerRequest {
this.#upgraded = () => {};
const upgradeRid = core.ops.op_upgrade_raw(slabId);
const upgradeRid = op_upgrade_raw(slabId);
const conn = new TcpConn(
upgradeRid,
@ -174,12 +208,11 @@ class InnerRequest {
(async () => {
try {
// Returns the connection and extra bytes, which we can pass directly to op_ws_server_create
const upgrade = await core.opAsync2(
"op_upgrade",
const upgrade = await op_upgrade(
slabId,
response.headerList,
);
const wsRid = core.ops.op_ws_server_create(upgrade[0], upgrade[1]);
const wsRid = op_ws_server_create(upgrade[0], upgrade[1]);
// We have to wait for the go-ahead signal
await goAhead;
@ -214,7 +247,7 @@ class InnerRequest {
}
// TODO(mmastrac): This is quite slow as we're serializing a large number of values. We may want to consider
// splitting this up into multiple ops.
this.#methodAndUri = core.ops.op_get_request_method_and_url(this.#slabId);
this.#methodAndUri = op_get_request_method_and_url(this.#slabId);
}
const path = this.#methodAndUri[2];
@ -249,7 +282,7 @@ class InnerRequest {
if (this.#slabId === undefined) {
throw new TypeError("request closed");
}
this.#methodAndUri = core.ops.op_get_request_method_and_url(this.#slabId);
this.#methodAndUri = op_get_request_method_and_url(this.#slabId);
}
return {
transport: "tcp",
@ -263,7 +296,7 @@ class InnerRequest {
if (this.#slabId === undefined) {
throw new TypeError("request closed");
}
this.#methodAndUri = core.ops.op_get_request_method_and_url(this.#slabId);
this.#methodAndUri = op_get_request_method_and_url(this.#slabId);
}
return this.#methodAndUri[0];
}
@ -281,7 +314,7 @@ class InnerRequest {
this.#body = null;
return null;
}
this.#streamRid = core.ops.op_read_request_body(this.#slabId);
this.#streamRid = op_read_request_body(this.#slabId);
this.#body = new InnerBody(readableStreamForRid(this.#streamRid, false));
return this.#body;
}
@ -290,7 +323,7 @@ class InnerRequest {
if (this.#slabId === undefined) {
throw new TypeError("request closed");
}
return core.ops.op_get_request_headers(this.#slabId);
return op_get_request_headers(this.#slabId);
}
get slabId() {
@ -331,12 +364,12 @@ function fastSyncResponseOrStream(req, respBody) {
const body = stream.body;
if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, body)) {
core.ops.op_set_response_body_bytes(req, body);
op_set_response_body_bytes(req, body);
return null;
}
if (typeof body === "string") {
core.ops.op_set_response_body_text(req, body);
op_set_response_body_text(req, body);
return null;
}
@ -346,7 +379,7 @@ function fastSyncResponseOrStream(req, respBody) {
}
const resourceBacking = getReadableStreamResourceBacking(stream);
if (resourceBacking) {
core.ops.op_set_response_body_resource(
op_set_response_body_resource(
req,
resourceBacking.rid,
resourceBacking.autoClose,
@ -382,9 +415,9 @@ async function asyncResponse(responseBodies, req, status, stream) {
// and we race it.
let timeoutPromise;
timeout = setTimeout(() => {
responseRid = core.ops.op_set_response_body_stream(req);
responseRid = op_set_response_body_stream(req);
SetPrototypeAdd(responseBodies, responseRid);
core.ops.op_set_promise_complete(req, status);
op_set_promise_complete(req, status);
timeoutPromise = core.writeAll(responseRid, value1);
}, 250);
const { value: value2, done: done2 } = await reader.read();
@ -409,13 +442,13 @@ async function asyncResponse(responseBodies, req, status, stream) {
// Reader will be closed by finally block
// No response stream
closed = true;
core.ops.op_set_response_body_bytes(req, value1);
op_set_response_body_bytes(req, value1);
return;
}
responseRid = core.ops.op_set_response_body_stream(req);
responseRid = op_set_response_body_stream(req);
SetPrototypeAdd(responseBodies, responseRid);
core.ops.op_set_promise_complete(req, status);
op_set_promise_complete(req, status);
// Write our first packet
await core.writeAll(responseRid, value1);
}
@ -447,7 +480,7 @@ async function asyncResponse(responseBodies, req, status, stream) {
core.tryClose(responseRid);
SetPrototypeDelete(responseBodies, responseRid);
} else {
core.ops.op_set_promise_complete(req, status);
op_set_promise_complete(req, status);
}
}
}
@ -511,9 +544,9 @@ function mapToCallback(responseBodies, context, signal, callback, onError) {
const headers = inner.headerList;
if (headers && headers.length > 0) {
if (headers.length == 1) {
core.ops.op_set_response_header(req, headers[0][0], headers[0][1]);
op_set_response_header(req, headers[0][0], headers[0][1]);
} else {
core.ops.op_set_response_headers(req, headers);
op_set_response_headers(req, headers);
}
}
@ -523,7 +556,7 @@ function mapToCallback(responseBodies, context, signal, callback, onError) {
// Handle the stream asynchronously
await asyncResponse(responseBodies, req, status, stream);
} else {
core.ops.op_set_promise_complete(req, status);
op_set_promise_complete(req, status);
}
innerRequest?.close();
@ -591,13 +624,13 @@ async function serve(arg1, arg2) {
listenOpts.alpnProtocols = ["h2", "http/1.1"];
const listener = Deno.listenTls(listenOpts);
listenOpts.port = listener.addr.port;
context.initialize(core.ops.op_serve_http(
context.initialize(op_serve_http(
listener.rid,
));
} else {
const listener = Deno.listen(listenOpts);
listenOpts.port = listener.addr.port;
context.initialize(core.ops.op_serve_http(
context.initialize(op_serve_http(
listener.rid,
));
}
@ -624,7 +657,7 @@ async function serve(arg1, arg2) {
const rid = context.serverRid;
let req;
try {
req = await core.opAsync2("op_http_wait", rid);
req = await op_http_wait(rid);
} catch (error) {
if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) {
break;

View file

@ -144,6 +144,8 @@ impl Op {
is_unstable: #is_unstable,
is_v8: #is_v8,
force_registration: false,
// TODO(mmastrac)
arg_count: 0,
}
}
@ -158,8 +160,8 @@ impl Op {
let has_fallible_fast_call = active && optimizer.returns_result;
let v8_body = if is_async {
let deferred = attrs.deferred;
let (v8_body, arg_count) = if is_async {
let deferred: bool = attrs.deferred;
codegen_v8_async(
&core,
&item,
@ -205,6 +207,7 @@ impl Op {
is_unstable: #is_unstable,
is_v8: #is_v8,
force_registration: false,
arg_count: #arg_count as u8,
}
}
@ -241,7 +244,7 @@ fn codegen_v8_async(
margs: Attributes,
asyncness: bool,
deferred: bool,
) -> TokenStream2 {
) -> (TokenStream2, usize) {
let Attributes { is_v8, .. } = margs;
let special_args = f
.sig
@ -309,7 +312,7 @@ fn codegen_v8_async(
}
};
quote! {
let token_stream = quote! {
use #core::futures::FutureExt;
// SAFETY: #core guarantees args.data() is a v8 External pointing to an OpCtx for the isolates lifetime
let ctx = unsafe {
@ -336,7 +339,10 @@ fn codegen_v8_async(
if let Some(response) = maybe_response {
rv.set(response);
}
}
};
// +1 arg for the promise ID
(token_stream, 1 + f.sig.inputs.len() - rust_i0)
}
fn scope_arg(arg: &FnArg) -> Option<TokenStream2> {
@ -373,7 +379,7 @@ fn codegen_v8_sync(
f: &syn::ItemFn,
margs: Attributes,
has_fallible_fast_call: bool,
) -> TokenStream2 {
) -> (TokenStream2, usize) {
let Attributes { is_v8, .. } = margs;
let special_args = f
.sig
@ -404,7 +410,7 @@ fn codegen_v8_sync(
quote! {}
};
quote! {
let token_stream = quote! {
// SAFETY: #core guarantees args.data() is a v8 External pointing to an OpCtx for the isolates lifetime
let ctx = unsafe {
&*(#core::v8::Local::<#core::v8::External>::cast(args.data()).value()
@ -421,7 +427,9 @@ fn codegen_v8_sync(
op_state.tracker.track_sync(ctx.id);
#ret
}
};
(token_stream, f.sig.inputs.len() - rust_i0)
}
/// (full declarations, idents, v8 argument count)

View file

@ -41,6 +41,7 @@ impl op_void_async {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_async_result {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_fallback {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_cow_str {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_f64_buf {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl op_sync_serialize_object_with_numbers_as_keys {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl send_stdin {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl send_stdin {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl op_blob_revoke_object_url {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_ffi_ptr_value {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl op_print {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_set_exit_code {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl foo {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -47,6 +47,7 @@ impl op_foo {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 0usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl foo {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_listen {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 0usize as u8,
}
}
#[inline]

View file

@ -47,6 +47,7 @@ impl op_now {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_add_4 {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 4usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl op_try_close {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_string_length {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl op_read_sync {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -52,6 +52,7 @@ impl op_ffi_ptr_of {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_is_proxy {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_string_length {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl op_string_length {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]

View file

@ -31,6 +31,7 @@ impl op_bench_now {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 0usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_import_spki_x25519 {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_unit_result {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 0usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_set_nodelay {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 2usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_unit {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 0usize as u8,
}
}
#[inline]

View file

@ -41,6 +41,7 @@ impl op_wasm {
is_unstable: false,
is_v8: false,
force_registration: false,
arg_count: 1usize as u8,
}
}
#[inline]