test: Add GC lifecycle test for identity-cached wrappers#6
Closed
test: Add GC lifecycle test for identity-cached wrappers#6
Conversation
…ntity caching Add identityMode: "pointer" option to BridgeJS instantiation. When enabled, a WeakRef-based identity cache keyed by pointer ensures the same Swift heap pointer returns the same JS wrapper (=== equality). Each class gets its own FinalizationRegistry and identity cache stored on the deinit function. Off by default, zero overhead when not enabled.
Replace per-class FinalizationRegistry instances with a single shared registry at module level. Move identity cache from deinit function property to per-class static __identityCache field. Cleaner codegen, fewer allocations, easier to inspect in DevTools.
…ence Each boundary crossing calls passRetained on the Swift side. On cache hit, the wrapper is returned without creating a new FinalizationRegistry entry, leaving the retain unbalanced. Call deinit(pointer) on cache hit to immediately release the extra retain. Also fix deinit reference for namespaced classes to use abiName instead of short class name.
Add Tests/BridgeJSRuntimeTests/IdentityModeSupportTests module covering: - Wrapper identity for shared Swift objects - Cache invalidation on release - Different classes don't collide on same pointer - Retain leak regression test for cache hits - Array identity preservation Wire IDENTITY_MODE env var through prelude.mjs to toggle instantiateOptions. Add unittest-pointer Makefile target for running tests with identityMode: "pointer".
Restore the no-op polyfill pattern for environments without FinalizationRegistry instead of null, matching the upstream convention. Remove finalizer parameter from makeFresh since the polyfill is always callable. Use has() guard before stale WeakRef cleanup. Remove formatting-only changes from instantiate.d.ts.
…ToGlobal pattern Add identityMode field to BridgeJSConfig, flow through SwiftToSkeleton and ExportedSkeleton to BridgeJSLink. Generated JS uses config value as default with runtime option as override via nullish coalescing. Create dedicated BridgeJSIdentityTests target with identityMode: pointer in its bridge-js.config.json. Remove IDENTITY_MODE env var, instantiateOptions spread from prelude.mjs, and unittest-pointer Makefile target. Identity tests now run as part of the normal test suite.
…x.js TypeScript strict excess property check rejects identityMode in the spread into DefaultNodeSetupOptions. Destructure it out before spreading, since it's already handled separately via the instantiateOptions pass-through.
…generate script The Generated files were incorrectly copied from BridgeJSRuntimeTests, containing types from the wrong module. Regenerate with BridgeJSTool for the BridgeJSIdentityTests target. Add target to bridge-js-generate.sh. Fix SwiftToSkeleton formatting.
When multiple targets share one createInstantiator (e.g. test package), use compactMap to find the first non-nil identityMode across all skeletons instead of reading from the first skeleton which may not have it set.
Extend run.js with --identity-mode, --identity-iterations, --identity-reuse-pools, and --identity-memory CLI flags. Extract identity scenarios into lib/identity-benchmarks.js: roundtrip reuse, bulk pool return (100 cached objects), churn (create-roundtrip-release), consume, and create paths. Memory telemetry via --identity-memory. Update README.md with identity mode flags and scenario descriptions.
Add IdentityCacheBenchmark with setupPool/getPoolRepeated for bulk array return scenarios. Update generated BridgeJS bindings for benchmark target.
Benchmark results can be noisy due to GC timing and V8 JIT compilation. IQR filtering discards values outside Q1-1.5*IQR to Q3+1.5*IQR before computing statistics. The Samples column shows retained count (e.g. '4 (-1)' means 4 kept, 1 discarded). Falls back to the full dataset if fewer than 4 samples. Applies to all benchmarks, not just identity mode.
feat: Add opt-in identityMode pointer for SwiftHeapObject wrapper identity caching
Add identityMode: Bool parameter to @js macro. When set to true on a class, that class uses pointer identity caching. When false or not set, the class uses the bridge-js.config.json default. Identity is resolved entirely at codegen time - no runtime option. Classes with identity mode get static __identityCache passed to __wrap. Classes without it pass null. No runtime branching in __construct. Resolution: @js(identityMode: true/false) > bridge-js.config.json > default (off).
feat: Add per-class identityMode via @js macro parameter
Verify that WeakRef-based identity cache does not prevent garbage collection. Creates an identity-mode object, crosses it 5 times (filling identity cache), drops all references, triggers GC + event loop ticks, and asserts the Swift object is deallocated and deinit fires exactly once.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Add a test proving that the WeakRef-based identity cache does not prevent garbage collection of Swift heap objects.
The existing GC lifecycle test (
testJSWrapperIsDeallocatedAfterFinalization) only covers non-identity classes. This adds an equivalent for@JS(identityMode: true)classes where the identity cache holds aWeakRefto the wrapper.What it tests
Creates a
RetainLeakSubject(identity-mode class), crosses it to JS 5 times (filling the identity cache with aWeakRefentry), drops all Swift-side references, triggers GC + event loop ticks, and verifies:weakSubject == nil)deinitfires exactly onceThis covers the scenario where only TypeScript holds a reference to an identity-cached Swift object, then that reference becomes unreachable.
What changed
IdentityModeTests.swift— AddedtestIdentityCachedWrapperIsReclaimedByGCasync test. Added@JSFunction gc()import (same pattern asSwiftClassSupportTests).Generated/BridgeJS.swift+Generated/JavaScript/BridgeJS.json— Regenerated to includegcimport binding.