Skip to content
This repository was archived by the owner on Jun 24, 2022. It is now read-only.

Commit cd0318c

Browse files
Finalizers shouldn't run if events can't fire
https://bugs.webkit.org/show_bug.cgi?id=214508 Reviewed by Ryosuke Niwa. Source/JavaScriptCore: This patch makes it so the DeferredWorkTimer won't run scheduled tasks if those would not have run if they were scheduled in WebCore. To do this there is now a concept of a ScriptExecutionOwner. The ScriptExecutionOwner is almost always the same as the global object of the pending task (referred to as the ticket). The only exception to this is if the global object is a JSDOMWindowBase, then the ScriptExecutionOwner is the Document's JS wrapper. To tell the status of a ScriptExecutionOwner, the DeferredWorkTimer calls a virtual function on the global object of the ticket, for JSC-only this just always returns Running. For WebCore, we ask the ScriptExecutionContext associated with the ScriptExecutionOwner. * API/JSAPIGlobalObject.cpp: * API/JSAPIGlobalObject.mm: * jsc.cpp: * runtime/DeferredWorkTimer.cpp: (JSC::DeferredWorkTimer::doWork): (JSC::DeferredWorkTimer::addPendingWork): (JSC::DeferredWorkTimer::hasDependancyInPendingWork): (JSC::DeferredWorkTimer::didResumeScriptExecutionOwner): * runtime/DeferredWorkTimer.h: * runtime/JSFinalizationRegistry.cpp: (JSC::JSFinalizationRegistry::create): (JSC::JSFinalizationRegistry::finishCreation): * runtime/JSFinalizationRegistry.h: * runtime/JSGlobalObject.cpp: * runtime/JSGlobalObject.h: (JSC::JSGlobalObject::currentScriptExecutionOwner): (JSC::JSGlobalObject::scriptExecutionStatus): Source/WebCore: This patch makes it so the DeferredWorkTimer won't run scheduled tasks if those would not have run if they were scheduled in WebCore. To do this there is now a concept of a ScriptExecutionOwner. The ScriptExecutionOwner is almost always the same as the global object of the pending task (referred to as the ticket). The only exception to this is if the global object is a JSDOMWindowBase, then the ScriptExecutionOwner is the Document's JS wrapper. To tell the status of a ScriptExecutionOwner, the DeferredWorkTimer calls a virtual function on the global object of the ticket, for JSC-only this just always returns Running. For WebCore, we ask the ScriptExecutionContext associated with the ScriptExecutionOwner. * bindings/js/JSDOMWindowBase.cpp: (WebCore::JSDOMWindowBase::currentScriptExecutionOwner): (WebCore::JSDOMWindowBase::scriptExecutionStatus): * bindings/js/JSDOMWindowBase.h: * bindings/js/JSDOMWrapperCache.h: * bindings/js/JSRemoteDOMWindowBase.cpp: * bindings/js/JSWorkerGlobalScopeBase.cpp: (WebCore::JSWorkerGlobalScopeBase::scriptExecutionStatus): * bindings/js/JSWorkerGlobalScopeBase.h: * bindings/js/JSWorkletGlobalScopeBase.cpp: (WebCore::JSWorkletGlobalScopeBase::scriptExecutionStatus): * bindings/js/JSWorkletGlobalScopeBase.h: * dom/ScriptExecutionContext.cpp: (WebCore::ScriptExecutionContext::contextIdentifier const): (WebCore::ScriptExecutionContext::removeFromContextsMap): (WebCore::ScriptExecutionContext::~ScriptExecutionContext): (WebCore::ScriptExecutionContext::jscScriptExecutionStatus const): (WebCore::ScriptExecutionContext::resumeActiveDOMObjects): (WebCore::ScriptExecutionContext::postTaskTo): * dom/ScriptExecutionContext.h: Source/WTF: Add a DropLockScope to make it easier to drop a lock for a short piece of code. Also, instead of deleting int Locker constructor we should just delete the underlying type of the NoLockingNecessary enum. * wtf/Locker.h: (WTF::Locker::~Locker): (WTF::Locker::unlockEarly): (WTF::Locker::Locker): (WTF::Locker::operator=): (WTF::Locker::unlock): (WTF::DropLockForScope::DropLockForScope): (WTF::DropLockForScope::~DropLockForScope): LayoutTests: Add tests that check we don't run any tasks from JSC's event loop while contexts are suspended/stopped. Also skip the WASM tests on Win because WASM doesn't work there. * fast/frames/detached-frame-wasm-resolve-expected.txt: Added. * fast/frames/detached-frame-wasm-resolve.html: Added. * fast/history/page-cache-active-finalization-registry-callback-expected.txt: Added. * fast/history/page-cache-active-finalization-registry-callback.html: Added. * fast/history/page-cache-wasm-promise-resolve-expected.txt: Added. * fast/history/page-cache-wasm-promise-resolve.html: Added. * platform/win/TestExpectations: git-svn-id: http://svn.webkit.org/repository/webkit/trunk@268271 268f45cc-cd09-0410-ab3c-d52691b4dbfc
1 parent ded661b commit cd0318c

