1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "WorkletThread.h"
9 #include "nsContentUtils.h"
10 #include "nsCycleCollector.h"
11 #include "nsJSEnvironment.h"
12 #include "nsJSPrincipals.h"
13 #include "mozilla/dom/AtomList.h"
14 #include "mozilla/dom/WorkletGlobalScope.h"
15 #include "mozilla/ipc/BackgroundChild.h"
16 #include "mozilla/Attributes.h"
17 #include "mozilla/CycleCollectedJSRuntime.h"
18 #include "mozilla/EventQueue.h"
19 #include "mozilla/ThreadEventQueue.h"
20 #include "js/ContextOptions.h"
21 #include "js/Exception.h"
22 #include "js/Initialization.h"
23 #include "XPCSelfHostedShmem.h"
25 namespace mozilla::dom
{
29 // The size of the worklet runtime heaps in bytes.
30 #define WORKLET_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024
32 // The C stack size. We use the same stack size on all platforms for
34 const uint32_t kWorkletStackSize
= 256 * sizeof(size_t) * 1024;
36 // Half the size of the actual C stack, to be safe.
37 #define WORKLET_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024
41 bool PreserveWrapper(JSContext
* aCx
, JS::Handle
<JSObject
*> aObj
) {
44 MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj
));
45 return mozilla::dom::TryPreserveWrapper(aObj
);
48 JSObject
* Wrap(JSContext
* aCx
, JS::Handle
<JSObject
*> aExisting
,
49 JS::Handle
<JSObject
*> aObj
) {
51 js::Wrapper::Renew(aExisting
, aObj
,
52 &js::OpaqueCrossCompartmentWrapper::singleton
);
55 return js::Wrapper::New(aCx
, aObj
,
56 &js::OpaqueCrossCompartmentWrapper::singleton
);
59 const JSWrapObjectCallbacks WrapObjectCallbacks
= {
66 // This classes control CC in the worklet thread.
68 class WorkletJSRuntime final
: public mozilla::CycleCollectedJSRuntime
{
70 explicit WorkletJSRuntime(JSContext
* aCx
) : CycleCollectedJSRuntime(aCx
) {}
72 ~WorkletJSRuntime() override
= default;
74 virtual void PrepareForForgetSkippable() override
{}
76 virtual void BeginCycleCollectionCallback(
77 mozilla::CCReason aReason
) override
{}
79 virtual void EndCycleCollectionCallback(
80 CycleCollectorResults
& aResults
) override
{}
82 virtual void DispatchDeferredDeletion(bool aContinuation
,
83 bool aPurge
) override
{
84 MOZ_ASSERT(!aContinuation
);
85 nsCycleCollector_doDeferredDeletion();
88 virtual void CustomGCCallback(JSGCStatus aStatus
) override
{
89 // nsCycleCollector_collect() requires a cycle collector but
90 // ~WorkletJSContext calls nsCycleCollector_shutdown() and the base class
91 // destructor will trigger a final GC. The nsCycleCollector_collect()
92 // call can be skipped in this GC as ~CycleCollectedJSContext removes the
93 // context from |this|.
94 if (aStatus
== JSGC_END
&& GetContext()) {
95 nsCycleCollector_collect(CCReason::GC_FINISHED
, nullptr);
100 class WorkletJSContext final
: public CycleCollectedJSContext
{
103 MOZ_ASSERT(!NS_IsMainThread());
105 nsCycleCollector_startup();
108 // MOZ_CAN_RUN_SCRIPT_BOUNDARY because otherwise we have to annotate the
109 // SpiderMonkey JS::JobQueue's destructor as MOZ_CAN_RUN_SCRIPT, which is a
111 MOZ_CAN_RUN_SCRIPT_BOUNDARY
~WorkletJSContext() override
{
112 MOZ_ASSERT(!NS_IsMainThread());
114 JSContext
* cx
= MaybeContext();
116 return; // Initialize() must have failed
119 nsCycleCollector_shutdown();
122 WorkletJSContext
* GetAsWorkletJSContext() override
{ return this; }
124 CycleCollectedJSRuntime
* CreateRuntime(JSContext
* aCx
) override
{
125 return new WorkletJSRuntime(aCx
);
128 nsresult
Initialize(JSRuntime
* aParentRuntime
) {
129 MOZ_ASSERT(!NS_IsMainThread());
131 nsresult rv
= CycleCollectedJSContext::Initialize(
132 aParentRuntime
, WORKLET_DEFAULT_RUNTIME_HEAPSIZE
);
133 if (NS_WARN_IF(NS_FAILED(rv
))) {
137 JSContext
* cx
= Context();
139 js::SetPreserveWrapperCallbacks(cx
, PreserveWrapper
, HasReleasedWrapper
);
140 JS_InitDestroyPrincipalsCallback(cx
, nsJSPrincipals::Destroy
);
141 JS_InitReadPrincipalsCallback(cx
, nsJSPrincipals::ReadPrincipals
);
142 JS_SetWrapObjectCallbacks(cx
, &WrapObjectCallbacks
);
143 JS_SetFutexCanWait(cx
);
148 void DispatchToMicroTask(
149 already_AddRefed
<MicroTaskRunnable
> aRunnable
) override
{
150 RefPtr
<MicroTaskRunnable
> runnable(aRunnable
);
152 MOZ_ASSERT(!NS_IsMainThread());
153 MOZ_ASSERT(runnable
);
155 JSContext
* cx
= Context();
159 JS::Rooted
<JSObject
*> global(cx
, JS::CurrentGlobalOrNull(cx
));
163 JS::JobQueueMayNotBeEmpty(cx
);
164 GetMicroTaskQueue().push_back(std::move(runnable
));
167 bool IsSystemCaller() const override
{
168 // Currently no support for special system worklet privileges.
172 void ReportError(JSErrorReport
* aReport
,
173 JS::ConstUTF8CharsZ aToStringResult
) override
;
175 uint64_t GetCurrentWorkletWindowID() {
176 JSObject
* global
= JS::CurrentGlobalOrNull(Context());
177 if (NS_WARN_IF(!global
)) {
180 nsIGlobalObject
* nativeGlobal
= xpc::NativeGlobal(global
);
181 nsCOMPtr
<WorkletGlobalScope
> workletGlobal
=
182 do_QueryInterface(nativeGlobal
);
183 if (NS_WARN_IF(!workletGlobal
)) {
186 return workletGlobal
->Impl()->LoadInfo().InnerWindowID();
190 void WorkletJSContext::ReportError(JSErrorReport
* aReport
,
191 JS::ConstUTF8CharsZ aToStringResult
) {
192 RefPtr
<xpc::ErrorReport
> xpcReport
= new xpc::ErrorReport();
193 xpcReport
->Init(aReport
, aToStringResult
.c_str(), IsSystemCaller(),
194 GetCurrentWorkletWindowID());
195 RefPtr
<AsyncErrorReporter
> reporter
= new AsyncErrorReporter(xpcReport
);
197 JSContext
* cx
= Context();
198 if (JS_IsExceptionPending(cx
)) {
199 JS::ExceptionStack
exnStack(cx
);
200 if (JS::StealPendingExceptionStack(cx
, &exnStack
)) {
201 JS::Rooted
<JSObject
*> stack(cx
);
202 JS::Rooted
<JSObject
*> stackGlobal(cx
);
203 xpc::FindExceptionStackForConsoleReport(nullptr, exnStack
.exception(),
204 exnStack
.stack(), &stack
,
207 reporter
->SerializeStack(cx
, stack
);
212 NS_DispatchToMainThread(reporter
);
215 // This is the first runnable to be dispatched. It calls the RunEventLoop() so
216 // basically everything happens into this runnable. The reason behind this
217 // approach is that, when the Worklet is terminated, it must not have any JS in
218 // stack, but, because we have CC, nsIThread creates an AutoNoJSAPI object by
219 // default. Using this runnable, CC exists only into it.
220 class WorkletThread::PrimaryRunnable final
: public Runnable
{
222 explicit PrimaryRunnable(WorkletThread
* aWorkletThread
)
223 : Runnable("WorkletThread::PrimaryRunnable"),
224 mWorkletThread(aWorkletThread
) {
225 MOZ_ASSERT(aWorkletThread
);
226 MOZ_ASSERT(NS_IsMainThread());
231 mWorkletThread
->RunEventLoop();
236 RefPtr
<WorkletThread
> mWorkletThread
;
239 // This is the last runnable to be dispatched. It calls the TerminateInternal()
240 class WorkletThread::TerminateRunnable final
: public Runnable
{
242 explicit TerminateRunnable(WorkletThread
* aWorkletThread
)
243 : Runnable("WorkletThread::TerminateRunnable"),
244 mWorkletThread(aWorkletThread
) {
245 MOZ_ASSERT(aWorkletThread
);
246 MOZ_ASSERT(NS_IsMainThread());
251 mWorkletThread
->TerminateInternal();
256 RefPtr
<WorkletThread
> mWorkletThread
;
259 WorkletThread::WorkletThread(WorkletImpl
* aWorkletImpl
)
261 MakeNotNull
<ThreadEventQueue
*>(MakeUnique
<mozilla::EventQueue
>()),
262 nsThread::NOT_MAIN_THREAD
, {.stackSize
= kWorkletStackSize
}),
263 mWorkletImpl(aWorkletImpl
),
265 mIsTerminating(false) {
266 MOZ_ASSERT(NS_IsMainThread());
267 nsContentUtils::RegisterShutdownObserver(this);
270 WorkletThread::~WorkletThread() = default;
273 already_AddRefed
<WorkletThread
> WorkletThread::Create(
274 WorkletImpl
* aWorkletImpl
) {
275 RefPtr
<WorkletThread
> thread
= new WorkletThread(aWorkletImpl
);
276 if (NS_WARN_IF(NS_FAILED(thread
->Init("DOM Worklet"_ns
)))) {
280 RefPtr
<PrimaryRunnable
> runnable
= new PrimaryRunnable(thread
);
281 if (NS_WARN_IF(NS_FAILED(thread
->DispatchRunnable(runnable
.forget())))) {
285 return thread
.forget();
288 nsresult
WorkletThread::DispatchRunnable(
289 already_AddRefed
<nsIRunnable
> aRunnable
) {
290 nsCOMPtr
<nsIRunnable
> runnable(aRunnable
);
291 return nsThread::Dispatch(runnable
.forget(), NS_DISPATCH_NORMAL
);
295 WorkletThread::DispatchFromScript(nsIRunnable
* aRunnable
, uint32_t aFlags
) {
296 nsCOMPtr
<nsIRunnable
> runnable(aRunnable
);
297 return Dispatch(runnable
.forget(), aFlags
);
301 WorkletThread::Dispatch(already_AddRefed
<nsIRunnable
> aRunnable
,
303 nsCOMPtr
<nsIRunnable
> runnable(aRunnable
);
305 // Worklet only supports asynchronous dispatch.
306 if (NS_WARN_IF(aFlags
!= NS_DISPATCH_NORMAL
)) {
307 return NS_ERROR_UNEXPECTED
;
310 return nsThread::Dispatch(runnable
.forget(), NS_DISPATCH_NORMAL
);
314 WorkletThread::DelayedDispatch(already_AddRefed
<nsIRunnable
>, uint32_t aFlags
) {
315 return NS_ERROR_NOT_IMPLEMENTED
;
318 static bool DispatchToEventLoop(void* aClosure
,
319 JS::Dispatchable
* aDispatchable
) {
320 // This callback may execute either on the worklet thread or a random
321 // JS-internal helper thread.
323 // See comment at JS::InitDispatchToEventLoop() below for how we know the
325 nsIThread
* thread
= static_cast<nsIThread
*>(aClosure
);
327 nsresult rv
= thread
->Dispatch(
328 NS_NewRunnableFunction(
329 "WorkletThread::DispatchToEventLoop",
331 CycleCollectedJSContext
* ccjscx
= CycleCollectedJSContext::Get();
336 WorkletJSContext
* wjc
= ccjscx
->GetAsWorkletJSContext();
341 aDispatchable
->run(wjc
->Context(),
342 JS::Dispatchable::NotShuttingDown
);
346 return NS_SUCCEEDED(rv
);
350 void WorkletThread::EnsureCycleCollectedJSContext(
351 JSRuntime
* aParentRuntime
, const JS::ContextOptions
& aOptions
) {
352 CycleCollectedJSContext
* ccjscx
= CycleCollectedJSContext::Get();
354 MOZ_ASSERT(ccjscx
->GetAsWorkletJSContext());
358 WorkletJSContext
* context
= new WorkletJSContext();
359 nsresult rv
= context
->Initialize(aParentRuntime
);
360 if (NS_WARN_IF(NS_FAILED(rv
))) {
361 // TODO: error propagation
365 JS::ContextOptionsRef(context
->Context()) = aOptions
;
367 JS_SetGCParameter(context
->Context(), JSGC_MAX_BYTES
, uint32_t(-1));
369 // FIXME: JS_SetDefaultLocale
371 // FIXME: JS_SetSecurityCallbacks
372 // FIXME: JS::SetAsyncTaskCallbacks
373 // FIXME: JS::SetCTypesActivityCallback
374 // FIXME: JS_SetGCZeal
376 // A thread lives strictly longer than its JSRuntime so we can safely
377 // store a raw pointer as the callback's closure argument on the JSRuntime.
378 JS::InitDispatchToEventLoop(context
->Context(), DispatchToEventLoop
,
379 NS_GetCurrentThread());
381 JS_SetNativeStackQuota(context
->Context(),
382 WORKLET_CONTEXT_NATIVE_STACK_LIMIT
);
384 // When available, set the self-hosted shared memory to be read, so that we
385 // can decode the self-hosted content instead of parsing it.
386 auto& shm
= xpc::SelfHostedShmem::GetSingleton();
387 JS::SelfHostedCache selfHostedContent
= shm
.Content();
389 if (!JS::InitSelfHostedCode(context
->Context(), selfHostedContent
)) {
390 // TODO: error propagation
395 void WorkletThread::RunEventLoop() {
396 MOZ_ASSERT(!NS_IsMainThread());
398 PR_SetCurrentThreadName("worklet");
401 MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(this, /* wait: */ true));
404 DeleteCycleCollectedJSContext();
407 void WorkletThread::Terminate() {
408 MOZ_ASSERT(NS_IsMainThread());
410 if (mIsTerminating
) {
411 // nsThread::Dispatch() would leak the runnable if the event queue is no
412 // longer accepting runnables.
416 mIsTerminating
= true;
418 nsContentUtils::UnregisterShutdownObserver(this);
420 RefPtr
<TerminateRunnable
> runnable
= new TerminateRunnable(this);
421 DispatchRunnable(runnable
.forget());
424 void WorkletThread::TerminateInternal() {
425 MOZ_ASSERT(!CycleCollectedJSContext::Get() || IsOnWorkletThread());
429 nsCOMPtr
<nsIRunnable
> runnable
= NewRunnableMethod(
430 "WorkletThread::Shutdown", this, &WorkletThread::Shutdown
);
431 NS_DispatchToMainThread(runnable
);
435 void WorkletThread::DeleteCycleCollectedJSContext() {
436 CycleCollectedJSContext
* ccjscx
= CycleCollectedJSContext::Get();
441 // Release any MessagePort kept alive by its ipc actor.
442 mozilla::ipc::BackgroundChild::CloseForCurrentThread();
444 WorkletJSContext
* workletjscx
= ccjscx
->GetAsWorkletJSContext();
445 MOZ_ASSERT(workletjscx
);
450 bool WorkletThread::IsOnWorkletThread() {
451 CycleCollectedJSContext
* ccjscx
= CycleCollectedJSContext::Get();
452 return ccjscx
&& ccjscx
->GetAsWorkletJSContext();
456 void WorkletThread::AssertIsOnWorkletThread() {
457 MOZ_ASSERT(IsOnWorkletThread());
462 WorkletThread::Observe(nsISupports
* aSubject
, const char* aTopic
,
464 MOZ_ASSERT(strcmp(aTopic
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
) == 0);
466 // The WorkletImpl will terminate the worklet thread after sending a message
467 // to release worklet thread objects.
468 mWorkletImpl
->NotifyWorkletFinished();
472 NS_IMPL_ISUPPORTS_INHERITED(WorkletThread
, nsThread
, nsIObserver
)
474 } // namespace mozilla::dom