forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			454 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			454 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* -*- 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 "mozilla/dom/AtomList.h"
 | |
| #include "mozilla/ipc/BackgroundChild.h"
 | |
| #include "mozilla/Attributes.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<MicroTaskRunnable> aRunnable) override {
 | |
|     RefPtr<MicroTaskRunnable> runnable(aRunnable);
 | |
| 
 | |
|     MOZ_ASSERT(!NS_IsMainThread());
 | |
|     MOZ_ASSERT(runnable);
 | |
| 
 | |
|     JSContext* cx = Context();
 | |
|     MOZ_ASSERT(cx);
 | |
| 
 | |
| #ifdef DEBUG
 | |
|     JS::Rooted<JSObject*> 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<WorkletGlobalScope> 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<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
 | |
|   xpcReport->Init(aReport, aToStringResult.c_str(), IsSystemCaller(),
 | |
|                   GetCurrentWorkletWindowID());
 | |
|   RefPtr<AsyncErrorReporter> reporter = new AsyncErrorReporter(xpcReport);
 | |
| 
 | |
|   JSContext* cx = Context();
 | |
|   if (JS_IsExceptionPending(cx)) {
 | |
|     JS::ExceptionStack exnStack(cx);
 | |
|     if (JS::StealPendingExceptionStack(cx, &exnStack)) {
 | |
|       JS::Rooted<JSObject*> stack(cx);
 | |
|       JS::Rooted<JSObject*> 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<WorkletThread> 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<WorkletThread> mWorkletThread;
 | |
| };
 | |
| 
 | |
| WorkletThread::WorkletThread(WorkletImpl* aWorkletImpl)
 | |
|     : nsThread(MakeNotNull<ThreadEventQueue<mozilla::EventQueue>*>(
 | |
|                    MakeUnique<mozilla::EventQueue>()),
 | |
|                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> WorkletThread::Create(
 | |
|     WorkletImpl* aWorkletImpl) {
 | |
|   RefPtr<WorkletThread> thread = new WorkletThread(aWorkletImpl);
 | |
|   if (NS_WARN_IF(NS_FAILED(thread->Init("DOM Worklet"_ns)))) {
 | |
|     return nullptr;
 | |
|   }
 | |
| 
 | |
|   RefPtr<PrimaryRunnable> runnable = new PrimaryRunnable(thread);
 | |
|   if (NS_WARN_IF(NS_FAILED(thread->DispatchRunnable(runnable.forget())))) {
 | |
|     return nullptr;
 | |
|   }
 | |
| 
 | |
|   return thread.forget();
 | |
| }
 | |
| 
 | |
| nsresult WorkletThread::DispatchRunnable(
 | |
|     already_AddRefed<nsIRunnable> aRunnable) {
 | |
|   nsCOMPtr<nsIRunnable> runnable(aRunnable);
 | |
|   return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
 | |
| }
 | |
| 
 | |
| NS_IMETHODIMP
 | |
| WorkletThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) {
 | |
|   nsCOMPtr<nsIRunnable> runnable(aRunnable);
 | |
|   return Dispatch(runnable.forget(), aFlags);
 | |
| }
 | |
| 
 | |
| NS_IMETHODIMP
 | |
| WorkletThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable,
 | |
|                         uint32_t aFlags) {
 | |
|   nsCOMPtr<nsIRunnable> 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<nsIRunnable>, 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<WorkletThread*>(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<TerminateRunnable> runnable = new TerminateRunnable(this);
 | |
|   DispatchRunnable(runnable.forget());
 | |
| }
 | |
| 
 | |
| void WorkletThread::TerminateInternal() {
 | |
|   MOZ_ASSERT(!CycleCollectedJSContext::Get() || IsOnWorkletThread());
 | |
| 
 | |
|   mExitLoop = true;
 | |
| 
 | |
|   nsCOMPtr<nsIRunnable> 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
 | 