25 files changed

+311
-44
lines changed

LayoutTests/ChangeLog

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
2020-10-09 Keith Miller <[email protected]>
2+
3+
Finalizers shouldn't run if events can't fire
4+
https://bugs.webkit.org/show_bug.cgi?id=214508
5+
6+
Reviewed by Ryosuke Niwa.
7+
8+
Add tests that check we don't run any tasks from JSC's event loop while contexts
9+
are suspended/stopped. Also skip the WASM tests on Win because WASM doesn't work
10+
there.
11+
12+
* fast/frames/detached-frame-wasm-resolve-expected.txt: Added.
13+
* fast/frames/detached-frame-wasm-resolve.html: Added.
14+
* fast/history/page-cache-active-finalization-registry-callback-expected.txt: Added.
15+
* fast/history/page-cache-active-finalization-registry-callback.html: Added.
16+
* fast/history/page-cache-wasm-promise-resolve-expected.txt: Added.
17+
* fast/history/page-cache-wasm-promise-resolve.html: Added.
18+
* platform/win/TestExpectations:
19+
120
2020-10-09 Andres Gonzalez <[email protected]>
221

322
Fix for accessibility tests keyevents-posted-for-increment-actions.html and keyevents-for-increment-actions-with-node-removal.html in isolated tree mode.

LayoutTests/platform/win/TestExpectations

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,10 @@ fast/visual-viewport/zoomed-fixed-scroll-down-then-up.html [ Failure ]
700700
webkit.org/b/77568 fast/text/locale-shaping.html [ ImageOnlyFailure ]
701701
webkit.org/b/77568 fast/text/locale-shaping-complex.html [ ImageOnlyFailure ]
702702

703+
# WebAssembly doesn't exist on Windows
704+
fast/frames/detached-frame-wasm-resolve.html [ Skip ]
705+
fast/history/page-cache-wasm-promise-resolve.html [ Skip ]
706+
703707
################################################################################
704708
########### End Missing Functionality Prevents Testing ##############
705709
################################################################################

