Skip to content

test: Add GC lifecycle test for identity-cached wrappers#731

Merged
kateinoigakukun merged 1 commit intomainfrom
feat/identity-gc-lifecycle-test-v2
Apr 28, 2026
Merged

test: Add GC lifecycle test for identity-cached wrappers#731
kateinoigakukun merged 1 commit intomainfrom
feat/identity-gc-lifecycle-test-v2

Conversation

@krodak
Copy link
Copy Markdown
Member

@krodak krodak commented Apr 28, 2026

Overview

Add a test verifying that the WeakRef-based identity cache (introduced in #723) does not prevent garbage collection of Swift heap objects.

The existing GC lifecycle test (testJSWrapperIsDeallocatedAfterFinalization in SwiftClassSupportTests) covers non-identity classes. This adds an equivalent for @JS(identityMode: true) classes where the identity cache holds a WeakRef to the wrapper.

What it tests

Creates a RetainLeakSubject (identity-mode class), crosses it to JS 5 times (filling the identity cache with a WeakRef entry), drops all Swift-side references, triggers GC + event loop ticks, and verifies:

  • The Swift object is deallocated (weakSubject == nil)
  • deinit fires exactly once

This covers the scenario where only JavaScript holds a reference to an identity-cached Swift object via WeakRef, then that reference becomes unreachable.

What changed

  • IdentityModeTests.swift — Added testIdentityCachedWrapperIsReclaimedByGC async test. Added @JSFunction gc() import (same pattern as SwiftClassSupportTests).
  • Generated/BridgeJS.swift + Generated/JavaScript/BridgeJS.json — Regenerated to include gc import binding.

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.
@krodak krodak requested a review from kateinoigakukun April 28, 2026 10:26
@krodak krodak self-assigned this Apr 28, 2026
@kateinoigakukun kateinoigakukun merged commit 016fa5b into main Apr 28, 2026
13 checks passed
@kateinoigakukun kateinoigakukun deleted the feat/identity-gc-lifecycle-test-v2 branch April 28, 2026 22:52
matthewa26 added a commit to matthewa26/JavaScriptKit that referenced this pull request Apr 29, 2026
swiftwasm#731 added the GC lifecycle test (with new imported function entries)
to main while this branch was open. Re-running the BridgeJS regen
against the merged tree fills in the `accessLevel` field on the new
entries that were absent at merge time.
wfltaylor pushed a commit to wfltaylor/JavaScriptKit that referenced this pull request May 1, 2026
…ace (swiftwasm#709) (swiftwasm#727)

* [BridgeJS] Synthesize typed-closure init access from declaration surface

Resolves swiftwasm#709: a public `@JSClass` exposing a
`JSTypedClosure<...>` parameter could not be consumed from another target
because the synthesized `extension JSTypedClosure { init(...) }` was always
internal, leaving downstream callers no way to construct the closure value
without hand-rolling a public wrapper.

Imported skeleton entries now record the source access level
(`public`/`package`/`internal`); the closure-signature collector takes the
maximum across every surface that references a given signature, and
`ClosureCodegen` prefixes the synthesized init with the resulting modifier
(internal stays bare). This matches the pattern `JSClassMacro` already uses
for `init(unsafelyWrapping:)`.

* [BridgeJS] Address PR feedback and refresh generated artifacts

- Make `accessLevel` decode-tolerant on imported skeleton structs
  (`ImportedFunctionSkeleton`, `ImportedConstructorSkeleton`,
  `ImportedGetterSkeleton`, `ImportedSetterSkeleton`,
  `ImportedTypeSkeleton`) by writing explicit `init(from:)` decoders
  that fall back to `.internal` when the key is missing. Without this,
  any pre-existing skeleton JSON without the new field fails decoding —
  the `build-examples` CI job hit `DecodingError.keyNotFound` for
  `accessLevel` against externally consumed skeletons.
- Extract a private `recordSignature` helper so `visitClosure` and
  `recordInjectedSignature` share a single merge implementation.
- Assert in `withAccessLevel(rawLevel:)` so unknown access strings
  ("open", "private", future schema additions) surface in debug
  builds instead of silently inheriting the outer level.
- Document the `.internal` seeding assumption on
  `ClosureSignatureCollectorVisitor.init(moduleName:signatures:)`.
- Regenerate the BridgeJS pre-generated artifacts under Benchmarks/,
  Examples/PlayBridgeJS/, Tests/BridgeJSIdentityTests/, and
  Tests/BridgeJSRuntimeTests/ via `./Utilities/bridge-js-generate.sh`,
  per CONTRIBUTING.md. The runtime-tests Swift output now emits
  `public init` on three `JSTypedClosure` extensions whose signatures
  surface through public exported types.

* [BridgeJS] Refresh identity tests skeleton after merge with main

swiftwasm#731 added the GC lifecycle test (with new imported function entries)
to main while this branch was open. Re-running the BridgeJS regen
against the merged tree fills in the `accessLevel` field on the new
entries that were absent at merge time.

* ci: retry flaky JSPromiseTests.testPromiseAndTimer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants