fix: runtime lifetime — deferred leaks, cross-isolate sharing, startup robustness - #2013
Draft
edusperoni wants to merge 1 commit into
Draft
fix: runtime lifetime — deferred leaks, cross-isolate sharing, startup robustness#2013edusperoni wants to merge 1 commit into
edusperoni wants to merge 1 commit into
Conversation
…p robustness Works through the tracking issue left by #2006/#2008, minus the HMRSupport item (handled separately). Startup: - Main-runtime election and the once-per-process V8 initialization now happen in one critical section, so two concurrent bootstraps cannot both elect themselves and overwrite Runtime::platform / s_mainEventLoop. A runtime that loses the election waits for the main runtime to publish the metadata tree it reads, instead of relying on call ordering. - A native initialization that throws after Isolate::New is unwound through the existing two teardown windows rather than left half-built; the Runtime itself is freed instead of leaking with its isolate still in the caches. Leaks: - MetadataNodeCache now owns every callback payload handed to V8 as External or FunctionTemplate data (MethodCallbackData, FieldCallbackData, PropertyCallbackData, TypeMetadata, ExtendedClassCallbackData). V8 finalizes none of them, so they leaked on every GC. An arena, because the same MethodCallbackData is shared between a prototype method, CtorCacheData and derived classes. - ModuleInternal::m_loadedModules is released at teardown, deduplicated by pointer (a module is cached under two keys), and a failed load no longer leaks its module handle. - The JS error handed to Java as jsValueAddress is now an id into a per-runtime table instead of a raw Persistent* Java could never free. The entry is dropped when the error is converted back, when the throwable is collected, or with the runtime — this was the only leak that grew inside a live runtime. Cross-isolate sharing: - MetadataNode's three process-wide node caches are guarded. The lock covers map access only and is dropped around the metadata reader. - MetadataReader's node vector, value-buffer bump allocator, type-name cache and memoized node types are guarded by a reentrant lock that can be released to zero mid-section, so it is never held across the Java call that resolves an unknown type (ART class loading, and dex generation on the .extend() path). GetNodeById is bounds-checked. Also makes IsolateDisposer.h's two namespace-scope definitions inline (ODR).
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Description
Works through the items tracked in #2010 — the lifetime problems found while fixing the intermittent worker
SIGSEGV(#2006) and theObjectManagerteardown (#2008), and deliberately deferred there. TheHMRSupportitem is excluded; it is handled in a separate PR.Two facts from the tracking issue underpin most of this, and every fix below is placed accordingly:
v8::Persistentdoes not reset in its destructor andv8::Globaldoes, which decides whether a fix belongs inDestroyRuntime(isolate alive) or~Runtime(isolate disposed).Startup robustness
Main-runtime initialization and election are now serialized.
initRuntimecalls thesynchronizedconstructor and thenruntime.init()outside that block, so thes_mainThreadInitializedcheck-then-act was unprotected: two concurrent bootstraps could both runInitializeV8(), both elect a main runtime, and overwriteRuntime::platform/s_mainEventLoop.ElectMainRuntime()now decides the winner and performs the once-per-process V8 initialization in one critical section.Election is kept separate from readiness, because the elected runtime is not usable by others until it has built the metadata tree they all read. A runtime that loses the election blocks until the main runtime signals ready (or fails) — today that wait returns immediately, since workers are only ever created from an initialized main runtime, but it no longer depends on that ordering. The four
s_mainThreadInitializedreads insidePrepareV8Runtimewere all really "am I the main runtime?" and now read the decidedm_isMainThread.Partial native initialization is now unwound. If
PrepareV8Runtimethrows afterIsolate::New(), the isolate was already ins_isolate2RuntimesCachewhile the Java-side rollback only unwound Java state.UnwindFailedInit()reuses the two existing teardown windows rather than adding a third cleanup path, and theRuntimeitself is freed. An in-flightNativeScriptExceptionmay hold a handle into the isolate about to be disposed, so it drops that handle first and reports from the message and stack it already extracted.Leaks
MetadataNodeCachenow owns the callback payloads.TypeMetadata,FieldCallbackData,PropertyCallbackDataandExtendedClassCallbackData(which also held a strongPersistent<Object>pinning the whole JS implementation object) had no finalizer at all and leaked on every GC;MetadataNode.cppcontained nodelete. They are now owned by the per-runtime cache and freed with it, while the isolate is still alive.This also resolves
CtorCacheData::instanceMethodCallbacks, which #2008 left pending an ownership analysis. An arena is the answer to that analysis: the sameMethodCallbackDatais reachable from a prototype method, fromCtorCacheData, and from theinstanceMethodsCallbackDataa derived class copies out of the cache, so a single owner sidesteps the sharing entirely.ModuleInternal::m_loadedModulesis released in~ModuleInternal, deduplicated by pointer —TempModuleinserts the samePersistentunder bothm_modulePathandm_cacheKey, so a naive loop would double-free. A failed module load also no longer leaks its module handle.NativeScriptException::m_javascriptException— the only one of these that grew inside a live runtime, including the long-lived main one. The rawPersistent<Value>*was handed to Java as ajlongand Java had no way to free it, so every JS error reaching Java pinned itsErrorand captured stack for the life of the process. Java now receives an opaque id into a per-runtime table (jsValueAddressstays along; no Java change). An entry is dropped when the error is converted back to JS, when the throwable carrying the id is collected (tracked by a JNI weak ref), and at the latest with the runtime. The handle held by the exception object itself is nowshared_ptr-owned, so an in-flight copy cannot double-free it.Cross-isolate sharing
MetadataNode's three static node caches (s_name2NodeCache,s_name2TreeNodeCache,s_treeNode2NodeCache) were mutated from any runtime's thread on every JS-wrapper creation. They are now guarded; the lock covers map access only and is dropped around the metadata reader, so losing a race is possible and resolved at the insert — the entry already in the map wins.The metadata tree and
MetadataReader's buffers.m_v.push_backreallocates a vector thatGetNodeByIdindexed with no bounds check, andm_valueData/m_valueLengthis a bump allocator. As the tracking issue notes, this one cannot take a coarse lock:GetOrCreateTreeNodeByNamemutates that state while calling back into Java, and a function-scope mutex would be held across ART class loading and, on the.extend()path, dex generation — inverting against the monitorcom.tns.Runtime's constructor takes and against the cross-threadcallJSMethodwait.It therefore uses a reentrant lock that can be released to zero mid-section.
std::recursive_mutexcannot express that (unlocking it once drops a single level, so a nested caller still excludes everyone), andGetOrCreateTreeNodeByNamerecurses into itself. The lock is dropped entirely around the Java callback and the child re-checked on reacquire. Ordering rule, as specified: the only permitted successor isRuntime::s_runtimeCacheMutex.GetNodeByIdis bounds-checked.Cosmetic
IsolateDisposer.h's two namespace-scope definitions areinline(C++17). They were a strong symbol in each of the three including TUs — benign, since the link collapsed them and the map genuinely is shared, but an ODR violation.Known limitation
While the reader's lock is released around the Java callback, a concurrently resolved type can produce a duplicate tree node (the new node is not published to
m_vuntil after the callback returns). Duplicates are wasteful but not corrupting, and the shape predates this change; removing it would mean restructuring the resolution loop.Does your commit message include the wording below to reference a specific issue in this repo?
Fixes #2010 (all items except
HMRSupport's global maps, handled separately).Related Pull Requests
Follows up #2006, #2007, #2008.
Does your pull request have unit tests?
No new specs — every item is a lifetime/ownership fix with no reachable JS-observable behaviour change, and the concurrency items need two runtimes bootstrapping at once, which the runtime does not currently allow.
Verified with the existing suite on an arm64 emulator (API 35): 878 specs, 0 failures, 0 errors, 4 pre-existing
xit(skips. The run exercises the paths these changes touch —.extend()and runtime dex generation (the reader's lock release), worker create/terminate cycles (the teardown windows), and the JNI reference-leak specs.