diff --git a/common.gypi b/common.gypi index 8cf118455ec827..0552460cb0b83d 100644 --- a/common.gypi +++ b/common.gypi @@ -42,7 +42,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.26', + 'v8_embedder_string': '-node.27', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/AUTHORS b/deps/v8/AUTHORS index 9329e34ae252be..ef0aeedaca519b 100644 --- a/deps/v8/AUTHORS +++ b/deps/v8/AUTHORS @@ -77,6 +77,7 @@ Artem Kobzar Arthur Islamov Asuka Shikina Aurèle Barrière +Aviv Keller Bala Avulapati Bangfu Tao Ben Coe diff --git a/deps/v8/src/inspector/injected-script.cc b/deps/v8/src/inspector/injected-script.cc index 1260f19be6de63..5aecd1efcb68e4 100644 --- a/deps/v8/src/inspector/injected-script.cc +++ b/deps/v8/src/inspector/injected-script.cc @@ -221,6 +221,16 @@ class InjectedScript::ProtocolPromiseHandler { m_throwOnSideEffect(throwOnSideEffect), m_callback(std::move(callback)), m_evaluationResult(m_inspector->isolate(), evaluationResult) { + if (!m_replMode) { + makeWeak(id); + } + } + + void makeWeak(PromiseHandlerTracker::Id id) { + if (m_isActive || m_evaluationResult.IsEmpty() || + m_evaluationResult.IsWeak()) { + return; + } m_evaluationResult.SetWeak(reinterpret_cast(id), cleanup, v8::WeakCallbackType::kParameter); } @@ -238,6 +248,7 @@ class InjectedScript::ProtocolPromiseHandler { } void thenCallback(v8::Local value) { + m_isActive = true; // We don't need the m_evaluationResult in the `thenCallback`, but we also // don't want `cleanup` running in case we re-enter JS. m_evaluationResult.Reset(); @@ -285,9 +296,10 @@ class InjectedScript::ProtocolPromiseHandler { } void catchCallback(v8::Local result) { + m_isActive = true; // Hold strongly onto m_evaluationResult now to prevent `cleanup` from // running in case any code below triggers GC. - m_evaluationResult.ClearWeak(); + if (m_evaluationResult.IsWeak()) m_evaluationResult.ClearWeak(); V8InspectorSessionImpl* session = m_inspector->sessionById(m_contextGroupId, m_sessionId); if (!session) return; @@ -393,6 +405,7 @@ class InjectedScript::ProtocolPromiseHandler { std::unique_ptr m_wrapOptions; bool m_replMode; bool m_throwOnSideEffect; + bool m_isActive = false; std::weak_ptr m_callback; v8::Global m_evaluationResult; }; @@ -1225,6 +1238,30 @@ InjectedScript::ProtocolPromiseHandler* PromiseHandlerTracker::get( return iter->second.get(); } +void PromiseHandlerTracker::makeWeakForContext(int executionContextId) { + for (auto& [id, handler] : m_promiseHandlers) { + if (handler->m_executionContextId == executionContextId) { + handler->makeWeak(id); + } + } +} + +void PromiseHandlerTracker::makeWeakForObjectGroup( + int sessionId, const String16& objectGroup) { + for (auto& [id, handler] : m_promiseHandlers) { + if (handler->m_sessionId == sessionId && + handler->m_objectGroup == objectGroup) { + handler->makeWeak(id); + } + } +} + +void PromiseHandlerTracker::makeWeakForSession(int sessionId) { + for (auto& [id, handler] : m_promiseHandlers) { + if (handler->m_sessionId == sessionId) handler->makeWeak(id); + } +} + void PromiseHandlerTracker::sendFailure( InjectedScript::ProtocolPromiseHandler* handler, const protocol::DispatchResponse& response) const { diff --git a/deps/v8/src/inspector/injected-script.h b/deps/v8/src/inspector/injected-script.h index 895f01fde9aa35..864bc8c53f9f48 100644 --- a/deps/v8/src/inspector/injected-script.h +++ b/deps/v8/src/inspector/injected-script.h @@ -298,6 +298,9 @@ class PromiseHandlerTracker { Id create(Args&&... args); void discard(Id id, DiscardReason reason); InjectedScript::ProtocolPromiseHandler* get(Id id) const; + void makeWeakForContext(int executionContextId); + void makeWeakForObjectGroup(int sessionId, const String16& objectGroup); + void makeWeakForSession(int sessionId); private: void sendFailure(InjectedScript::ProtocolPromiseHandler* handler, diff --git a/deps/v8/src/inspector/v8-inspector-impl.cc b/deps/v8/src/inspector/v8-inspector-impl.cc index 9dae9ef1f3693d..a4610e0d9b33aa 100644 --- a/deps/v8/src/inspector/v8-inspector-impl.cc +++ b/deps/v8/src/inspector/v8-inspector-impl.cc @@ -319,6 +319,7 @@ void V8InspectorImpl::contextCollected(int groupId, int contextId) { session->runtimeAgent()->reportExecutionContextDestroyed(inspectedContext); }); discardInspectedContext(groupId, contextId); + m_promiseHandlerTracker.makeWeakForContext(contextId); } void V8InspectorImpl::resetContextGroup(int contextGroupId) { diff --git a/deps/v8/src/inspector/v8-inspector-session-impl.cc b/deps/v8/src/inspector/v8-inspector-session-impl.cc index d51bd1a56ac200..95ae57032b329e 100644 --- a/deps/v8/src/inspector/v8-inspector-session-impl.cc +++ b/deps/v8/src/inspector/v8-inspector-session-impl.cc @@ -224,6 +224,7 @@ void V8InspectorSessionImpl::discardInjectedScripts() { [&sessionId](InspectedContext* context) { context->discardInjectedScript(sessionId); }); + m_inspector->promiseHandlerTracker().makeWeakForSession(sessionId); } Response V8InspectorSessionImpl::findInjectedScript( @@ -260,6 +261,10 @@ void V8InspectorSessionImpl::releaseObjectGroup(const String16& objectGroup) { InjectedScript* injectedScript = context->getInjectedScript(sessionId); if (injectedScript) injectedScript->releaseObjectGroup(objectGroup); }); + if (!objectGroup.isEmpty()) { + m_inspector->promiseHandlerTracker().makeWeakForObjectGroup(m_sessionId, + objectGroup); + } } bool V8InspectorSessionImpl::unwrapObject( diff --git a/deps/v8/test/inspector/runtime/evaluate-repl-mode-promise-lifetime-expected.txt b/deps/v8/test/inspector/runtime/evaluate-repl-mode-promise-lifetime-expected.txt new file mode 100644 index 00000000000000..d6caa466291afa --- /dev/null +++ b/deps/v8/test/inspector/runtime/evaluate-repl-mode-promise-lifetime-expected.txt @@ -0,0 +1,31 @@ +Tests the lifetime of Runtime.evaluate REPL promises. + +Running test: testPromiseIsKeptAlive +{ + id : + result : { + result : { + description : 42 + type : number + value : 42 + } + } +} + +Running test: testObjectGroupReleaseMakesPromiseCollectible +{ + error : { + code : -32000 + message : Promise was collected + } + id : +} + +Running test: testContextDestructionDiscardsPromise +{ + error : { + code : -32000 + message : Execution context was destroyed. + } + id : +} diff --git a/deps/v8/test/inspector/runtime/evaluate-repl-mode-promise-lifetime.js b/deps/v8/test/inspector/runtime/evaluate-repl-mode-promise-lifetime.js new file mode 100644 index 00000000000000..14a2c8a871b715 --- /dev/null +++ b/deps/v8/test/inspector/runtime/evaluate-repl-mode-promise-lifetime.js @@ -0,0 +1,48 @@ +// Copyright 2026 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flags: --no-stress-incremental-marking + +let {Protocol} = InspectorTest.start( + 'Tests the lifetime of Runtime.evaluate REPL promises.'); + +InspectorTest.runAsyncTestSuite([ + async function testPromiseIsKeptAlive() { + const evaluation = Protocol.Runtime.evaluate({ + expression: + 'await new Promise(resolve => globalThis.resolve = resolve); 42', + replMode: true, + }); + + await Protocol.HeapProfiler.collectGarbage(); + await Protocol.Runtime.evaluate({expression: 'resolve()'}); + + InspectorTest.logMessage(await evaluation); + }, + + async function testObjectGroupReleaseMakesPromiseCollectible() { + const evaluation = Protocol.Runtime.evaluate({ + expression: 'await new Promise(() => {})', + objectGroup: 'evaluation', + replMode: true, + }); + + await Protocol.Runtime.releaseObjectGroup({objectGroup: 'evaluation'}); + await Protocol.HeapProfiler.collectGarbage(); + + InspectorTest.logMessage(await evaluation); + }, + + async function testContextDestructionDiscardsPromise() { + const evaluation = Protocol.Runtime.evaluate({ + expression: 'await new Promise(() => {})', + replMode: true, + }); + + await Protocol.Runtime.evaluate( + {expression: 'inspector.fireContextDestroyed()'}); + + InspectorTest.logMessage(await evaluation); + }, +]);