Skip to content

Commit 37fbaf0

Browse files
committed
refactor: drop freshBit side-channel — derive hit/miss symmetrically
With Swift's Set<pointer> and JS's Map<pointer, wrapper> updated at the same cache boundaries and keyed identically, the freshBit signal is redundant. Swift can use Set.insert(ptr).inserted to detect a miss in a single op; JS can use Map.get(pointer) and check for undefined. Swift return-lowering simplifies to: let ptr = Unmanaged.passUnretained(ret).toOpaque() if _<Class>_identityTable.insert(ptr).inserted { _ = Unmanaged.passRetained(ret) } return ptr JS __wrap simplifies to: const cached = __swiftIdentityWrappers.get(pointer) if (cached !== undefined) return cached // build wrapper, insert into Map No stack push/pop per return on the swift-mode path. Swift heap-object lifetime invariants unchanged. Performance (500k iters, median ms, swift mode only): v3 v4 delta vs v3 vs pointer vs none passBothWaysRoundtrip 27 26 -4% -17% -84% getPoolRepeated_100 34 32 -6% -31% -83% swiftCreatesObject 347 593 +71% (CV noise; see below) churnObjects 332 317 -5% -60% -99% swiftConsumesSameObject 17 10 parity parity -42% swift mode is now a strict Pareto improvement over pointer mode on every scenario: faster on hit, faster on miss, faster on churn, parity on the one-way path. The swiftCreatesObject v3→v4 number is noisy across runs (37% CV) — the v3 standalone 347ms and v4 three-way 593ms overlap within CV; both are ~5x faster than pointer mode's 2021ms and within 15-30% of the 'none' baseline (514ms). 165/165 tests green.
1 parent 03df22f commit 37fbaf0

8 files changed

Lines changed: 84 additions & 232 deletions

File tree

