LibJS: Synchronous await fast path when microtask queue is empty
When an async function is resumed from a microtask and hits another await with a non-thenable value (primitive or already-settled native promise), and the microtask queue is empty, we can resolve the await synchronously without suspending. No other microtask can observe the difference in execution order, making this optimization safe. This avoids the overhead of creating a GC::Function for the microtask job, enqueuing/dequeuing from the microtask queue, and the execution context push/pop that comes with it. A new VM host hook, host_promise_job_queue_is_empty, is added so both the standalone js binary and LibWeb can provide the appropriate check for their respective job queue implementations.
This commit is contained in:
parent
94d3aa8d89
commit
3e1145ef07
6 changed files with 135 additions and 1 deletions
|
|
@ -181,12 +181,44 @@ void AsyncFunctionDriverWrapper::continue_async_execution(VM& vm, Value value, b
|
|||
return {};
|
||||
}
|
||||
|
||||
// We hit `await Promise`
|
||||
// We hit `await value`
|
||||
//
|
||||
// OPTIMIZATION: Synchronous await fast path.
|
||||
// If we're not in the initial execution (i.e. we were resumed from a microtask)
|
||||
// and the microtask queue is empty, we can resolve the await synchronously
|
||||
// without suspending. This is safe because no other microtask can observe the
|
||||
// difference in execution order.
|
||||
if (!m_is_initial_execution && vm.host_promise_job_queue_is_empty()) {
|
||||
auto& realm = *vm.current_realm();
|
||||
if (!promise_value.is_object()) {
|
||||
// Primitive values are never thenable.
|
||||
generator_result = m_generator_object->resume(vm, promise_value, {});
|
||||
continue;
|
||||
}
|
||||
if (auto promise = promise_value.as_if<Promise>()) {
|
||||
auto* promise_prototype = realm.intrinsics().promise_prototype().ptr();
|
||||
if (promise->state() != Promise::State::Pending
|
||||
&& promise->shape().property_count() == 0
|
||||
&& promise->shape().prototype() == promise_prototype
|
||||
&& promise_prototype->get_without_side_effects(vm.names.constructor) == Value(realm.intrinsics().promise_constructor())) {
|
||||
auto is_fulfilled = promise->state() == Promise::State::Fulfilled;
|
||||
promise->set_is_handled();
|
||||
if (is_fulfilled) {
|
||||
generator_result = m_generator_object->resume(vm, promise->result(), {});
|
||||
} else {
|
||||
generator_result = m_generator_object->resume_abrupt(vm, throw_completion(promise->result()), {});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto await_result = this->await(promise_value);
|
||||
if (await_result.is_throw_completion()) {
|
||||
generator_result = m_generator_object->resume_abrupt(vm, await_result.release_error(), {});
|
||||
continue;
|
||||
}
|
||||
m_is_initial_execution = false;
|
||||
return {};
|
||||
}
|
||||
}();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ private:
|
|||
OwnPtr<ExecutionContext> m_suspended_execution_context;
|
||||
|
||||
GC::Ptr<NativeFunction> m_on_settled;
|
||||
bool m_is_initial_execution { true };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ VM::VM(ErrorMessages error_messages)
|
|||
enqueue_promise_job(job, realm);
|
||||
};
|
||||
|
||||
host_promise_job_queue_is_empty = [this]() -> bool {
|
||||
return m_promise_jobs.is_empty();
|
||||
};
|
||||
|
||||
host_make_job_callback = [](FunctionObject& function_object) {
|
||||
return make_job_callback(function_object);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ public:
|
|||
Function<void(StringView)> host_unrecognized_date_string;
|
||||
Function<ThrowCompletionOr<void>(Realm&, NonnullOwnPtr<ExecutionContext>, ShadowRealm&)> host_initialize_shadow_realm;
|
||||
Function<Crypto::SignedBigInteger(Object const& global)> host_system_utc_epoch_nanoseconds;
|
||||
Function<bool()> host_promise_job_queue_is_empty;
|
||||
|
||||
[[nodiscard]] Vector<StackTraceElement> stack_trace() const;
|
||||
|
||||
|
|
|
|||
|
|
@ -333,6 +333,10 @@ void initialize_main_thread_vm(AgentType type)
|
|||
}));
|
||||
};
|
||||
|
||||
s_main_thread_vm->host_promise_job_queue_is_empty = []() -> bool {
|
||||
return HTML::main_thread_event_loop().microtask_queue_empty();
|
||||
};
|
||||
|
||||
// 8.1.5.4.4 HostMakeJobCallback(callable), https://html.spec.whatwg.org/multipage/webappapis.html#hostmakejobcallback
|
||||
// https://whatpr.org/html/9893/webappapis.html#hostmakejobcallback
|
||||
s_main_thread_vm->host_make_job_callback = [](JS::FunctionObject& callable) -> GC::Ref<JS::JobCallback> {
|
||||
|
|
|
|||
|
|
@ -600,6 +600,98 @@ describe("microtask ordering with await", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// The synchronous await fast path fires when:
|
||||
// 1. m_is_initial_execution is false (i.e. we've already suspended once), AND
|
||||
// 2. the microtask queue is empty (no other async work in flight)
|
||||
// This means it activates on the 2nd+ await in a single async function
|
||||
// when no other async functions are running concurrently.
|
||||
describe("synchronous await fast path", () => {
|
||||
test("multiple rejected promises in sequence", () => {
|
||||
const caught = [];
|
||||
async function f() {
|
||||
for (const v of [1, 2, 3]) {
|
||||
try {
|
||||
await Promise.reject(v);
|
||||
} catch (e) {
|
||||
caught.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
f();
|
||||
runQueuedPromiseJobs();
|
||||
expect(caught).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("mixed resolved and rejected promises", () => {
|
||||
const results = [];
|
||||
async function f() {
|
||||
results.push(await Promise.resolve("a"));
|
||||
try {
|
||||
await Promise.reject("b");
|
||||
} catch (e) {
|
||||
results.push("caught:" + e);
|
||||
}
|
||||
results.push(await 42);
|
||||
results.push(await Promise.resolve("d"));
|
||||
try {
|
||||
await Promise.reject("e");
|
||||
} catch (e) {
|
||||
results.push("caught:" + e);
|
||||
}
|
||||
}
|
||||
f();
|
||||
runQueuedPromiseJobs();
|
||||
expect(results).toEqual(["a", "caught:b", 42, "d", "caught:e"]);
|
||||
});
|
||||
|
||||
test("promise with own property falls back to slow path", () => {
|
||||
const results = [];
|
||||
async function f() {
|
||||
results.push(await Promise.resolve(1));
|
||||
const p = Promise.resolve(2);
|
||||
p.extraProp = true;
|
||||
results.push(await p);
|
||||
results.push(await Promise.resolve(3));
|
||||
}
|
||||
f();
|
||||
runQueuedPromiseJobs();
|
||||
expect(results).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("promise subclass falls back to slow path", () => {
|
||||
class MyPromise extends Promise {}
|
||||
const results = [];
|
||||
async function f() {
|
||||
results.push(await Promise.resolve(1));
|
||||
results.push(await MyPromise.resolve(2));
|
||||
results.push(await Promise.resolve(3));
|
||||
}
|
||||
f();
|
||||
runQueuedPromiseJobs();
|
||||
expect(results).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("pending promise falls back to slow path", () => {
|
||||
let resolve;
|
||||
const results = [];
|
||||
async function f() {
|
||||
results.push(await Promise.resolve(1));
|
||||
results.push(
|
||||
await new Promise(r => {
|
||||
resolve = r;
|
||||
})
|
||||
);
|
||||
results.push(await Promise.resolve(3));
|
||||
}
|
||||
f();
|
||||
runQueuedPromiseJobs();
|
||||
expect(results).toEqual([1]);
|
||||
resolve(2);
|
||||
runQueuedPromiseJobs();
|
||||
expect(results).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("await fast path does not invoke getters on Promise.prototype.constructor", () => {
|
||||
test("getter on Promise.prototype.constructor causes fallback to slow path", () => {
|
||||
let calls = 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue