LibJS: Add fast path in async function await for non-thenable values

Per spec, every `await` goes through PromiseResolve (which wraps the
value in a new Promise via NewPromiseCapability) and then
PerformPromiseThen (which creates PromiseReaction and JobCallback
objects). This results in 13-16 GC cell allocations per await.

Add a fast path that detects two common cases:

1. Primitive values: These can never have a "then" property, so we
   can skip all promise wrapping and directly schedule the async
   function's continuation as a microtask.

2. Already-settled native Promises: If the promise has no own
   properties and its prototype is the intrinsic %Promise.prototype%,
   we can extract the result directly and schedule continuation.

For these cases, we bypass promise_resolve(), new_promise_capability(),
create_resolving_functions(), perform_then(), PromiseReaction creation,
and JobCallback creation -- replacing ~13 GC allocations with 1
(the GC::Function for the microtask job).
This commit is contained in:
Andreas Kling
2026-03-15 11:21:35 -05:00
committed by Andreas Kling
parent b34274a2a0
commit 3a2f2f3926
Notes: github-actions[bot] 2026-03-16 17:02:59 +00:00
3 changed files with 419 additions and 0 deletions

View File

@@ -46,6 +46,37 @@ ThrowCompletionOr<void> AsyncFunctionDriverWrapper::await(JS::Value value)
if (!m_suspended_execution_context)
m_suspended_execution_context = vm.running_execution_context().copy();
// OPTIMIZATION: Fast path for non-thenable values.
//
// Per spec, PromiseResolve wraps non-Promise values in a new resolved promise,
// then PerformPromiseThen attaches reaction handlers and schedules a microtask.
// This creates 10+ GC objects per await.
//
// Since primitives can never have a "then" property, and already-settled native
// Promises with the %Promise% constructor don't need wrapping, we can skip all
// of that machinery and directly schedule the async function's continuation.
//
// For pending promises, or promises with a non-standard constructor, we fall
// through to the spec-compliant slow path.
if (!value.is_object()) {
// Primitive values are never thenable -- schedule resume directly.
schedule_resume(value, true);
return {};
}
if (auto promise = value.as_if<Promise>()) {
// Already-settled native Promises whose constructor is the intrinsic %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())) {
schedule_resume(promise->result(), promise->state() == Promise::State::Fulfilled);
promise->set_is_handled();
return {};
}
}
// 2. Let promise be ? PromiseResolve(%Promise%, value).
auto* promise_object = TRY(promise_resolve(vm, realm.intrinsics().promise_constructor(), value));
@@ -105,6 +136,19 @@ ThrowCompletionOr<void> AsyncFunctionDriverWrapper::await(JS::Value value)
return {};
}
void AsyncFunctionDriverWrapper::schedule_resume(Value value, bool is_fulfilled)
{
auto& vm = this->vm();
vm.host_enqueue_promise_job(
GC::create_function(vm.heap(), [this, value, is_fulfilled, &vm]() -> ThrowCompletionOr<Value> {
TRY(vm.push_execution_context(*m_suspended_execution_context, {}));
continue_async_execution(vm, value, is_fulfilled);
vm.pop_execution_context();
return js_undefined();
}),
vm.current_realm());
}
void AsyncFunctionDriverWrapper::continue_async_execution(VM& vm, Value value, bool is_successful)
{
auto generator_result = is_successful