Skip to content

Commit d2ca6eb

Browse files
Add opt-in tracing hooks for JS bridge (#548)
* 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). * Applied the review feedback: hooked tracing to the “Tracing” trait and removed the extra compdef, made JSClosure tracing use `String` file IDs, and avoided unknown placeholders for method names by allowing nil method names. - 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. * Added tracing unit coverage and wired the trait into the Makefile’s `unittest` target. - 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=<id>`; expect the same toolchain crash until Swift fixes that issue. * Add Swift 6.2 manifest and trait opt-out * Add Swift 6.2 manifest with tracing trait * Make tracing trait opt-out via env only * Add option to disable tracing trait in JavaScriptKit tests * Revert changes in Package.swift * Remove metadata fields from non-tracing builds
1 parent 59734eb commit d2ca6eb

File tree

10 files changed

+601
-23
lines changed

10 files changed

+601
-23
lines changed

.github/workflows/test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ jobs:
1414
download-url: https://download.swift.org/swift-6.1-release/ubuntu2204/swift-6.1-RELEASE/swift-6.1-RELEASE-ubuntu22.04.tar.gz
1515
wasi-backend: Node
1616
target: "wasm32-unknown-wasi"
17+
env: |
18+
JAVASCRIPTKIT_DISABLE_TRACING_TRAIT=1
1719
- os: ubuntu-24.04
1820
toolchain:
1921
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:
3638
steps:
3739
- name: Checkout
3840
uses: actions/checkout@v6
41+
- name: Export matrix env
42+
if: ${{ matrix.entry.env != '' && matrix.entry.env != null }}
43+
run: |
44+
cat <<'EOF' >> "$GITHUB_ENV"
45+
${{ matrix.entry.env }}
46+
EOF
3947
- uses: ./.github/actions/install-swift
4048
with:
4149
download-url: ${{ matrix.entry.toolchain.download-url }}

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
SWIFT_SDK_ID ?=
2+
ifeq ($(JAVASCRIPTKIT_DISABLE_TRACING_TRAIT),1)
3+
TRACING_ARGS :=
4+
else
5+
TRACING_ARGS := --traits Tracing
6+
endif
27

38
.PHONY: bootstrap
49
bootstrap:
@@ -12,6 +17,7 @@ unittest:
1217
exit 2; \
1318
}
1419
env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \
20+
$(TRACING_ARGS) \
1521
--disable-sandbox \
1622
js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc
1723

Package@swift-6.2.swift

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// swift-tools-version:6.2
2+
3+
import CompilerPluginSupport
4+
import PackageDescription
5+
6+
// NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits
7+
let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false
8+
let useLegacyResourceBundling =
9+
Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false
10+
let enableTracingByEnv = Context.environment["JAVASCRIPTKIT_ENABLE_TRACING"].flatMap(Bool.init) ?? false
11+
12+
let tracingTrait = Trait(
13+
name: "Tracing",
14+
description: "Enable opt-in Swift <-> JavaScript bridge tracing hooks.",
15+
enabledTraits: []
16+
)
17+
18+
let testingLinkerFlags: [LinkerSetting] = [
19+
.unsafeFlags([
20+
"-Xlinker", "--stack-first",
21+
"-Xlinker", "--global-base=524288",
22+
"-Xlinker", "-z",
23+
"-Xlinker", "stack-size=524288",
24+
])
25+
]
26+
27+
let package = Package(
28+
name: "JavaScriptKit",
29+
platforms: [
30+
.macOS(.v13),
31+
.iOS(.v13),
32+
.tvOS(.v13),
33+
.watchOS(.v6),
34+
.macCatalyst(.v13),
35+
],
36+
products: [
37+
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
38+
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
39+
.library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]),
40+
.library(name: "JavaScriptFoundationCompat", targets: ["JavaScriptFoundationCompat"]),
41+
.library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]),
42+
.plugin(name: "PackageToJS", targets: ["PackageToJS"]),
43+
.plugin(name: "BridgeJS", targets: ["BridgeJS"]),
44+
.plugin(name: "BridgeJSCommandPlugin", targets: ["BridgeJSCommandPlugin"]),
45+
],
46+
traits: [tracingTrait],
47+
dependencies: [
48+
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"603.0.0")
49+
],
50+
targets: [
51+
.target(
52+
name: "JavaScriptKit",
53+
dependencies: ["_CJavaScriptKit", "BridgeJSMacros"],
54+
exclude: useLegacyResourceBundling ? [] : ["Runtime"],
55+
resources: useLegacyResourceBundling ? [.copy("Runtime")] : [],
56+
cSettings: shouldBuildForEmbedded
57+
? [
58+
.unsafeFlags(["-fdeclspec"])
59+
] : nil,
60+
swiftSettings: [
61+
.enableExperimentalFeature("Extern"),
62+
.define("Tracing", .when(traits: ["Tracing"])),
63+
]
64+
+ (enableTracingByEnv ? [.define("Tracing")] : [])
65+
+ (shouldBuildForEmbedded
66+
? [
67+
.enableExperimentalFeature("Embedded"),
68+
.unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]),
69+
] : [])
70+
),
71+
.target(name: "_CJavaScriptKit"),
72+
.macro(
73+
name: "BridgeJSMacros",
74+
dependencies: [
75+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
76+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
77+
]
78+
),
79+
80+
.testTarget(
81+
name: "JavaScriptKitTests",
82+
dependencies: ["JavaScriptKit"],
83+
swiftSettings: [
84+
.enableExperimentalFeature("Extern"),
85+
.define("Tracing", .when(traits: ["Tracing"])),
86+
] + (enableTracingByEnv ? [.define("Tracing")] : []),
87+
linkerSettings: testingLinkerFlags
88+
),
89+
90+
.target(
91+
name: "JavaScriptBigIntSupport",
92+
dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"],
93+
swiftSettings: shouldBuildForEmbedded
94+
? [
95+
.enableExperimentalFeature("Embedded"),
96+
.unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]),
97+
] : []
98+
),
99+
.target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]),
100+
.testTarget(
101+
name: "JavaScriptBigIntSupportTests",
102+
dependencies: ["JavaScriptBigIntSupport", "JavaScriptKit"],
103+
linkerSettings: testingLinkerFlags
104+
),
105+
106+
.target(
107+
name: "JavaScriptEventLoop",
108+
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"],
109+
swiftSettings: shouldBuildForEmbedded
110+
? [
111+
.enableExperimentalFeature("Embedded"),
112+
.unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]),
113+
] : []
114+
),
115+
.target(name: "_CJavaScriptEventLoop"),
116+
.testTarget(
117+
name: "JavaScriptEventLoopTests",
118+
dependencies: [
119+
"JavaScriptEventLoop",
120+
"JavaScriptKit",
121+
"JavaScriptEventLoopTestSupport",
122+
],
123+
swiftSettings: [
124+
.enableExperimentalFeature("Extern")
125+
],
126+
linkerSettings: testingLinkerFlags
127+
),
128+
.target(
129+
name: "JavaScriptEventLoopTestSupport",
130+
dependencies: [
131+
"_CJavaScriptEventLoopTestSupport",
132+
"JavaScriptEventLoop",
133+
]
134+
),
135+
.target(name: "_CJavaScriptEventLoopTestSupport"),
136+
.testTarget(
137+
name: "JavaScriptEventLoopTestSupportTests",
138+
dependencies: [
139+
"JavaScriptKit",
140+
"JavaScriptEventLoopTestSupport",
141+
],
142+
linkerSettings: testingLinkerFlags
143+
),
144+
.target(
145+
name: "JavaScriptFoundationCompat",
146+
dependencies: [
147+
"JavaScriptKit"
148+
]
149+
),
150+
.testTarget(
151+
name: "JavaScriptFoundationCompatTests",
152+
dependencies: [
153+
"JavaScriptFoundationCompat"
154+
],
155+
linkerSettings: testingLinkerFlags
156+
),
157+
.plugin(
158+
name: "PackageToJS",
159+
capability: .command(
160+
intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package")
161+
),
162+
path: "Plugins/PackageToJS/Sources"
163+
),
164+
.plugin(
165+
name: "BridgeJS",
166+
capability: .buildTool(),
167+
dependencies: ["BridgeJSTool"],
168+
path: "Plugins/BridgeJS/Sources/BridgeJSBuildPlugin"
169+
),
170+
.plugin(
171+
name: "BridgeJSCommandPlugin",
172+
capability: .command(
173+
intent: .custom(verb: "bridge-js", description: "Generate bridging code"),
174+
permissions: [.writeToPackageDirectory(reason: "Generate bridging code")]
175+
),
176+
dependencies: ["BridgeJSTool"],
177+
path: "Plugins/BridgeJS/Sources/BridgeJSCommandPlugin"
178+
),
179+
.executableTarget(
180+
name: "BridgeJSTool",
181+
dependencies: [
182+
.product(name: "SwiftParser", package: "swift-syntax"),
183+
.product(name: "SwiftSyntax", package: "swift-syntax"),
184+
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
185+
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
186+
],
187+
exclude: ["TS2Swift/JavaScript", "README.md"]
188+
),
189+
.testTarget(
190+
name: "BridgeJSRuntimeTests",
191+
dependencies: ["JavaScriptKit", "JavaScriptEventLoop"],
192+
exclude: [
193+
"bridge-js.config.json",
194+
"bridge-js.d.ts",
195+
"bridge-js.global.d.ts",
196+
"Generated/JavaScript",
197+
],
198+
swiftSettings: [
199+
.enableExperimentalFeature("Extern")
200+
],
201+
linkerSettings: testingLinkerFlags
202+
),
203+
.testTarget(
204+
name: "BridgeJSGlobalTests",
205+
dependencies: ["JavaScriptKit", "JavaScriptEventLoop"],
206+
exclude: [
207+
"bridge-js.config.json",
208+
"bridge-js.d.ts",
209+
"Generated/JavaScript",
210+
],
211+
swiftSettings: [
212+
.enableExperimentalFeature("Extern")
213+
],
214+
linkerSettings: testingLinkerFlags
215+
),
216+
]
217+
)

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 `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:
64+
65+
```bash
66+
swift build --traits Tracing
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+
```

0 commit comments

Comments
 (0)