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
9 changed files with 148 additions and 185 deletions

View File

@@ -546,7 +546,6 @@ set(BUN_OBJECT_LUT_SOURCES
${CWD}/src/bun.js/bindings/ProcessBindingHTTPParser.cpp
${CWD}/src/bun.js/modules/NodeModuleModule.cpp
${CODEGEN_PATH}/ZigGeneratedClasses.lut.txt
${CWD}/src/bun.js/bindings/webcore/JSEvent.cpp
)
set(BUN_OBJECT_LUT_OUTPUTS
@@ -561,7 +560,6 @@ set(BUN_OBJECT_LUT_OUTPUTS
${CODEGEN_PATH}/ProcessBindingHTTPParser.lut.h
${CODEGEN_PATH}/NodeModuleModule.lut.h
${CODEGEN_PATH}/ZigGeneratedClasses.lut.h
${CODEGEN_PATH}/JSEvent.lut.h
)
macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps)
@@ -595,7 +593,6 @@ foreach(i RANGE 0 ${BUN_OBJECT_LUT_SOURCES_MAX_INDEX})
"Generating ${filename}.lut.h"
DEPENDS
${BUN_OBJECT_LUT_SOURCE}
${CWD}/src/codegen/create_hash_table
COMMAND
${BUN_EXECUTABLE}
${BUN_FLAGS}
@@ -605,7 +602,6 @@ foreach(i RANGE 0 ${BUN_OBJECT_LUT_SOURCES_MAX_INDEX})
${BUN_OBJECT_LUT_OUTPUT}
SOURCES
${BUN_OBJECT_LUT_SCRIPT}
${CWD}/src/codegen/create_hash_table
${BUN_OBJECT_LUT_SOURCE}
OUTPUTS
${BUN_OBJECT_LUT_OUTPUT}

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 2b0822aee577b4da18cd2b5b20c9f2b63614a6f3)
set(WEBKIT_VERSION preview-pr-160-8680a32c)
endif()
# Use preview build URL for Windows ARM64 until the fix is merged to main
@@ -95,9 +95,6 @@ if(WEBKIT_LOCAL)
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-DENABLE_REMOTE_INSPECTOR=ON
-DENABLE_MEDIA_SOURCE=OFF
-DENABLE_MEDIA_STREAM=OFF
-DENABLE_WEB_RTC=OFF
)
if(WIN32)

View File

@@ -122,18 +122,16 @@ STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSEventPrototype, JSEventPrototype::Base);
using JSEventDOMConstructor = JSDOMConstructor<JSEvent>;
/* Source for JSEvent.lut.h
@begin JSEventTable
isTrusted jsEvent_isTrusted DontDelete|ReadOnly|CustomAccessor|DOMAttribute
@end
*/
/* Hash table */
// The generated .lut.h defines JSEventTable with nullptr for classForThis,
// but DOMAttribute properties require it for type checking. Rename the
// generated table and redefine it with the correct classForThis.
#define JSEventTable JSEventTable_GENERATED
#include "JSEvent.lut.h"
#undef JSEventTable
static const struct CompactHashIndex JSEventTableIndex[2] = {
{ 0, -1 },
{ -1, -1 },
};
static const HashTableValue JSEventTableValues[] = {
{ "isTrusted"_s, static_cast<unsigned>(JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_isTrusted, 0 } },
};
static const HashTable JSEventTable = { 1, 1, true, JSEvent::info(), JSEventTableValues, JSEventTableIndex };
/* Hash table for constructor */

View File

