Bug 1882465 - Update .hg-annotate-ignore-revs and .git-blame-ignore-revs to reflect...
[gecko.git] / dom / worklet / WorkletThread.cpp
blobc5ad7070b074ecf53218891b9ce42f8a73e72288
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"
8 #include "prthread.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 {
27 namespace {
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
33 // consistency.
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
39 // Helper functions
41 bool PreserveWrapper(JSContext* aCx, JS::Handle<JSObject*> aObj) {
42 MOZ_ASSERT(aCx);
43 MOZ_ASSERT(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) {
50 if (aExisting) {
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 = {
60 Wrap,
61 nullptr,
64 } // namespace
66 // This classes control CC in the worklet thread.
68 class WorkletJSRuntime final : public mozilla::CycleCollectedJSRuntime {
69 public:
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 {
101 public:
102 WorkletJSContext() {
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
110 // bit of a pain.
111 MOZ_CAN_RUN_SCRIPT_BOUNDARY ~WorkletJSContext() override {
112 MOZ_ASSERT(!NS_IsMainThread());
114 JSContext* cx = MaybeContext();
115 if (!cx) {
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))) {
134 return 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);
145 return NS_OK;
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();
156 MOZ_ASSERT(cx);
158 #ifdef DEBUG
159 JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
160 MOZ_ASSERT(global);
161 #endif
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.
169 return false;
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)) {
178 return 0;
180 nsIGlobalObject* nativeGlobal = xpc::NativeGlobal(global);
181 nsCOMPtr<WorkletGlobalScope> workletGlobal =
182 do_QueryInterface(nativeGlobal);
183 if (NS_WARN_IF(!workletGlobal)) {
184 return 0;
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,
205 &stackGlobal);
206 if (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 {
221 public:
222 explicit PrimaryRunnable(WorkletThread* aWorkletThread)
223 : Runnable("WorkletThread::PrimaryRunnable"),
224 mWorkletThread(aWorkletThread) {
225 MOZ_ASSERT(aWorkletThread);
226 MOZ_ASSERT(NS_IsMainThread());
229 NS_IMETHOD
230 Run() override {
231 mWorkletThread->RunEventLoop();
232 return NS_OK;
235 private:
236 RefPtr<WorkletThread> mWorkletThread;
239 // This is the last runnable to be dispatched. It calls the TerminateInternal()
240 class WorkletThread::TerminateRunnable final : public Runnable {
241 public:
242 explicit TerminateRunnable(WorkletThread* aWorkletThread)
243 : Runnable("WorkletThread::TerminateRunnable"),
244 mWorkletThread(aWorkletThread) {
245 MOZ_ASSERT(aWorkletThread);
246 MOZ_ASSERT(NS_IsMainThread());
249 NS_IMETHOD
250 Run() override {
251 mWorkletThread->TerminateInternal();
252 return NS_OK;
255 private:
256 RefPtr<WorkletThread> mWorkletThread;
259 WorkletThread::WorkletThread(WorkletImpl* aWorkletImpl)
260 : nsThread(
261 MakeNotNull<ThreadEventQueue*>(MakeUnique<mozilla::EventQueue>()),
262 nsThread::NOT_MAIN_THREAD, {.stackSize = kWorkletStackSize}),
263 mWorkletImpl(aWorkletImpl),
264 mExitLoop(false),
265 mIsTerminating(false) {
266 MOZ_ASSERT(NS_IsMainThread());
267 nsContentUtils::RegisterShutdownObserver(this);
270 WorkletThread::~WorkletThread() = default;
272 // static
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)))) {
277 return nullptr;
280 RefPtr<PrimaryRunnable> runnable = new PrimaryRunnable(thread);
281 if (NS_WARN_IF(NS_FAILED(thread->DispatchRunnable(runnable.forget())))) {
282 return nullptr;
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);
294 NS_IMETHODIMP
295 WorkletThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) {
296 nsCOMPtr<nsIRunnable> runnable(aRunnable);
297 return Dispatch(runnable.forget(), aFlags);
300 NS_IMETHODIMP
301 WorkletThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable,
302 uint32_t aFlags) {
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);
313 NS_IMETHODIMP
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
324 // thread is alive.
325 nsIThread* thread = static_cast<nsIThread*>(aClosure);
327 nsresult rv = thread->Dispatch(
328 NS_NewRunnableFunction(
329 "WorkletThread::DispatchToEventLoop",
330 [aDispatchable]() {
331 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
332 if (!ccjscx) {
333 return;
336 WorkletJSContext* wjc = ccjscx->GetAsWorkletJSContext();
337 if (!wjc) {
338 return;
341 aDispatchable->run(wjc->Context(),
342 JS::Dispatchable::NotShuttingDown);
344 NS_DISPATCH_NORMAL);
346 return NS_SUCCEEDED(rv);
349 // static
350 void WorkletThread::EnsureCycleCollectedJSContext(
351 JSRuntime* aParentRuntime, const JS::ContextOptions& aOptions) {
352 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
353 if (ccjscx) {
354 MOZ_ASSERT(ccjscx->GetAsWorkletJSContext());
355 return;
358 WorkletJSContext* context = new WorkletJSContext();
359 nsresult rv = context->Initialize(aParentRuntime);
360 if (NS_WARN_IF(NS_FAILED(rv))) {
361 // TODO: error propagation
362 return;
365 JS::ContextOptionsRef(context->Context()) = aOptions;
367 JS_SetGCParameter(context->Context(), JSGC_MAX_BYTES, uint32_t(-1));
369 // FIXME: JS_SetDefaultLocale
370 // FIXME: JSSettings
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
391 return;
395 void WorkletThread::RunEventLoop() {
396 MOZ_ASSERT(!NS_IsMainThread());
398 PR_SetCurrentThreadName("worklet");
400 while (!mExitLoop) {
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.
413 return;
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());
427 mExitLoop = true;
429 nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
430 "WorkletThread::Shutdown", this, &WorkletThread::Shutdown);
431 NS_DispatchToMainThread(runnable);
434 /* static */
435 void WorkletThread::DeleteCycleCollectedJSContext() {
436 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
437 if (!ccjscx) {
438 return;
441 // Release any MessagePort kept alive by its ipc actor.
442 mozilla::ipc::BackgroundChild::CloseForCurrentThread();
444 WorkletJSContext* workletjscx = ccjscx->GetAsWorkletJSContext();
445 MOZ_ASSERT(workletjscx);
446 delete workletjscx;
449 /* static */
450 bool WorkletThread::IsOnWorkletThread() {
451 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
452 return ccjscx && ccjscx->GetAsWorkletJSContext();
455 /* static */
456 void WorkletThread::AssertIsOnWorkletThread() {
457 MOZ_ASSERT(IsOnWorkletThread());
460 // nsIObserver
461 NS_IMETHODIMP
462 WorkletThread::Observe(nsISupports* aSubject, const char* aTopic,
463 const char16_t*) {
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();
469 return NS_OK;
472 NS_IMPL_ISUPPORTS_INHERITED(WorkletThread, nsThread, nsIObserver)
474 } // namespace mozilla::dom