From afad884a66547d53710a198ac056966246c94042 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 29 Jan 2026 16:41:42 +0900 Subject: [PATCH 1/9] =?UTF-8?q?Built=20an=20opt-in=20tracing=20surface=20a?= =?UTF-8?q?nd=20wired=20it=20through=20the=20bridge=20when=20enabled=20by?= =?UTF-8?q?=20a=20new=20trait.=20Implemented=20`JSTracing`=20(start/end=20?= =?UTF-8?q?hooks=20for=20Swift=E2=86=92JS=20calls=20and=20JSClosure=20invo?= =?UTF-8?q?cations)=20with=20per-thread=20storage=20in=20`Sources/JavaScri?= =?UTF-8?q?ptKit/JSTracing.swift`.=20Added=20tracing=20entry=20points=20to?= =?UTF-8?q?=20Swift=E2=86=92JS=20calls=20(functions,=20methods=20via=20dyn?= =?UTF-8?q?amic=20members=20with=20method=20names,=20constructors,=20and?= =?UTF-8?q?=20throwing=20calls)=20and=20JS=E2=86=92Swift=20closures=20so?= =?UTF-8?q?=20hooks=20fire=20around=20each=20bridge=20crossing=20when=20co?= =?UTF-8?q?mpiled=20with=20tracing;=20closure=20creation=20now=20records?= =?UTF-8?q?=20`StaticString`=20file=20IDs=20for=20reporting=20(`Sources/Ja?= =?UTF-8?q?vaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift`,?= =?UTF-8?q?=20`Sources/JavaScriptKit/FundamentalObjects/JSObject.swift`,?= =?UTF-8?q?=20`Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction?= =?UTF-8?q?.swift`,=20`Sources/JavaScriptKit/FundamentalObjects/JSClosure.?= =?UTF-8?q?swift`).=20Introduced=20a=20`JavaScriptKitTracing`=20package=20?= =?UTF-8?q?trait=20that=20gates=20`JAVASCRIPTKIT=5FENABLE=5FTRACING`=20and?= =?UTF-8?q?=20updated=20docs=20with=20enablement=20and=20usage=20guidance?= =?UTF-8?q?=20(`Package.swift`,=20`Sources/JavaScriptKit/Documentation.doc?= =?UTF-8?q?c/Articles/Debugging.md`).=20Verified=20the=20manifest=20parses?= =?UTF-8?q?=20with=20`swift=20package=20dump-package`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notes: Hooks are compiled out unless `--traits JavaScriptKitTracing` is provided, and JSClosure initializers now take `StaticString` for `file`. Next steps: try `swift build --traits JavaScriptKitTracing` and exercise hooks in your app; consider adding focused tests for tracing callbacks if desired. Tests not run (not requested). --- Package.swift | 10 +- .../Documentation.docc/Articles/Debugging.md | 22 +++ .../FundamentalObjects/JSClosure.swift | 64 ++++++--- .../JSObject+CallAsFunction.swift | 90 ++++++++++-- .../FundamentalObjects/JSObject.swift | 20 ++- .../JSThrowingFunction.swift | 9 ++ Sources/JavaScriptKit/JSTracing.swift | 133 ++++++++++++++++++ 7 files changed, 316 insertions(+), 32 deletions(-) create mode 100644 Sources/JavaScriptKit/JSTracing.swift diff --git a/Package.swift b/Package.swift index 1d4c8fb06..f30ec9b4f 100644 --- a/Package.swift +++ b/Package.swift @@ -8,6 +8,12 @@ let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMB let useLegacyResourceBundling = Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false +let tracingTrait = Trait( + name: "JavaScriptKitTracing", + description: "Enable opt-in Swift <-> JavaScript bridge tracing hooks.", + enabledTraits: [] +) + let testingLinkerFlags: [LinkerSetting] = [ .unsafeFlags([ "-Xlinker", "--stack-first", @@ -36,6 +42,7 @@ let package = Package( .plugin(name: "BridgeJS", targets: ["BridgeJS"]), .plugin(name: "BridgeJSCommandPlugin", targets: ["BridgeJSCommandPlugin"]), ], + traits: [tracingTrait], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"603.0.0") ], @@ -50,7 +57,8 @@ let package = Package( .unsafeFlags(["-fdeclspec"]) ] : nil, swiftSettings: [ - .enableExperimentalFeature("Extern") + .enableExperimentalFeature("Extern"), + .define("JAVASCRIPTKIT_ENABLE_TRACING", .when(traits: ["JavaScriptKitTracing"])), ] + (shouldBuildForEmbedded ? [ diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md index f766be2ed..1b2729f42 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md @@ -57,3 +57,25 @@ Alternatively, you can use the official [`C/C++ DevTools Support (DWARF)`](https ![Chrome DevTools](chrome-devtools.png) See [the DevTools team's official introduction](https://developer.chrome.com/blog/wasm-debugging-2020) for more details about the extension. + +## Bridge Call Tracing + +Enable the `JavaScriptKitTracing` package trait to compile lightweight hook points for Swift <-> JavaScript calls. Tracing is off by default and adds no runtime overhead unless the trait is enabled: + +```bash +swift build --traits JavaScriptKitTracing +``` + +The hooks are invoked at the start and end of each bridge crossing without collecting data for you. For example: + +```swift +let removeCallHook = JSTracing.default.addJSCallHook { info in + let started = Date() + return { print("JS call \(info) finished in \(Date().timeIntervalSince(started))s") } +} + +let removeClosureHook = JSTracing.default.addJSClosureCallHook { info in + print("JSClosure created at \(info.fileID):\(info.line)") + return nil +} +``` diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index baeb29847..5c937491a 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -18,23 +18,29 @@ public protocol JSClosureProtocol: JSValueCompatible { public class JSOneshotClosure: JSObject, JSClosureProtocol { private var hostFuncRef: JavaScriptHostFuncRef = 0 - public init(file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue) { + public init( + file: StaticString = #fileID, + line: UInt32 = #line, + _ body: @escaping (sending [JSValue]) -> JSValue + ) { // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) // 2. Create a new JavaScript function which calls the given Swift function. hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) - _id = withExtendedLifetime(JSString(file)) { file in + _id = withExtendedLifetime(JSString(String(file))) { file in swjs_create_oneshot_function(hostFuncRef, line, file.asInternalJSRef()) } // 3. Retain the given body in static storage by `funcRef`. - JSClosure.sharedClosures.wrappedValue[hostFuncRef] = ( - self, - { + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = .init( + object: self, + body: { defer { self.release() } return body($0) - } + }, + fileID: file, + line: line ) } @@ -54,7 +60,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async( priority: TaskPriority? = nil, - file: String = #fileID, + file: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSOneshotClosure { @@ -73,7 +79,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { public static func async( executorPreference taskExecutor: (any TaskExecutor)? = nil, priority: TaskPriority? = nil, - file: String = #fileID, + file: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSOneshotClosure { @@ -114,14 +120,17 @@ public class JSClosure: JSObject, JSClosureProtocol { // `removeValue(forKey:)` on a dictionary with value type containing // `sending`. Wrap the value type with a struct to avoid the crash. struct Entry { - let item: (object: JSObject, body: (sending [JSValue]) -> JSValue) + let object: JSObject + let body: (sending [JSValue]) -> JSValue + let fileID: StaticString + let line: UInt32 } private var storage: [JavaScriptHostFuncRef: Entry] = [:] init() {} - subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: (sending [JSValue]) -> JSValue)? { - get { storage[key]?.item } - set { storage[key] = newValue.map { Entry(item: $0) } } + subscript(_ key: JavaScriptHostFuncRef) -> Entry? { + get { storage[key] } + set { storage[key] = newValue } } } @@ -150,18 +159,27 @@ public class JSClosure: JSObject, JSClosureProtocol { }) } - public init(file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue) { + public init( + file: StaticString = #fileID, + line: UInt32 = #line, + _ body: @escaping (sending [JSValue]) -> JSValue + ) { // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) // 2. Create a new JavaScript function which calls the given Swift function. hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) - _id = withExtendedLifetime(JSString(file)) { file in + _id = withExtendedLifetime(JSString(String(file))) { file in swjs_create_function(hostFuncRef, line, file.asInternalJSRef()) } // 3. Retain the given body in static storage by `funcRef`. - Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body) + Self.sharedClosures.wrappedValue[hostFuncRef] = .init( + object: self, + body: body, + fileID: file, + line: line + ) } @available(*, unavailable, message: "JSClosure does not support dictionary literal initialization") @@ -180,7 +198,7 @@ public class JSClosure: JSObject, JSClosureProtocol { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async( priority: TaskPriority? = nil, - file: String = #fileID, + file: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping @isolated(any) (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSClosure { @@ -199,7 +217,7 @@ public class JSClosure: JSObject, JSClosureProtocol { public static func async( executorPreference taskExecutor: (any TaskExecutor)? = nil, priority: TaskPriority? = nil, - file: String = #fileID, + file: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSClosure { @@ -317,14 +335,22 @@ func _call_host_function_impl( _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { - guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { + guard let entry = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { return true } +#if JAVASCRIPTKIT_ENABLE_TRACING + let traceEnd = JSTracingHooks.beginJSClosureCall( + JSTracing.JSClosureCallInfo(fileID: entry.fileID, line: UInt(entry.line)) + ) +#endif var arguments: [JSValue] = [] for i in 0.. JSObject { - arguments.withRawJSValues { rawValues in + #if JAVASCRIPTKIT_ENABLE_TRACING + let jsValues = arguments.map { $0.jsValue } + return new(arguments: jsValues) + #else + return arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer in JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } + #endif } /// A variadic arguments version of `new`. @@ -89,8 +94,22 @@ extension JSObject { invokeNonThrowingJSFunction(arguments: arguments).jsValue } + /// Instantiate an object from this function as a constructor. + /// + /// Guaranteed to return an object because either: + /// + /// - a. the constructor explicitly returns an object, or + /// - b. the constructor returns nothing, which causes JS to return the `this` value, or + /// - c. the constructor returns undefined, null or a non-object, in which case JS also returns `this`. + /// + /// - Parameter arguments: Arguments to be passed to this constructor function. + /// - Returns: A new instance of this constructor. public func new(arguments: [JSValue]) -> JSObject { - arguments.withRawJSValues { rawValues in + #if JAVASCRIPTKIT_ENABLE_TRACING + let traceEnd = JSTracingHooks.beginJSCall(.function(function: self, arguments: arguments)) + defer { traceEnd?() } + #endif + return arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer in JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } @@ -103,20 +122,71 @@ extension JSObject { } final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue { - arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } - } - - final func invokeNonThrowingJSFunction(arguments: [JSValue], this: JSObject) -> RawJSValue { - arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } + #if JAVASCRIPTKIT_ENABLE_TRACING + let traceEnd = JSTracingHooks.beginJSCall(.function(function: self, arguments: arguments)) + #endif + let result = arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } + #if JAVASCRIPTKIT_ENABLE_TRACING + traceEnd?() + #endif + return result + } + + final func invokeNonThrowingJSFunction( + arguments: [JSValue], + this: JSObject + #if JAVASCRIPTKIT_ENABLE_TRACING + , tracedMethodName: String? = nil + #endif + ) -> RawJSValue { + #if JAVASCRIPTKIT_ENABLE_TRACING + let traceEnd = JSTracingHooks.beginJSCall( + .method( + receiver: this, + methodName: tracedMethodName ?? "", + arguments: arguments + ) + ) + #endif + let result = arguments.withRawJSValues { + invokeNonThrowingJSFunction( + rawValues: $0, + this: this + ) + } + #if JAVASCRIPTKIT_ENABLE_TRACING + traceEnd?() + #endif + return result } #if !hasFeature(Embedded) final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { + #if JAVASCRIPTKIT_ENABLE_TRACING + let jsValues = arguments.map { $0.jsValue } + return invokeNonThrowingJSFunction(arguments: jsValues) + #else arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } - } - - final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue], this: JSObject) -> RawJSValue { + #endif + } + + final func invokeNonThrowingJSFunction( + arguments: [ConvertibleToJSValue], + this: JSObject + #if JAVASCRIPTKIT_ENABLE_TRACING + , tracedMethodName: String? = nil + #endif + ) -> RawJSValue { + #if JAVASCRIPTKIT_ENABLE_TRACING + let jsValues = arguments.map { $0.jsValue } + return invokeNonThrowingJSFunction( + arguments: jsValues, + this: this, + tracedMethodName: tracedMethodName + ) + #else arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } + #endif } #endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 101f13a95..27bc0a7b1 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -94,7 +94,15 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral { public subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)? { guard let function = self[name].function else { return nil } return { (arguments: ConvertibleToJSValue...) in - function(this: self, arguments: arguments) + #if JAVASCRIPTKIT_ENABLE_TRACING + return function.invokeNonThrowingJSFunction( + arguments: arguments, + this: self, + tracedMethodName: name + ).jsValue + #else + return function.invokeNonThrowingJSFunction(arguments: arguments, this: self).jsValue + #endif } } @@ -112,7 +120,15 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral { public subscript(_ name: JSString) -> ((ConvertibleToJSValue...) -> JSValue)? { guard let function = self[name].function else { return nil } return { (arguments: ConvertibleToJSValue...) in - function(this: self, arguments: arguments) + #if JAVASCRIPTKIT_ENABLE_TRACING + return function.invokeNonThrowingJSFunction( + arguments: arguments, + this: self, + tracedMethodName: String(name) + ).jsValue + #else + return function.invokeNonThrowingJSFunction(arguments: arguments, this: self).jsValue + #endif } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 7c75ad556..76dcfa598 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -77,6 +77,15 @@ private func invokeJSFunction( arguments: [ConvertibleToJSValue], this: JSObject? ) throws -> JSValue { + #if JAVASCRIPTKIT_ENABLE_TRACING + let jsValues = arguments.map { $0.jsValue } + let traceEnd = JSTracingHooks.beginJSCall( + this.map { + .method(receiver: $0, methodName: "", arguments: jsValues) + } ?? .function(function: jsFunc, arguments: jsValues) + ) + defer { traceEnd?() } + #endif let id = jsFunc.id let (result, isException) = arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in diff --git a/Sources/JavaScriptKit/JSTracing.swift b/Sources/JavaScriptKit/JSTracing.swift new file mode 100644 index 000000000..e755b81c0 --- /dev/null +++ b/Sources/JavaScriptKit/JSTracing.swift @@ -0,0 +1,133 @@ +#if JAVASCRIPTKIT_ENABLE_TRACING + +/// Hooks for tracing Swift <-> JavaScript bridge calls. +public struct JSTracing { + public static let `default` = JSTracing() + + public enum JSCallInfo { + case function(function: JSObject, arguments: [JSValue]) + case method(receiver: JSObject, methodName: String, arguments: [JSValue]) + } + + /// Register a hook for Swift to JavaScript calls. + /// + /// The hook is invoked at the start of the call. Return a closure to run when + /// the call finishes, or `nil` to skip the end hook. + /// + /// - Returns: A cleanup closure that unregisters the hook. + @discardableResult + public func addJSCallHook( + _ hook: @escaping (_ info: JSCallInfo) -> (() -> Void)? + ) -> () -> Void { + JSTracingHooks.addJSCallHook(hook) + } + + public struct JSClosureCallInfo { + /// The file identifier where the called `JSClosure` was created. + public let fileID: StaticString + /// The line number where the called `JSClosure` was created. + public let line: UInt + } + + /// Register a hook for JavaScript to Swift calls via `JSClosure`. + /// + /// The hook is invoked at the start of the call. Return a closure to run when + /// the call finishes, or `nil` to skip the end hook. + /// + /// - Returns: A cleanup closure that unregisters the hook. + @discardableResult + public func addJSClosureCallHook( + _ hook: @escaping (_ info: JSClosureCallInfo) -> (() -> Void)? + ) -> () -> Void { + JSTracingHooks.addJSClosureCallHook(hook) + } +} + +enum JSTracingHooks { + typealias HookEnd = () -> Void + typealias JSCallHook = (JSTracing.JSCallInfo) -> HookEnd? + typealias JSClosureCallHook = (JSTracing.JSClosureCallInfo) -> HookEnd? + + private final class HookList { + private var hooks: [(id: UInt, hook: Hook)] = [] + private var nextID: UInt = 0 + + var isEmpty: Bool { hooks.isEmpty } + + func add(_ hook: Hook) -> UInt { + let id = nextID + nextID &+= 1 + hooks.append((id, hook)) + return id + } + + func remove(id: UInt) { + hooks.removeAll { $0.id == id } + } + + func forEach(_ body: (Hook) -> Void) { + for entry in hooks { + body(entry.hook) + } + } + } + + private final class Storage { + let jsCallHooks = HookList() + let jsClosureCallHooks = HookList() + } + + private static let storage = LazyThreadLocal(initialize: Storage.init) + + static func addJSCallHook(_ hook: @escaping JSCallHook) -> () -> Void { + let storage = storage.wrappedValue + let id = storage.jsCallHooks.add(hook) + return { storage.jsCallHooks.remove(id: id) } + } + + static func addJSClosureCallHook(_ hook: @escaping JSClosureCallHook) -> () -> Void { + let storage = storage.wrappedValue + let id = storage.jsClosureCallHooks.add(hook) + return { storage.jsClosureCallHooks.remove(id: id) } + } + + static func beginJSCall(_ info: JSTracing.JSCallInfo) -> HookEnd? { + let storage = storage.wrappedValue + guard !storage.jsCallHooks.isEmpty else { return nil } + + var callbacks: [HookEnd] = [] + storage.jsCallHooks.forEach { hook in + if let callback = hook(info) { + callbacks.append(callback) + } + } + + guard !callbacks.isEmpty else { return nil } + return { + for callback in callbacks.reversed() { + callback() + } + } + } + + static func beginJSClosureCall(_ info: JSTracing.JSClosureCallInfo) -> HookEnd? { + let storage = storage.wrappedValue + guard !storage.jsClosureCallHooks.isEmpty else { return nil } + + var callbacks: [HookEnd] = [] + storage.jsClosureCallHooks.forEach { hook in + if let callback = hook(info) { + callbacks.append(callback) + } + } + + guard !callbacks.isEmpty else { return nil } + return { + for callback in callbacks.reversed() { + callback() + } + } + } +} + +#endif From 56a18d72bcd90888a937bd03769ab583d25c9d09 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 29 Jan 2026 18:25:23 +0900 Subject: [PATCH 2/9] =?UTF-8?q?Applied=20the=20review=20feedback:=20hooked?= =?UTF-8?q?=20tracing=20to=20the=20=E2=80=9CTracing=E2=80=9D=20trait=20and?= =?UTF-8?q?=20removed=20the=20extra=20compdef,=20made=20JSClosure=20tracin?= =?UTF-8?q?g=20use=20`String`=20file=20IDs,=20and=20avoided=20unknown=20pl?= =?UTF-8?q?aceholders=20for=20method=20names=20by=20allowing=20nil=20metho?= =?UTF-8?q?d=20names.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated trait name and removed redundant define; tracing is now gated by trait “Tracing” and uses `#if Tracing` (`Package.swift`). - Hooks now accept optional method names and closure file IDs as `String`, with all tracing conditionals using the trait flag (`Sources/JavaScriptKit/JSTracing.swift`, `Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift`, `Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift`, `Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift`, `Sources/JavaScriptKit/FundamentalObjects/JSObject.swift`). - Documentation updated to instruct enabling tracing via `--traits Tracing` (`Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md`). Tests not run. Suggest running `swift build --traits Tracing` to verify the tracing variant compiles. --- Package.swift | 5 ++-- .../Documentation.docc/Articles/Debugging.md | 4 +-- .../FundamentalObjects/JSClosure.swift | 22 ++++++++-------- .../JSObject+CallAsFunction.swift | 26 ++++++++----------- .../FundamentalObjects/JSObject.swift | 4 +-- .../JSThrowingFunction.swift | 4 +-- Sources/JavaScriptKit/JSTracing.swift | 6 ++--- 7 files changed, 33 insertions(+), 38 deletions(-) diff --git a/Package.swift b/Package.swift index f30ec9b4f..47b07945c 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let useLegacyResourceBundling = Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false let tracingTrait = Trait( - name: "JavaScriptKitTracing", + name: "Tracing", description: "Enable opt-in Swift <-> JavaScript bridge tracing hooks.", enabledTraits: [] ) @@ -57,8 +57,7 @@ let package = Package( .unsafeFlags(["-fdeclspec"]) ] : nil, swiftSettings: [ - .enableExperimentalFeature("Extern"), - .define("JAVASCRIPTKIT_ENABLE_TRACING", .when(traits: ["JavaScriptKitTracing"])), + .enableExperimentalFeature("Extern") ] + (shouldBuildForEmbedded ? [ diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md index 1b2729f42..a8e5d77fd 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md @@ -60,10 +60,10 @@ See [the DevTools team's official introduction](https://developer.chrome.com/blo ## Bridge Call Tracing -Enable the `JavaScriptKitTracing` package trait to compile lightweight hook points for Swift <-> JavaScript calls. Tracing is off by default and adds no runtime overhead unless the trait is enabled: +Enable the `Tracing` package trait to compile lightweight hook points for Swift <-> JavaScript calls. Tracing is off by default and adds no runtime overhead unless the trait is enabled: ```bash -swift build --traits JavaScriptKitTracing +swift build --traits Tracing ``` The hooks are invoked at the start and end of each bridge crossing without collecting data for you. For example: diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 5c937491a..91b578091 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -19,7 +19,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { private var hostFuncRef: JavaScriptHostFuncRef = 0 public init( - file: StaticString = #fileID, + file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue ) { @@ -28,7 +28,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { // 2. Create a new JavaScript function which calls the given Swift function. hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) - _id = withExtendedLifetime(JSString(String(file))) { file in + _id = withExtendedLifetime(JSString(file)) { file in swjs_create_oneshot_function(hostFuncRef, line, file.asInternalJSRef()) } @@ -60,7 +60,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async( priority: TaskPriority? = nil, - file: StaticString = #fileID, + file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSOneshotClosure { @@ -79,7 +79,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { public static func async( executorPreference taskExecutor: (any TaskExecutor)? = nil, priority: TaskPriority? = nil, - file: StaticString = #fileID, + file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSOneshotClosure { @@ -122,7 +122,7 @@ public class JSClosure: JSObject, JSClosureProtocol { struct Entry { let object: JSObject let body: (sending [JSValue]) -> JSValue - let fileID: StaticString + let fileID: String let line: UInt32 } private var storage: [JavaScriptHostFuncRef: Entry] = [:] @@ -160,7 +160,7 @@ public class JSClosure: JSObject, JSClosureProtocol { } public init( - file: StaticString = #fileID, + file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue ) { @@ -169,7 +169,7 @@ public class JSClosure: JSObject, JSClosureProtocol { // 2. Create a new JavaScript function which calls the given Swift function. hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) - _id = withExtendedLifetime(JSString(String(file))) { file in + _id = withExtendedLifetime(JSString(file)) { file in swjs_create_function(hostFuncRef, line, file.asInternalJSRef()) } @@ -198,7 +198,7 @@ public class JSClosure: JSObject, JSClosureProtocol { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async( priority: TaskPriority? = nil, - file: StaticString = #fileID, + file: String = #fileID, line: UInt32 = #line, _ body: @escaping @isolated(any) (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSClosure { @@ -217,7 +217,7 @@ public class JSClosure: JSObject, JSClosureProtocol { public static func async( executorPreference taskExecutor: (any TaskExecutor)? = nil, priority: TaskPriority? = nil, - file: StaticString = #fileID, + file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> JSClosure { @@ -338,7 +338,7 @@ func _call_host_function_impl( guard let entry = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { return true } -#if JAVASCRIPTKIT_ENABLE_TRACING +#if Tracing let traceEnd = JSTracingHooks.beginJSClosureCall( JSTracing.JSClosureCallInfo(fileID: entry.fileID, line: UInt(entry.line)) ) @@ -347,7 +347,7 @@ func _call_host_function_impl( for i in 0.. JSObject { - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing let jsValues = arguments.map { $0.jsValue } return new(arguments: jsValues) #else @@ -105,7 +105,7 @@ extension JSObject { /// - Parameter arguments: Arguments to be passed to this constructor function. /// - Returns: A new instance of this constructor. public func new(arguments: [JSValue]) -> JSObject { - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing let traceEnd = JSTracingHooks.beginJSCall(.function(function: self, arguments: arguments)) defer { traceEnd?() } #endif @@ -122,11 +122,11 @@ extension JSObject { } final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue { - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing let traceEnd = JSTracingHooks.beginJSCall(.function(function: self, arguments: arguments)) #endif let result = arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing traceEnd?() #endif return result @@ -135,17 +135,13 @@ extension JSObject { final func invokeNonThrowingJSFunction( arguments: [JSValue], this: JSObject - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing , tracedMethodName: String? = nil #endif ) -> RawJSValue { - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing let traceEnd = JSTracingHooks.beginJSCall( - .method( - receiver: this, - methodName: tracedMethodName ?? "", - arguments: arguments - ) + .method(receiver: this, methodName: tracedMethodName, arguments: arguments) ) #endif let result = arguments.withRawJSValues { @@ -154,7 +150,7 @@ extension JSObject { this: this ) } - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing traceEnd?() #endif return result @@ -162,7 +158,7 @@ extension JSObject { #if !hasFeature(Embedded) final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing let jsValues = arguments.map { $0.jsValue } return invokeNonThrowingJSFunction(arguments: jsValues) #else @@ -173,11 +169,11 @@ extension JSObject { final func invokeNonThrowingJSFunction( arguments: [ConvertibleToJSValue], this: JSObject - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing , tracedMethodName: String? = nil #endif ) -> RawJSValue { - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing let jsValues = arguments.map { $0.jsValue } return invokeNonThrowingJSFunction( arguments: jsValues, diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 27bc0a7b1..caacd49f2 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -94,7 +94,7 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral { public subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)? { guard let function = self[name].function else { return nil } return { (arguments: ConvertibleToJSValue...) in - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing return function.invokeNonThrowingJSFunction( arguments: arguments, this: self, @@ -120,7 +120,7 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral { public subscript(_ name: JSString) -> ((ConvertibleToJSValue...) -> JSValue)? { guard let function = self[name].function else { return nil } return { (arguments: ConvertibleToJSValue...) in - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing return function.invokeNonThrowingJSFunction( arguments: arguments, this: self, diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 76dcfa598..94b5b0eca 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -77,11 +77,11 @@ private func invokeJSFunction( arguments: [ConvertibleToJSValue], this: JSObject? ) throws -> JSValue { - #if JAVASCRIPTKIT_ENABLE_TRACING + #if Tracing let jsValues = arguments.map { $0.jsValue } let traceEnd = JSTracingHooks.beginJSCall( this.map { - .method(receiver: $0, methodName: "", arguments: jsValues) + .method(receiver: $0, methodName: nil, arguments: jsValues) } ?? .function(function: jsFunc, arguments: jsValues) ) defer { traceEnd?() } diff --git a/Sources/JavaScriptKit/JSTracing.swift b/Sources/JavaScriptKit/JSTracing.swift index e755b81c0..dbef33596 100644 --- a/Sources/JavaScriptKit/JSTracing.swift +++ b/Sources/JavaScriptKit/JSTracing.swift @@ -1,4 +1,4 @@ -#if JAVASCRIPTKIT_ENABLE_TRACING +#if Tracing /// Hooks for tracing Swift <-> JavaScript bridge calls. public struct JSTracing { @@ -6,7 +6,7 @@ public struct JSTracing { public enum JSCallInfo { case function(function: JSObject, arguments: [JSValue]) - case method(receiver: JSObject, methodName: String, arguments: [JSValue]) + case method(receiver: JSObject, methodName: String?, arguments: [JSValue]) } /// Register a hook for Swift to JavaScript calls. @@ -24,7 +24,7 @@ public struct JSTracing { public struct JSClosureCallInfo { /// The file identifier where the called `JSClosure` was created. - public let fileID: StaticString + public let fileID: String /// The line number where the called `JSClosure` was created. public let line: UInt } From f3448a3668653c48fdb1d0e0aa7a482386ede59a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 29 Jan 2026 18:30:33 +0900 Subject: [PATCH 3/9] =?UTF-8?q?Added=20tracing=20unit=20coverage=20and=20w?= =?UTF-8?q?ired=20the=20trait=20into=20the=20Makefile=E2=80=99s=20`unittes?= =?UTF-8?q?t`=20target.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New `Tests/JavaScriptKitTests/JSTracingTests.swift` exercises JS→JS call hooks (ensures method info/method name/args) and JSClosure hooks (verifies file/line metadata and end callbacks) under `#if Tracing`. - Refactored tracing-overloaded helpers to avoid conditional parameters and added `Sendable` to `JSTracing` to satisfy Swift 6 safety (`Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift`, `Sources/JavaScriptKit/JSTracing.swift`). - `make unittest` now enables the `Tracing` trait so tracing hooks compile during test runs (`Makefile`). I attempted `swift test --traits Tracing`; the build passed the new tracing warnings but the compiler crashed later with an unrelated wasm memory.grow codegen bug, so tests didn’t finish. You can rerun `make unittest SWIFT_SDK_ID=`; expect the same toolchain crash until Swift fixes that issue. --- Makefile | 1 + .../JSObject+CallAsFunction.swift | 41 ++++++++------ Sources/JavaScriptKit/JSTracing.swift | 2 +- Tests/JavaScriptKitTests/JSTracingTests.swift | 53 +++++++++++++++++++ 4 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 Tests/JavaScriptKitTests/JSTracingTests.swift diff --git a/Makefile b/Makefile index 0c5d0122a..4b695ebe3 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ unittest: exit 2; \ } env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \ + --traits Tracing \ --disable-sandbox \ js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift index 5365b233b..7b3879136 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift @@ -132,29 +132,37 @@ extension JSObject { return result } + #if Tracing final func invokeNonThrowingJSFunction( arguments: [JSValue], - this: JSObject - #if Tracing - , tracedMethodName: String? = nil - #endif + this: JSObject, + tracedMethodName: String? = nil ) -> RawJSValue { - #if Tracing let traceEnd = JSTracingHooks.beginJSCall( .method(receiver: this, methodName: tracedMethodName, arguments: arguments) ) - #endif let result = arguments.withRawJSValues { invokeNonThrowingJSFunction( rawValues: $0, this: this ) } - #if Tracing traceEnd?() - #endif return result } + #else + final func invokeNonThrowingJSFunction( + arguments: [JSValue], + this: JSObject + ) -> RawJSValue { + arguments.withRawJSValues { + invokeNonThrowingJSFunction( + rawValues: $0, + this: this + ) + } + } + #endif #if !hasFeature(Embedded) final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { @@ -166,25 +174,28 @@ extension JSObject { #endif } + #if Tracing final func invokeNonThrowingJSFunction( arguments: [ConvertibleToJSValue], - this: JSObject - #if Tracing - , tracedMethodName: String? = nil - #endif + this: JSObject, + tracedMethodName: String? = nil ) -> RawJSValue { - #if Tracing let jsValues = arguments.map { $0.jsValue } return invokeNonThrowingJSFunction( arguments: jsValues, this: this, tracedMethodName: tracedMethodName ) - #else + } + #else + final func invokeNonThrowingJSFunction( + arguments: [ConvertibleToJSValue], + this: JSObject + ) -> RawJSValue { arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } - #endif } #endif + #endif final private func invokeNonThrowingJSFunction(rawValues: [RawJSValue]) -> RawJSValue { rawValues.withUnsafeBufferPointer { [id] bufferPointer in diff --git a/Sources/JavaScriptKit/JSTracing.swift b/Sources/JavaScriptKit/JSTracing.swift index dbef33596..8804e9afb 100644 --- a/Sources/JavaScriptKit/JSTracing.swift +++ b/Sources/JavaScriptKit/JSTracing.swift @@ -1,7 +1,7 @@ #if Tracing /// Hooks for tracing Swift <-> JavaScript bridge calls. -public struct JSTracing { +public struct JSTracing: Sendable { public static let `default` = JSTracing() public enum JSCallInfo { diff --git a/Tests/JavaScriptKitTests/JSTracingTests.swift b/Tests/JavaScriptKitTests/JSTracingTests.swift new file mode 100644 index 000000000..84fb9bfc6 --- /dev/null +++ b/Tests/JavaScriptKitTests/JSTracingTests.swift @@ -0,0 +1,53 @@ +#if Tracing +import JavaScriptKit +import XCTest + +final class JSTracingTests: XCTestCase { + func testJSCallHookReportsMethod() throws { + var startInfo: [JSTracing.JSCallInfo] = [] + var ended = 0 + let remove = JSTracing.default.addJSCallHook { info in + startInfo.append(info) + return { ended += 1 } + } + defer { remove() } + + let globalObject1 = JSObject.global.globalObject1 + let prop5 = try XCTUnwrap(globalObject1.prop_5.object) + _ = prop5.func6!(true, 1, 2) + + XCTAssertEqual(startInfo.count, 1) + guard case let .method(receiver, methodName, arguments) = startInfo.first else { + XCTFail("Expected method info") + return + } + XCTAssertEqual(receiver.id, prop5.id) + XCTAssertEqual(methodName, "func6") + XCTAssertEqual(arguments, [.boolean(true), .number(1), .number(2)]) + XCTAssertEqual(ended, 1) + } + + func testJSClosureCallHookReportsMetadata() throws { + var startInfo: [JSTracing.JSClosureCallInfo] = [] + var ended = 0 + let remove = JSTracing.default.addJSClosureCallHook { info in + startInfo.append(info) + return { ended += 1 } + } + defer { remove() } + + let globalObject1 = JSObject.global.globalObject1 + let prop6 = try XCTUnwrap(globalObject1.prop_6.object) + let closure = JSClosure(file: "TracingTests.swift", line: 4242) { _ in .number(7) } + prop6.host_func_1 = .object(closure) + + let callHost = try XCTUnwrap(prop6.call_host_1.function) + XCTAssertEqual(callHost(), .number(7)) + + XCTAssertEqual(startInfo.count, 1) + XCTAssertEqual(startInfo.first?.fileID, "TracingTests.swift") + XCTAssertEqual(startInfo.first?.line, 4242) + XCTAssertEqual(ended, 1) + } +} +#endif From bc73c8629d25b592416cf2c09229b4a811b92844 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 29 Jan 2026 18:42:57 +0900 Subject: [PATCH 4/9] Add Swift 6.2 manifest and trait opt-out --- Makefile | 7 ++++++- Package.swift | 11 +++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 4b695ebe3..f4aa88713 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,13 @@ unittest: echo "SWIFT_SDK_ID is not set. Run 'swift sdk list' and pass a matching SDK, e.g. 'make unittest SWIFT_SDK_ID='."; \ exit 2; \ } +ifeq ($(JAVASCRIPTKIT_DISABLE_TRACING_TRAIT),1) + TRACING_ARGS := +else + TRACING_ARGS := --traits Tracing +endif env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \ - --traits Tracing \ + $(TRACING_ARGS) \ --disable-sandbox \ js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc diff --git a/Package.swift b/Package.swift index 47b07945c..d76a318b3 100644 --- a/Package.swift +++ b/Package.swift @@ -7,12 +7,7 @@ import PackageDescription let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false let useLegacyResourceBundling = Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false - -let tracingTrait = Trait( - name: "Tracing", - description: "Enable opt-in Swift <-> JavaScript bridge tracing hooks.", - enabledTraits: [] -) +let enableTracingByEnv = Context.environment["JAVASCRIPTKIT_ENABLE_TRACING"].flatMap(Bool.init) ?? false let testingLinkerFlags: [LinkerSetting] = [ .unsafeFlags([ @@ -42,7 +37,6 @@ let package = Package( .plugin(name: "BridgeJS", targets: ["BridgeJS"]), .plugin(name: "BridgeJSCommandPlugin", targets: ["BridgeJSCommandPlugin"]), ], - traits: [tracingTrait], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"603.0.0") ], @@ -59,6 +53,7 @@ let package = Package( swiftSettings: [ .enableExperimentalFeature("Extern") ] + + (enableTracingByEnv ? [.define("Tracing")] : []) + (shouldBuildForEmbedded ? [ .enableExperimentalFeature("Embedded"), @@ -79,7 +74,7 @@ let package = Package( dependencies: ["JavaScriptKit"], swiftSettings: [ .enableExperimentalFeature("Extern") - ], + ] + (enableTracingByEnv ? [.define("Tracing")] : []), linkerSettings: testingLinkerFlags ), From 8cb14ee45c00dd8eb90793b0d0ae321de5cbae47 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 29 Jan 2026 18:43:07 +0900 Subject: [PATCH 5/9] Add Swift 6.2 manifest with tracing trait --- Package@swift-6.2.swift | 217 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 Package@swift-6.2.swift diff --git a/Package@swift-6.2.swift b/Package@swift-6.2.swift new file mode 100644 index 000000000..456a87ca0 --- /dev/null +++ b/Package@swift-6.2.swift @@ -0,0 +1,217 @@ +// swift-tools-version:6.2 + +import CompilerPluginSupport +import PackageDescription + +// NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits +let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false +let useLegacyResourceBundling = + Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false +let enableTracingByEnv = Context.environment["JAVASCRIPTKIT_ENABLE_TRACING"].flatMap(Bool.init) ?? false + +let tracingTrait = Trait( + name: "Tracing", + description: "Enable opt-in Swift <-> JavaScript bridge tracing hooks.", + enabledTraits: [] +) + +let testingLinkerFlags: [LinkerSetting] = [ + .unsafeFlags([ + "-Xlinker", "--stack-first", + "-Xlinker", "--global-base=524288", + "-Xlinker", "-z", + "-Xlinker", "stack-size=524288", + ]) +] + +let package = Package( + name: "JavaScriptKit", + platforms: [ + .macOS(.v13), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13), + ], + products: [ + .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), + .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), + .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), + .library(name: "JavaScriptFoundationCompat", targets: ["JavaScriptFoundationCompat"]), + .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), + .plugin(name: "PackageToJS", targets: ["PackageToJS"]), + .plugin(name: "BridgeJS", targets: ["BridgeJS"]), + .plugin(name: "BridgeJSCommandPlugin", targets: ["BridgeJSCommandPlugin"]), + ], + traits: [tracingTrait], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"603.0.0") + ], + targets: [ + .target( + name: "JavaScriptKit", + dependencies: ["_CJavaScriptKit", "BridgeJSMacros"], + exclude: useLegacyResourceBundling ? [] : ["Runtime"], + resources: useLegacyResourceBundling ? [.copy("Runtime")] : [], + cSettings: shouldBuildForEmbedded + ? [ + .unsafeFlags(["-fdeclspec"]) + ] : nil, + swiftSettings: [ + .enableExperimentalFeature("Extern"), + .define("Tracing", .when(traits: ["Tracing"])), + ] + + (enableTracingByEnv ? [.define("Tracing")] : []) + + (shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : []) + ), + .target(name: "_CJavaScriptKit"), + .macro( + name: "BridgeJSMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + ] + ), + + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"], + swiftSettings: [ + .enableExperimentalFeature("Extern"), + .define("Tracing", .when(traits: ["Tracing"])), + ] + (enableTracingByEnv ? [.define("Tracing")] : []), + linkerSettings: testingLinkerFlags + ), + + .target( + name: "JavaScriptBigIntSupport", + dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"], + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : [] + ), + .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), + .testTarget( + name: "JavaScriptBigIntSupportTests", + dependencies: ["JavaScriptBigIntSupport", "JavaScriptKit"], + linkerSettings: testingLinkerFlags + ), + + .target( + name: "JavaScriptEventLoop", + dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"], + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : [] + ), + .target(name: "_CJavaScriptEventLoop"), + .testTarget( + name: "JavaScriptEventLoopTests", + dependencies: [ + "JavaScriptEventLoop", + "JavaScriptKit", + "JavaScriptEventLoopTestSupport", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + linkerSettings: testingLinkerFlags + ), + .target( + name: "JavaScriptEventLoopTestSupport", + dependencies: [ + "_CJavaScriptEventLoopTestSupport", + "JavaScriptEventLoop", + ] + ), + .target(name: "_CJavaScriptEventLoopTestSupport"), + .testTarget( + name: "JavaScriptEventLoopTestSupportTests", + dependencies: [ + "JavaScriptKit", + "JavaScriptEventLoopTestSupport", + ], + linkerSettings: testingLinkerFlags + ), + .target( + name: "JavaScriptFoundationCompat", + dependencies: [ + "JavaScriptKit" + ] + ), + .testTarget( + name: "JavaScriptFoundationCompatTests", + dependencies: [ + "JavaScriptFoundationCompat" + ], + linkerSettings: testingLinkerFlags + ), + .plugin( + name: "PackageToJS", + capability: .command( + intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package") + ), + path: "Plugins/PackageToJS/Sources" + ), + .plugin( + name: "BridgeJS", + capability: .buildTool(), + dependencies: ["BridgeJSTool"], + path: "Plugins/BridgeJS/Sources/BridgeJSBuildPlugin" + ), + .plugin( + name: "BridgeJSCommandPlugin", + capability: .command( + intent: .custom(verb: "bridge-js", description: "Generate bridging code"), + permissions: [.writeToPackageDirectory(reason: "Generate bridging code")] + ), + dependencies: ["BridgeJSTool"], + path: "Plugins/BridgeJS/Sources/BridgeJSCommandPlugin" + ), + .executableTarget( + name: "BridgeJSTool", + dependencies: [ + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftBasicFormat", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + ], + exclude: ["TS2Swift/JavaScript", "README.md"] + ), + .testTarget( + name: "BridgeJSRuntimeTests", + dependencies: ["JavaScriptKit", "JavaScriptEventLoop"], + exclude: [ + "bridge-js.config.json", + "bridge-js.d.ts", + "bridge-js.global.d.ts", + "Generated/JavaScript", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + linkerSettings: testingLinkerFlags + ), + .testTarget( + name: "BridgeJSGlobalTests", + dependencies: ["JavaScriptKit", "JavaScriptEventLoop"], + exclude: [ + "bridge-js.config.json", + "bridge-js.d.ts", + "Generated/JavaScript", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + linkerSettings: testingLinkerFlags + ), + ] +) From 362af79400c0544f24f1b3d1f137a670b9655cb5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 29 Jan 2026 18:51:30 +0900 Subject: [PATCH 6/9] Make tracing trait opt-out via env only --- Makefile | 10 +++++----- .../JavaScriptKit/FundamentalObjects/JSClosure.swift | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index f4aa88713..135465a73 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,9 @@ SWIFT_SDK_ID ?= +ifeq ($(JAVASCRIPTKIT_DISABLE_TRACING_TRAIT),1) + TRACING_ARGS := +else + TRACING_ARGS := --traits Tracing +endif .PHONY: bootstrap bootstrap: @@ -11,11 +16,6 @@ unittest: echo "SWIFT_SDK_ID is not set. Run 'swift sdk list' and pass a matching SDK, e.g. 'make unittest SWIFT_SDK_ID='."; \ exit 2; \ } -ifeq ($(JAVASCRIPTKIT_DISABLE_TRACING_TRAIT),1) - TRACING_ARGS := -else - TRACING_ARGS := --traits Tracing -endif env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \ $(TRACING_ARGS) \ --disable-sandbox \ diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 91b578091..b8e29e215 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -338,18 +338,18 @@ func _call_host_function_impl( guard let entry = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { return true } -#if Tracing + #if Tracing let traceEnd = JSTracingHooks.beginJSClosureCall( JSTracing.JSClosureCallInfo(fileID: entry.fileID, line: UInt(entry.line)) ) -#endif + #endif var arguments: [JSValue] = [] for i in 0.. Date: Fri, 30 Jan 2026 00:26:49 +0900 Subject: [PATCH 7/9] Add option to disable tracing trait in JavaScriptKit tests --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f1eb0a5b..fc7392f7f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,8 @@ jobs: download-url: https://download.swift.org/swift-6.1-release/ubuntu2204/swift-6.1-RELEASE/swift-6.1-RELEASE-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasi" + env: | + JAVASCRIPTKIT_DISABLE_TRACING_TRAIT=1 - os: ubuntu-24.04 toolchain: download-url: https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a-ubuntu24.04.tar.gz @@ -36,6 +38,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + - name: Export matrix env + if: ${{ matrix.entry.env != '' && matrix.entry.env != null }} + run: | + cat <<'EOF' >> "$GITHUB_ENV" + ${{ matrix.entry.env }} + EOF - uses: ./.github/actions/install-swift with: download-url: ${{ matrix.entry.toolchain.download-url }} From c3825bfb36a90dd397b08f9492180f0942af6b44 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Jan 2026 00:31:15 +0900 Subject: [PATCH 8/9] Revert changes in Package.swift --- Package.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index d76a318b3..1d4c8fb06 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,6 @@ import PackageDescription let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false let useLegacyResourceBundling = Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false -let enableTracingByEnv = Context.environment["JAVASCRIPTKIT_ENABLE_TRACING"].flatMap(Bool.init) ?? false let testingLinkerFlags: [LinkerSetting] = [ .unsafeFlags([ @@ -53,7 +52,6 @@ let package = Package( swiftSettings: [ .enableExperimentalFeature("Extern") ] - + (enableTracingByEnv ? [.define("Tracing")] : []) + (shouldBuildForEmbedded ? [ .enableExperimentalFeature("Embedded"), @@ -74,7 +72,7 @@ let package = Package( dependencies: ["JavaScriptKit"], swiftSettings: [ .enableExperimentalFeature("Extern") - ] + (enableTracingByEnv ? [.define("Tracing")] : []), + ], linkerSettings: testingLinkerFlags ), From 2804274bef5114dbdcc458aec27ea6410ef9677f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Jan 2026 17:37:41 +0900 Subject: [PATCH 9/9] Remove metadata fields from non-tracing builds --- .../JavaScriptKit/FundamentalObjects/JSClosure.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index b8e29e215..941b3f468 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -122,8 +122,19 @@ public class JSClosure: JSObject, JSClosureProtocol { struct Entry { let object: JSObject let body: (sending [JSValue]) -> JSValue + #if Tracing let fileID: String let line: UInt32 + #endif + + init(object: JSObject, body: @escaping (sending [JSValue]) -> JSValue, fileID: String, line: UInt32) { + self.object = object + self.body = body + #if Tracing + self.fileID = fileID + self.line = line + #endif + } } private var storage: [JavaScriptHostFuncRef: Entry] = [:] init() {}