The final CC does not happen until after threads are shutdown and so we can't depend on CC to trigger termination. This management of the worklet thread by worklet code is an intermediate situation until worklets run on the threads managed by other objects. MozReview-Commit-ID: 8hWsdRCppC2
490 lines
12 KiB
C++
490 lines
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());
|
|
}
|
|
|
|
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
|