gecko-dev/dom/worklet/WorkletThread.cpp
Iain Ireland 5617e628be Bug 1467846: Part 10: Introduce DelayedDispatchToEventLoopCallback r=arai,dom-worker-reviewers,smaug
[Atomics.waitAsync](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) creates a promise that can be resolved via Atomics.notify, or after an optional timeout. The timeout is [implemented](https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuetimeoutjob) using the [HostEnqueueTimeoutJob hook](https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuetimeoutjob), which queues a global task (not a microtask).

This patch adds a new API (DelayedDispatchToEventLoopCallback) accessible by the JS engine that utilized the TimeoutManager for executing the steps outlined in the HostEnqueTimeoutJob. Unlike DispatchToEventCallback, this is currently restricted to only threads which support TimeoutManager (the Main thread and the Workers Thread). This should not be enabled on worklets per the discussion [here](https://phabricator.services.mozilla.com/D212876#inline-1206374). The next patch adds an implementation in worker threads. A later patch in the stack adds an implementation for the shell.

Depends on D212875

Differential Revision: https://phabricator.services.mozilla.com/D212876
2025-05-13 10:04:54 +00:00

490 lines
15 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 "nsJSEnvironment.h"
#include "nsJSPrincipals.h"
#include "mozilla/dom/AtomList.h"
#include "mozilla/dom/WorkletGlobalScope.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/Attributes.h"
#include "mozilla/CycleCollectedJSRuntime.h"
#include "mozilla/EventQueue.h"
#include "mozilla/ThreadEventQueue.h"
#include "js/ContextOptions.h"
#include "js/Exception.h"
#include "js/Initialization.h"
#include "XPCSelfHostedShmem.h"
namespace mozilla::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::Handle<JSObject*> aObj) {
MOZ_ASSERT(aCx);
MOZ_ASSERT(aObj);
MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj));
return mozilla::dom::TryPreserveWrapper(aObj);
}
JSObject* Wrap(JSContext* aCx, JS::Handle<JSObject*> aExisting,
JS::Handle<JSObject*> 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(
mozilla::CCReason aReason) 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(CCReason::GC_FINISHED, 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, nsJSPrincipals::Destroy);
JS_InitReadPrincipalsCallback(cx, nsJSPrincipals::ReadPrincipals);
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_back(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*>(MakeUnique<mozilla::EventQueue>()),
nsThread::NOT_MAIN_THREAD, {.stackSize = 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::UniquePtr<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
// thread is alive.
nsIThread* thread = static_cast<nsIThread*>(aClosure);
nsresult rv = thread->Dispatch(
NS_NewRunnableFunction(
"WorkletThread::DispatchToEventLoop",
[dispatchable = std::move(aDispatchable)]() mutable {
CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
if (!ccjscx) {
JS::Dispatchable::ReleaseFailedTask(std::move(dispatchable));
return;
}
WorkletJSContext* wjc = ccjscx->GetAsWorkletJSContext();
if (!wjc) {
JS::Dispatchable::ReleaseFailedTask(std::move(dispatchable));
return;
}
AutoJSAPI jsapi;
jsapi.Init();
JS::Dispatchable::Run(wjc->Context(), std::move(dispatchable),
JS::Dispatchable::NotShuttingDown);
}),
NS_DISPATCH_NORMAL);
return NS_SUCCEEDED(rv);
}
static bool DelayedDispatchToEventLoop(
void* aClosure, js::UniquePtr<JS::Dispatchable>&& aDispatchable,
uint32_t delay) {
// Worklets do not support delayed dispatch. If something is trying to use it,
// it should fail. For now we are warning.
NS_WARNING("Trying to perform a delayed dispatch on a worklet.");
return false;
}
// static
void WorkletThread::EnsureCycleCollectedJSContext(
JSRuntime* aParentRuntime, const JS::ContextOptions& aOptions) {
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::ContextOptionsRef(context->Context()) = aOptions;
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 thread lives strictly longer than its JSRuntime so we can safely
// store a raw pointer as the callback's closure argument on the JSRuntime.
JS::InitDispatchsToEventLoop(context->Context(), DispatchToEventLoop,
DelayedDispatchToEventLoop,
NS_GetCurrentThread());
JS_SetNativeStackQuota(context->Context(),
WORKLET_CONTEXT_NATIVE_STACK_LIMIT);
// When available, set the self-hosted shared memory to be read, so that we
// can decode the self-hosted content instead of parsing it.
auto& shm = xpc::SelfHostedShmem::GetSingleton();
JS::SelfHostedCache selfHostedContent = shm.Content();
if (!JS::InitSelfHostedCode(context->Context(), selfHostedContent)) {
// 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());
}
uint32_t WorkletThread::StackSize() { return kWorkletStackSize; }
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 mozilla::dom