Skip to content

Commit

Permalink
Reland "perf(core): generate inlined wrappers for async ops" (denolan…
Browse files Browse the repository at this point in the history
  • Loading branch information
littledivy committed Oct 28, 2022
1 parent d9e425a commit e189502
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 113 deletions.
5 changes: 3 additions & 2 deletions bench_util/js_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ pub fn bench_js_sync_with(

let code = v8::String::new(scope, looped_src.as_ref()).unwrap();
let script = v8::Script::compile(scope, code, None).unwrap();

// Run once if profiling, otherwise regular bench loop
if is_profiling() {
script.run(scope).unwrap();
Expand Down Expand Up @@ -102,7 +101,9 @@ pub fn bench_js_async_with(
};
let looped = loop_code(inner_iters, src);
let src = looped.as_ref();

runtime
.execute_script("init", "Deno.core.initializeAsyncOps();")
.unwrap();
if is_profiling() {
for _ in 0..opts.profiling_outer {
tokio_runtime.block_on(inner_async(src, &mut runtime));
Expand Down
5 changes: 3 additions & 2 deletions cli/bench/async_ops.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ let [total, count] = typeof Deno !== "undefined"
: [process.argv[2], process.argv[3]];

total = total ? parseInt(total, 0) : 50;
count = count ? parseInt(count, 10) : 100000;
count = count ? parseInt(count, 10) : 1000000;

async function bench(fun) {
const start = Date.now();
Expand All @@ -16,4 +16,5 @@ async function bench(fun) {
if (--total) queueMicrotask(() => bench(fun));
}

bench(() => Deno.core.opAsync("op_void_async"));
const { ops } = Deno.core;
bench(() => ops.op_void_async());
2 changes: 1 addition & 1 deletion cli/tests/unit/metrics_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ Deno.test(function opNamesMatch() {
// @ts-ignore: Deno.core allowed
Deno.core.opNames().sort(),
// @ts-ignore: Deno.core allowed
Object.keys(Deno.core.ops).sort(),
Object.keys(Deno.core.ops).sort().filter((name) => name !== "asyncOpsInfo"),
);
});
88 changes: 66 additions & 22 deletions core/01_core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -159,31 +159,74 @@
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(
p,
() => 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) {
Expand Down Expand Up @@ -303,6 +346,7 @@
// Extra Deno.core.* exports
const core = ObjectAssign(globalThis.Deno.core, {
opAsync,
initializeAsyncOps,
resources,
metrics,
registerErrorBuilder,
Expand All @@ -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),
Expand Down
92 changes: 89 additions & 3 deletions core/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}

Expand All @@ -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)
}

Expand Down Expand Up @@ -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());
}
4 changes: 3 additions & 1 deletion core/examples/http_bench_json_ops.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions core/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>,
}
Expand Down
Loading

0 comments on commit e189502

Please sign in to comment.