mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 01:05:45 +00:00
c53e11c2b2
WeakRef targets that are wrappers to DOM objects are preserved when the WeakRef is created. This checks whether the wrapper is still preserved in deref() and if it is found to have been released, the target is cleared. The patch adds a new DOMJSClass hook to deal with getting the wrapper cache for non-nsISupports objects. Differential Revision: https://phabricator.services.mozilla.com/D78061
455 lines
13 KiB
C++
455 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(NS_LITERAL_CSTRING("DOM Worklet"))))) {
|
|
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
|