WorkletThread.cpp (15605B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "WorkletThread.h" 8 9 #include "XPCSelfHostedShmem.h" 10 #include "js/ContextOptions.h" 11 #include "js/Exception.h" 12 #include "js/Initialization.h" 13 #include "js/friend/MicroTask.h" 14 #include "mozilla/Attributes.h" 15 #include "mozilla/CycleCollectedJSRuntime.h" 16 #include "mozilla/EventQueue.h" 17 #include "mozilla/FlowMarkers.h" 18 #include "mozilla/StaticPrefs_javascript.h" 19 #include "mozilla/ThreadEventQueue.h" 20 #include "mozilla/dom/AtomList.h" 21 #include "mozilla/dom/WorkletGlobalScope.h" 22 #include "mozilla/ipc/BackgroundChild.h" 23 #include "nsContentUtils.h" 24 #include "nsCycleCollector.h" 25 #include "nsJSEnvironment.h" 26 #include "nsJSPrincipals.h" 27 #include "prthread.h" 28 29 namespace mozilla::dom { 30 31 namespace { 32 33 // The size of the worklet runtime heaps in bytes. 34 #define WORKLET_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024 35 36 // The C stack size. We use the same stack size on all platforms for 37 // consistency. 38 const uint32_t kWorkletStackSize = 256 * sizeof(size_t) * 1024; 39 40 // Half the size of the actual C stack, to be safe. 41 #define WORKLET_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024 42 43 // Helper functions 44 45 bool PreserveWrapper(JSContext* aCx, JS::Handle<JSObject*> aObj) { 46 MOZ_ASSERT(aCx); 47 MOZ_ASSERT(aObj); 48 MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj)); 49 return mozilla::dom::TryPreserveWrapper(aObj); 50 } 51 52 JSObject* Wrap(JSContext* aCx, JS::Handle<JSObject*> aExisting, 53 JS::Handle<JSObject*> aObj) { 54 if (aExisting) { 55 js::Wrapper::Renew(aExisting, aObj, 56 &js::OpaqueCrossCompartmentWrapper::singleton); 57 } 58 59 return js::Wrapper::New(aCx, aObj, 60 &js::OpaqueCrossCompartmentWrapper::singleton); 61 } 62 63 const JSWrapObjectCallbacks WrapObjectCallbacks = { 64 Wrap, 65 nullptr, 66 }; 67 68 } // namespace 69 70 // This classes control CC in the worklet thread. 71 72 class WorkletJSRuntime final : public mozilla::CycleCollectedJSRuntime { 73 public: 74 explicit WorkletJSRuntime(JSContext* aCx) : CycleCollectedJSRuntime(aCx) {} 75 76 ~WorkletJSRuntime() override = default; 77 78 virtual void PrepareForForgetSkippable() override {} 79 80 virtual void BeginCycleCollectionCallback( 81 mozilla::CCReason aReason) override {} 82 83 virtual void EndCycleCollectionCallback( 84 CycleCollectorResults& aResults) override {} 85 86 virtual void DispatchDeferredDeletion(bool aContinuation, 87 bool aPurge) override { 88 MOZ_ASSERT(!aContinuation); 89 nsCycleCollector_doDeferredDeletion(); 90 } 91 92 virtual void CustomGCCallback(JSGCStatus aStatus) override { 93 // nsCycleCollector_collect() requires a cycle collector but 94 // ~WorkletJSContext calls nsCycleCollector_shutdown() and the base class 95 // destructor will trigger a final GC. The nsCycleCollector_collect() 96 // call can be skipped in this GC as ~CycleCollectedJSContext removes the 97 // context from |this|. 98 if (aStatus == JSGC_END && GetContext()) { 99 nsCycleCollector_collect(CCReason::GC_FINISHED, nullptr); 100 } 101 } 102 }; 103 104 class WorkletJSContext final : public CycleCollectedJSContext { 105 public: 106 WorkletJSContext() { 107 MOZ_ASSERT(!NS_IsMainThread()); 108 109 nsCycleCollector_startup(); 110 } 111 112 // MOZ_CAN_RUN_SCRIPT_BOUNDARY because otherwise we have to annotate the 113 // SpiderMonkey JS::JobQueue's destructor as MOZ_CAN_RUN_SCRIPT, which is a 114 // bit of a pain. 115 MOZ_CAN_RUN_SCRIPT_BOUNDARY ~WorkletJSContext() override { 116 MOZ_ASSERT(!NS_IsMainThread()); 117 118 JSContext* cx = MaybeContext(); 119 if (!cx) { 120 return; // Initialize() must have failed 121 } 122 123 nsCycleCollector_shutdown(); 124 } 125 126 WorkletJSContext* GetAsWorkletJSContext() override { return this; } 127 128 CycleCollectedJSRuntime* CreateRuntime(JSContext* aCx) override { 129 return new WorkletJSRuntime(aCx); 130 } 131 132 nsresult Initialize(JSRuntime* aParentRuntime) { 133 MOZ_ASSERT(!NS_IsMainThread()); 134 135 nsresult rv = CycleCollectedJSContext::Initialize( 136 aParentRuntime, WORKLET_DEFAULT_RUNTIME_HEAPSIZE); 137 if (NS_WARN_IF(NS_FAILED(rv))) { 138 return rv; 139 } 140 141 JSContext* cx = Context(); 142 143 js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper); 144 JS_InitDestroyPrincipalsCallback(cx, nsJSPrincipals::Destroy); 145 JS_InitReadPrincipalsCallback(cx, nsJSPrincipals::ReadPrincipals); 146 JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); 147 JS_SetFutexCanWait(cx); 148 149 return NS_OK; 150 } 151 152 void DispatchToMicroTask( 153 already_AddRefed<MicroTaskRunnable> aRunnable) override { 154 RefPtr<MicroTaskRunnable> runnable(aRunnable); 155 156 MOZ_ASSERT(!NS_IsMainThread()); 157 MOZ_ASSERT(runnable); 158 159 JSContext* cx = Context(); 160 MOZ_ASSERT(cx); 161 162 #ifdef DEBUG 163 JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); 164 MOZ_ASSERT(global); 165 #endif 166 167 JS::JobQueueMayNotBeEmpty(cx); 168 if (StaticPrefs::javascript_options_use_js_microtask_queue()) { 169 PROFILER_MARKER_FLOW_ONLY("WorkletJSContext::DispatchToMicroTask", OTHER, 170 {}, FlowMarker, 171 Flow::FromPointer(runnable.get())); 172 bool ret = mozilla::EnqueueMicroTask(cx, std::move(aRunnable)); 173 MOZ_RELEASE_ASSERT(ret); 174 } else { 175 if (!runnable->isInList()) { 176 // A recycled object may be in the list already. 177 mMicrotasksToTrace.insertBack(runnable); 178 } 179 PROFILER_MARKER_FLOW_ONLY("WorkletJSContext::DispatchToMicroTask", OTHER, 180 {}, FlowMarker, 181 Flow::FromPointer(runnable.get())); 182 GetMicroTaskQueue().push_back(std::move(runnable)); 183 } 184 } 185 186 bool IsSystemCaller() const override { 187 // Currently no support for special system worklet privileges. 188 return false; 189 } 190 191 void ReportError(JSErrorReport* aReport, 192 JS::ConstUTF8CharsZ aToStringResult) override; 193 194 uint64_t GetCurrentWorkletWindowID() { 195 JSObject* global = JS::CurrentGlobalOrNull(Context()); 196 if (NS_WARN_IF(!global)) { 197 return 0; 198 } 199 nsIGlobalObject* nativeGlobal = xpc::NativeGlobal(global); 200 nsCOMPtr<WorkletGlobalScope> workletGlobal = 201 do_QueryInterface(nativeGlobal); 202 if (NS_WARN_IF(!workletGlobal)) { 203 return 0; 204 } 205 return workletGlobal->Impl()->LoadInfo().InnerWindowID(); 206 } 207 }; 208 209 void WorkletJSContext::ReportError(JSErrorReport* aReport, 210 JS::ConstUTF8CharsZ aToStringResult) { 211 RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); 212 xpcReport->Init(aReport, aToStringResult.c_str(), IsSystemCaller(), 213 GetCurrentWorkletWindowID()); 214 RefPtr<AsyncErrorReporter> reporter = new AsyncErrorReporter(xpcReport); 215 216 JSContext* cx = Context(); 217 // NOTE: This function is used both for errors and warnings, and warnings 218 // can be reported while there's a pending exception. 219 // Warnings are always reported with non-null JSErrorReport. 220 if (!aReport || !aReport->isWarning()) { 221 MOZ_ASSERT(JS_IsExceptionPending(cx)); 222 JS::ExceptionStack exnStack(cx); 223 if (JS::StealPendingExceptionStack(cx, &exnStack)) { 224 JS::Rooted<JSObject*> stack(cx); 225 JS::Rooted<JSObject*> stackGlobal(cx); 226 xpc::FindExceptionStackForConsoleReport(nullptr, exnStack.exception(), 227 exnStack.stack(), &stack, 228 &stackGlobal); 229 if (stack) { 230 reporter->SerializeStack(cx, stack); 231 } 232 } 233 } 234 235 NS_DispatchToMainThread(reporter); 236 } 237 238 // This is the first runnable to be dispatched. It calls the RunEventLoop() so 239 // basically everything happens into this runnable. The reason behind this 240 // approach is that, when the Worklet is terminated, it must not have any JS in 241 // stack, but, because we have CC, nsIThread creates an AutoNoJSAPI object by 242 // default. Using this runnable, CC exists only into it. 243 class WorkletThread::PrimaryRunnable final : public Runnable { 244 public: 245 explicit PrimaryRunnable(WorkletThread* aWorkletThread) 246 : Runnable("WorkletThread::PrimaryRunnable"), 247 mWorkletThread(aWorkletThread) { 248 MOZ_ASSERT(aWorkletThread); 249 MOZ_ASSERT(NS_IsMainThread()); 250 } 251 252 NS_IMETHOD 253 Run() override { 254 mWorkletThread->RunEventLoop(); 255 return NS_OK; 256 } 257 258 private: 259 RefPtr<WorkletThread> mWorkletThread; 260 }; 261 262 // This is the last runnable to be dispatched. It calls the TerminateInternal() 263 class WorkletThread::TerminateRunnable final : public Runnable { 264 public: 265 explicit TerminateRunnable(WorkletThread* aWorkletThread) 266 : Runnable("WorkletThread::TerminateRunnable"), 267 mWorkletThread(aWorkletThread) { 268 MOZ_ASSERT(aWorkletThread); 269 MOZ_ASSERT(NS_IsMainThread()); 270 } 271 272 NS_IMETHOD 273 Run() override { 274 mWorkletThread->TerminateInternal(); 275 return NS_OK; 276 } 277 278 private: 279 RefPtr<WorkletThread> mWorkletThread; 280 }; 281 282 WorkletThread::WorkletThread(WorkletImpl* aWorkletImpl) 283 : nsThread( 284 MakeNotNull<ThreadEventQueue*>(MakeUnique<mozilla::EventQueue>()), 285 nsThread::NOT_MAIN_THREAD, {.stackSize = kWorkletStackSize}), 286 mWorkletImpl(aWorkletImpl), 287 mExitLoop(false), 288 mIsTerminating(false) { 289 MOZ_ASSERT(NS_IsMainThread()); 290 nsContentUtils::RegisterShutdownObserver(this); 291 } 292 293 WorkletThread::~WorkletThread() = default; 294 295 // static 296 already_AddRefed<WorkletThread> WorkletThread::Create( 297 WorkletImpl* aWorkletImpl) { 298 RefPtr<WorkletThread> thread = new WorkletThread(aWorkletImpl); 299 if (NS_WARN_IF(NS_FAILED(thread->Init("DOM Worklet"_ns)))) { 300 return nullptr; 301 } 302 303 RefPtr<PrimaryRunnable> runnable = new PrimaryRunnable(thread); 304 if (NS_WARN_IF(NS_FAILED(thread->DispatchRunnable(runnable.forget())))) { 305 return nullptr; 306 } 307 308 return thread.forget(); 309 } 310 311 nsresult WorkletThread::DispatchRunnable( 312 already_AddRefed<nsIRunnable> aRunnable) { 313 nsCOMPtr<nsIRunnable> runnable(aRunnable); 314 return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); 315 } 316 317 static bool DispatchToEventLoop( 318 void* aClosure, js::UniquePtr<JS::Dispatchable>&& aDispatchable) { 319 // This callback may execute either on the worklet thread or a random 320 // JS-internal helper thread. 321 322 // See comment at JS::InitDispatchToEventLoop() below for how we know the 323 // thread is alive. 324 nsIThread* thread = static_cast<nsIThread*>(aClosure); 325 326 nsresult rv = thread->Dispatch( 327 NS_NewRunnableFunction( 328 "WorkletThread::DispatchToEventLoop", 329 [dispatchable = std::move(aDispatchable)]() mutable { 330 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); 331 if (!ccjscx) { 332 JS::Dispatchable::ReleaseFailedTask(std::move(dispatchable)); 333 return; 334 } 335 336 WorkletJSContext* wjc = ccjscx->GetAsWorkletJSContext(); 337 if (!wjc) { 338 JS::Dispatchable::ReleaseFailedTask(std::move(dispatchable)); 339 return; 340 } 341 342 AutoJSAPI jsapi; 343 jsapi.Init(); 344 JS::Dispatchable::Run(wjc->Context(), std::move(dispatchable), 345 JS::Dispatchable::NotShuttingDown); 346 }), 347 NS_DISPATCH_NORMAL); 348 349 return NS_SUCCEEDED(rv); 350 } 351 352 static bool DelayedDispatchToEventLoop( 353 void* aClosure, js::UniquePtr<JS::Dispatchable>&& aDispatchable, 354 uint32_t delay) { 355 // Worklets do not support delayed dispatch. If something is trying to use it, 356 // it should fail. For now we are warning. 357 NS_WARNING("Trying to perform a delayed dispatch on a worklet."); 358 return false; 359 } 360 361 // static 362 void WorkletThread::EnsureCycleCollectedJSContext( 363 JSRuntime* aParentRuntime, const JS::ContextOptions& aOptions) { 364 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); 365 if (ccjscx) { 366 MOZ_ASSERT(ccjscx->GetAsWorkletJSContext()); 367 return; 368 } 369 370 WorkletJSContext* context = new WorkletJSContext(); 371 nsresult rv = context->Initialize(aParentRuntime); 372 if (NS_WARN_IF(NS_FAILED(rv))) { 373 // TODO: error propagation 374 return; 375 } 376 377 JS::ContextOptionsRef(context->Context()) = aOptions; 378 379 JS_SetGCParameter(context->Context(), JSGC_MAX_BYTES, uint32_t(-1)); 380 381 // FIXME: JS_SetDefaultLocale 382 // FIXME: JSSettings 383 // FIXME: JS_SetSecurityCallbacks 384 // FIXME: JS::SetAsyncTaskCallbacks 385 // FIXME: JS::SetCTypesActivityCallback 386 // FIXME: JS::SetGCZeal 387 388 // A thread lives strictly longer than its JSRuntime so we can safely 389 // store a raw pointer as the callback's closure argument on the JSRuntime. 390 JS::InitAsyncTaskCallbacks(context->Context(), DispatchToEventLoop, 391 DelayedDispatchToEventLoop, nullptr, nullptr, 392 NS_GetCurrentThread()); 393 394 JS_SetNativeStackQuota(context->Context(), 395 WORKLET_CONTEXT_NATIVE_STACK_LIMIT); 396 397 // When available, set the self-hosted shared memory to be read, so that we 398 // can decode the self-hosted content instead of parsing it. 399 auto& shm = xpc::SelfHostedShmem::GetSingleton(); 400 JS::SelfHostedCache selfHostedContent = shm.Content(); 401 402 if (!JS::InitSelfHostedCode(context->Context(), selfHostedContent)) { 403 // TODO: error propagation 404 return; 405 } 406 } 407 408 void WorkletThread::RunEventLoop() { 409 MOZ_ASSERT(!NS_IsMainThread()); 410 411 PR_SetCurrentThreadName("worklet"); 412 413 while (!mExitLoop) { 414 MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(this, /* wait: */ true)); 415 } 416 417 DeleteCycleCollectedJSContext(); 418 } 419 420 void WorkletThread::Terminate() { 421 MOZ_ASSERT(NS_IsMainThread()); 422 423 if (mIsTerminating) { 424 // nsThread::Dispatch() would leak the runnable if the event queue is no 425 // longer accepting runnables. 426 return; 427 } 428 429 mIsTerminating = true; 430 431 nsContentUtils::UnregisterShutdownObserver(this); 432 433 RefPtr<TerminateRunnable> runnable = new TerminateRunnable(this); 434 DispatchRunnable(runnable.forget()); 435 } 436 437 uint32_t WorkletThread::StackSize() { return kWorkletStackSize; } 438 439 void WorkletThread::TerminateInternal() { 440 MOZ_ASSERT(!CycleCollectedJSContext::Get() || IsOnWorkletThread()); 441 442 mExitLoop = true; 443 444 nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod( 445 "WorkletThread::Shutdown", this, &WorkletThread::Shutdown); 446 NS_DispatchToMainThread(runnable); 447 } 448 449 /* static */ 450 void WorkletThread::DeleteCycleCollectedJSContext() { 451 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); 452 if (!ccjscx) { 453 return; 454 } 455 456 // Release any MessagePort kept alive by its ipc actor. 457 mozilla::ipc::BackgroundChild::CloseForCurrentThread(); 458 459 WorkletJSContext* workletjscx = ccjscx->GetAsWorkletJSContext(); 460 MOZ_ASSERT(workletjscx); 461 delete workletjscx; 462 } 463 464 /* static */ 465 bool WorkletThread::IsOnWorkletThread() { 466 CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); 467 return ccjscx && ccjscx->GetAsWorkletJSContext(); 468 } 469 470 /* static */ 471 void WorkletThread::AssertIsOnWorkletThread() { 472 MOZ_ASSERT(IsOnWorkletThread()); 473 } 474 475 // nsIObserver 476 NS_IMETHODIMP 477 WorkletThread::Observe(nsISupports* aSubject, const char* aTopic, 478 const char16_t*) { 479 MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0); 480 481 // The WorkletImpl will terminate the worklet thread after sending a message 482 // to release worklet thread objects. 483 mWorkletImpl->NotifyWorkletFinished(); 484 return NS_OK; 485 } 486 487 NS_IMPL_ISUPPORTS_INHERITED(WorkletThread, nsThread, nsIObserver) 488 489 } // namespace mozilla::dom