tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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