gecko-dev/dom/worklet/WorkletThread.cpp
Jon Coppeard c53e11c2b2 Bug 1642974 - Don't expose WeakRef targets which are DOM wrappers whose target has been collected r=smaug,sfink
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
2020-06-06 06:58:42 +00:00

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