Source/JavaScriptCore/API/JSAPIGlobalObject.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ const GlobalObjectMethodTable JSAPIGlobalObject::s_globalObjectMethodTable = {
4545
nullptr, // moduleLoaderEvaluate
4646
nullptr, // promiseRejectionTracker
4747
&reportUncaughtExceptionAtEventLoop,
48+
&currentScriptExecutionOwner,
49+
&scriptExecutionStatus,
4850
nullptr, // defaultLanguage
4951
nullptr, // compileStreaming
5052
nullptr, // instantiateStreaming

Source/JavaScriptCore/API/JSAPIGlobalObject.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
&moduleLoaderEvaluate, // moduleLoaderEvaluate
6666
nullptr, // promiseRejectionTracker
6767
&reportUncaughtExceptionAtEventLoop,
68+
&currentScriptExecutionOwner,
69+
&scriptExecutionStatus,
6870
nullptr, // defaultLanguage
6971
nullptr, // compileStreaming
7072
nullptr, // instantiateStreaming

Source/JavaScriptCore/ChangeLog

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
2020-10-09 Keith Miller <[email protected]>
2+
3+
Finalizers shouldn't run if events can't fire
4+
https://bugs.webkit.org/show_bug.cgi?id=214508
5+
6+
Reviewed by Ryosuke Niwa.
7+
8+
This patch makes it so the DeferredWorkTimer won't run scheduled
9+
tasks if those would not have run if they were scheduled in
10+
WebCore. To do this there is now a concept of a
11+
ScriptExecutionOwner. The ScriptExecutionOwner is almost always
12+
the same as the global object of the pending task (referred to as
13+
the ticket). The only exception to this is if the global object
14+
is a JSDOMWindowBase, then the ScriptExecutionOwner is the
15+
Document's JS wrapper. To tell the status of a
16+
ScriptExecutionOwner, the DeferredWorkTimer calls a virtual
17+
function on the global object of the ticket, for JSC-only this
18+
just always returns Running. For WebCore, we ask the
19+
ScriptExecutionContext associated with the ScriptExecutionOwner.
20+
21+
* API/JSAPIGlobalObject.cpp:
22+
* API/JSAPIGlobalObject.mm:
23+
* jsc.cpp:
24+
* runtime/DeferredWorkTimer.cpp:
25+
(JSC::DeferredWorkTimer::doWork):
26+
(JSC::DeferredWorkTimer::addPendingWork):
27+
(JSC::DeferredWorkTimer::hasDependancyInPendingWork):
28+
(JSC::DeferredWorkTimer::didResumeScriptExecutionOwner):
29+
* runtime/DeferredWorkTimer.h:
30+
* runtime/JSFinalizationRegistry.cpp:
31+
(JSC::JSFinalizationRegistry::create):
32+
(JSC::JSFinalizationRegistry::finishCreation):
33+
* runtime/JSFinalizationRegistry.h:
34+
* runtime/JSGlobalObject.cpp:
35+
* runtime/JSGlobalObject.h:
36+
(JSC::JSGlobalObject::currentScriptExecutionOwner):
37+
(JSC::JSGlobalObject::scriptExecutionStatus):
38+
139
2020-10-08 Yusuke Suzuki <[email protected]>
240

341
Unreviewed, reland r268170

Source/JavaScriptCore/jsc.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,8 @@ const GlobalObjectMethodTable GlobalObject::s_globalObjectMethodTable = {
720720
nullptr, // moduleLoaderEvaluate
721721
nullptr, // promiseRejectionTracker
722722
&reportUncaughtExceptionAtEventLoop,
723+
&currentScriptExecutionOwner,
724+
&scriptExecutionStatus,
723725
nullptr, // defaultLanguage
724726
nullptr, // compileStreaming
725727
nullptr, // instantinateStreaming

Source/JavaScriptCore/runtime/DeferredWorkTimer.cpp

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,37 @@ DeferredWorkTimer::DeferredWorkTimer(VM& vm)
4545
void DeferredWorkTimer::doWork(VM& vm)
4646
{
4747
ASSERT(vm.currentThreadIsHoldingAPILock());
48-
m_taskLock.lock();
48+
auto locker = holdLock(m_taskLock);
4949
cancelTimer();
50-
if (!m_runTasks) {
51-
m_taskLock.unlock();
50+
if (!m_runTasks)
5251
return;
53-
}
52+
53+
Vector<std::tuple<Ticket, Task>> suspendedTasks;
5454

5555
while (!m_tasks.isEmpty()) {
5656
auto [ticket, task] = m_tasks.takeFirst();
57+
auto globalObject = ticket->structure(vm)->globalObject();
5758
dataLogLnIf(DeferredWorkTimerInternal::verbose, "Doing work on: ", RawPointer(ticket));
5859

59-
// We may have already canceled these promises.
60-
if (m_pendingTickets.contains(ticket)) {
61-
// Allow tasks we run now to schedule work.
62-
m_currentlyRunningTask = true;
63-
m_taskLock.unlock();
60+
auto pendingTicket = m_pendingTickets.find(ticket);
61+
// We may have already canceled this task or its owner may have been canceled.
62+
if (pendingTicket == m_pendingTickets.end())
63+
continue;
64+
65+
switch (globalObject->globalObjectMethodTable()->scriptExecutionStatus(globalObject, pendingTicket->value.scriptExecutionOwner.get())) {
66+
case ScriptExecutionStatus::Suspended:
67+
suspendedTasks.append(std::make_tuple(ticket, WTFMove(task)));
68+
continue;
69+
case ScriptExecutionStatus::Stopped:
70+
continue;
71+
case ScriptExecutionStatus::Running:
72+
break;
73+
}
74+
75+
// Allow tasks we are about to run to schedule work.
76+
m_currentlyRunningTask = true;
77+
{
78+
auto dropper = DropLockForScope(locker);
6479

6580
// This is the start of a runloop turn, we can release any weakrefs here.
6681
vm.finalizeSynchronousJSExecution();
@@ -75,16 +90,17 @@ void DeferredWorkTimer::doWork(VM& vm)
7590

7691
vm.drainMicrotasks();
7792
ASSERT(!vm.exceptionForInspection());
78-
79-
m_taskLock.lock();
80-
m_currentlyRunningTask = false;
8193
}
94+
m_currentlyRunningTask = false;
8295
}
8396

84-
if (m_pendingTickets.isEmpty() && m_shouldStopRunLoopWhenAllTicketsFinish)
85-
RunLoop::current().stop();
97+
while (!suspendedTasks.isEmpty())
98+
m_tasks.prepend(suspendedTasks.takeLast());
8699

87-
m_taskLock.unlock();
100+
if (m_pendingTickets.isEmpty() && m_shouldStopRunLoopWhenAllTicketsFinish) {
101+
ASSERT(m_tasks.isEmpty());
102+
RunLoop::current().stop();
103+
}
88104
}
89105

90106
void DeferredWorkTimer::runRunLoop()
@@ -102,14 +118,16 @@ void DeferredWorkTimer::addPendingWork(VM& vm, Ticket ticket, Vector<Strong<JSCe
102118
for (unsigned i = 0; i < dependencies.size(); ++i)
103119
ASSERT(dependencies[i].get() != ticket);
104120

105-
auto result = m_pendingTickets.add(ticket, Vector<Strong<JSCell>>());
106-
if (result.isNewEntry) {
121+
auto globalObject = ticket->globalObject();
122+
auto result = m_pendingTickets.ensure(ticket, [&] {
107123
dataLogLnIf(DeferredWorkTimerInternal::verbose, "Adding new pending ticket: ", RawPointer(ticket));
124+
JSObject* scriptExecutionOwner = globalObject->globalObjectMethodTable()->currentScriptExecutionOwner(globalObject);
108125
dependencies.append(Strong<JSCell>(vm, ticket));
109-
result.iterator->value = WTFMove(dependencies);
110-
} else {
126+
return TicketData { WTFMove(dependencies), Strong<JSObject>(vm, scriptExecutionOwner) };
127+
});
128+
if (!result.isNewEntry) {
111129
dataLogLnIf(DeferredWorkTimerInternal::verbose, "Adding new dependencies for ticket: ", RawPointer(ticket));
112-
result.iterator->value.appendVector(dependencies);
130+
result.iterator->value.dependencies.appendVector(WTFMove(dependencies));
113131
}
114132
}
115133

@@ -125,7 +143,7 @@ bool DeferredWorkTimer::hasDependancyInPendingWork(Ticket ticket, JSCell* depend
125143
ASSERT(m_pendingTickets.contains(ticket));
126144

127145
auto result = m_pendingTickets.get(ticket);
128-
return result.contains(dependency);
146+
return result.dependencies.contains(dependency);
129147
}
130148

131149
bool DeferredWorkTimer::cancelPendingWork(Ticket ticket)
@@ -147,4 +165,12 @@ void DeferredWorkTimer::scheduleWorkSoon(Ticket ticket, Task&& task)
147165
setTimeUntilFire(0_s);
148166
}
149167

168+
void DeferredWorkTimer::didResumeScriptExecutionOwner()
169+
{
170+
ASSERT(!m_currentlyRunningTask);
171+
LockHolder locker(m_taskLock);
172+
if (!isScheduled() && m_tasks.size())
173+
setTimeUntilFire(0_s);
174+
}
175+
150176
} // namespace JSC

Source/JavaScriptCore/runtime/DeferredWorkTimer.h

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,19 @@ class JS_EXPORT_PRIVATE DeferredWorkTimer final : public JSRunLoopTimer {
5151
bool hasDependancyInPendingWork(Ticket, JSCell* dependency);
5252
bool cancelPendingWork(Ticket);
5353

54+
// If the script execution owner your ticket is associated with gets canceled
55+
// the Task will not be called and will be deallocated. So it's important
56+
// to make sure your memory ownership model won't leak memory when
57+
// this occurs. The easiest way is to make sure everything is either owned
58+
// by a GC'd value in dependencies or by the Task lambda.
5459
using Task = Function<void()>;
5560
void scheduleWorkSoon(Ticket, Task&&);
61+
void didResumeScriptExecutionOwner();
5662

5763
void stopRunningTasks() { m_runTasks = false; }
58-
5964
void runRunLoop();
6065

61-
static Ref<DeferredWorkTimer> create(VM& vm)
62-
{
63-
return adoptRef(*new DeferredWorkTimer(vm));
64-
}
65-
66+
static Ref<DeferredWorkTimer> create(VM& vm) { return adoptRef(*new DeferredWorkTimer(vm)); }
6667
private:
6768
DeferredWorkTimer(VM&);
6869

@@ -71,7 +72,11 @@ class JS_EXPORT_PRIVATE DeferredWorkTimer final : public JSRunLoopTimer {
7172
bool m_shouldStopRunLoopWhenAllTicketsFinish { false };
7273
bool m_currentlyRunningTask { false };
7374
Deque<std::tuple<Ticket, Task>> m_tasks;
74-
HashMap<Ticket, Vector<Strong<JSCell>>> m_pendingTickets;
75+
struct TicketData {
76+
Vector<Strong<JSCell>> dependencies;
77+
Strong<JSObject> scriptExecutionOwner;
78+
};
79+
HashMap<Ticket, TicketData> m_pendingTickets;
7580
};
7681

7782
} // namespace JSC

Source/JavaScriptCore/runtime/JSFinalizationRegistry.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,22 @@ Structure* JSFinalizationRegistry::createStructure(VM& vm, JSGlobalObject* globa
4242
JSFinalizationRegistry* JSFinalizationRegistry::create(VM& vm, Structure* structure, JSObject* callback)
4343
{
4444
JSFinalizationRegistry* instance = new (NotNull, allocateCell<JSFinalizationRegistry>(vm.heap)) JSFinalizationRegistry(vm, structure);
45-
instance->finishCreation(vm, callback);
45+
instance->finishCreation(vm, structure->globalObject(), callback);
4646
return instance;
4747
}
4848

49-
void JSFinalizationRegistry::finishCreation(VM& vm, JSObject* callback)
49+
void JSFinalizationRegistry::finishCreation(VM& vm, JSGlobalObject* globalObject, JSObject* callback)
5050
{
5151
Base::finishCreation(vm);
5252
ASSERT(callback->isCallable(vm));
5353
auto values = initialValues();
5454
for (unsigned index = 0; index < values.size(); ++index)
5555
Base::internalField(index).setWithoutWriteBarrier(values[index]);
5656
internalField(Field::Callback).setWithoutWriteBarrier(callback);
57+
58+
// Make sure we init the DOM wrapper for our document since it must be allocated before finalizeUnconditionally is called. finalizeUnconditionally,
59+
// is called during the GC flip so no JS objects can be allocated there. This only works because we no longer weakly hold on to DOM wrappers.
60+
globalObject->globalObjectMethodTable()->currentScriptExecutionOwner(globalObject);
5761
}
5862

5963
void JSFinalizationRegistry::visitChildren(JSCell* cell, SlotVisitor& visitor)

Source/JavaScriptCore/runtime/JSFinalizationRegistry.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class JSFinalizationRegistry final : public JSInternalFieldObjectImpl<1> {
8989
{
9090
}
9191

92-
JS_EXPORT_PRIVATE void finishCreation(VM&, JSObject* callback);
92+
JS_EXPORT_PRIVATE void finishCreation(VM&, JSGlobalObject*, JSObject* callback);
9393

9494
static String toStringName(const JSObject*, JSGlobalObject*);
9595

0 commit comments

Comments
 (0)