Benchmarks/Sources/Generated/BridgeJS.swift

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,14 +1824,9 @@ public func _bjs_SimpleClassSwiftIdentity_init(_ nameBytes: Int32, _ nameLength:
18241824
let ret = SimpleClassSwiftIdentity(name: String.bridgeJSLiftParameter(nameBytes, nameLength), count: Int.bridgeJSLiftParameter(count), flag: Bool.bridgeJSLiftParameter(flag), rate: Float.bridgeJSLiftParameter(rate), precise: Double.bridgeJSLiftParameter(precise))
18251825
return withExtendedLifetime(ret) {
18261826
let ptr = Unmanaged.passUnretained(ret).toOpaque()
1827-
if _SimpleClassSwiftIdentity_identityTable.contains(ptr) {
1828-
// Cache hit: do NOT retain. JS has the wrapper cached.
1829-
_swift_js_push_i32(0)
1830-
return ptr
1827+
if _SimpleClassSwiftIdentity_identityTable.insert(ptr).inserted {
1828+
_ = Unmanaged.passRetained(ret)
18311829
}
1832-
_ = Unmanaged.passRetained(ret)
1833-
_SimpleClassSwiftIdentity_identityTable.insert(ptr)
1834-
_swift_js_push_i32(1)
18351830
return ptr
18361831
}
18371832
#else
@@ -1969,13 +1964,9 @@ extension SimpleClassSwiftIdentity {
19691964
@_spi(BridgeJS) public consuming func bridgeJSStackPush() {
19701965
let ptr: UnsafeMutableRawPointer = withExtendedLifetime(self) {
19711966
let ptr = Unmanaged.passUnretained(self).toOpaque()
1972-
if _SimpleClassSwiftIdentity_identityTable.contains(ptr) {
1973-
_swift_js_push_i32(0)
1974-
return ptr
1967+
if _SimpleClassSwiftIdentity_identityTable.insert(ptr).inserted {
1968+
_ = Unmanaged.passRetained(self)
19751969
}
1976-
_ = Unmanaged.passRetained(self)
1977-
_SimpleClassSwiftIdentity_identityTable.insert(ptr)
1978-
_swift_js_push_i32(1)
19791970
return ptr
19801971
}
19811972
_swift_js_push_pointer(ptr)
@@ -2012,14 +2003,9 @@ public func _bjs_ClassRoundtripSwiftIdentity_init() -> UnsafeMutableRawPointer {
20122003
let ret = ClassRoundtripSwiftIdentity()
20132004
return withExtendedLifetime(ret) {
20142005
let ptr = Unmanaged.passUnretained(ret).toOpaque()
2015-
if _ClassRoundtripSwiftIdentity_identityTable.contains(ptr) {
2016-
// Cache hit: do NOT retain. JS has the wrapper cached.
2017-
_swift_js_push_i32(0)
2018-
return ptr
2006+
if _ClassRoundtripSwiftIdentity_identityTable.insert(ptr).inserted {
2007+
_ = Unmanaged.passRetained(ret)
20192008
}
2020-
_ = Unmanaged.passRetained(ret)
2021-
_ClassRoundtripSwiftIdentity_identityTable.insert(ptr)
2022-
_swift_js_push_i32(1)
20232009
return ptr
20242010
}
20252011
#else
@@ -2034,14 +2020,9 @@ public func _bjs_ClassRoundtripSwiftIdentity_roundtripSimpleClassSwiftIdentity(_
20342020
let ret = ClassRoundtripSwiftIdentity.bridgeJSLiftParameter(_self).roundtripSimpleClassSwiftIdentity(_: SimpleClassSwiftIdentity.bridgeJSLiftParameter(obj))
20352021
return withExtendedLifetime(ret) {
20362022
let ptr = Unmanaged.passUnretained(ret).toOpaque()
2037-
if _SimpleClassSwiftIdentity_identityTable.contains(ptr) {
2038-
// Cache hit: do NOT retain. JS has the wrapper cached.
2039-
_swift_js_push_i32(0)
2040-
return ptr
2023+
if _SimpleClassSwiftIdentity_identityTable.insert(ptr).inserted {
2024+
_ = Unmanaged.passRetained(ret)
20412025
}
2042-
_ = Unmanaged.passRetained(ret)
2043-
_SimpleClassSwiftIdentity_identityTable.insert(ptr)
2044-
_swift_js_push_i32(1)
20452026
return ptr
20462027
}
20472028
#else
@@ -2056,14 +2037,9 @@ public func _bjs_ClassRoundtripSwiftIdentity_makeSimpleClassSwiftIdentity(_ _sel
20562037
let ret = ClassRoundtripSwiftIdentity.bridgeJSLiftParameter(_self).makeSimpleClassSwiftIdentity()
20572038
return withExtendedLifetime(ret) {
20582039
let ptr = Unmanaged.passUnretained(ret).toOpaque()
2059-
if _SimpleClassSwiftIdentity_identityTable.contains(ptr) {
2060-
// Cache hit: do NOT retain. JS has the wrapper cached.
2061-
_swift_js_push_i32(0)
2062-
return ptr
2040+
if _SimpleClassSwiftIdentity_identityTable.insert(ptr).inserted {
2041+
_ = Unmanaged.passRetained(ret)
20632042
}
2064-
_ = Unmanaged.passRetained(ret)
2065-
_SimpleClassSwiftIdentity_identityTable.insert(ptr)
2066-
_swift_js_push_i32(1)
20672043
return ptr
20682044
}
20692045
#else
@@ -2106,13 +2082,9 @@ extension ClassRoundtripSwiftIdentity {
21062082
@_spi(BridgeJS) public consuming func bridgeJSStackPush() {
21072083
let ptr: UnsafeMutableRawPointer = withExtendedLifetime(self) {
21082084
let ptr = Unmanaged.passUnretained(self).toOpaque()
2109-
if _ClassRoundtripSwiftIdentity_identityTable.contains(ptr) {
2110-
_swift_js_push_i32(0)
2111-
return ptr
2085+
if _ClassRoundtripSwiftIdentity_identityTable.insert(ptr).inserted {
2086+
_ = Unmanaged.passRetained(self)
21122087
}
2113-
_ = Unmanaged.passRetained(self)
2114-
_ClassRoundtripSwiftIdentity_identityTable.insert(ptr)
2115-
_swift_js_push_i32(1)
21162088
return ptr
21172089
}
21182090
_swift_js_push_pointer(ptr)
@@ -2149,14 +2121,9 @@ public func _bjs_IdentityCacheBenchmarkSwiftIdentity_init() -> UnsafeMutableRawP
21492121
let ret = IdentityCacheBenchmarkSwiftIdentity()
21502122
return withExtendedLifetime(ret) {
21512123
let ptr = Unmanaged.passUnretained(ret).toOpaque()
2152-
if _IdentityCacheBenchmarkSwiftIdentity_identityTable.contains(ptr) {
2153-
// Cache hit: do NOT retain. JS has the wrapper cached.
2154-
_swift_js_push_i32(0)
2155-
return ptr
2124+
if _IdentityCacheBenchmarkSwiftIdentity_identityTable.insert(ptr).inserted {
2125+
_ = Unmanaged.passRetained(ret)
21562126
}
2157-
_ = Unmanaged.passRetained(ret)
2158-
_IdentityCacheBenchmarkSwiftIdentity_identityTable.insert(ptr)
2159-
_swift_js_push_i32(1)
21602127
return ptr
21612128
}
21622129
#else
@@ -2210,13 +2177,9 @@ extension IdentityCacheBenchmarkSwiftIdentity {
22102177
@_spi(BridgeJS) public consuming func bridgeJSStackPush() {
22112178
let ptr: UnsafeMutableRawPointer = withExtendedLifetime(self) {
22122179
let ptr = Unmanaged.passUnretained(self).toOpaque()
2213-
if _IdentityCacheBenchmarkSwiftIdentity_identityTable.contains(ptr) {
2214-
_swift_js_push_i32(0)
2215-
return ptr
2180+
if _IdentityCacheBenchmarkSwiftIdentity_identityTable.insert(ptr).inserted {
2181+
_ = Unmanaged.passRetained(self)
22162182
}
2217-
_ = Unmanaged.passRetained(self)
2218-
_IdentityCacheBenchmarkSwiftIdentity_identityTable.insert(ptr)
2219-
_swift_js_push_i32(1)
22202183
return ptr
22212184
}
22222185
_swift_js_push_pointer(ptr)

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -352,28 +352,21 @@ public class ExportSwift {
352352
"""
353353
)
354354
case .swiftHeapObject(let className) where isSwiftIdentityMode(className):
355-
// identityMode: "swift" — Swift tracks which pointers have
356-
// an associated JS wrapper via a per-class Set. On hit, skip
357-
// the passRetained; JS keeps the wrapper alive via its
358-
// strong `Map<pointer, wrapper>`. On miss, retain once and
359-
// signal freshBit=1 so JS builds the wrapper.
360-
//
361-
// ABI: one i32 push after the return pointer — `freshBit`.
362-
// JS pops `freshBit` and either returns its cached wrapper
363-
// (0) or builds a fresh one and calls `register_wrapper` to
364-
// give Swift the retained JS ref (1).
355+
// identityMode: "swift" — Swift's per-class Set tracks which
356+
// pointers have been retained. On miss, retain once; on hit,
357+
// skip. No signalling to JS needed: JS checks its own
358+
// `Map<pointer, wrapper>` and derives hit/miss symmetrically.
359+
// (See DECISIONS.md D20 for why dropping freshBit is safe:
360+
// the Swift Set and the JS Map stay in lockstep by
361+
// construction — both are keyed by pointer and updated at
362+
// the same cache boundaries.)
365363
append(
366364
"""
367365
return withExtendedLifetime(ret) {
368366
let ptr = Unmanaged.passUnretained(ret).toOpaque()
369-
if _\(raw: className)_identityTable.contains(ptr) {
370-
// Cache hit: do NOT retain. JS has the wrapper cached.
371-
_swift_js_push_i32(0)
372-
return ptr
367+
if _\(raw: className)_identityTable.insert(ptr).inserted {
368+
_ = Unmanaged.passRetained(ret)
373369
}
374-
_ = Unmanaged.passRetained(ret)
375-
_\(raw: className)_identityTable.insert(ptr)
376-
_swift_js_push_i32(1)
377370
return ptr
378371
}
379372
"""
@@ -781,20 +774,16 @@ public class ExportSwift {
781774

782775
// Override the default `_BridgedSwiftHeapObject.bridgeJSStackPush`
783776
// so array-element returns (`[SwiftCached]`) go through the same
784-
// identity-cache handshake. See DECISIONS.md D15 (still applies —
785-
// only the internals changed; D18 simplified them).
777+
// identity-cache handshake. Post-D20: no `freshBit` push — JS
778+
// checks its own Map for hit/miss.
786779
let stackPushExt: DeclSyntax = """
787780
extension \(raw: klass.swiftCallName) {
788781
@_spi(BridgeJS) public consuming func bridgeJSStackPush() {
789782
let ptr: UnsafeMutableRawPointer = withExtendedLifetime(self) {
790783
let ptr = Unmanaged.passUnretained(self).toOpaque()
791-
if _\(raw: klass.name)_identityTable.contains(ptr) {
792-
_swift_js_push_i32(0)
793-
return ptr
784+
if _\(raw: klass.name)_identityTable.insert(ptr).inserted {
785+
_ = Unmanaged.passRetained(self)
794786
}
795-
_ = Unmanaged.passRetained(self)
796-
_\(raw: klass.name)_identityTable.insert(ptr)
797-
_swift_js_push_i32(1)
798787
return ptr
799788
}
800789
_swift_js_push_pointer(ptr)

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,26 +2060,23 @@ extension BridgeJSLink {
20602060
dtsExportEntryPrinter.write("\(klass.name): {")
20612061

20622062
if useSwiftIdentity {
2063-
// DECISIONS.md D18 + D19: the `.swift`-mode class is standalone — it does NOT
2064-
// extend `SwiftHeapObject`. It keeps a strong `Map<pointer, wrapper>` so
2065-
// re-exports of the same Swift pointer get the same JS wrapper.
2063+
// DECISIONS.md D18 + D19 + D20: the `.swift`-mode class is standalone —
2064+
// it does NOT extend `SwiftHeapObject`. It keeps a strong
2065+
// `Map<pointer, wrapper>` so re-exports of the same Swift pointer get the
2066+
// same JS wrapper.
20662067
//
2067-
// Swift signals "cache hit" via a single `freshBit` push on the i32 stack
2068-
// (0 = cached, 1 = fresh). Post-D19 the JS side owns the wrapper lifetime
2069-
// entirely — no `swift.memory.retain`, no `register_wrapper` callback.
2070-
// Swift only tracks a `Set<pointer>` of pointers it has retained.
2068+
// D20: no side-channel at all. JS's `Map.get(pointer)` serves as both
2069+
// the "is it cached?" test and the lookup: `undefined` ⇒ miss ⇒ build
2070+
// wrapper. Swift's per-class `Set<pointer>` stays in lockstep because
2071+
// Swift updates it at the same cache boundaries as JS updates its Map.
20712072
jsPrinter.write("class \(klass.name) {")
20722073
jsPrinter.indent {
20732074
jsPrinter.write("static __swiftIdentityWrappers = new Map();")
20742075
jsPrinter.nextLine()
20752076
jsPrinter.write("static __wrap(pointer) {")
20762077
jsPrinter.indent {
2077-
jsPrinter.write("const freshBit = bjs.swift_js_pop_i32();")
2078-
jsPrinter.write("if (freshBit === 0) {")
2079-
jsPrinter.indent {
2080-
jsPrinter.write("return \(klass.name).__swiftIdentityWrappers.get(pointer);")
2081-
}
2082-
jsPrinter.write("}")
2078+
jsPrinter.write("const cached = \(klass.name).__swiftIdentityWrappers.get(pointer);")
2079+
jsPrinter.write("if (cached !== undefined) return cached;")
20832080
jsPrinter.write("const obj = Object.create(\(klass.name).prototype);")
20842081
jsPrinter.write("obj.pointer = pointer;")
20852082
jsPrinter.write("obj.__swiftIdentityHasReleased = false;")

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeSwiftClass.swift

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,9 @@ public func _bjs_SwiftCached_init(_ nameBytes: Int32, _ nameLength: Int32) -> Un
77
let ret = SwiftCached(name: String.bridgeJSLiftParameter(nameBytes, nameLength))
88
return withExtendedLifetime(ret) {
99
let ptr = Unmanaged.passUnretained(ret).toOpaque()
10-
if _SwiftCached_identityTable.contains(ptr) {
11-
// Cache hit: do NOT retain. JS has the wrapper cached.
12-
_swift_js_push_i32(0)
13-
return ptr
10+
if _SwiftCached_identityTable.insert(ptr).inserted {
11+
_ = Unmanaged.passRetained(ret)
1412
}
15-
_ = Unmanaged.passRetained(ret)
16-
_SwiftCached_identityTable.insert(ptr)
17-
_swift_js_push_i32(1)
1813
return ptr
1914
}
2015
#else
@@ -68,13 +63,9 @@ extension SwiftCached {
6863
@_spi(BridgeJS) public consuming func bridgeJSStackPush() {
6964
let ptr: UnsafeMutableRawPointer = withExtendedLifetime(self) {
7065
let ptr = Unmanaged.passUnretained(self).toOpaque()
71-
if _SwiftCached_identityTable.contains(ptr) {
72-
_swift_js_push_i32(0)
73-
return ptr
66+
if _SwiftCached_identityTable.insert(ptr).inserted {
67+
_ = Unmanaged.passRetained(self)
7468
}
75-
_ = Unmanaged.passRetained(self)
76-
_SwiftCached_identityTable.insert(ptr)
77-
_swift_js_push_i32(1)
7869
return ptr
7970
}
8071
_swift_js_push_pointer(ptr)

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.ConfigSwift.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,8 @@ export async function createInstantiator(options, swift) {
308308
static __swiftIdentityWrappers = new Map();
309309

310310
static __wrap(pointer) {
311-
const freshBit = bjs.swift_js_pop_i32();
312-
if (freshBit === 0) {
313-
return UncachedModel.__swiftIdentityWrappers.get(pointer);
314-
}
311+
const cached = UncachedModel.__swiftIdentityWrappers.get(pointer);
312+
if (cached !== undefined) return cached;
315313
const obj = Object.create(UncachedModel.prototype);
316314
obj.pointer = pointer;
317315
obj.__swiftIdentityHasReleased = false;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeSwiftClass.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,8 @@ export async function createInstantiator(options, swift) {
273273
static __swiftIdentityWrappers = new Map();
274274

275275
static __wrap(pointer) {
276-
const freshBit = bjs.swift_js_pop_i32();
277-
if (freshBit === 0) {
278-
return SwiftCached.__swiftIdentityWrappers.get(pointer);
279-
}
276+
const cached = SwiftCached.__swiftIdentityWrappers.get(pointer);
277+
if (cached !== undefined) return cached;
280278
const obj = Object.create(SwiftCached.prototype);
281279
obj.pointer = pointer;
282280
obj.__swiftIdentityHasReleased = false;

0 commit comments

Comments
 (0)