Compare commits

...

4 Commits

Author SHA1 Message Date
SUZUKI Sosuke
76582063b8 Merge branch 'main' into claude/optimize-microtask-dispatch 2026-02-11 21:42:01 +09:00
Sosuke Suzuki
d6a5cfa69e build: update WebKit to preview-pr-160-8680a32c
Points to the WebKit build with maxMicrotaskArguments 5→4 reduction,
which removes the performMicrotaskFunction argument from
BunPerformMicrotaskJob.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:38:56 +09:00
Sosuke Suzuki
9bd4ad7166 bench: add microtask throughput benchmark
Tests queueMicrotask, Promise chains, Promise.all,
AsyncLocalStorage + queueMicrotask, and async/await chains.
2026-02-11 19:52:05 +09:00
Sosuke Suzuki
d6d41d58d1 perf: remove performMicrotaskFunction from BunPerformMicrotaskJob call sites
Companion change to WebKit's maxMicrotaskArguments 5→4 reduction.

- Remove performMicrotaskFunction from all QueuedTask construction sites
  (functionQueueMicrotask, JSC__JSPromise__reject_, queueMicrotaskJob)
- Delete jsFunctionPerformMicrotask (now inlined in JSC's handler)
- Remove m_performMicrotaskFunction LazyProperty and accessor

sizeof(QueuedTask) drops from 56 to 48 bytes (-14%).
2026-02-11 19:17:17 +09:00
5 changed files with 115 additions and 77 deletions

View File

@@ -0,0 +1,108 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { bench, group, run } from "../runner.mjs";
// Benchmark 1: queueMicrotask throughput
// Tests the BunPerformMicrotaskJob handler path directly.
// The optimization removes the JS trampoline and uses callMicrotask.
group("queueMicrotask throughput", () => {
bench("queueMicrotask 1k", () => {
return new Promise(resolve => {
let remaining = 1000;
const tick = () => {
if (--remaining === 0) resolve();
else queueMicrotask(tick);
};
queueMicrotask(tick);
});
});
bench("queueMicrotask 10k", () => {
return new Promise(resolve => {
let remaining = 10000;
const tick = () => {
if (--remaining === 0) resolve();
else queueMicrotask(tick);
};
queueMicrotask(tick);
});
});
});
// Benchmark 2: Promise.resolve chain
// Each .then() queues a microtask via the promise machinery.
// Benefits from smaller QueuedTask (better cache locality in the Deque).
group("Promise.resolve chain", () => {
bench("Promise chain 1k", () => {
let p = Promise.resolve();
for (let i = 0; i < 1000; i++) {
p = p.then(() => {});
}
return p;
});
bench("Promise chain 10k", () => {
let p = Promise.resolve();
for (let i = 0; i < 10000; i++) {
p = p.then(() => {});
}
return p;
});
});
// Benchmark 3: Promise.all (many simultaneous resolves)
// All promises resolve at once, flooding the microtask queue.
// Smaller QueuedTask = less memory, better cache utilization.
group("Promise.all simultaneous", () => {
bench("Promise.all 1k", () => {
const promises = [];
for (let i = 0; i < 1000; i++) {
promises.push(Promise.resolve(i));
}
return Promise.all(promises);
});
bench("Promise.all 10k", () => {
const promises = [];
for (let i = 0; i < 10000; i++) {
promises.push(Promise.resolve(i));
}
return Promise.all(promises);
});
});
// Benchmark 4: queueMicrotask with AsyncLocalStorage
// Tests the inlined async context save/restore path.
// Previously went through performMicrotaskFunction JS trampoline.
group("queueMicrotask + AsyncLocalStorage", () => {
const als = new AsyncLocalStorage();
bench("ALS.run + queueMicrotask 1k", () => {
return als.run({ id: 1 }, () => {
return new Promise(resolve => {
let remaining = 1000;
const tick = () => {
als.getStore(); // force context read
if (--remaining === 0) resolve();
else queueMicrotask(tick);
};
queueMicrotask(tick);
});
});
});
});
// Benchmark 5: async/await (each await queues microtasks)
group("async/await chain", () => {
async function asyncChain(n) {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += await Promise.resolve(i);
}
return sum;
}
bench("async/await 1k", () => asyncChain(1000));
bench("async/await 10k", () => asyncChain(10000));
});
await run();

