Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f16f065
feat: Add opt-in identityMode pointer for SwiftHeapObject wrapper ide…
krodak Apr 17, 2026
2518e09
refactor: Simplify pointer identity wrapping and class cache generation
krodak Apr 17, 2026
6189043
fix: Fix retain leak on identity cache hit and namespace deinit refer…
krodak Apr 17, 2026
a865a00
test: Add identity mode E2E tests with dual-mode assertions
krodak Apr 17, 2026
df960e6
fix: Restore FinalizationRegistry polyfill and clean up formatting noise
krodak Apr 17, 2026
89deed0
refactor: Move identityMode to bridge-js.config.json following expose…
krodak Apr 17, 2026
3d1907d
fix: Exclude identityMode from DefaultNodeSetupOptions spread in inde…
krodak Apr 17, 2026
5bf23cd
fix: Regenerate BridgeJSIdentityTests with correct target and add to …
krodak Apr 17, 2026
7f61a8b
fix: Read identityMode from first skeleton that defines it
krodak Apr 17, 2026
40ed8d9
feat: Add identity mode benchmark infrastructure
krodak Apr 21, 2026
19c3f8e
feat: Add identity mode benchmark Swift sources
krodak Apr 21, 2026
2aac6d6
perf: Add IQR outlier removal and median to benchmark statistics
krodak Apr 21, 2026
f562fcb
Merge pull request #2 from PassiveLogic/feat/pointer-mode-v2
krodak Apr 21, 2026
b47bd9c
feat: Add per-class identityMode via @JS macro parameter
krodak Apr 21, 2026
34c5d0d
Merge pull request #3 from PassiveLogic/feat/per-class-identity-mode
krodak Apr 21, 2026
d7e34a5
feat: add opt-in "swift" identity mode with Swift-owned strong cache
krodak Apr 22, 2026
cb04d8c
refactor: simplify identity cache — drop compact id, key by pointer
krodak Apr 22, 2026
03df22f
refactor: drop register_wrapper callback — JS owns the wrapper ref
krodak Apr 22, 2026
37fbaf0
refactor: drop freshBit side-channel — derive hit/miss symmetrically
krodak Apr 22, 2026
52ec56c
feat: preserve identity across all return shapes
krodak Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Benchmarks/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ let package = Package(
.macCatalyst(.v13),
],
dependencies: [
.package(path: "../")
.package(name: "JavaScriptKit", path: "../")
],
targets: [
.executableTarget(
name: "Benchmarks",
dependencies: [
"JavaScriptKit",
.product(name: "JavaScriptKit", package: "JavaScriptKit"),
.product(name: "JavaScriptFoundationCompat", package: "JavaScriptKit"),
],
exclude: ["Generated/JavaScript", "bridge-js.d.ts"],
Expand Down
57 changes: 57 additions & 0 deletions Benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,60 @@ node run.js --filter=Call
node run.js --filter=/^Property access\//
node run.js --filter=/string/i
```

## Identity Mode Benchmarks

Compare `identityMode: "pointer"` vs default (`"none"`) for SwiftHeapObject wrapper caching. Both class variants are compiled into the **same build** via per-class `@JS(identityMode: true)` annotations, so no separate builds or config files are needed.

Requires `--expose-gc` for memory benchmarks.

### Build once

```bash
swift package --swift-sdk $SWIFT_SDK_ID js -c release
```

### Compare both modes in one run

```bash
node --expose-gc run.js --identity-mode=both --identity-iterations=1000000
```

### Run only one mode

```bash
# Only pointer (identity-cached) classes
node --expose-gc run.js --identity-mode=pointer --identity-iterations=1000000

# Only non-identity classes
node --expose-gc run.js --identity-mode=none --identity-iterations=1000000
```

### Additional options

```bash
# Control iteration count (default: 1000000)
node --expose-gc run.js --identity-mode=both --identity-iterations=500000

# Control pool sizes for reuse scenarios (default: 1)
node --expose-gc run.js --identity-mode=both --identity-reuse-pools=1,16

# Include memory profiling (heap snapshots before/during/after)
node --expose-gc run.js --identity-mode=both --identity-memory

# Combine with adaptive sampling
node --expose-gc run.js --identity-mode=both --adaptive

# Filter to specific identity benchmarks
node --expose-gc run.js --adaptive --filter=passBothWaysRoundtrip --identity-mode=both --identity-iterations=1000000
```

### Identity Mode Scenarios

| Scenario | What it measures |
|----------|-----------------|
| `passBothWaysRoundtrip` | Same object crossing boundary repeatedly (cache hit path) |
| `getPoolRepeated_100` | Bulk return of 100 cached objects (model collection pattern) |
| `churnObjects` | Create, roundtrip, release cycle (FinalizationRegistry cleanup pressure) |
| `swiftConsumesSameObject` | JS passes same object to Swift repeatedly |
| `swiftCreatesObject` | Fresh object creation overhead (cache miss path) |
158 changes: 158 additions & 0 deletions Benchmarks/Sources/Benchmarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,164 @@ enum ComplexResult {
}
}

// MARK: - Class Array Performance Tests

nonisolated(unsafe) var _classArrayPool: [SimpleClass] = []

@JS class ClassArrayRoundtrip {
@JS init() {}

@JS func setupPool(_ count: Int) {
_classArrayPool = (0..<count).map {
SimpleClass(name: "Item \($0)", count: $0, flag: true, rate: 0.5, precise: 3.14)
}
}

@JS func getPool() -> [SimpleClass] {
return _classArrayPool
}

@JS func makeClassArray() -> [SimpleClass] {
return (0..<100).map {
SimpleClass(name: "Item \($0)", count: $0, flag: true, rate: 0.5, precise: 3.14)
}
}

@JS func takeClassArray(_ values: [SimpleClass]) {}

@JS func roundtripClassArray(_ values: [SimpleClass]) -> [SimpleClass] {
return values
}
}

// MARK: - Identity Cache Benchmark

nonisolated(unsafe) var _cachedPool: [SimpleClass] = []

@JS class IdentityCacheBenchmark {
@JS init() {}

@JS func setupPool(_ count: Int) {
_cachedPool = (0..<count).map {
SimpleClass(name: "Item \($0)", count: $0, flag: true, rate: 0.5, precise: 3.14)
}
}

@JS func getPoolRepeated() -> [SimpleClass] {
return _cachedPool
}
}

// MARK: - Identity Mode Benchmark Variants (pointer mode)
// These classes use @JS(identityMode: .pointer) so that identity cache benchmarks
// can run in the SAME build alongside the non-identity classes above.

@JS(identityMode: .pointer)
class SimpleClassIdentity {
@JS var name: String
@JS var count: Int
@JS var flag: Bool
@JS var rate: Float
@JS var precise: Double

@JS init(name: String, count: Int, flag: Bool, rate: Float, precise: Double) {
self.name = name
self.count = count
self.flag = flag
self.rate = rate
self.precise = precise
}
}

@JS(identityMode: .pointer)
class ClassRoundtripIdentity {
@JS init() {}

@JS func roundtripSimpleClassIdentity(_ obj: SimpleClassIdentity) -> SimpleClassIdentity {
return obj
}

@JS func makeSimpleClassIdentity() -> SimpleClassIdentity {
return SimpleClassIdentity(name: "Hello", count: 42, flag: true, rate: 0.5, precise: 3.14159)
}

@JS func takeSimpleClassIdentity(_ obj: SimpleClassIdentity) {
// consume without returning
}
}

nonisolated(unsafe) var _cachedPoolIdentity: [SimpleClassIdentity] = []

@JS(identityMode: .pointer)
class IdentityCacheBenchmarkIdentity {
@JS init() {}

@JS func setupPool(_ count: Int) {
_cachedPoolIdentity = (0..<count).map {
SimpleClassIdentity(name: "Item \($0)", count: $0, flag: true, rate: 0.5, precise: 3.14)
}
}

@JS func getPoolRepeated() -> [SimpleClassIdentity] {
return _cachedPoolIdentity
}
}

// MARK: - Identity Mode Benchmark Variants (swift-owned cache)
// Parallel set using @JS(identityMode: .swift) so the third mode can run
// in the same build alongside the other two.

@JS(identityMode: .swift)
class SimpleClassSwiftIdentity {
@JS var name: String
@JS var count: Int
@JS var flag: Bool
@JS var rate: Float
@JS var precise: Double

@JS init(name: String, count: Int, flag: Bool, rate: Float, precise: Double) {
self.name = name
self.count = count
self.flag = flag
self.rate = rate
self.precise = precise
}
}

@JS(identityMode: .swift)
class ClassRoundtripSwiftIdentity {
@JS init() {}

@JS func roundtripSimpleClassSwiftIdentity(_ obj: SimpleClassSwiftIdentity) -> SimpleClassSwiftIdentity {
return obj
}

@JS func makeSimpleClassSwiftIdentity() -> SimpleClassSwiftIdentity {
return SimpleClassSwiftIdentity(name: "Hello", count: 42, flag: true, rate: 0.5, precise: 3.14159)
}

@JS func takeSimpleClassSwiftIdentity(_ obj: SimpleClassSwiftIdentity) {
// consume without returning
}
}

nonisolated(unsafe) var _cachedPoolSwiftIdentity: [SimpleClassSwiftIdentity] = []

@JS(identityMode: .swift)
class IdentityCacheBenchmarkSwiftIdentity {
@JS init() {}

@JS func setupPool(_ count: Int) {
_cachedPoolSwiftIdentity = (0..<count).map {
SimpleClassSwiftIdentity(name: "Item \($0)", count: $0, flag: true, rate: 0.5, precise: 3.14)
}
}

@JS func getPoolRepeated() -> [SimpleClassSwiftIdentity] {
return _cachedPoolSwiftIdentity
}
}

// MARK: - Array Performance Tests

@JS struct Point {
Expand Down
Loading
Loading