/* -*- 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/. */ #ifndef mozilla_CycleCollectedJSContext_h #define mozilla_CycleCollectedJSContext_h #include #include "js/TracingAPI.h" #include "mozilla/Attributes.h" #include "mozilla/LinkedList.h" #include "mozilla/MemoryReporting.h" #include "mozilla/dom/AtomList.h" #include "mozilla/dom/Promise.h" #include "js/GCVector.h" #include "js/Promise.h" #include "js/friend/MicroTask.h" #include "nsCOMPtr.h" #include "nsRefPtrHashtable.h" #include "nsTArray.h" class nsCycleCollectionNoteRootCallback; class nsIRunnable; class nsThread; namespace mozilla { class AutoSlowOperation; class CycleCollectedJSContext; class CycleCollectedJSRuntime; class PromiseJobRunnable; namespace dom { class Exception; class WorkerJSContext; class WorkletJSContext; } // namespace dom // Contains various stats about the cycle collection. struct CycleCollectorResults { CycleCollectorResults() { // Initialize here so when we increment mNumSlices the first time we're // not using uninitialized memory. Init(); } void Init() { mForcedGC = false; mSuspectedAtCCStart = 0; mMergedZones = false; mAnyManual = false; mVisitedRefCounted = 0; mVisitedGCed = 0; mFreedRefCounted = 0; mFreedGCed = 0; mFreedJSZones = 0; mNumSlices = 1; // mNumSlices is initialized to one, because we call Init() after the // per-slice increment of mNumSlices has already occurred. } bool mForcedGC; bool mMergedZones; // mAnyManual is true if any slice was manually triggered, and at shutdown. bool mAnyManual; uint32_t mSuspectedAtCCStart; uint32_t mVisitedRefCounted; uint32_t mVisitedGCed; uint32_t mFreedRefCounted; uint32_t mFreedGCed; uint32_t mFreedJSZones; uint32_t mNumSlices; }; class MicroTaskRunnable : public LinkedListElement { public: MicroTaskRunnable() = default; NS_INLINE_DECL_REFCOUNTING(MicroTaskRunnable) MOZ_CAN_RUN_SCRIPT virtual void Run(AutoSlowOperation& aAso) = 0; virtual bool Suppressed() { return false; } virtual void TraceMicroTask(JSTracer* aTracer) {} protected: virtual ~MicroTaskRunnable() { if (isInList()) { remove(); } } }; // Store the suppressed mictotasks in another microtask so that operations // for the microtask queue as a whole keep working. class SuppressedMicroTasks : public MicroTaskRunnable { public: explicit SuppressedMicroTasks(CycleCollectedJSContext* aContext); MOZ_CAN_RUN_SCRIPT_BOUNDARY void Run(AutoSlowOperation& aAso) final {} virtual bool Suppressed(); CycleCollectedJSContext* mContext; uint64_t mSuppressionGeneration; std::deque> mSuppressedMicroTaskRunnables; }; // A gecko wrapper for the JS::MicroTask type. Used to enforce both // that this is handled move only, but also that we have succesfully // consumed this microtask before destruction. // // This type must be rooted, it holds onto a JS reference. class MOZ_STACK_CLASS MustConsumeMicroTask { public: // We need a public constructor to allow forward declared Rooted MustConsumeMicroTask() = default; // The only way to get a (filled) MustConsumeMicroTask is through these // mechanisms. friend MustConsumeMicroTask DequeueNextMicroTask(JSContext* aCx); friend MustConsumeMicroTask DequeueNextRegularMicroTask(JSContext* aCx); friend MustConsumeMicroTask DequeueNextDebuggerMicroTask(JSContext* aCx); ~MustConsumeMicroTask() { if (!mMicroTask.isUndefined()) { MOZ_CRASH("Didn't consume MicroTask"); } } // Move only semantics MustConsumeMicroTask(const MustConsumeMicroTask&) = delete; MustConsumeMicroTask& operator=(const MustConsumeMicroTask&) = delete; MustConsumeMicroTask(MustConsumeMicroTask&& other) { mMicroTask = other.mMicroTask; other.mMicroTask.setUndefined(); } MustConsumeMicroTask& operator=(MustConsumeMicroTask&& other) noexcept { if (this != &other) { mMicroTask = other.mMicroTask; other.mMicroTask.setUndefined(); } return *this; } // Indicate if this still holds a task or not. bool IsConsumed() const { return mMicroTask.isUndefined(); } // Allow testing for contentfulness. explicit operator bool() const { return !IsConsumed(); } // Check if this holds a "JS Microtask" (see MicroTask.h), // which is a task enqueued by the JS engine rather than // Gecko. bool IsJSMicroTask() const { return JS::IsJSMicroTask(mMicroTask); } // Unwrap (without interacting with refcounting) a Gecko MicroTaskRunnable if // the task is not a JS MicroTask (see MicroTask.h for "JS MicroTask"); // otherwise, return nullptr. // // This is a non-owning conversion: This class still owns the refcount. MicroTaskRunnable* MaybeUnwrapTaskToRunnable() const; // Take ownership of a non-JS task inside a JS::GenericMicroTask - This clears // the contents of the value to make it clear that we've transfered ownership. // `this` is marked is only edited if unwrapping succeeds, and so // you can conditionally try to consume as owned; // // MOZ_ASSERT(!mustConsume.IsConsumed()) // if (RefPtr geckoTask = // mustConsume.MaybeConsumeAsOwnedRunnable()) { // // mustConsume is now empty // } else { // // mustConsume still holds a JS microtask // } // already_AddRefed MaybeConsumeAsOwnedRunnable(); // Intentionally ignore a JS microtask. This can happen when script // execution is disallowed during CallSetup void IgnoreJSMicroTask() { MOZ_ASSERT(IsJSMicroTask()); mMicroTask.setUndefined(); } // Consume this by prepending this MustConsumeMicroTask back into // the MicroTaskQueue. void ConsumeByPrependToQueue(JSContext* aCx) { MOZ_ASSERT(!IsConsumed(), "Attempting to consume an already-consumed task"); if (!JS::PrependMicroTask(aCx, mMicroTask)) { // Can't lose tasks. NS_ABORT_OOM(0); } mMicroTask.setUndefined(); } // Get the execution global for this task without // consuming the contents. JSObject* GetExecutionGlobalFromJSMicroTask(JSContext* aCx) const { MOZ_ASSERT(IsJSMicroTask()); JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(mMicroTask); MOZ_ASSERT(task); return JS::GetExecutionGlobalFromJSMicroTask(task); } // Below: A number of wrappers to allow working with a MustConsume without // exposing the contained task which could then be misused. // // These are documented in MicroTask.h. bool GetFlowIdFromJSMicroTask(uint64_t* aFlowId) { JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(mMicroTask); MOZ_ASSERT(task); return JS::GetFlowIdFromJSMicroTask(task, aFlowId); } JSObject* MaybeGetPromiseFromJSMicroTask() { JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(mMicroTask); MOZ_ASSERT(task); return JS::MaybeGetPromiseFromJSMicroTask(task); } bool MaybeGetHostDefinedDataFromJSMicroTask( JS::MutableHandle out) { JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(mMicroTask); if (!task) { return false; } return JS::MaybeGetHostDefinedDataFromJSMicroTask(task, out); } bool MaybeGetAllocationSiteFromJSMicroTask(JS::MutableHandle out) { JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(mMicroTask); if (!task) { return false; } return JS::MaybeGetAllocationSiteFromJSMicroTask(task, out); } JSObject* MaybeGetHostDefinedGlobalFromJSMicroTask() { JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(mMicroTask); if (!task) { return nullptr; } return JS::MaybeGetHostDefinedGlobalFromJSMicroTask(task); } bool RunAndConsumeJSMicroTask(JSContext* aCx) { JS::Rooted task( aCx, JS::ToMaybeWrappedJSMicroTask(mMicroTask)); MOZ_ASSERT(task); bool v = JS::RunJSMicroTask(aCx, task); mMicroTask.setUndefined(); return v; } void trace(JSTracer* aTrc) { TraceRoot(aTrc, &mMicroTask, "MustConsumeMicroTask value"); } private: explicit MustConsumeMicroTask(JS::GenericMicroTask&& aMicroTask) : mMicroTask(aMicroTask) {} JS::GenericMicroTask mMicroTask; }; class SuppressedMicroTaskList final : public MicroTaskRunnable { public: SuppressedMicroTaskList() = delete; explicit SuppressedMicroTaskList(CycleCollectedJSContext* aContext); virtual bool Suppressed() override; virtual void Run(AutoSlowOperation& aso) override { // Does nothing; the only action occurs as part of the // call to Suppressed(). } CycleCollectedJSContext* mContext = nullptr; uint64_t mSuppressionGeneration = 0; JS::PersistentRooted> mSuppressedMicroTaskRunnables; private: ~SuppressedMicroTaskList(); }; // Support for JS FinalizationRegistry objects, which allow a JS callback to be // registered that is called when objects die. // // We keep a vector of functions that call back into the JS engine along // with their associated incumbent globals, one per FinalizationRegistry object // that has pending cleanup work. These are run in their own task. class FinalizationRegistryCleanup { public: explicit FinalizationRegistryCleanup(CycleCollectedJSContext* aContext); void Init(); void Destroy(); void QueueCallback(JSFunction* aDoCleanup, JSObject* aHostDefinedData); MOZ_CAN_RUN_SCRIPT void DoCleanup(); private: static void QueueCallback(JSFunction* aDoCleanup, JSObject* aHostDefinedData, void* aData); class CleanupRunnable; struct Callback { JSFunction* mCallbackFunction; JSObject* mIncumbentGlobal; void trace(JSTracer* trc); }; // This object is part of CycleCollectedJSContext, so it's safe to have a raw // pointer to its containing context here. CycleCollectedJSContext* mContext; using CallbackVector = JS::GCVector; JS::PersistentRooted mCallbacks; }; bool EnqueueMicroTask(JSContext* aCx, already_AddRefed aRunnable); bool EnqueueDebugMicroTask(JSContext* aCx, already_AddRefed aRunnable); MustConsumeMicroTask DequeueNextMicroTask(JSContext* aCx); MustConsumeMicroTask DequeueNextRegularMicroTask(JSContext* aCx); MustConsumeMicroTask DequeueNextDebuggerMicroTask(JSContext* aCx); class CycleCollectedJSContext : dom::PerThreadAtomCache, public JS::JobQueue { friend class CycleCollectedJSRuntime; friend class SuppressedMicroTasks; friend class SuppressedMicroTaskList; protected: CycleCollectedJSContext(); virtual ~CycleCollectedJSContext(); MOZ_IS_CLASS_INIT nsresult Initialize(JSRuntime* aParentRuntime, uint32_t aMaxBytes); virtual CycleCollectedJSRuntime* CreateRuntime(JSContext* aCx) = 0; size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; private: static void PromiseRejectionTrackerCallback( JSContext* aCx, bool aMutedErrors, JS::Handle aPromise, JS::PromiseRejectionHandlingState state, void* aData); void AfterProcessMicrotasks(); public: void ProcessStableStateQueue(); void ClearUncaughtRejectionObservers() { mUncaughtRejectionObservers.Clear(); } private: void CleanupIDBTransactions(uint32_t aRecursionDepth); public: virtual dom::WorkerJSContext* GetAsWorkerJSContext() { return nullptr; } virtual dom::WorkletJSContext* GetAsWorkletJSContext() { return nullptr; } CycleCollectedJSRuntime* Runtime() const { MOZ_ASSERT(mRuntime); return mRuntime; } already_AddRefed GetPendingException() const; void SetPendingException(dom::Exception* aException); std::deque>& GetMicroTaskQueue(); std::deque>& GetDebuggerMicroTaskQueue(); void TraceMicroTasks(JSTracer* aTracer); JSContext* Context() const { MOZ_ASSERT(mJSContext); return mJSContext; } JS::RootingContext* RootingCx() const { MOZ_ASSERT(mJSContext); return JS::RootingContext::get(mJSContext); } void SetTargetedMicroTaskRecursionDepth(uint32_t aDepth) { mTargetedMicroTaskRecursionDepth = aDepth; } void UpdateMicroTaskSuppressionGeneration() { ++mSuppressionGeneration; } protected: JSContext* MaybeContext() const { return mJSContext; } public: // nsThread entrypoints // // MOZ_CAN_RUN_SCRIPT_BOUNDARY so we don't need to annotate // nsThread::ProcessNextEvent and all its callers MOZ_CAN_RUN_SCRIPT for now. // But we really should! MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void BeforeProcessTask(bool aMightBlock); // MOZ_CAN_RUN_SCRIPT_BOUNDARY so we don't need to annotate // nsThread::ProcessNextEvent and all its callers MOZ_CAN_RUN_SCRIPT for now. // But we really should! MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void AfterProcessTask(uint32_t aRecursionDepth); // Check whether any eager thresholds have been reached, which would mean // an idle GC task (minor or major) would be useful. virtual void MaybePokeGC(); uint32_t RecursionDepth() const; // Run in stable state (call through nsContentUtils) void RunInStableState(already_AddRefed&& aRunnable); void AddPendingIDBTransaction(already_AddRefed&& aTransaction); // Get the CycleCollectedJSContext for a JSContext. // Returns null only if Initialize() has not completed on or during // destruction of the CycleCollectedJSContext. static CycleCollectedJSContext* GetFor(JSContext* aCx); // Get the current thread's CycleCollectedJSContext. Returns null if there // isn't one. static CycleCollectedJSContext* Get(); // Queue an async microtask to the current main or worker thread. virtual void DispatchToMicroTask( already_AddRefed aRunnable); // Call EnterMicroTask when you're entering JS execution. // Usually the best way to do this is to use nsAutoMicroTask. void EnterMicroTask() { ++mMicroTaskLevel; } MOZ_CAN_RUN_SCRIPT void LeaveMicroTask() { if (--mMicroTaskLevel == 0) { PerformMicroTaskCheckPoint(); } } uint32_t MicroTaskLevel() const { return mMicroTaskLevel; } void SetMicroTaskLevel(uint32_t aLevel) { mMicroTaskLevel = aLevel; } void EnterSyncOperation() { ++mSyncOperations; } void LeaveSyncOperation() { --mSyncOperations; } bool IsInSyncOperation() const { return mSyncOperations > 0; } MOZ_CAN_RUN_SCRIPT bool PerformMicroTaskCheckPoint(bool aForce = false); MOZ_CAN_RUN_SCRIPT void PerformDebuggerMicroTaskCheckpoint(); bool IsInStableOrMetaStableState() const { return mDoingStableStates; } // Storage for watching rejected promises waiting for some client to // consume their rejection. // Promises in this list have been rejected in the last turn of the // event loop without the rejection being handled. // Note that this can contain nullptrs in place of promises removed because // they're consumed before it'd be reported. JS::PersistentRooted> mUncaughtRejections; // Promises in this list have previously been reported as rejected // (because they were in the above list), but the rejection was handled // in the last turn of the event loop. JS::PersistentRooted> mConsumedRejections; nsTArray> mUncaughtRejectionObservers; virtual bool IsSystemCaller() const = 0; // Unused on main thread. Used by AutoJSAPI on Worker and Worklet threads. virtual void ReportError(JSErrorReport* aReport, JS::ConstUTF8CharsZ aToStringResult) { MOZ_ASSERT_UNREACHABLE("Not supported"); } // These two functions control a special flag variable which lets us turn // tracing on and off from a thread other than this JSContext's main thread. // This is useful because we want to be able to start tracing many threads // all at once from the Gecko Profiler in Firefox. // // NOTE: the caller must ensure that this CycleCollectedJSContext is not // being destroyed when this is called. At the time of this API being added, // the only consumer is the Gecko Profiler, which guarantees this via a mutex // around unregistering the context, which always occurs before the context // is destroyed. void BeginExecutionTracingAsync(); void EndExecutionTracingAsync(); private: // JS::JobQueue implementation: see js/public/Promise.h. // SpiderMonkey uses some of these methods to enqueue promise resolution jobs. // Others protect the debuggee microtask queue from the debugger's // interruptions; see the comments on JS::AutoDebuggerJobQueueInterruption for // details. bool getHostDefinedData(JSContext* cx, JS::MutableHandle aData) const override; // Fills in the JS Object used to represent the current incumbent global. // Used when running MicroTasks which don't have host-defined data as // they will still need an incumbent global. bool getHostDefinedGlobal(JSContext* cx, JS::MutableHandle) const override; bool enqueuePromiseJob(JSContext* cx, JS::Handle promise, JS::Handle job, JS::Handle allocationSite, JS::Handle hostDefinedData) override; // MOZ_CAN_RUN_SCRIPT_BOUNDARY for now so we don't have to change SpiderMonkey // headers. The caller presumably knows this can run script (like everything // in SpiderMonkey!) and will deal. MOZ_CAN_RUN_SCRIPT_BOUNDARY void runJobs(JSContext* cx) override; bool empty() const override; bool isDrainingStopped() const override { return false; } // Trace hook for non-GCThing microtask values (e.g., Private values // containing MicroTaskRunnable pointers). void traceNonGCThingMicroTask(JSTracer* trc, JS::Value* valuePtr) override; class SavedMicroTaskQueue; js::UniquePtr saveJobQueue(JSContext*) override; private: CycleCollectedJSRuntime* mRuntime; JSContext* mJSContext; nsCOMPtr mPendingException; nsThread* mOwningThread; // Manual refcounting to avoid include hell. struct PendingIDBTransactionData { nsCOMPtr mTransaction; uint32_t mRecursionDepth; }; nsTArray> mStableStateEvents; nsTArray mPendingIDBTransactions; uint32_t mBaseRecursionDepth; bool mDoingStableStates; // If set to none 0, microtasks will be processed only when recursion depth // is the set value. uint32_t mTargetedMicroTaskRecursionDepth; uint32_t mMicroTaskLevel; uint32_t mSyncOperations; std::deque> mPendingMicroTaskRunnables; std::deque> mDebuggerMicroTaskQueue; RefPtr mSuppressedMicroTasks; RefPtr mSuppressedMicroTaskList; uint64_t mSuppressionGeneration; protected: mozilla::LinkedList mMicrotasksToTrace; private: friend class PromiseJobRunnable; RefPtr mRecycledPromiseJob; // How many times the debugger has interrupted execution, possibly creating // microtask checkpoints in places that they would not normally occur. uint32_t mDebuggerRecursionDepth; Maybe mMicroTaskRecursionDepth; // This implements about-to-be-notified rejected promises list in the spec. // https://html.spec.whatwg.org/multipage/webappapis.html#about-to-be-notified-rejected-promises-list typedef nsTArray> PromiseArray; PromiseArray mAboutToBeNotifiedRejectedPromises; // This is for the "outstanding rejected promises weak set" in the spec, // https://html.spec.whatwg.org/multipage/webappapis.html#outstanding-rejected-promises-weak-set // We use different data structure and opposite logic here to achieve the same // effect. Basically this is used for tracking the rejected promise that does // NOT need firing a rejectionhandled event. We will check the table to see if // firing rejectionhandled event is required when a rejected promise is being // handled. // // The rejected promise will be stored in the table if // - it is unhandled, and // - The unhandledrejection is not yet fired. // // And be removed when // - it is handled, or // - A unhandledrejection is fired and it isn't being handled in event // handler. typedef nsRefPtrHashtable PromiseHashtable; PromiseHashtable mPendingUnhandledRejections; class NotifyUnhandledRejections final : public CancelableRunnable { public: explicit NotifyUnhandledRejections(PromiseArray&& aPromises) : CancelableRunnable("NotifyUnhandledRejections"), mUnhandledRejections(std::move(aPromises)) {} NS_IMETHOD Run() final; nsresult Cancel() final; private: PromiseArray mUnhandledRejections; }; FinalizationRegistryCleanup mFinalizationRegistryCleanup; }; class MOZ_STACK_CLASS nsAutoMicroTask { public: nsAutoMicroTask() { CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get(); if (ccjs) { ccjs->EnterMicroTask(); } } MOZ_CAN_RUN_SCRIPT ~nsAutoMicroTask() { CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get(); if (ccjs) { ccjs->LeaveMicroTask(); } } }; } // namespace mozilla #endif // mozilla_CycleCollectedJSContext_h