View File

@@ -6,7 +6,7 @@ option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of down
option(WEBKIT_BUILD_TYPE "The build type for local WebKit (defaults to CMAKE_BUILD_TYPE)")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 8af7958ff0e2a4787569edf64641a1ae7cfe074a)
set(WEBKIT_VERSION preview-pr-160-8680a32c)
endif()
# Use preview build URL for Windows ARM64 until the fix is merged to main

View File

@@ -1061,9 +1061,7 @@ JSC_DEFINE_HOST_FUNCTION(functionQueueMicrotask,
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
JSC::JSValue asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0);
auto function = globalObject->performMicrotaskFunction();
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(function, "Invalid microtask function");
ASSERT_WITH_MESSAGE(!callback.isEmpty(), "Invalid microtask callback");
#endif
@@ -1071,10 +1069,8 @@ JSC_DEFINE_HOST_FUNCTION(functionQueueMicrotask,
asyncContext = JSC::jsUndefined();
}
// BunPerformMicrotaskJob accepts a variable number of arguments (up to: performMicrotask, job, asyncContext, arg0, arg1).
// The runtime inspects argumentCount to determine which arguments are present, so callers may pass only the subset they need.
// Here we pass: function, callback, asyncContext.
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, function, callback, asyncContext };
// BunPerformMicrotaskJob: callback, asyncContext
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, callback, asyncContext };
globalObject->vm().queueMicrotask(WTF::move(task));
return JSC::JSValue::encode(JSC::jsUndefined());
@@ -1554,63 +1550,6 @@ extern "C" napi_env ZigGlobalObject__makeNapiEnvForFFI(Zig::GlobalObject* global
return globalObject->makeNapiEnvForFFI();
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotask, (JSGlobalObject * globalObject, CallFrame* callframe))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
auto job = callframe->argument(0);
if (!job || job.isUndefinedOrNull()) [[unlikely]] {
return JSValue::encode(jsUndefined());
}
auto callData = JSC::getCallData(job);
MarkedArgumentBuffer arguments;
if (callData.type == CallData::Type::None) [[unlikely]] {
return JSValue::encode(jsUndefined());
}
JSValue result;
WTF::NakedPtr<JSC::Exception> exceptionPtr;
JSValue restoreAsyncContext = {};
InternalFieldTuple* asyncContextData = nullptr;
auto setAsyncContext = callframe->argument(1);
if (!setAsyncContext.isUndefined()) {
asyncContextData = globalObject->m_asyncContextData.get();
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, setAsyncContext);
}
size_t argCount = callframe->argumentCount();
switch (argCount) {
case 3: {
arguments.append(callframe->uncheckedArgument(2));
break;
}
case 4: {
arguments.append(callframe->uncheckedArgument(2));
arguments.append(callframe->uncheckedArgument(3));
break;
}
default:
break;
}
JSC::profiledCall(globalObject, ProfilingReason::API, job, callData, jsUndefined(), arguments, exceptionPtr);
if (asyncContextData) {
asyncContextData->putInternalField(vm, 0, restoreAsyncContext);
}
if (auto* exception = exceptionPtr.get()) {
Bun__reportUnhandledError(globalObject, JSValue::encode(exception));
}
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotaskVariadic, (JSGlobalObject * globalObject, CallFrame* callframe))
{
auto& vm = JSC::getVM(globalObject);
@@ -1940,11 +1879,6 @@ void GlobalObject::finishCreation(VM& vm)
scope.assertNoExceptionExceptTermination();
init.set(subclassStructure);
});
m_performMicrotaskFunction.initLater(
[](const Initializer<JSFunction>& init) {
init.set(JSFunction::create(init.vm, init.owner, 4, "performMicrotask"_s, jsFunctionPerformMicrotask, ImplementationVisibility::Public));
});
m_performMicrotaskVariadicFunction.initLater(
[](const Initializer<JSFunction>& init) {
init.set(JSFunction::create(init.vm, init.owner, 4, "performMicrotaskVariadic"_s, jsFunctionPerformMicrotaskVariadic, ImplementationVisibility::Public));

View File

@@ -272,7 +272,6 @@ public:
JSC::JSObject* performanceObject() const { return m_performanceObject.getInitializedOnMainThread(this); }
JSC::JSFunction* performMicrotaskFunction() const { return m_performMicrotaskFunction.getInitializedOnMainThread(this); }
JSC::JSFunction* performMicrotaskVariadicFunction() const { return m_performMicrotaskVariadicFunction.getInitializedOnMainThread(this); }
JSC::Structure* utilInspectOptionsStructure() const { return m_utilInspectOptionsStructure.getInitializedOnMainThread(this); }
@@ -569,7 +568,6 @@ public:
V(private, LazyPropertyOfGlobalObject<Structure>, m_jsonlParseResultStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_pathParsedObjectStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_pendingVirtualModuleResultStructure) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_nativeMicrotaskTrampoline) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskVariadicFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectFunction) \

