From f3e70531244157b6581d479bac304705e9a00dd4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 27 Jan 2026 14:31:33 +0900 Subject: [PATCH 1/3] Benchmarks: add optional-return cases and improve runner --- Benchmarks/Sources/Benchmarks.swift | 16 ++ Benchmarks/Sources/Generated/BridgeJS.swift | 124 ++++++++++++ .../Generated/JavaScript/BridgeJS.json | 188 ++++++++++++++++++ Benchmarks/run.js | 72 ++++++- 4 files changed, 396 insertions(+), 4 deletions(-) diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift index 661aba63e..921db50ea 100644 --- a/Benchmarks/Sources/Benchmarks.swift +++ b/Benchmarks/Sources/Benchmarks.swift @@ -111,6 +111,22 @@ enum ComplexResult { } } +@JS class OptionalReturnRoundtrip { + @JS init() {} + + @JS func makeIntSome() -> Int? { 42 } + @JS func makeIntNone() -> Int? { nil } + + @JS func makeBoolSome() -> Bool? { true } + @JS func makeBoolNone() -> Bool? { nil } + + @JS func makeDoubleSome() -> Double? { 0.5 } + @JS func makeDoubleNone() -> Double? { nil } + + @JS func makeStringSome() -> String? { "Hello, world" } + @JS func makeStringNone() -> String? { nil } +} + // MARK: - Struct Performance Tests @JS struct SimpleStruct { diff --git a/Benchmarks/Sources/Generated/BridgeJS.swift b/Benchmarks/Sources/Generated/BridgeJS.swift index 7d90eb82b..0bbc68bb2 100644 --- a/Benchmarks/Sources/Generated/BridgeJS.swift +++ b/Benchmarks/Sources/Generated/BridgeJS.swift @@ -839,6 +839,130 @@ fileprivate func _bjs_StringRoundtrip_wrap(_ pointer: UnsafeMutableRawPointer) - } #endif +@_expose(wasm, "bjs_OptionalReturnRoundtrip_init") +@_cdecl("bjs_OptionalReturnRoundtrip_init") +public func _bjs_OptionalReturnRoundtrip_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeIntSome") +@_cdecl("bjs_OptionalReturnRoundtrip_makeIntSome") +public func _bjs_OptionalReturnRoundtrip_makeIntSome(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeIntSome() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeIntNone") +@_cdecl("bjs_OptionalReturnRoundtrip_makeIntNone") +public func _bjs_OptionalReturnRoundtrip_makeIntNone(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeIntNone() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeBoolSome") +@_cdecl("bjs_OptionalReturnRoundtrip_makeBoolSome") +public func _bjs_OptionalReturnRoundtrip_makeBoolSome(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeBoolSome() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeBoolNone") +@_cdecl("bjs_OptionalReturnRoundtrip_makeBoolNone") +public func _bjs_OptionalReturnRoundtrip_makeBoolNone(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeBoolNone() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeDoubleSome") +@_cdecl("bjs_OptionalReturnRoundtrip_makeDoubleSome") +public func _bjs_OptionalReturnRoundtrip_makeDoubleSome(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeDoubleSome() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeDoubleNone") +@_cdecl("bjs_OptionalReturnRoundtrip_makeDoubleNone") +public func _bjs_OptionalReturnRoundtrip_makeDoubleNone(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeDoubleNone() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeStringSome") +@_cdecl("bjs_OptionalReturnRoundtrip_makeStringSome") +public func _bjs_OptionalReturnRoundtrip_makeStringSome(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeStringSome() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_makeStringNone") +@_cdecl("bjs_OptionalReturnRoundtrip_makeStringNone") +public func _bjs_OptionalReturnRoundtrip_makeStringNone(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = OptionalReturnRoundtrip.bridgeJSLiftParameter(_self).makeStringNone() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_OptionalReturnRoundtrip_deinit") +@_cdecl("bjs_OptionalReturnRoundtrip_deinit") +public func _bjs_OptionalReturnRoundtrip_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension OptionalReturnRoundtrip: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_OptionalReturnRoundtrip_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Benchmarks", name: "bjs_OptionalReturnRoundtrip_wrap") +fileprivate func _bjs_OptionalReturnRoundtrip_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_OptionalReturnRoundtrip_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + @_expose(wasm, "bjs_StructRoundtrip_init") @_cdecl("bjs_StructRoundtrip_init") public func _bjs_StructRoundtrip_init() -> UnsafeMutableRawPointer { diff --git a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json index 27a245f56..42cbcb707 100644 --- a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json +++ b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json @@ -424,6 +424,194 @@ ], "swiftCallName" : "StringRoundtrip" }, + { + "constructor" : { + "abiName" : "bjs_OptionalReturnRoundtrip_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeIntSome", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeIntSome", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "int" : { + + } + } + } + } + }, + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeIntNone", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeIntNone", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "int" : { + + } + } + } + } + }, + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeBoolSome", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeBoolSome", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "bool" : { + + } + } + } + } + }, + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeBoolNone", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeBoolNone", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "bool" : { + + } + } + } + } + }, + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeDoubleSome", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeDoubleSome", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "double" : { + + } + } + } + } + }, + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeDoubleNone", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeDoubleNone", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "double" : { + + } + } + } + } + }, + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeStringSome", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeStringSome", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + }, + { + "abiName" : "bjs_OptionalReturnRoundtrip_makeStringNone", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeStringNone", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "string" : { + + } + } + } + } + } + ], + "name" : "OptionalReturnRoundtrip", + "properties" : [ + + ], + "swiftCallName" : "OptionalReturnRoundtrip" + }, { "constructor" : { "abiName" : "bjs_StructRoundtrip_init", diff --git a/Benchmarks/run.js b/Benchmarks/run.js index 5c27e6fff..b2a04a62a 100644 --- a/Benchmarks/run.js +++ b/Benchmarks/run.js @@ -17,9 +17,17 @@ function updateProgress(current, total, label = '', width) { const completed = Math.round(width * (percent / 100)); const remaining = width - completed; const bar = '█'.repeat(completed) + '░'.repeat(remaining); - process.stdout.clearLine(); - process.stdout.cursorTo(0); - process.stdout.write(`${label} [${bar}] ${current}/${total}`); + const canUpdateLine = + process.stdout.isTTY && + typeof process.stdout.clearLine === "function" && + typeof process.stdout.cursorTo === "function"; + if (canUpdateLine) { + process.stdout.clearLine(); + process.stdout.cursorTo(0); + process.stdout.write(`${label} [${bar}] ${current}/${total}`); + } else if (current === 0 || current === total) { + console.log(`${label} [${bar}] ${current}/${total}`); + } } /** @@ -271,6 +279,11 @@ async function singleRun(results, nameFilter) { if (nameFilter && !nameFilter(name)) { return; } + // Warmup to reduce JIT/IC noise. + body(); + if (typeof globalThis.gc === "function") { + globalThis.gc(); + } const startTime = performance.now(); body(); const endTime = performance.now(); @@ -291,7 +304,7 @@ async function singleRun(results, nameFilter) { exports.run(); const enumRoundtrip = new exports.EnumRoundtrip(); - const iterations = 100_000; + const iterations = globalThis.__benchIterations ?? 100_000; benchmarkRunner("EnumRoundtrip/takeEnum success", () => { for (let i = 0; i < iterations; i++) { enumRoundtrip.take({ tag: APIResult.Tag.Success, param0: "Hello, world" }) @@ -455,6 +468,48 @@ async function singleRun(results, nameFilter) { } }) + const optionalReturnRoundtrip = new exports.OptionalReturnRoundtrip(); + benchmarkRunner("OptionalReturnRoundtrip/makeIntSome", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeIntSome() + } + }) + benchmarkRunner("OptionalReturnRoundtrip/makeIntNone", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeIntNone() + } + }) + benchmarkRunner("OptionalReturnRoundtrip/makeBoolSome", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeBoolSome() + } + }) + benchmarkRunner("OptionalReturnRoundtrip/makeBoolNone", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeBoolNone() + } + }) + benchmarkRunner("OptionalReturnRoundtrip/makeDoubleSome", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeDoubleSome() + } + }) + benchmarkRunner("OptionalReturnRoundtrip/makeDoubleNone", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeDoubleNone() + } + }) + benchmarkRunner("OptionalReturnRoundtrip/makeStringSome", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeStringSome() + } + }) + benchmarkRunner("OptionalReturnRoundtrip/makeStringNone", () => { + for (let i = 0; i < iterations; i++) { + optionalReturnRoundtrip.makeStringNone() + } + }) + // Struct performance tests const structRoundtrip = new exports.StructRoundtrip(); @@ -674,6 +729,7 @@ Usage: node run.js [options] Options: --runs=NUMBER Number of benchmark runs (default: 10) + --iterations=NUMBER Loop iterations per JS benchmark (default: 100000) --output=FILENAME Save JSON results to specified file --baseline=FILENAME Compare results with baseline JSON file --adaptive Enable adaptive sampling (run until stable) @@ -689,6 +745,7 @@ async function main() { const args = parseArgs({ options: { runs: { type: 'string', default: '10' }, + iterations: { type: 'string', default: '100000' }, output: { type: 'string' }, baseline: { type: 'string' }, help: { type: 'boolean', default: false }, @@ -710,6 +767,13 @@ async function main() { const filterArg = args.values.filter; const nameFilter = createNameFilter(filterArg); + const iterations = parseInt(args.values.iterations, 10); + if (isNaN(iterations) || iterations <= 0) { + console.error('Invalid --iterations value:', args.values.iterations); + process.exit(1); + } + globalThis.__benchIterations = iterations; + if (args.values.adaptive) { // Adaptive sampling mode const options = { From 4e965d2df3a12dd989c2da5151e2d2d371d299ac Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 27 Jan 2026 14:36:38 +0900 Subject: [PATCH 2/3] Benchmarks: pass iterations without globals --- Benchmarks/run.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Benchmarks/run.js b/Benchmarks/run.js index b2a04a62a..7771887c5 100644 --- a/Benchmarks/run.js +++ b/Benchmarks/run.js @@ -271,9 +271,11 @@ function saveJsonResults(filePath, data) { /** * Run a single benchmark iteration * @param {Object} results - Results object to store benchmark data + * @param {(name: string) => boolean} nameFilter - Name filter + * @param {number} iterations - Loop iterations per JS benchmark * @returns {Promise} */ -async function singleRun(results, nameFilter) { +async function singleRun(results, nameFilter, iterations) { const options = await defaultNodeSetup({}) const benchmarkRunner = (name, body) => { if (nameFilter && !nameFilter(name)) { @@ -304,7 +306,6 @@ async function singleRun(results, nameFilter) { exports.run(); const enumRoundtrip = new exports.EnumRoundtrip(); - const iterations = globalThis.__benchIterations ?? 100_000; benchmarkRunner("EnumRoundtrip/takeEnum success", () => { for (let i = 0; i < iterations; i++) { enumRoundtrip.take({ tag: APIResult.Tag.Success, param0: "Hello, world" }) @@ -645,9 +646,10 @@ async function singleRun(results, nameFilter) { * Run until the coefficient of variation of measurements is below the threshold * @param {Object} results - Benchmark results object * @param {Object} options - Adaptive sampling options + * @param {number} iterations - Loop iterations per JS benchmark * @returns {Promise} */ -async function runUntilStable(results, options, width, nameFilter, filterArg) { +async function runUntilStable(results, options, width, nameFilter, filterArg, iterations) { const { minRuns = 5, maxRuns = 50, @@ -666,7 +668,7 @@ async function runUntilStable(results, options, width, nameFilter, filterArg) { // Update progress with estimated completion updateProgress(runs, maxRuns, "Benchmark Progress:", width); - await singleRun(results, nameFilter); + await singleRun(results, nameFilter, iterations); runs++; if (runs === 1 && Object.keys(results).length === 0) { @@ -772,7 +774,6 @@ async function main() { console.error('Invalid --iterations value:', args.values.iterations); process.exit(1); } - globalThis.__benchIterations = iterations; if (args.values.adaptive) { // Adaptive sampling mode @@ -787,7 +788,7 @@ async function main() { console.log(`Results will be saved to: ${args.values.output}`); } - await runUntilStable(results, options, width, nameFilter, filterArg); + await runUntilStable(results, options, width, nameFilter, filterArg, iterations); } else { // Fixed number of runs mode const runs = parseInt(args.values.runs, 10); @@ -809,7 +810,7 @@ async function main() { console.log("\nOverall Progress:"); for (let i = 0; i < runs; i++) { updateProgress(i, runs, "Benchmark Runs:", width); - await singleRun(results, nameFilter); + await singleRun(results, nameFilter, iterations); if (i === 0 && Object.keys(results).length === 0) { process.stdout.write("\n"); console.error(`No benchmarks matched filter: ${filterArg}`); From 26fc14c22d4dc8bfb20bd87fe0631f814989a5ce Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Tue, 27 Jan 2026 16:24:46 +0700 Subject: [PATCH 3/3] Run ./Utilities/bridge-js-generate.sh --- .../Generated/BridgeJS.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index c4fd3a722..27440deba 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -2229,8 +2229,50 @@ extension PointerFields: _BridgedSwiftStruct { _swift_js_push_pointer(self.ptr.bridgeJSLowerReturn()) _swift_js_push_pointer(self.mutPtr.bridgeJSLowerReturn()) } + + init(unsafelyCopying jsObject: JSObject) { + let __bjs_cleanupId = _PointerFieldsHelpers.lower(jsObject) + defer { + _swift_js_struct_cleanup(__bjs_cleanupId) + } + self = Self.bridgeJSLiftParameter() + } + + func toJSObject() -> JSObject { + var __bjs_self = self + __bjs_self.bridgeJSLowerReturn() + return _PointerFieldsHelpers.raise() + } } +fileprivate enum _PointerFieldsHelpers { + static func lower(_ jsObject: JSObject) -> Int32 { + return _bjs_struct_lower_PointerFields(jsObject.bridgeJSLowerParameter()) + } + + static func raise() -> JSObject { + return JSObject(id: UInt32(bitPattern: _bjs_struct_raise_PointerFields())) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_PointerFields") +fileprivate func _bjs_struct_lower_PointerFields(_ objectId: Int32) -> Int32 +#else +fileprivate func _bjs_struct_lower_PointerFields(_ objectId: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_raise_PointerFields") +fileprivate func _bjs_struct_raise_PointerFields() -> Int32 +#else +fileprivate func _bjs_struct_raise_PointerFields() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + @_expose(wasm, "bjs_PointerFields_init") @_cdecl("bjs_PointerFields_init") public func _bjs_PointerFields_init(_ raw: UnsafeMutableRawPointer, _ mutRaw: UnsafeMutableRawPointer, _ opaque: UnsafeMutableRawPointer, _ ptr: UnsafeMutableRawPointer, _ mutPtr: UnsafeMutableRawPointer) -> Void {