@@ -832,7 +832,7 @@ template<> bool writeLittleEndian<uint8_t>(Vector<uint8_t>& buffer, const uint8_
return true;
}
class CloneSerializer : public CloneBase {
class CloneSerializer : CloneBase {
WTF_FORBID_HEAP_ALLOCATION;
public:
@@ -2831,7 +2831,7 @@ SerializationReturnCode CloneSerializer::serialize(JSValue in)
return SerializationReturnCode::SuccessfullyCompleted;
}
class CloneDeserializer : public CloneBase {
class CloneDeserializer : CloneBase {
WTF_FORBID_HEAP_ALLOCATION;
public:
@@ -4688,28 +4688,36 @@ private:
return tryConvertToBigInt32(bigInt);
}
#endif
Vector<JSBigInt::Digit, 16> digits;
JSBigInt* bigInt = nullptr;
if constexpr (sizeof(JSBigInt::Digit) == sizeof(uint64_t)) {
digits.reserveInitialCapacity(lengthInUint64);
bigInt = JSBigInt::tryCreateWithLength(m_lexicalGlobalObject->vm(), lengthInUint64);
if (!bigInt) {
fail();
return JSValue();
}
for (uint32_t index = 0; index < lengthInUint64; ++index) {
uint64_t digit64 = 0;
if (!read(digit64))
return JSValue();
digits.append(digit64);
bigInt->setDigit(index, digit64);
}
} else {
ASSERT(sizeof(JSBigInt::Digit) == sizeof(uint32_t));
digits.reserveInitialCapacity(lengthInUint64 * 2);
bigInt = JSBigInt::tryCreateWithLength(m_lexicalGlobalObject->vm(), lengthInUint64 * 2);
if (!bigInt) {
fail();
return JSValue();
}
for (uint32_t index = 0; index < lengthInUint64; ++index) {
uint64_t digit64 = 0;
if (!read(digit64))
return JSValue();
digits.append(static_cast<uint32_t>(digit64));
digits.append(static_cast<uint32_t>(digit64 >> 32));
bigInt->setDigit(index * 2, static_cast<uint32_t>(digit64));
bigInt->setDigit(index * 2 + 1, static_cast<uint32_t>(digit64 >> 32));
}
}
auto* bigInt = JSBigInt::tryCreateFrom(nullptr, m_lexicalGlobalObject->vm(), sign, digits.span());
bigInt->setSign(sign);
bigInt = bigInt->tryRightTrim(m_lexicalGlobalObject->vm());
if (!bigInt) {
fail();
return JSValue();

View File

@@ -948,7 +948,6 @@ pub const CommandLineReporter = struct {
this.printSummary();
Output.prettyError("\nBailed out after {d} failure{s}<r>\n", .{ this.jest.bail, if (this.jest.bail == 1) "" else "s" });
Output.flush();
this.writeJUnitReportIfNeeded();
Global.exit(1);
}
},
@@ -971,20 +970,6 @@ pub const CommandLineReporter = struct {
Output.printStartEnd(bun.start_time, std.time.nanoTimestamp());
}
/// Writes the JUnit reporter output file if a JUnit reporter is active and
/// an outfile path was configured. This must be called before any early exit
/// (e.g. bail) so that the report is not lost.
pub fn writeJUnitReportIfNeeded(this: *CommandLineReporter) void {
if (this.reporters.junit) |junit| {
if (this.jest.test_options.reporter_outfile) |outfile| {
if (junit.current_file.len > 0) {
junit.endTestSuite() catch {};
}
junit.writeToFile(outfile) catch {};
}
}
}
pub fn generateCodeCoverage(this: *CommandLineReporter, vm: *jsc.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, comptime reporters: TestCommand.Reporters, comptime enable_ansi_colors: bool) !void {
if (comptime !reporters.text and !reporters.lcov) {
return;
@@ -1787,7 +1772,12 @@ pub const TestCommand = struct {
Output.prettyError("\n", .{});
Output.flush();
reporter.writeJUnitReportIfNeeded();
if (reporter.reporters.junit) |junit| {
if (junit.current_file.len > 0) {
junit.endTestSuite() catch {};
}
junit.writeToFile(ctx.test_options.reporter_outfile.?) catch {};
}
if (vm.hot_reload == .watch) {
vm.runWithAPILock(jsc.VirtualMachine, vm, runEventLoopForWatch);
@@ -1930,7 +1920,6 @@ pub const TestCommand = struct {
if (reporter.jest.bail == reporter.summary().fail) {
reporter.printSummary();
Output.prettyError("\nBailed out after {d} failure{s}<r>\n", .{ reporter.jest.bail, if (reporter.jest.bail == 1) "" else "s" });
reporter.writeJUnitReportIfNeeded();
vm.exit_handler.exit_code = 1;
vm.is_shutting_down = true;

View File

@@ -24,7 +24,7 @@
use strict;
use warnings;
use bigint;
use Math::BigInt;
use Getopt::Long qw(:config pass_through);
my $file = shift @ARGV or die("Must provide source file as final argument.");
@@ -42,17 +42,17 @@ my $hasSetter = "false";
my $includeBuiltin = 0;
my $inside = 0;
my $name;
my $perfectHashSize;
my $pefectHashSize;
my $compactSize;
my $compactHashSizeMask;
my $banner = 0;
my $mask64 = 2**64 - 1;
my $mask32 = 2**32 - 1;
sub calcPerfectHashSize();
sub calcCompactHashSize();
sub calcPerfectHashSize($);
sub calcCompactHashSize($);
sub output();
sub jsc_ucfirst($);
sub hashValue($);
sub hashValue($$);
while (<IN>) {
chomp;
@@ -140,26 +140,24 @@ sub jsc_ucfirst($)
sub ceilingToPowerOf2
{
my ($perfectHashSize) = @_;
my ($pefectHashSize) = @_;
my $powerOf2 = 1;
while ($perfectHashSize > $powerOf2) {
while ($pefectHashSize > $powerOf2) {
$powerOf2 <<= 1;
}
return $powerOf2;
}
sub calcPerfectHashSize()
sub calcPerfectHashSize($)
{
my ($isMac) = @_;
tableSizeLoop:
for ($perfectHashSize = ceilingToPowerOf2(scalar @keys); ; $perfectHashSize += $perfectHashSize) {
if ($perfectHashSize > 2**15) {
die "The hash size is far too big. This should not be reached.";
}
for ($pefectHashSize = ceilingToPowerOf2(scalar @keys); ; $pefectHashSize += $pefectHashSize) {
my @table = ();
foreach my $key (@keys) {
my $h = hashValue($key) % $perfectHashSize;
my $h = hashValue($key, $isMac) % $pefectHashSize;
next tableSizeLoop if $table[$h];
$table[$h] = 1;
}
@@ -167,8 +165,14 @@ tableSizeLoop:
}
}
sub calcCompactHashSize()
sub leftShift($$) {
my ($value, $distance) = @_;
return (($value << $distance) & 0xFFFFFFFF);
}
sub calcCompactHashSize($)
{
my ($isMac) = @_;
my $compactHashSize = ceilingToPowerOf2(2 * @keys);
$compactHashSizeMask = $compactHashSize - 1;
$compactSize = $compactHashSize;
@@ -177,14 +181,8 @@ sub calcCompactHashSize()
my $i = 0;
foreach my $key (@keys) {
my $depth = 0;
my $h = hashValue($key) % $compactHashSize;
my $h = hashValue($key, $isMac) % $compactHashSize;
while (defined($table[$h])) {
if ($compactSize > 1000) {
die "The hash size is far too big. This should not be reached.";
}
if ($depth > 100) {
die "The depth is far too big. This should not be reached.";
}
if (defined($links[$h])) {
$h = $links[$h];
$depth++;
@@ -197,10 +195,27 @@ sub calcCompactHashSize()
}
$table[$h] = $i;
$i++;
$maxdepth = $depth if ($depth > $maxdepth);
$maxdepth = $depth if ( $depth > $maxdepth);
}
}
sub avalancheBits($) {
my ($value) = @_;
$value &= $mask32;
# Force "avalanching" of lower 32 bits
$value ^= leftShift($value, 3);
$value += ($value >> 5);
$value = ($value & $mask32);
$value ^= (leftShift($value, 2) & $mask32);
$value += ($value >> 15);
$value = $value & $mask32;
$value ^= (leftShift($value, 10) & $mask32);
return $value;
}
sub maskTop8BitsAndAvoidZero($) {
my ($value) = @_;
@@ -218,6 +233,43 @@ sub maskTop8BitsAndAvoidZero($) {
return $value;
}
# Paul Hsieh's SuperFastHash
# http://www.azillionmonkeys.com/qed/hash.html
sub superFastHash {
my @chars = @_;
# This hash is designed to work on 16-bit chunks at a time. But since the normal case
# (above) is to hash UTF-16 characters, we just treat the 8-bit chars as if they
# were 16-bit chunks, which should give matching results
my $hash = 0x9e3779b9;
my $l = scalar @chars; #I wish this was in Ruby --- Maks
my $rem = $l & 1;
$l = $l >> 1;
my $s = 0;
# Main loop
for (; $l > 0; $l--) {
$hash += ord($chars[$s]);
my $tmp = leftShift(ord($chars[$s+1]), 11) ^ $hash;
$hash = (leftShift($hash, 16) & $mask32) ^ $tmp;
$s += 2;
$hash += $hash >> 11;
$hash &= $mask32;
}
# Handle end case
if ($rem != 0) {
$hash += ord($chars[$s]);
$hash ^= (leftShift($hash, 11) & $mask32);
$hash += $hash >> 17;
}
$hash = avalancheBits($hash);
return maskTop8BitsAndAvoidZero($hash);
}
sub uint64_add($$) {
my ($a, $b) = @_;
my $sum = $a + $b;
@@ -351,10 +403,18 @@ sub wyhash {
return maskTop8BitsAndAvoidZero($hash);
}
sub hashValue($) {
my ($string) = @_;
sub hashValue($$) {
my ($string, $isMac) = @_;
my @chars = split(/ */, $string);
return wyhash(@chars);
my $charCount = scalar @chars;
if ($isMac) {
if ($charCount <= 48) {
return superFastHash(@chars);
}
return wyhash(@chars);
} else {
return superFastHash(@chars);
}
}
sub output() {
@@ -376,13 +436,16 @@ sub output() {
print "\n";
local *generateHashTableHelper = sub {
calcPerfectHashSize();
calcCompactHashSize();
my ($isMac, $setToOldValues) = @_;
my $oldCompactSize = $compactSize;
my $oldCompactHashSizeMask = $compactHashSizeMask;
calcPerfectHashSize($isMac);
calcCompactHashSize($isMac);
my $hashTableString = "";
if ($compactSize != 0) {
$hashTableString .= "static constinit const struct CompactHashIndex ${nameIndex}\[$compactSize\] = {\n";
$hashTableString .= "static const struct CompactHashIndex ${nameIndex}\[$compactSize\] = {\n";
for (my $i = 0; $i < $compactSize; $i++) {
my $T = -1;
if (defined($table[$i])) { $T = $table[$i]; }
@@ -392,7 +455,7 @@ sub output() {
}
} else {
# MSVC dislikes empty arrays.
$hashTableString .= "static constinit const struct CompactHashIndex ${nameIndex}\[1\] = {\n";
$hashTableString .= "static const struct CompactHashIndex ${nameIndex}\[1\] = {\n";
$hashTableString .= " { 0, 0 }\n";
}
$hashTableString .= "};\n";
@@ -400,10 +463,10 @@ sub output() {
my $packedSize = scalar @keys;
if ($packedSize != 0) {
$hashTableString .= "static constinit const struct HashTableValue ${nameEntries}\[$packedSize\] = {\n";
$hashTableString .= "static const struct HashTableValue ${nameEntries}\[$packedSize\] = {\n";
} else {
# MSVC dislikes empty arrays.
$hashTableString .= "static constinit const struct HashTableValue ${nameEntries}\[1\] = {\n";
$hashTableString .= "static const struct HashTableValue ${nameEntries}\[1\] = {\n";
$hashTableString .= " { { }, 0, NoIntrinsic, { HashTableValue::End } }\n";
}
@@ -463,16 +526,26 @@ sub output() {
}
$hashTableString .= "};\n";
$hashTableString .= "\n";
$hashTableString .= "static constinit const struct HashTable $name =\n";
$hashTableString .= "static const struct HashTable $name =\n";
$hashTableString .= " \{ $packedSize, $compactHashSizeMask, $hasSetter, nullptr, $nameEntries, $nameIndex \};\n";
$hashTableString .= "\n";
@table = ();
@links = ();
if ($setToOldValues) {
$compactSize = $oldCompactSize;
$compactHashSizeMask = $oldCompactHashSizeMask;
}
return $hashTableString;
};
print generateHashTableHelper();
my $hashTableForMacOS = generateHashTableHelper(1, 1);
my $hashTableForIOS = generateHashTableHelper(0, 0);
my $hashTableToWrite = $hashTableForMacOS;
if ($hashTableForMacOS ne $hashTableForIOS) {
$hashTableToWrite = "#if PLATFORM(MAC)\n" . $hashTableForMacOS . "#else\n" . $hashTableForIOS . "#endif\n";
}
print $hashTableToWrite;
print "} // namespace JSC\n";
}

View File

@@ -1154,7 +1154,7 @@ pub const Interpreter = struct {
_ = callframe; // autofix
if (this.setupIOBeforeRun().asErr()) |e| {
defer this.#derefRootShellAndIOIfNeeded(true);
defer this.#deinitFromExec();
const shellerr = bun.shell.ShellErr.newSys(e);
return try throwShellErr(&shellerr, .{ .js = globalThis.bunVM().event_loop });
}

View File

@@ -260,35 +260,14 @@ devTest("hmr handles rapid consecutive edits", {
await Bun.sleep(1);
}
// Wait event-driven for "render 10" to appear. Intermediate renders may
// be skipped (watcher coalescing) and the final render may fire multiple
// times (duplicate reloads), so we just listen for any occurrence.
const finalRender = "render 10";
await new Promise<void>((resolve, reject) => {
const check = () => {
for (const msg of client.messages) {
if (typeof msg === "string" && msg.includes("HMR_ERROR")) {
cleanup();
reject(new Error("Unexpected HMR error message: " + msg));
return;
}
if (msg === finalRender) {
cleanup();
resolve();
return;
}
}
};
const cleanup = () => {
client.off("message", check);
};
client.on("message", check);
// Check messages already buffered.
check();
});
// Drain all buffered messages — intermediate renders and possible
// duplicates of the final render are expected and harmless.
client.messages.length = 0;
while (true) {
const message = await client.getStringMessage();
if (message === finalRender) break;
if (typeof message === "string" && message.includes("HMR_ERROR")) {
throw new Error("Unexpected HMR error message: " + message);
}
}
const hmrErrors = await client.js`return globalThis.__hmrErrors ? [...globalThis.__hmrErrors] : [];`;
if (hmrErrors.length > 0) {

View File

@@ -1,77 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
test("--bail writes JUnit reporter outfile", async () => {
using dir = tempDir("bail-junit", {
"fail.test.ts": `
import { test, expect } from "bun:test";
test("failing test", () => { expect(1).toBe(2); });
`,
});
const outfile = join(String(dir), "results.xml");
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--bail", "--reporter=junit", `--reporter-outfile=${outfile}`, "fail.test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
// The test should fail and bail
expect(exitCode).not.toBe(0);
// The JUnit report file should still be written despite bail
const file = Bun.file(outfile);
expect(await file.exists()).toBe(true);
const xml = await file.text();
expect(xml).toContain("<?xml");
expect(xml).toContain("<testsuites");
expect(xml).toContain("</testsuites>");
expect(xml).toContain("failing test");
});
test("--bail writes JUnit reporter outfile with multiple files", async () => {
using dir = tempDir("bail-junit-multi", {
"a_pass.test.ts": `
import { test, expect } from "bun:test";
test("passing test", () => { expect(1).toBe(1); });
`,
"b_fail.test.ts": `
import { test, expect } from "bun:test";
test("another failing test", () => { expect(1).toBe(2); });
`,
});
const outfile = join(String(dir), "results.xml");
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--bail", "--reporter=junit", `--reporter-outfile=${outfile}`],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
// The test should fail and bail
expect(exitCode).not.toBe(0);
// The JUnit report file should still be written despite bail
const file = Bun.file(outfile);
expect(await file.exists()).toBe(true);
const xml = await file.text();
expect(xml).toContain("<?xml");
expect(xml).toContain("<testsuites");
expect(xml).toContain("</testsuites>");
// Both the passing and failing tests should be recorded
expect(xml).toContain("passing test");
expect(xml).toContain("another failing test");
});