View File

@@ -3538,13 +3538,11 @@ void JSC__JSPromise__rejectOnNextTickWithHandled(JSC::JSPromise* promise, JSC::J
promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(flags | JSC::JSPromise::isFirstResolvingFunctionCalledFlag));
auto* globalObject = jsCast<Zig::GlobalObject*>(promise->globalObject());
auto microtaskFunction = globalObject->performMicrotaskFunction();
auto rejectPromiseFunction = globalObject->rejectPromiseFunction();
auto asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0);
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(microtaskFunction, "Invalid microtask function");
ASSERT_WITH_MESSAGE(rejectPromiseFunction, "Invalid microtask callback");
ASSERT_WITH_MESSAGE(!value.isEmpty(), "Invalid microtask value");
#endif
@@ -3557,7 +3555,8 @@ void JSC__JSPromise__rejectOnNextTickWithHandled(JSC::JSPromise* promise, JSC::J
value = jsUndefined();
}
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, microtaskFunction, rejectPromiseFunction, globalObject->m_asyncContextData.get()->getInternalField(0), promise, value };
// BunPerformMicrotaskJob: rejectPromiseFunction, asyncContext, promise, value
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, rejectPromiseFunction, globalObject->m_asyncContextData.get()->getInternalField(0), promise, value };
globalObject->vm().queueMicrotask(WTF::move(task));
RETURN_IF_EXCEPTION(scope, );
}
@@ -5438,9 +5437,7 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC::JSGlobalObject* arg0
if (microtaskArgs[3].isEmpty()) {
microtaskArgs[3] = jsUndefined();
}
JSC::JSFunction* microTaskFunction = globalObject->performMicrotaskFunction();
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(microTaskFunction, "Invalid microtask function");
auto& vm = globalObject->vm();
if (microtaskArgs[0].isCell()) {
JSC::Integrity::auditCellFully(vm, microtaskArgs[0].asCell());
@@ -5460,7 +5457,8 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC::JSGlobalObject* arg0
#endif
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, microTaskFunction, WTF::move(microtaskArgs[0]), WTF::move(microtaskArgs[1]), WTF::move(microtaskArgs[2]), WTF::move(microtaskArgs[3]) };
// BunPerformMicrotaskJob: job, asyncContext, arg0, arg1
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, WTF::move(microtaskArgs[0]), WTF::move(microtaskArgs[1]), WTF::move(microtaskArgs[2]), WTF::move(microtaskArgs[3]) };
globalObject->vm().queueMicrotask(WTF::move(task));
}