Repository navigation
fix(android): re-attach NativeScript after a JavaScript reload - #89
NathanWalker wants to merge 3 commits into
Conversation
install() returned early on a process-wide flag, so after a reload the new jsi::Runtime had none of NativeScript's globals. The old com.tns.Runtime also stayed subscribed to GC notifications and released handles into the destroyed jsi::Runtime. - The TurboModule tracks the Runtime it attached; React Native creates one module per JS runtime, so isInstalled()/install() are per runtime. - invalidate() (after the JS thread stops, before RN destroys the runtime) detaches: unsubscribes from GcListener, releases every handle held in the jsi::Runtime and deletes the native Runtime and its EngineHost. The next attach becomes the main runtime again; metadata is built once. - GcListener delivers a batch under the lock unsubscribe() takes, so no GC thread can hold a runtime that is being torn down. notifyGc no longer throws on the GC thread for a runtime that is gone. - A Java callback into a reloaded runtime throws a NativeScriptException naming the class and method; ownership is remembered in a weak-keyed map. A failed post to a stopped JS thread throws instead of returning null. - Per-runtime teardown frees the LRU cache's weak global refs, the Java Runtime's global ref, and the static/extension method callback data MetadataNode never tracked. Applied to the napi tree as well.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
|
Tracking the install per module instance instead of with a process-wide flag is exactly the fix for the reload bug, and detaching in But the PR does a lot more than the bug needs, and some of it changes behaviour for every NativeScript app, not just React Native. A reload is the same as disposing a workerWe already create and tear down runtimes all the time for workers, and that path works. The new native The one thing that's genuinely new for React Native is that the attached runtime is the "main" runtime, and nothing expected the main runtime to go away. Resetting that state when it's deleted, and only building the metadata once, is needed. That part makes sense. Changes that affect all apps
Not needed for this bug
Suggested shape
That should be a much smaller PR with no behaviour change for standalone apps. The leak fixes and the GC race can follow as their own small PRs. |
Narrows the reload fix to what React Native needs: the module tracks its own install, invalidate() detaches, and detaching reuses the teardown a worker already goes through. - Runtime.detach() is detachRuntime() (formerly detachWorkerRuntime) plus Runtime::DisposeRuntime (formerly DisposeWorkerRuntime), now built in the host-runtime flavour too. The separate Runtime::Detach is gone. - ~Runtime still resets the main-runtime statics, and BuildMetadata still builds the tree once. Dropped, to land separately or not at all: throwing on a failed post to the JS thread, the GcListener delivery lock and the notifyGc change, the orphaned-object map, the LRU cache clear, the global-ref delete in ~Runtime, MethodCallbackData ownership, and their napi mirrors.
|
Thanks, agreed with all of it. I've cut this down to the shape you suggested (900ee4c, 8d6e841):
I removed these from this PR:
Standalone apps only see the renames. A callback that fires into a reloaded runtime now fails the same way a callback into a disposed worker does ( The split-out parts are their own PRs:
|
|
Please squash merge this one. The first commit (1e1f35b) still describes the original, larger change that review cut down. The description now starts with the squash commit message to use. |
Important
Squash merge this PR. The first commit (1e1f35b) describes the original, larger change. Review cut it down in the later commits, so its message describes code that is not in the final diff. Use this as the squash commit message (title + body), replacing GitHub's default list of commit messages:
Split out of #77. Reworked after review to the smallest change that fixes the reload. The leak fixes are now #92, and the GC-delivery race is #91.
Summary
After a JS reload,
install()returned early because of a process-wide flag, so the newjsi::Runtimehad none of NativeScript's globals. The oldcom.tns.Runtimealso stayed subscribed to GC notifications.A reload is now handled like disposing a worker:
Renames. A reload now uses the worker teardown, so it gets generic names, with bodies unchanged:
detachWorkerRuntime→detachRuntime;DisposeWorkerRuntime→DisposeRuntime, in both the jsi and napi trees.DisposeRuntimeis now also built in the host-runtime flavour.Teardown order. In RN 0.86,
ReactInstance.destroy()stops and joins the JS thread, then invalidates TurboModules, then destroys the runtime.invalidate()therefore detaches while the runtime is alive and idle.Main runtime. The attached runtime is the main runtime, which nothing expected to go away.
~Runtimenow resets the main-runtime statics, so the next attach becomes the main runtime.BuildMetadatabuilds the tree only once, becauseMetadataNode's static caches point into it.Standalone apps: the only changes are the renames.
Evidence
Tested on an emulator (Pixel 9, API 36) with the
ng-native-with-nstestbed, using synced sources.global.java/global.androidwereundefined.After: the globals are present after every reload, across 126 reloads in the same process, with one detach per reload.
Runnable: one that fires in its own runtime runs normally. One posted just before a reload throwsNativeScriptException: Cannot find runtime for instance=…, the same error as a callback into a disposed worker.Not verified:
invalidate()path (RN 0.79–0.81 with the old architecture).Merge Danger
Door: two-way
Blast Radius: Android reload in React Native. Worker teardown is renamed but otherwise unchanged.
Reviewer call: a Java callback still queued from the old runtime throws
Cannot find runtime for instance=…when it fires after a reload. On the UI thread that crashes the app, exactly as a callback into a disposed worker does today. This also applies toexpo-updatesreloadAsync()in production.Known, unchanged, and shared with workers: the GC thread can deliver a batch while a runtime is being disposed. #91 adds a per-runtime lock for it.