gecko-dev/dom/worklet/WorkletThread.cpp

496 строки
12 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/EventQueue.h"
#include "mozilla/ThreadEventQueue.h"
namespace mozilla {
namespace dom {
namespace {
// The size of the worklet runtime heaps in bytes.
#define WORKLET_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024
// The size of the generational GC nursery for worklet, in bytes.
#define WORKLET_DEFAULT_NURSERY_SIZE 1 * 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;
// This class is allocated per thread and can be retrieved from CC.
// It's used to get the current WorkletThread object.
class WorkletThreadContextPrivate : private PerThreadAtomCache
{
public:
explicit
WorkletThreadContextPrivate(WorkletThread* aWorkletThread)
: mWorkletThread(aWorkletThread)
{
MOZ_ASSERT(!NS_IsMainThread());
// Zero out the base class members.
memset(this, 0, sizeof(PerThreadAtomCache));
MOZ_ASSERT(mWorkletThread);
}
~WorkletThreadContextPrivate()
{
MOZ_ASSERT(!NS_IsMainThread());
}
WorkletThread*
GetWorkletThread() const
{
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWorkletThread);
return mWorkletThread;
}
private:
WorkletThreadContextPrivate(const WorkletThreadContextPrivate&) = delete;
WorkletThreadContextPrivate& operator=(const WorkletThreadContextPrivate&) = delete;
RefPtr<WorkletThread> mWorkletThread;
};
// Helper functions
bool
PreserveWrapper(JSContext* aCx, JSObject* aObj)
{
MOZ_ASSERT(aCx);
MOZ_ASSERT(aObj);
MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj));
return mozilla::dom::TryPreserveWrapper(aObj);
}
void
DestroyWorkletPrincipals(JSPrincipals* aPrincipals)
{
MOZ_ASSERT_UNREACHABLE("Worklet principals refcount should never fall below one");
}
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 && !Contexts().isEmpty()) {
nsCycleCollector_collect(nullptr);
}
}
};
class WorkletJSContext final : public CycleCollectedJSContext
{
public:
explicit WorkletJSContext(WorkletThread* aWorkletThread)
: mWorkletThread(aWorkletThread)
{
MOZ_ASSERT(aWorkletThread);
MOZ_ASSERT(!NS_IsMainThread());
nsCycleCollector_startup();
}
~WorkletJSContext() override
{
MOZ_ASSERT(!NS_IsMainThread());
JSContext* cx = MaybeContext();
if (!cx) {
return; // Initialize() must have failed
}
delete static_cast<WorkletThreadContextPrivate*>(JS_GetContextPrivate(cx));
JS_SetContextPrivate(cx, nullptr);
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,
WORKLET_DEFAULT_NURSERY_SIZE);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
JSContext* cx = Context();
JS_SetContextPrivate(cx, new WorkletThreadContextPrivate(mWorkletThread));
js::SetPreserveWrapperCallback(cx, PreserveWrapper);
JS_InitDestroyPrincipalsCallback(cx, DestroyWorkletPrincipals);
JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks);
JS_SetFutexCanWait(cx);
return NS_OK;
}
void
DispatchToMicroTask(already_AddRefed<MicroTaskRunnable> aRunnable) override
{
RefPtr<MicroTaskRunnable> runnable(aRunnable);
#ifdef DEBUG
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(runnable);
WorkletThread* workletThread = WorkletThread::Get();
MOZ_ASSERT(workletThread);
JSContext* cx = workletThread->GetJSContext();
MOZ_ASSERT(cx);
JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
MOZ_ASSERT(global);
#endif
GetMicroTaskQueue().push(runnable.forget());
}
bool IsSystemCaller() const override
{
// Currently no support for special system worklet privileges.
return false;
}
private:
RefPtr<WorkletThread> mWorkletThread;
};
// 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());
mParentRuntime =
JS_GetParentRuntime(CycleCollectedJSContext::Get()->Context());
MOZ_ASSERT(mParentRuntime);
}
NS_IMETHOD
Run() override
{
mWorkletThread->RunEventLoop(mParentRuntime);
return NS_OK;
}
private:
RefPtr<WorkletThread> mWorkletThread;
JSRuntime* mParentRuntime;
};
// 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(const WorkletLoadInfo& aWorkletLoadInfo)
: nsThread(MakeNotNull<ThreadEventQueue<mozilla::EventQueue>*>(
MakeUnique<mozilla::EventQueue>()),
nsThread::NOT_MAIN_THREAD, kWorkletStackSize)
, mWorkletLoadInfo(aWorkletLoadInfo)
, mCreationTimeStamp(TimeStamp::Now())
, mJSContext(nullptr)
, mIsTerminating(false)
{
MOZ_ASSERT(NS_IsMainThread());
nsContentUtils::RegisterShutdownObserver(this);
}
WorkletThread::~WorkletThread()
{
// This should be gone during the termination step.
MOZ_ASSERT(!mJSContext);
}
// static
already_AddRefed<WorkletThread>
WorkletThread::Create(const WorkletLoadInfo& aWorkletLoadInfo)
{
RefPtr<WorkletThread> thread =
new WorkletThread(aWorkletLoadInfo);
if (NS_WARN_IF(NS_FAILED(thread->Init()))) {
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;
}
void
WorkletThread::RunEventLoop(JSRuntime* aParentRuntime)
{
MOZ_ASSERT(!NS_IsMainThread());
PR_SetCurrentThreadName("worklet");
WorkletJSContext context(this);
nsresult rv = context.Initialize(aParentRuntime);
if (NS_WARN_IF(NS_FAILED(rv))) {
// TODO: error propagation
return;
}
// FIXME: JS_SetDefaultLocale
// FIXME: JSSettings
// FIXME: JS_SetNativeStackQuota
// FIXME: JS_SetSecurityCallbacks
// FIXME: JS::SetAsmJSCacheOps
// FIXME: JS::SetAsyncTaskCallbacks
// FIXME: JS_AddInterruptCallback
// FIXME: JS::SetCTypesActivityCallback
// FIXME: JS_SetGCZeal
if (!JS::InitSelfHostedCode(context.Context())) {
// TODO: error propagation
return;
}
mJSContext = context.Context();
while (mJSContext) {
MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(this, /* wait: */ true));
}
MOZ_ASSERT(mJSContext == nullptr);
}
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()
{
AssertIsOnWorkletThread();
mJSContext = nullptr;
nsCOMPtr<nsIRunnable> runnable =
NewRunnableMethod("WorkletThread::Shutdown", this,
&WorkletThread::Shutdown);
NS_DispatchToMainThread(runnable);
}
JSContext*
WorkletThread::GetJSContext() const
{
AssertIsOnWorkletThread();
MOZ_ASSERT(mJSContext);
return mJSContext;
}
const WorkletLoadInfo&
WorkletThread::GetWorkletLoadInfo() const
{
return mWorkletLoadInfo;
}
/* static */ bool
WorkletThread::IsOnWorkletThread()
{
CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
return ccjscx && ccjscx->GetAsWorkletJSContext();
}
/* static */ void
WorkletThread::AssertIsOnWorkletThread()
{
MOZ_ASSERT(IsOnWorkletThread());
}
/* static */ WorkletThread*
WorkletThread::Get()
{
AssertIsOnWorkletThread();
CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
MOZ_ASSERT(ccjscx);
void* cxPrivate = JS_GetContextPrivate(ccjscx->Context());
MOZ_ASSERT(cxPrivate);
return
static_cast<WorkletThreadContextPrivate*>(cxPrivate)->GetWorkletThread();
}
// nsIObserver
NS_IMETHODIMP
WorkletThread::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t*)
{
MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0);
Terminate();
return NS_OK;
}
NS_IMPL_ISUPPORTS_INHERITED(WorkletThread, nsThread, nsIObserver)
} // namespace dom
} // namespace mozilla