Skip to content

Commit b939aad

Browse files
Built an opt-in tracing surface and wired it through the bridge when enabled by a new trait. Implemented JSTracing (start/end hooks for Swift→JS calls and JSClosure invocations) with per-thread storage in Sources/JavaScriptKit/JSTracing.swift. Added tracing entry points to Swift→JS calls (functions, methods via dynamic members with method names, constructors, and throwing calls) and JS→Swift closures so hooks fire around each bridge crossing when compiled with tracing; closure creation now records StaticString file IDs for reporting (Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift, Sources/JavaScriptKit/FundamentalObjects/JSObject.swift, Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift, Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift). Introduced a JavaScriptKitTracing package trait that gates JAVASCRIPTKIT_ENABLE_TRACING and updated docs with enablement and usage guidance (Package.swift, Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md). Verified the manifest parses with swift package dump-package.
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).
1 parent 8784a38 commit b939aad

File tree

7 files changed

+316
-32
lines changed

7 files changed

+316
-32
lines changed

Package.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMB
88
let useLegacyResourceBundling =
99
Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false
1010

11+
let tracingTrait = Trait(
12+
name: "JavaScriptKitTracing",
13+
description: "Enable opt-in Swift <-> JavaScript bridge tracing hooks.",
14+
enabledTraits: []
15+
)
16+
1117
let testingLinkerFlags: [LinkerSetting] = [
1218
.unsafeFlags([
1319
"-Xlinker", "--stack-first",
@@ -36,6 +42,7 @@ let package = Package(
3642
.plugin(name: "BridgeJS", targets: ["BridgeJS"]),
3743
.plugin(name: "BridgeJSCommandPlugin", targets: ["BridgeJSCommandPlugin"]),
3844
],
45+
traits: [tracingTrait],
3946
dependencies: [
4047
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"603.0.0")
4148
],
@@ -50,7 +57,8 @@ let package = Package(
5057
.unsafeFlags(["-fdeclspec"])
5158
] : nil,
5259
swiftSettings: [
53-
.enableExperimentalFeature("Extern")
60+
.enableExperimentalFeature("Extern"),
61+
.define("JAVASCRIPTKIT_ENABLE_TRACING", .when(traits: ["JavaScriptKitTracing"])),
5462
]
5563
+ (shouldBuildForEmbedded
5664
? [

Sources/JavaScriptKit/Documentation.docc/Articles/Debugging.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,25 @@ Alternatively, you can use the official [`C/C++ DevTools Support (DWARF)`](https
5757
![Chrome DevTools](chrome-devtools.png)
5858

5959
See [the DevTools team's official introduction](https://developer.chrome.com/blog/wasm-debugging-2020) for more details about the extension.
60+
61+
## Bridge Call Tracing
62+
63+
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:
64+
65+
```bash
66+
swift build --traits JavaScriptKitTracing
67+
```
68+
69+
The hooks are invoked at the start and end of each bridge crossing without collecting data for you. For example:
70+
71+
```swift
72+
let removeCallHook = JSTracing.default.addJSCallHook { info in
73+
let started = Date()
74+
return { print("JS call \(info) finished in \(Date().timeIntervalSince(started))s") }
75+
}
76+
77+
let removeClosureHook = JSTracing.default.addJSClosureCallHook { info in
78+
print("JSClosure created at \(info.fileID):\(info.line)")
79+
return nil
80+
}
81+
```

Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,29 @@ public protocol JSClosureProtocol: JSValueCompatible {
1818
public class JSOneshotClosure: JSObject, JSClosureProtocol {
1919
private var hostFuncRef: JavaScriptHostFuncRef = 0
2020

21-
public init(file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue) {
21+
public init(
22+
file: StaticString = #fileID,
23+
line: UInt32 = #line,
24+
_ body: @escaping (sending [JSValue]) -> JSValue
25+
) {
2226
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
2327
super.init(id: 0)
2428

2529
// 2. Create a new JavaScript function which calls the given Swift function.
2630
hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self))
27-
_id = withExtendedLifetime(JSString(file)) { file in
31+
_id = withExtendedLifetime(JSString(String(file))) { file in
2832
swjs_create_oneshot_function(hostFuncRef, line, file.asInternalJSRef())
2933
}
3034

3135
// 3. Retain the given body in static storage by `funcRef`.
32-
JSClosure.sharedClosures.wrappedValue[hostFuncRef] = (
33-
self,
34-
{
36+
JSClosure.sharedClosures.wrappedValue[hostFuncRef] = .init(
37+
object: self,
38+
body: {
3539
defer { self.release() }
3640
return body($0)
37-
}
41+
},
42+
fileID: file,
43+
line: line
3844
)
3945
}
4046

@@ -54,7 +60,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
5460
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
5561
public static func async(
5662
priority: TaskPriority? = nil,
57-
file: String = #fileID,
63+
file: StaticString = #fileID,
5864
line: UInt32 = #line,
5965
_ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue
6066
) -> JSOneshotClosure {
@@ -73,7 +79,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
7379
public static func async(
7480
executorPreference taskExecutor: (any TaskExecutor)? = nil,
7581
priority: TaskPriority? = nil,
76-
file: String = #fileID,
82+
file: StaticString = #fileID,
7783
line: UInt32 = #line,
7884
_ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue
7985
) -> JSOneshotClosure {
@@ -114,14 +120,17 @@ public class JSClosure: JSObject, JSClosureProtocol {
114120
// `removeValue(forKey:)` on a dictionary with value type containing
115121
// `sending`. Wrap the value type with a struct to avoid the crash.
116122
struct Entry {
117-
let item: (object: JSObject, body: (sending [JSValue]) -> JSValue)
123+
let object: JSObject
124+
let body: (sending [JSValue]) -> JSValue
125+
let fileID: StaticString
126+
let line: UInt32
118127
}
119128
private var storage: [JavaScriptHostFuncRef: Entry] = [:]
120129
init() {}
121130

122-
subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: (sending [JSValue]) -> JSValue)? {
123-
get { storage[key]?.item }
124-
set { storage[key] = newValue.map { Entry(item: $0) } }
131+
subscript(_ key: JavaScriptHostFuncRef) -> Entry? {
132+
get { storage[key] }
133+
set { storage[key] = newValue }
125134
}
126135
}
127136

@@ -150,18 +159,27 @@ public class JSClosure: JSObject, JSClosureProtocol {
150159
})
151160
}
152161

153-
public init(file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue) {
162+
public init(
163+
file: StaticString = #fileID,
164+
line: UInt32 = #line,
165+
_ body: @escaping (sending [JSValue]) -> JSValue
166+
) {
154167
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
155168
super.init(id: 0)
156169

157170
// 2. Create a new JavaScript function which calls the given Swift function.
158171
hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self))
159-
_id = withExtendedLifetime(JSString(file)) { file in
172+
_id = withExtendedLifetime(JSString(String(file))) { file in
160173
swjs_create_function(hostFuncRef, line, file.asInternalJSRef())
161174
}
162175

163176
// 3. Retain the given body in static storage by `funcRef`.
164-
Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body)
177+
Self.sharedClosures.wrappedValue[hostFuncRef] = .init(
178+
object: self,
179+
body: body,
180+
fileID: file,
181+
line: line
182+
)
165183
}
166184

167185
@available(*, unavailable, message: "JSClosure does not support dictionary literal initialization")
@@ -180,7 +198,7 @@ public class JSClosure: JSObject, JSClosureProtocol {
180198
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
181199
public static func async(
182200
priority: TaskPriority? = nil,
183-
file: String = #fileID,
201+
file: StaticString = #fileID,
184202
line: UInt32 = #line,
185203
_ body: @escaping @isolated(any) (sending [JSValue]) async throws(JSException) -> JSValue
186204
) -> JSClosure {
@@ -199,7 +217,7 @@ public class JSClosure: JSObject, JSClosureProtocol {
199217
public static func async(
200218
executorPreference taskExecutor: (any TaskExecutor)? = nil,
201219
priority: TaskPriority? = nil,
202-
file: String = #fileID,
220+
file: StaticString = #fileID,
203221
line: UInt32 = #line,
204222
_ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue
205223
) -> JSClosure {
@@ -317,14 +335,22 @@ func _call_host_function_impl(
317335
_ argc: Int32,
318336
_ callbackFuncRef: JavaScriptObjectRef
319337
) -> Bool {
320-
guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else {
338+
guard let entry = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else {
321339
return true
322340
}
341+
#if JAVASCRIPTKIT_ENABLE_TRACING
342+
let traceEnd = JSTracingHooks.beginJSClosureCall(
343+
JSTracing.JSClosureCallInfo(fileID: entry.fileID, line: UInt(entry.line))
344+
)
345+
#endif
323346
var arguments: [JSValue] = []
324347
for i in 0..<Int(argc) {
325348
arguments.append(argv[i].jsValue)
326349
}
327-
let result = hostFunc(arguments)
350+
#if JAVASCRIPTKIT_ENABLE_TRACING
351+
defer { traceEnd?() }
352+
#endif
353+
let result = entry.body(arguments)
328354
let callbackFuncRef = JSObject(id: callbackFuncRef)
329355
_ = callbackFuncRef(result)
330356
return false

Sources/JavaScriptKit/FundamentalObjects/JSObject+CallAsFunction.swift

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,16 @@ extension JSObject {
5252
/// - Parameter arguments: Arguments to be passed to this constructor function.
5353
/// - Returns: A new instance of this constructor.
5454
public func new(arguments: [ConvertibleToJSValue]) -> JSObject {
55-
arguments.withRawJSValues { rawValues in
55+
#if JAVASCRIPTKIT_ENABLE_TRACING
56+
let jsValues = arguments.map { $0.jsValue }
57+
return new(arguments: jsValues)
58+
#else
59+
return arguments.withRawJSValues { rawValues in
5660
rawValues.withUnsafeBufferPointer { bufferPointer in
5761
JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count)))
5862
}
5963
}
64+
#endif
6065
}
6166

6267
/// A variadic arguments version of `new`.
@@ -89,8 +94,22 @@ extension JSObject {
8994
invokeNonThrowingJSFunction(arguments: arguments).jsValue
9095
}
9196

97+
/// Instantiate an object from this function as a constructor.
98+
///
99+
/// Guaranteed to return an object because either:
100+
///
101+
/// - a. the constructor explicitly returns an object, or
102+
/// - b. the constructor returns nothing, which causes JS to return the `this` value, or
103+
/// - c. the constructor returns undefined, null or a non-object, in which case JS also returns `this`.
104+
///
105+
/// - Parameter arguments: Arguments to be passed to this constructor function.
106+
/// - Returns: A new instance of this constructor.
92107
public func new(arguments: [JSValue]) -> JSObject {
93-
arguments.withRawJSValues { rawValues in
108+
#if JAVASCRIPTKIT_ENABLE_TRACING
109+
let traceEnd = JSTracingHooks.beginJSCall(.function(function: self, arguments: arguments))
110+
defer { traceEnd?() }
111+
#endif
112+
return arguments.withRawJSValues { rawValues in
94113
rawValues.withUnsafeBufferPointer { bufferPointer in
95114
JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count)))
96115
}
@@ -103,20 +122,71 @@ extension JSObject {
103122
}
104123

105124
final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue {
106-
arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) }
107-
}
108-
109-
final func invokeNonThrowingJSFunction(arguments: [JSValue], this: JSObject) -> RawJSValue {
110-
arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) }
125+
#if JAVASCRIPTKIT_ENABLE_TRACING
126+
let traceEnd = JSTracingHooks.beginJSCall(.function(function: self, arguments: arguments))
127+
#endif
128+
let result = arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) }
129+
#if JAVASCRIPTKIT_ENABLE_TRACING
130+
traceEnd?()
131+
#endif
132+
return result
133+
}
134+
135+
final func invokeNonThrowingJSFunction(
136+
arguments: [JSValue],
137+
this: JSObject
138+
#if JAVASCRIPTKIT_ENABLE_TRACING
139+
, tracedMethodName: String? = nil
140+
#endif
141+
) -> RawJSValue {
142+
#if JAVASCRIPTKIT_ENABLE_TRACING
143+
let traceEnd = JSTracingHooks.beginJSCall(
144+
.method(
145+
receiver: this,
146+
methodName: tracedMethodName ?? "<unknown>",
147+
arguments: arguments
148+
)
149+
)
150+
#endif
151+
let result = arguments.withRawJSValues {
152+
invokeNonThrowingJSFunction(
153+
rawValues: $0,
154+
this: this
155+
)
156+
}
157+
#if JAVASCRIPTKIT_ENABLE_TRACING
158+
traceEnd?()
159+
#endif
160+
return result
111161
}
112162

113163
#if !hasFeature(Embedded)
114164
final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue {
165+
#if JAVASCRIPTKIT_ENABLE_TRACING
166+
let jsValues = arguments.map { $0.jsValue }
167+
return invokeNonThrowingJSFunction(arguments: jsValues)
168+
#else
115169
arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) }
116-
}
117-
118-
final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue], this: JSObject) -> RawJSValue {
170+
#endif
171+
}
172+
173+
final func invokeNonThrowingJSFunction(
174+
arguments: [ConvertibleToJSValue],
175+
this: JSObject
176+
#if JAVASCRIPTKIT_ENABLE_TRACING
177+
, tracedMethodName: String? = nil
178+
#endif
179+
) -> RawJSValue {
180+
#if JAVASCRIPTKIT_ENABLE_TRACING
181+
let jsValues = arguments.map { $0.jsValue }
182+
return invokeNonThrowingJSFunction(
183+
arguments: jsValues,
184+
this: this,
185+
tracedMethodName: tracedMethodName
186+
)
187+
#else
119188
arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) }
189+
#endif
120190
}
121191
#endif
122192

Sources/JavaScriptKit/FundamentalObjects/JSObject.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,15 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral {
9494
public subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)? {
9595
guard let function = self[name].function else { return nil }
9696
return { (arguments: ConvertibleToJSValue...) in
97-
function(this: self, arguments: arguments)
97+
#if JAVASCRIPTKIT_ENABLE_TRACING
98+
return function.invokeNonThrowingJSFunction(
99+
arguments: arguments,
100+
this: self,
101+
tracedMethodName: name
102+
).jsValue
103+
#else
104+
return function.invokeNonThrowingJSFunction(arguments: arguments, this: self).jsValue
105+
#endif
98106
}
99107
}
100108

@@ -112,7 +120,15 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral {
112120
public subscript(_ name: JSString) -> ((ConvertibleToJSValue...) -> JSValue)? {
113121
guard let function = self[name].function else { return nil }
114122
return { (arguments: ConvertibleToJSValue...) in
115-
function(this: self, arguments: arguments)
123+
#if JAVASCRIPTKIT_ENABLE_TRACING
124+
return function.invokeNonThrowingJSFunction(
125+
arguments: arguments,
126+
this: self,
127+
tracedMethodName: String(name)
128+
).jsValue
129+
#else
130+
return function.invokeNonThrowingJSFunction(arguments: arguments, this: self).jsValue
131+
#endif
116132
}
117133
}
118134

Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ private func invokeJSFunction(
7777
arguments: [ConvertibleToJSValue],
7878
this: JSObject?
7979
) throws -> JSValue {
80+
#if JAVASCRIPTKIT_ENABLE_TRACING
81+
let jsValues = arguments.map { $0.jsValue }
82+
let traceEnd = JSTracingHooks.beginJSCall(
83+
this.map {
84+
.method(receiver: $0, methodName: "<unknown>", arguments: jsValues)
85+
} ?? .function(function: jsFunc, arguments: jsValues)
86+
)
87+
defer { traceEnd?() }
88+
#endif
8089
let id = jsFunc.id
8190
let (result, isException) = arguments.withRawJSValues { rawValues in
8291
rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in

0 commit comments

Comments
 (0)