/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "WorkletThread.h" #include "prthread.h" #include "nsContentUtils.h" #include "nsCycleCollector.h" #include "nsJSEnvironment.h" #include "mozilla/dom/AtomList.h" #include "mozilla/dom/WorkletGlobalScope.h" #include "mozilla/dom/WorkletPrincipals.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/Attributes.h" #include "mozilla/CycleCollectedJSRuntime.h" #include "mozilla/EventQueue.h" #include "mozilla/ThreadEventQueue.h" #include "js/Exception.h" namespace mozilla { namespace dom { namespace { // The size of the worklet runtime heaps in bytes. #define WORKLET_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024 // The C stack size. We use the same stack size on all platforms for // consistency. const uint32_t kWorkletStackSize = 256 * sizeof(size_t) * 1024; // Half the size of the actual C stack, to be safe. #define WORKLET_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024 // Helper functions bool PreserveWrapper(JSContext* aCx, JS::HandleObject aObj) { MOZ_ASSERT(aCx); MOZ_ASSERT(aObj); MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj)); return mozilla::dom::TryPreserveWrapper(aObj); } JSObject* Wrap(JSContext* aCx, JS::HandleObject aExisting, JS::HandleObject aObj) { if (aExisting) { js::Wrapper::Renew(aExisting, aObj, &js::OpaqueCrossCompartmentWrapper::singleton); } return js::Wrapper::New(aCx, aObj, &js::OpaqueCrossCompartmentWrapper::singleton); } const JSWrapObjectCallbacks WrapObjectCallbacks = { Wrap, nullptr, }; } // namespace // This classes control CC in the worklet thread. class WorkletJSRuntime final : public mozilla::CycleCollectedJSRuntime { public: explicit WorkletJSRuntime(JSContext* aCx) : CycleCollectedJSRuntime(aCx) {} ~WorkletJSRuntime() override = default; virtual void PrepareForForgetSkippable() override {} virtual void BeginCycleCollectionCallback() override {} virtual void EndCycleCollectionCallback( CycleCollectorResults& aResults) override {} virtual void DispatchDeferredDeletion(bool aContinuation, bool aPurge) override { MOZ_ASSERT(!aContinuation); nsCycleCollector_doDeferredDeletion(); } virtual void CustomGCCallback(JSGCStatus aStatus) override { // nsCycleCollector_collect() requires a cycle collector but // ~WorkletJSContext calls nsCycleCollector_shutdown() and the base class // destructor will trigger a final GC. The nsCycleCollector_collect() // call can be skipped in this GC as ~CycleCollectedJSContext removes the // context from |this|. if (aStatus == JSGC_END && GetContext()) { nsCycleCollector_collect(nullptr); } } }; class WorkletJSContext final : public CycleCollectedJSContext { public: WorkletJSContext() { MOZ_ASSERT(!NS_IsMainThread()); nsCycleCollector_startup(); } // MOZ_CAN_RUN_SCRIPT_BOUNDARY because otherwise we have to annotate the // SpiderMonkey JS::JobQueue's destructor as MOZ_CAN_RUN_SCRIPT, which is a // bit of a pain. MOZ_CAN_RUN_SCRIPT_BOUNDARY ~WorkletJSContext() override { MOZ_ASSERT(!NS_IsMainThread()); JSContext* cx = MaybeContext(); if (!cx) { return; // Initialize() must have failed } nsCycleCollector_shutdown(); } WorkletJSContext* GetAsWorkletJSContext() override { return this; } CycleCollectedJSRuntime* CreateRuntime(JSContext* aCx) override { return new WorkletJSRuntime(aCx); } nsresult Initialize(JSRuntime* aParentRuntime) { MOZ_ASSERT(!NS_IsMainThread()); nsresult rv = CycleCollectedJSContext::Initialize( aParentRuntime, WORKLET_DEFAULT_RUNTIME_HEAPSIZE); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } JSContext* cx = Context(); js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper); JS_InitDestroyPrincipalsCallback(cx, WorkletPrincipals::Destroy); JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); JS_SetFutexCanWait(cx); return NS_OK; } void DispatchToMicroTask( already_AddRefed aRunnable) override { RefPtr runnable(aRunnable); MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(runnable); JSContext* cx = Context(); MOZ_ASSERT(cx); #ifdef DEBUG JS::Rooted global(cx, JS::CurrentGlobalOrNull(cx)); MOZ_ASSERT(global); #endif JS::JobQueueMayNotBeEmpty(cx); GetMicroTaskQueue().push(std::move(runnable)); } bool IsSystemCaller() const override { // Currently no support for special system worklet privileges. return false; } void ReportError(JSErrorReport* aReport, JS::ConstUTF8CharsZ aToStringResult) override; uint64_t GetCurrentWorkletWindowID() { JSObject* global = JS::CurrentGlobalOrNull(Context()); if (NS_WARN_IF(!global)) { return 0; } nsIGlobalObject* nativeGlobal = xpc::NativeGlobal(global); nsCOMPtr workletGlobal = do_QueryInterface(nativeGlobal); if (NS_WARN_IF(!workletGlobal)) { return 0; } return workletGlobal->Impl()->LoadInfo().InnerWindowID(); } }; void WorkletJSContext::ReportError(JSErrorReport* aReport, JS::ConstUTF8CharsZ aToStringResult) { RefPtr xpcReport = new xpc::ErrorReport(); xpcReport->Init(aReport, aToStringResult.c_str(), IsSystemCaller(), GetCurrentWorkletWindowID()); RefPtr reporter = new AsyncErrorReporter(xpcReport); JSContext* cx = Context(); if (JS_IsExceptionPending(cx)) { JS::ExceptionStack exnStack(cx); if (JS::StealPendingExceptionStack(cx, &exnStack)) { JS::Rooted stack(cx); JS::Rooted stackGlobal(cx); xpc::FindExceptionStackForConsoleReport(nullptr, exnStack.exception(), exnStack.stack(), &stack, &stackGlobal); if (stack) { reporter->SerializeStack(cx, stack); } } } NS_DispatchToMainThread(reporter); } // This is the first runnable to be dispatched. It calls the RunEventLoop() so // basically everything happens into this runnable. The reason behind this // approach is that, when the Worklet is terminated, it must not have any JS in // stack, but, because we have CC, nsIThread creates an AutoNoJSAPI object by // default. Using this runnable, CC exists only into it. class WorkletThread::PrimaryRunnable final : public Runnable { public: explicit PrimaryRunnable(WorkletThread* aWorkletThread) : Runnable("WorkletThread::PrimaryRunnable"), mWorkletThread(aWorkletThread) { MOZ_ASSERT(aWorkletThread); MOZ_ASSERT(NS_IsMainThread()); } NS_IMETHOD Run() override { mWorkletThread->RunEventLoop(); return NS_OK; } private: RefPtr mWorkletThread; }; // This is the last runnable to be dispatched. It calls the TerminateInternal() class WorkletThread::TerminateRunnable final : public Runnable { public: explicit TerminateRunnable(WorkletThread* aWorkletThread) : Runnable("WorkletThread::TerminateRunnable"), mWorkletThread(aWorkletThread) { MOZ_ASSERT(aWorkletThread); MOZ_ASSERT(NS_IsMainThread()); } NS_IMETHOD Run() override { mWorkletThread->TerminateInternal(); return NS_OK; } private: RefPtr mWorkletThread; }; WorkletThread::WorkletThread(WorkletImpl* aWorkletImpl) : nsThread( MakeNotNull(MakeUnique()), nsThread::NOT_MAIN_THREAD, kWorkletStackSize), mWorkletImpl(aWorkletImpl), mExitLoop(false), mIsTerminating(false) { MOZ_ASSERT(NS_IsMainThread()); nsContentUtils::RegisterShutdownObserver(this); } WorkletThread::~WorkletThread() = default; // static already_AddRefed WorkletThread::Create( WorkletImpl* aWorkletImpl) { RefPtr thread = new WorkletThread(aWorkletImpl); if (NS_WARN_IF(NS_FAILED(thread->Init("DOM Worklet"_ns)))) { return nullptr; } RefPtr runnable = new PrimaryRunnable(thread); if (NS_WARN_IF(NS_FAILED(thread->DispatchRunnable(runnable.forget())))) { return nullptr; } return thread.forget(); } nsresult WorkletThread::DispatchRunnable( already_AddRefed aRunnable) { nsCOMPtr runnable(aRunnable); return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); } NS_IMETHODIMP WorkletThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) { nsCOMPtr runnable(aRunnable); return Dispatch(runnable.forget(), aFlags); } NS_IMETHODIMP WorkletThread::Dispatch(already_AddRefed aRunnable, uint32_t aFlags) { nsCOMPtr runnable(aRunnable); // Worklet only supports asynchronous dispatch. if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { return NS_ERROR_UNEXPECTED; } return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); } NS_IMETHODIMP WorkletThread::DelayedDispatch(already_AddRefed, uint32_t aFlags) { return NS_ERROR_NOT_IMPLEMENTED; } static bool DispatchToEventLoop(void* aClosure, JS::Dispatchable* aDispatchable) { // This callback may execute either on the worklet thread or a random // JS-internal helper thread. // See comment at JS::InitDispatchToEventLoop() below for how we know the // WorkletThread is alive. WorkletThread* workletThread = reinterpret_cast(aClosure); nsresult rv = workletThread->DispatchRunnable(NS_NewRunnableFunction( "WorkletThread::DispatchToEventLoop", [aDispatchable]() { CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); if (!ccjscx) { return; } WorkletJSContext* wjc = ccjscx->GetAsWorkletJSContext(); if (!wjc) { return; } aDispatchable->run(wjc->Context(), JS::Dispatchable::NotShuttingDown); })); return NS_SUCCEEDED(rv); } void WorkletThread::EnsureCycleCollectedJSContext(JSRuntime* aParentRuntime) { CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); if (ccjscx) { MOZ_ASSERT(ccjscx->GetAsWorkletJSContext()); return; } WorkletJSContext* context = new WorkletJSContext(); nsresult rv = context->Initialize(aParentRuntime); if (NS_WARN_IF(NS_FAILED(rv))) { // TODO: error propagation return; } JS_SetGCParameter(context->Context(), JSGC_MAX_BYTES, uint32_t(-1)); // FIXME: JS_SetDefaultLocale // FIXME: JSSettings // FIXME: JS_SetSecurityCallbacks // FIXME: JS::SetAsyncTaskCallbacks // FIXME: JS::SetCTypesActivityCallback // FIXME: JS_SetGCZeal // A WorkletThread lives strictly longer than its JSRuntime so we can safely // store a raw pointer as the callback's closure argument on the JSRuntime. JS::InitDispatchToEventLoop(context->Context(), DispatchToEventLoop, (void*)this); JS_SetNativeStackQuota(context->Context(), WORKLET_CONTEXT_NATIVE_STACK_LIMIT); if (!JS::InitSelfHostedCode(context->Context())) { // TODO: error propagation return; } } void WorkletThread::RunEventLoop() { MOZ_ASSERT(!NS_IsMainThread()); PR_SetCurrentThreadName("worklet"); while (!mExitLoop) { MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(this, /* wait: */ true)); } DeleteCycleCollectedJSContext(); } void WorkletThread::Terminate() { MOZ_ASSERT(NS_IsMainThread()); if (mIsTerminating) { // nsThread::Dispatch() would leak the runnable if the event queue is no // longer accepting runnables. return; } mIsTerminating = true; nsContentUtils::UnregisterShutdownObserver(this); RefPtr runnable = new TerminateRunnable(this); DispatchRunnable(runnable.forget()); } void WorkletThread::TerminateInternal() { MOZ_ASSERT(!CycleCollectedJSContext::Get() || IsOnWorkletThread()); mExitLoop = true; nsCOMPtr runnable = NewRunnableMethod( "WorkletThread::Shutdown", this, &WorkletThread::Shutdown); NS_DispatchToMainThread(runnable); } /* static */ void WorkletThread::DeleteCycleCollectedJSContext() { CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); if (!ccjscx) { return; } // Release any MessagePort kept alive by its ipc actor. mozilla::ipc::BackgroundChild::CloseForCurrentThread(); WorkletJSContext* workletjscx = ccjscx->GetAsWorkletJSContext(); MOZ_ASSERT(workletjscx); delete workletjscx; } /* static */ bool WorkletThread::IsOnWorkletThread() { CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); return ccjscx && ccjscx->GetAsWorkletJSContext(); } /* static */ void WorkletThread::AssertIsOnWorkletThread() { MOZ_ASSERT(IsOnWorkletThread()); } // nsIObserver NS_IMETHODIMP WorkletThread::Observe(nsISupports* aSubject, const char* aTopic, const char16_t*) { MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0); // The WorkletImpl will terminate the worklet thread after sending a message // to release worklet thread objects. mWorkletImpl->NotifyWorkletFinished(); return NS_OK; } NS_IMPL_ISUPPORTS_INHERITED(WorkletThread, nsThread, nsIObserver) } // namespace dom } // namespace mozilla