Skip to content

test: add JSICache leak repro harness test (#1464) - #1518

Open
hmelonjp wants to merge 1 commit into
mrousavy:mainfrom
hmelonjp:jsicache-leak-repro-1464
Open

test: add JSICache leak repro harness test (#1464)#1518
hmelonjp wants to merge 1 commit into
mrousavy:mainfrom
hmelonjp:jsicache-leak-repro-1464

Conversation

@hmelonjp

Copy link
Copy Markdown

What this is

A repro-only PR that illustrates the JSICache leak described in #1464.
#1467's thread asked for exactly this:

Hey - I think we still don't have a reproducer for this issue (Harness
test), do we? I have never seen the issue before and I use Nitro quite a
lot

This doesn't attempt a fix - just a deterministic, CI-runnable test that
shows the leak is real, using the existing leak-test pattern already in
example/src/getTests.ts ('HybridObjects do not leak memory when automatically reclaimed by JS GC') as the template.

What it adds

  • JSICacheReference::getTotalSize() (JSICache.hpp) — sums the six cache
    vectors' sizes for the current Runtime. Debug-only introspection, no
    behavior change.
  • HybridNitroModulesProxy::debug_getTotalJSICacheSize() — exposes that to
    JS, wired the same way as the existing debug_getTotalAllocatedHybridObjects().
  • NitroModules.debug_getTotalJSICacheSize() TS declaration.
  • A new harness test, 'JSICache does not leak memory (nitro#1464)', next to
    the existing HybridObject leak test: calls bounceArrayBuffers([new ArrayBuffer(8)]) 55,000 times with fresh plain ArrayBuffers (no
    NativeState, so each one takes the makeShared(...) path), GCs, and
    asserts the JSICache slot count grew by less than 10% of the iteration
    count — same threshold the existing HybridObject test uses.

Verified locally

On this PR, against unpatched main, the test fails on both platforms -
confirming it actually reproduces the leak:

iOS
  TestObject (C++)          baselineSize=122423  currentSize=177423  (+55000, 0% reclaimed)
  TestObject (Swift/Kotlin)  baselineSize=299658  currentSize=354658  (+55000, 0% reclaimed)

Android
  TestObject (C++)          baselineSize=121919  currentSize=176919  (+55000, 0% reclaimed)
  TestObject (Swift/Kotlin)  baselineSize=299125  currentSize=354125  (+55000, 0% reclaimed)

Note, not a claim about any fix: out of curiosity I tried this test against
a local build with #1467/#1468/#1469 applied. iOS came back fully clean,
both variants. Android's C++ variant also came back clean. Android's
Swift/Kotlin variant was better than unpatched but still failed across
several runs - somewhere between 0% and 25% of the growth reclaimed
depending on the run, never close to the 90% the test needs, and flat
across several more seconds of GC + waiting when checked (not just slow to
catch up). Didn't dig into why - out of scope for this PR, just flagging it
in case it's useful.

Steps used to reproduce the failure, in case anyone wants to run this
locally:

# 1. From repo root
bun install
bun example pods          # iOS only: `bundle install` + `pod install`

# 2. Build
cd example
bun run build:ios         # or build:android

# 3a. iOS: boot a simulator, then run the harness against it. HARNESS_APP_PATH
#     must point at the just-built .app - the CLI does not infer it from
#     DerivedData.
xcrun simctl list devices                     # find/boot a simulator UDID
xcrun simctl uninstall <udid> com.mrousavy.nitro.example   # force a fresh install - the
                                                            # CLI skips reinstall if the
                                                            # bundle ID is already present,
                                                            # even if the binary is stale
DEVICE_MODEL="<device name>" IOS_VERSION="<runtime version>" \
  HARNESS_APP_PATH="$(find ~/Library/Developer/Xcode/DerivedData -iname NitroExample.app -path '*iphonesimulator*' | head -1)" \
  node_modules/.bin/react-native-harness --harnessRunner=ios

# 3b. Android: boot/create an AVD, then the same idea - HARNESS_APP_PATH points
#     at the built APK, and a stale install needs uninstalling first.
adb uninstall com.margelo.nitroexample
AVD_NAME="<your avd>" DEVICE_API_LEVEL="<api level>" DEVICE_PROFILE="<device profile>" \
  HARNESS_APP_PATH="android/app/build/outputs/apk/debug/app-debug.apk" \
  node_modules/.bin/react-native-harness --harnessRunner=android

A few non-obvious things that cost real time getting a clean run on both
platforms, noting them here so they're not rediscovered:

  • HARNESS_APP_PATH is required (both platforms) and not documented
    anywhere in this repo - without it, getHarnessAppPath() throws
    synchronously before Metro/the platform runner ever start, and on iOS that
    error gets swallowed by an unrelated AbortError racing it out of the
    other half of the Promise.all in createHarnessSession - the actual
    cause never reaches stdout. If that's worth a separate issue, happy to
    file it.
  • The harness only installs the app if it's not already installed (checked
    via simctl appinfo / equivalent on Android). It does not check
    whether the installed binary is stale, so re-running after a rebuild
    silently tests the old binary unless you uninstall first. Bit us more than
    once - a "clean" run reporting the exact same numbers as a previous one is
    a sign this happened.
  • The harness doesn't forward the app's console.log into its own stdout.
    adb logcat (tag ReactNativeJS) works; no equivalent found for iOS in
    the time we spent on this.

Scope

This PR only illustrates the issue - it's a repro, not a fix.

Checklist

  • Targets main, narrow scope (one test + the debug accessor it needs).
  • Test pins the bug - repro-only PR, so per the checklist: the test
    fails in CI on this PR alone
    (see numbers above).
  • bun specs run - no diff. This PR doesn't touch any .nitro.ts spec,
    so nitrogen output is unaffected.
  • Lint - clang-format -style=file:./config/.clang-format on the three
    touched C++ files and eslint on the two touched TS files, both clean and
    idempotent. (Did not run bun lint-all wholesale - my local clang-format
    is v22 against the repo's pinned v16, and running it repo-wide risked
    reformatting unrelated files under a different binary than CI uses. Happy
    to have CI's own lint job confirm instead.)
  • No existing specs or test cases removed.
  • No unrelated refactors/reformatting - diff is 5 files, additions only.
  • Commit message is Conventional Commits, single line: test: add JSICache leak repro harness test (#1464).

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
nitro-docs Skipped Skipped Aug 20, 2026 11:51pm

Request Review

@mrousavy

Copy link
Copy Markdown
Owner

Nice! Clean and efficiently. Let's see CI

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