Repository navigation
Drain deferred V8 N-API finalizers after each dispatcher turn - #2004
Open
matthargett wants to merge 1 commit into
Open
matthargett wants to merge 1 commit into
matthargett wants to merge 1 commit into
Conversation
On V8, a garbage collection does not run N-API finalizers directly. The weak callback enqueues each finalizer on napi_env::pending_finalizers for a second pass that has to run from a safe host boundary, and the host owns draining that queue. JsRuntimeHost never drained it, so every collected wrapper (Napi::External, ObjectWrap, external array buffers) kept its native resources alive until the environment was torn down. Plugins that hand out many short-lived wrappers per frame, such as WebGPU buffers, textures and command encoders, leaked without bound. AppRuntime now calls an engine-specific DrainPostDispatchWork hook after each dispatcher turn that ran work. On V8 the hook is Napi::DrainFinalizers, which pops finalizers from the pending queue one at a time (a finalizer may enqueue or dequeue others) and stops once an 8 ms budget is spent so a large batch cannot monopolize the JavaScript thread; the remainder runs on the following turns. Running once per turn rather than after each callback keeps the drain off the per-callback path. The hook is a no-op on Chakra, JavaScriptCore, QuickJS, Hermes and JSI, which finalize through their own mechanisms. Two V8-only unit tests cover the behaviour: the finalizers of collected externals have all run by the dispatcher turn after the collection, and a batch whose finalizers exceed the budget is spread across turns instead of completing in one. This supersedes plugin-local workarounds such as the finalizer pump in the NativeDawn plugin (BabylonJS#1781), which reaches into napi_env internals from outside JsRuntimeHost. Port of BabylonJS/JsRuntimeHost#217.
Contributor
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
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.
On V8, a garbage collection does not run N-API finalizers directly. The weak callback enqueues each finalizer on
napi_env::pending_finalizersfor a second pass that has to run from a safe host boundary, and the host owns draining that queue. JsRuntimeHost never drained it, so every collected wrapper (Napi::External,ObjectWrap, external array buffers) kept its native resources alive until the environment was torn down. Plugins that hand out many short-lived wrappers per frame, such as WebGPU buffers, textures and command encoders, leaked without bound.Change
AppRuntime::Runcalls a new engine-specificDrainPostDispatchWork(Napi::Env)hook after each dispatcher turn that ran work. Running once per turn, rather than after each callback, keeps the drain off the per-callback path.Napi::DrainFinalizers, which pops finalizers from the pending queue one at a time (a finalizer may enqueue or dequeue others) and stops once an 8 ms budget is spent, so a large batch cannot monopolize the JavaScript thread; the remainder runs on the following turns.Tests
Two V8-only unit tests in
Tests.AppRuntime.cpp, compiled under a newJSRUNTIMEHOST_NAPI_ENGINE_V8definition in both unit-test CMake files:AppRuntime.V8FinalizersDrainAfterDispatch: the finalizers of 32 collected externals have all run by the dispatcher turn after the collection.AppRuntime.V8FinalizerDrainYieldsBetweenDispatcherTurns: a batch whose finalizers exceed the budget is spread across turns instead of completing in one, and completes within a bounded number of turns.The JavaScriptCore build of
AppRuntime,napiandJsRuntimeHostUnitTestspasses its full suite on macOS.Notes
This supersedes plugin-local workarounds such as the finalizer pump in the NativeDawn plugin (#1781), which reaches into
napi_envinternals from outside JsRuntimeHost.Port of BabylonJS/JsRuntimeHost#217.