Global Metrics

path: .metrics.nexits.average
old: 0.4878048780487805
new: 0.625

path: .metrics.nexits.sum
old: 20.0
new: 30.0

path: .metrics.halstead.n1
old: 25.0
new: 36.0

path: .metrics.halstead.effort
old: 378162.4170361553
new: 1379247.0120300471

path: .metrics.halstead.difficulty
old: 53.45744680851064
new: 56.72727272727273

path: .metrics.halstead.volume
old: 7074.08302515395
new: 24313.649250529677

path: .metrics.halstead.length
old: 1026.0
new: 2814.0

path: .metrics.halstead.level
old: 0.018706467661691543
new: 0.017628205128205128

path: .metrics.halstead.time
old: 21009.023168675296
new: 76624.83400166928

path: .metrics.halstead.n2
old: 94.0
new: 363.0

path: .metrics.halstead.N2
old: 402.0
new: 1144.0

path: .metrics.halstead.purity_ratio
old: 0.7136722775848596
new: 1.1631151538537243

path: .metrics.halstead.estimated_program_length
old: 732.227756802066
new: 3273.006042944381

path: .metrics.halstead.N1
old: 624.0
new: 1670.0

path: .metrics.halstead.vocabulary
old: 119.0
new: 399.0

path: .metrics.halstead.bugs
old: 1.7431348619807203
new: 4.130222483102637

path: .metrics.cognitive.average
old: 0.2682926829268293
new: 1.6666666666666667

path: .metrics.cognitive.sum
old: 11.0
new: 80.0

path: .metrics.cyclomatic.sum
old: 62.0
new: 117.0

path: .metrics.cyclomatic.average
old: 1.1923076923076923
new: 2.1666666666666665

path: .metrics.mi.mi_original
old: 16.169699714937437
new: -17.62013810539986

path: .metrics.mi.mi_sei
old: -9.435135225245354
new: -63.81864955874518

path: .metrics.mi.mi_visual_studio
old: 9.455964745577448
new: 0.0

path: .metrics.nargs.average
old: 0.7073170731707317
new: 0.7916666666666666

path: .metrics.nargs.sum
old: 29.0
new: 38.0

path: .metrics.loc.blank
old: 46.0
new: 144.0

path: .metrics.loc.cloc
old: 96.0
new: 100.0

path: .metrics.loc.sloc
old: 341.0
new: 846.0

path: .metrics.loc.lloc
old: 68.0
new: 244.0

path: .metrics.loc.ploc
old: 199.0
new: 602.0

path: .metrics.nom.closures
old: 0.0
new: 1.0

path: .metrics.nom.functions
old: 41.0
new: 47.0

path: .metrics.nom.total
old: 41.0
new: 48.0

Spaces Data

Minimal test - lines (56, 846)

path: .spaces[0].metrics.mi.mi_original
old: 17.6021940662407
new: -16.117964318349593

path: .spaces[0].metrics.mi.mi_visual_studio
old: 10.293680740491638
new: 0.0

path: .spaces[0].metrics.mi.mi_sei
old: -7.596460608291643
new: -61.69191024611536

path: .spaces[0].metrics.cyclomatic.average
old: 1.196078431372549
new: 2.188679245283019

path: .spaces[0].metrics.cyclomatic.sum
old: 61.0
new: 116.0

path: .spaces[0].metrics.cognitive.sum
old: 11.0
new: 80.0

path: .spaces[0].metrics.cognitive.average
old: 0.2682926829268293
new: 1.6666666666666667

path: .spaces[0].metrics.loc.sloc
old: 319.0
new: 791.0

path: .spaces[0].metrics.loc.blank
old: 41.0
new: 140.0

path: .spaces[0].metrics.loc.lloc
old: 68.0
new: 244.0

path: .spaces[0].metrics.loc.ploc
old: 189.0
new: 557.0

path: .spaces[0].metrics.loc.cloc
old: 89.0
new: 94.0

path: .spaces[0].metrics.nargs.sum
old: 29.0
new: 38.0

path: .spaces[0].metrics.nargs.average
old: 0.7073170731707317
new: 0.7916666666666666

path: .spaces[0].metrics.nom.total
old: 41.0
new: 48.0

path: .spaces[0].metrics.nom.closures
old: 0.0
new: 1.0

path: .spaces[0].metrics.nom.functions
old: 41.0
new: 47.0

path: .spaces[0].metrics.nexits.average
old: 0.4878048780487805
new: 0.625

path: .spaces[0].metrics.nexits.sum
old: 20.0
new: 30.0

path: .spaces[0].metrics.halstead.bugs
old: 1.7936190399834078
new: 4.268569883759849

path: .spaces[0].metrics.halstead.estimated_program_length
old: 668.7551736487485
new: 2858.9011654181827

path: .spaces[0].metrics.halstead.n1
old: 25.0
new: 36.0

path: .spaces[0].metrics.halstead.difficulty
old: 57.122093023255815
new: 61.73831775700935

path: .spaces[0].metrics.halstead.purity_ratio
old: 0.6575763752691726
new: 1.032840016408303

path: .spaces[0].metrics.halstead.N2
old: 393.0
new: 1101.0

path: .spaces[0].metrics.halstead.vocabulary
old: 111.0
new: 357.0

path: .spaces[0].metrics.halstead.time
old: 21928.285916332985
new: 80506.87532612658

path: .spaces[0].metrics.halstead.length
old: 1017.0
new: 2768.0

path: .spaces[0].metrics.halstead.n2
old: 86.0
new: 321.0

path: .spaces[0].metrics.halstead.level
old: 0.017506361323155216
new: 0.016197396306388133

path: .spaces[0].metrics.halstead.N1
old: 624.0
new: 1667.0

path: .spaces[0].metrics.halstead.effort
old: 394709.1464939937
new: 1449123.7558702785

path: .spaces[0].metrics.halstead.volume
old: 6909.920936078058
new: 23472.031770832546

Code

namespace mozilla {

CycleCollectedJSContext::CycleCollectedJSContext()
    : mRuntime(nullptr),
      mJSContext(nullptr),
      mDoingStableStates(false),
      mTargetedMicroTaskRecursionDepth(0),
      mMicroTaskLevel(0),
      mDebuggerRecursionDepth(0),
      mMicroTaskRecursionDepth(0),
      mFinalizationRegistryCleanup(this) {
  MOZ_COUNT_CTOR(CycleCollectedJSContext);

  nsCOMPtr thread = do_GetCurrentThread();
  mOwningThread = thread.forget().downcast().take();
  MOZ_RELEASE_ASSERT(mOwningThread);
}

CycleCollectedJSContext::~CycleCollectedJSContext() {
  MOZ_COUNT_DTOR(CycleCollectedJSContext);
  // If the allocation failed, here we are.
  if (!mJSContext) {
    return;
  }

  JS::SetHostCleanupFinalizationRegistryCallback(mJSContext, nullptr, nullptr);

  JS_SetContextPrivate(mJSContext, nullptr);

  mRuntime->SetContext(nullptr);
  mRuntime->Shutdown(mJSContext);

  // Last chance to process any events.
  CleanupIDBTransactions(mBaseRecursionDepth);
  MOZ_ASSERT(mPendingIDBTransactions.IsEmpty());

  ProcessStableStateQueue();
  MOZ_ASSERT(mStableStateEvents.IsEmpty());

  // Clear mPendingException first, since it might be cycle collected.
  mPendingException = nullptr;

  MOZ_ASSERT(mDebuggerMicroTaskQueue.empty());
  MOZ_ASSERT(mPendingMicroTaskRunnables.empty());

  mUncaughtRejections.reset();
  mConsumedRejections.reset();

  mAboutToBeNotifiedRejectedPromises.Clear();
  mPendingUnhandledRejections.Clear();

  mFinalizationRegistryCleanup.Destroy();

  JS_DestroyContext(mJSContext);
  mJSContext = nullptr;

  nsCycleCollector_forgetJSContext();

  mozilla::dom::DestroyScriptSettings();

  mOwningThread->SetScriptObserver(nullptr);
  NS_RELEASE(mOwningThread);

  delete mRuntime;
  mRuntime = nullptr;
}

nsresult CycleCollectedJSContext::Initialize(JSRuntime* aParentRuntime,
                                             uint32_t aMaxBytes) {
  MOZ_ASSERT(!mJSContext);

  mozilla::dom::InitScriptSettings();
  mJSContext = JS_NewContext(aMaxBytes, aParentRuntime);
  if (!mJSContext) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  mRuntime = CreateRuntime(mJSContext);
  mRuntime->SetContext(this);

  mOwningThread->SetScriptObserver(this);
  // The main thread has a base recursion depth of 0, workers of 1.
  mBaseRecursionDepth = RecursionDepth();

  NS_GetCurrentThread()->SetCanInvokeJS(true);

  JS::SetJobQueue(mJSContext, this);
  JS::SetPromiseRejectionTrackerCallback(mJSContext,
                                         PromiseRejectionTrackerCallback, this);
  mUncaughtRejections.init(mJSContext,
                           JS::GCVector(
                               js::SystemAllocPolicy()));
  mConsumedRejections.init(mJSContext,
                           JS::GCVector(
                               js::SystemAllocPolicy()));

  mFinalizationRegistryCleanup.Init();

  // Cast to PerThreadAtomCache for dom::GetAtomCache(JSContext*).
  JS_SetContextPrivate(mJSContext, static_cast(this));

  nsCycleCollector_registerJSContext(this);

  return NS_OK;
}

/* static */
CycleCollectedJSContext* CycleCollectedJSContext::GetFor(JSContext* aCx) {
  // Cast from void* matching JS_SetContextPrivate.
  auto atomCache = static_cast(JS_GetContextPrivate(aCx));
  // Down cast.
  return static_cast(atomCache);
}

size_t CycleCollectedJSContext::SizeOfExcludingThis(
    MallocSizeOf aMallocSizeOf) const {
  return 0;
}

class PromiseJobRunnable final : public MicroTaskRunnable {
 public:
  PromiseJobRunnable(JS::HandleObject aPromise, JS::HandleObject aCallback,
                     JS::HandleObject aCallbackGlobal,
                     JS::HandleObject aAllocationSite,
                     nsIGlobalObject* aIncumbentGlobal)
      : mCallback(new PromiseJobCallback(aCallback, aCallbackGlobal,
                                         aAllocationSite, aIncumbentGlobal)),
        mPropagateUserInputEventHandling(false) {
    MOZ_ASSERT(js::IsFunctionObject(aCallback));

    if (aPromise) {
      JS::PromiseUserInputEventHandlingState state =
          JS::GetPromiseUserInputEventHandlingState(aPromise);
      mPropagateUserInputEventHandling =
          state ==
          JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation;
    }
  }

  virtual ~PromiseJobRunnable() = default;

 protected:
  MOZ_CAN_RUN_SCRIPT
  virtual void Run(AutoSlowOperation& aAso) override {
    JSObject* callback = mCallback->CallbackPreserveColor();
    nsIGlobalObject* global = callback ? xpc::NativeGlobal(callback) : nullptr;
    if (global && !global->IsDying()) {
      // Propagate the user input event handling bit if needed.
      nsCOMPtr win = do_QueryInterface(global);
      RefPtr doc;
      if (win) {
        doc = win->GetExtantDoc();
      }
      AutoHandlingUserInputStatePusher userInpStatePusher(
          mPropagateUserInputEventHandling);

      mCallback->Call("promise callback");
      aAso.CheckForInterrupt();
    }
    // Now that mCallback is no longer needed, clear any pointers it contains to
    // JS GC things. This removes any storebuffer entries associated with those
    // pointers, which can cause problems by taking up memory and by triggering
    // minor GCs. This otherwise would not happen until the next minor GC or
    // cycle collection.
    mCallback->Reset();
  }

  virtual bool Suppressed() override {
    JSObject* callback = mCallback->CallbackPreserveColor();
    nsIGlobalObject* global = callback ? xpc::NativeGlobal(callback) : nullptr;
    return global && global->IsInSyncOperation();
  }

 private:
  const RefPtr mCallback;
  bool mPropagateUserInputEventHandling;
};

JSObject* CycleCollectedJSContext::getIncumbentGlobal(JSContext* aCx) {
  nsIGlobalObject* global = mozilla::dom::GetIncumbentGlobal();
  if (global) {
    return global->GetGlobalJSObject();
  }
  return nullptr;
}

bool CycleCollectedJSContext::enqueuePromiseJob(
    JSContext* aCx, JS::HandleObject aPromise, JS::HandleObject aJob,
    JS::HandleObject aAllocationSite, JS::HandleObject aIncumbentGlobal) {
  MOZ_ASSERT(aCx == Context());
  MOZ_ASSERT(Get() == this);

  nsIGlobalObject* global = nullptr;
  if (aIncumbentGlobal) {
    global = xpc::NativeGlobal(aIncumbentGlobal);
  }
  JS::RootedObject jobGlobal(aCx, JS::CurrentGlobalOrNull(aCx));
  RefPtr runnable = new PromiseJobRunnable(
      aPromise, aJob, jobGlobal, aAllocationSite, global);
  DispatchToMicroTask(runnable.forget());
  return true;
}

// Used only by the SpiderMonkey Debugger API, and even then only via
// JS::AutoDebuggerJobQueueInterruption, to ensure that the debuggee's queue is
// not affected; see comments in js/public/Promise.h.
void CycleCollectedJSContext::runJobs(JSContext* aCx) {
  MOZ_ASSERT(aCx == Context());
  MOZ_ASSERT(Get() == this);
  PerformMicroTaskCheckPoint();
}

bool CycleCollectedJSContext::empty() const {
  // This is our override of JS::JobQueue::empty. Since that interface is only
  // concerned with the ordinary microtask queue, not the debugger microtask
  // queue, we only report on the former.
  return mPendingMicroTaskRunnables.empty();
}

// Preserve a debuggee's microtask queue while it is interrupted by the
// debugger. See the comments for JS::AutoDebuggerJobQueueInterruption.
class CycleCollectedJSContext::SavedMicroTaskQueue
    : public JS::JobQueue::SavedJobQueue {
 public:
  explicit SavedMicroTaskQueue(CycleCollectedJSContext* ccjs) : ccjs(ccjs) {
    ccjs->mDebuggerRecursionDepth++;
    ccjs->mPendingMicroTaskRunnables.swap(mQueue);
  }

  ~SavedMicroTaskQueue() {
    MOZ_RELEASE_ASSERT(ccjs->mPendingMicroTaskRunnables.empty());
    MOZ_RELEASE_ASSERT(ccjs->mDebuggerRecursionDepth);
    ccjs->mDebuggerRecursionDepth--;
    ccjs->mPendingMicroTaskRunnables.swap(mQueue);
  }

 private:
  CycleCollectedJSContext* ccjs;
  std::queue> mQueue;
};

js::UniquePtr
CycleCollectedJSContext::saveJobQueue(JSContext* cx) {
  auto saved = js::MakeUnique(this);
  if (!saved) {
    // When MakeUnique's allocation fails, the SavedMicroTaskQueue constructor
    // is never called, so mPendingMicroTaskRunnables is still initialized.
    JS_ReportOutOfMemory(cx);
    return nullptr;
  }

  return saved;
}

/* static */
void CycleCollectedJSContext::PromiseRejectionTrackerCallback(
    JSContext* aCx, bool aMutedErrors, JS::HandleObject aPromise,
    JS::PromiseRejectionHandlingState state, void* aData) {
  CycleCollectedJSContext* self = static_cast(aData);

  MOZ_ASSERT(aCx == self->Context());
  MOZ_ASSERT(Get() == self);

  // TODO: Bug 1549351 - Promise rejection event should not be sent for
  // cross-origin scripts

  PromiseArray& aboutToBeNotified = self->mAboutToBeNotifiedRejectedPromises;
  PromiseHashtable& unhandled = self->mPendingUnhandledRejections;
  uint64_t promiseID = JS::GetPromiseID(aPromise);

  if (state == JS::PromiseRejectionHandlingState::Unhandled) {
    PromiseDebugging::AddUncaughtRejection(aPromise);
    if (!aMutedErrors) {
      RefPtr promise =
          Promise::CreateFromExisting(xpc::NativeGlobal(aPromise), aPromise);
      aboutToBeNotified.AppendElement(promise);
      unhandled.InsertOrUpdate(promiseID, std::move(promise));
    }
  } else {
    PromiseDebugging::AddConsumedRejection(aPromise);
    for (size_t i = 0; i < aboutToBeNotified.Length(); i++) {
      if (aboutToBeNotified[i] &&
          aboutToBeNotified[i]->PromiseObj() == aPromise) {
        // To avoid large amounts of memmoves, we don't shrink the vector
        // here. Instead, we filter out nullptrs when iterating over the
        // vector later.
        aboutToBeNotified[i] = nullptr;
        DebugOnly isFound = unhandled.Remove(promiseID);
        MOZ_ASSERT(isFound);
        return;
      }
    }
    RefPtr promise;
    unhandled.Remove(promiseID, getter_AddRefs(promise));
    if (!promise && !aMutedErrors) {
      nsIGlobalObject* global = xpc::NativeGlobal(aPromise);
      if (nsCOMPtr owner = do_QueryInterface(global)) {
        RootedDictionary init(aCx);
        init.mPromise = Promise::CreateFromExisting(global, aPromise);
        init.mReason = JS::GetPromiseResult(aPromise);

        RefPtr event =
            PromiseRejectionEvent::Constructor(owner, u"rejectionhandled"_ns,
                                               init);

        RefPtr asyncDispatcher =
            new AsyncEventDispatcher(owner, event);
        asyncDispatcher->PostDOMEvent();
      }
    }
  }
}

already_AddRefed CycleCollectedJSContext::GetPendingException()
    const {
  MOZ_ASSERT(mJSContext);

  nsCOMPtr out = mPendingException;
  return out.forget();
}

void CycleCollectedJSContext::SetPendingException(Exception* aException) {
  MOZ_ASSERT(mJSContext);
  mPendingException = aException;
}

std::queue>&
CycleCollectedJSContext::GetMicroTaskQueue() {
  MOZ_ASSERT(mJSContext);
  return mPendingMicroTaskRunnables;
}

std::queue>&
CycleCollectedJSContext::GetDebuggerMicroTaskQueue() {
  MOZ_ASSERT(mJSContext);
  return mDebuggerMicroTaskQueue;
}

void CycleCollectedJSContext::ProcessStableStateQueue() {
  MOZ_ASSERT(mJSContext);
  MOZ_RELEASE_ASSERT(!mDoingStableStates);
  mDoingStableStates = true;

  // When run, one event can add another event to the mStableStateEvents, as
  // such you can't use iterators here.
  for (uint32_t i = 0; i < mStableStateEvents.Length(); ++i) {
    nsCOMPtr event = std::move(mStableStateEvents[i]);
    event->Run();
  }

  mStableStateEvents.Clear();
  mDoingStableStates = false;
}

void CycleCollectedJSContext::CleanupIDBTransactions(uint32_t aRecursionDepth) {
  MOZ_ASSERT(mJSContext);
  MOZ_RELEASE_ASSERT(!mDoingStableStates);
  mDoingStableStates = true;

  nsTArray localQueue =
      std::move(mPendingIDBTransactions);

  localQueue.RemoveLastElements(
      localQueue.end() -
      std::remove_if(localQueue.begin(), localQueue.end(),
                     [aRecursionDepth](PendingIDBTransactionData& data) {
                       if (data.mRecursionDepth != aRecursionDepth) {
                         return false;
                       }

                       {
                         nsCOMPtr transaction =
                             std::move(data.mTransaction);
                         transaction->Run();
                       }

                       return true;
                     }));

  // If mPendingIDBTransactions has events in it now, they were added from
  // something we called, so they belong at the end of the queue.
  localQueue.AppendElements(std::move(mPendingIDBTransactions));
  mPendingIDBTransactions = std::move(localQueue);
  mDoingStableStates = false;
}

void CycleCollectedJSContext::BeforeProcessTask(bool aMightBlock) {
  // If ProcessNextEvent was called during a microtask callback, we
  // must process any pending microtasks before blocking in the event loop,
  // otherwise we may deadlock until an event enters the queue later.
  if (aMightBlock && PerformMicroTaskCheckPoint()) {
    // If any microtask was processed, we post a dummy event in order to
    // force the ProcessNextEvent call not to block.  This is required
    // to support nested event loops implemented using a pattern like
    // "while (condition) thread.processNextEvent(true)", in case the
    // condition is triggered here by a Promise "then" callback.
    NS_DispatchToMainThread(new Runnable("BeforeProcessTask"));
  }
}

void CycleCollectedJSContext::AfterProcessTask(uint32_t aRecursionDepth) {
  MOZ_ASSERT(mJSContext);

  // See HTML 6.1.4.2 Processing model

  // Step 4.1: Execute microtasks.
  PerformMicroTaskCheckPoint();

  // Step 4.2 Execute any events that were waiting for a stable state.
  ProcessStableStateQueue();

  // This should be a fast test so that it won't affect the next task
  // processing.
  IsIdleGCTaskNeeded();
}

void CycleCollectedJSContext::AfterProcessMicrotasks() {
  MOZ_ASSERT(mJSContext);
  // Notify unhandled promise rejections:
  // https://html.spec.whatwg.org/multipage/webappapis.html#notify-about-rejected-promises
  if (mAboutToBeNotifiedRejectedPromises.Length()) {
    RefPtr runnable = new NotifyUnhandledRejections(
        this, std::move(mAboutToBeNotifiedRejectedPromises));
    NS_DispatchToCurrentThread(runnable);
  }
  // Cleanup Indexed Database transactions:
  // https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
  CleanupIDBTransactions(RecursionDepth());

  // Clear kept alive objects in JS WeakRef.
  // https://whatpr.org/html/4571/webappapis.html#perform-a-microtask-checkpoint
  //
  // ECMAScript implementations are expected to call ClearKeptObjects when a
  // synchronous sequence of ECMAScript execution completes.
  //
  // https://tc39.es/proposal-weakrefs/#sec-clear-kept-objects
  JS::ClearKeptObjects(mJSContext);
}

void CycleCollectedJSContext::IsIdleGCTaskNeeded() const {
  class IdleTimeGCTaskRunnable : public mozilla::IdleRunnable {
   public:
    using mozilla::IdleRunnable::IdleRunnable;

   public:
    NS_IMETHOD Run() override {
      CycleCollectedJSRuntime* ccrt = CycleCollectedJSRuntime::Get();
      if (ccrt) {
        ccrt->RunIdleTimeGCTask();
      }
      return NS_OK;
    }
  };

  if (Runtime()->IsIdleGCTaskNeeded()) {
    nsCOMPtr gc_task = new IdleTimeGCTaskRunnable();
    NS_DispatchToCurrentThreadQueue(gc_task.forget(), EventQueuePriority::Idle);
    Runtime()->SetPendingIdleGCTask();
  }
}

uint32_t CycleCollectedJSContext::RecursionDepth() const {
  // Debugger interruptions are included in the recursion depth so that debugger
  // microtask checkpoints do not run IDB transactions which were initiated
  // before the interruption.
  return mOwningThread->RecursionDepth() + mDebuggerRecursionDepth;
}

void CycleCollectedJSContext::RunInStableState(
    already_AddRefed&& aRunnable) {
  MOZ_ASSERT(mJSContext);
  mStableStateEvents.AppendElement(std::move(aRunnable));
}

void CycleCollectedJSContext::AddPendingIDBTransaction(
    already_AddRefed&& aTransaction) {
  MOZ_ASSERT(mJSContext);

  PendingIDBTransactionData data;
  data.mTransaction = aTransaction;

  MOZ_ASSERT(mOwningThread);
  data.mRecursionDepth = RecursionDepth();

  // There must be an event running to get here.
#ifndef MOZ_WIDGET_COCOA
  MOZ_ASSERT(data.mRecursionDepth > mBaseRecursionDepth);
#else
  // XXX bug 1261143
  // Recursion depth should be greater than mBaseRecursionDepth,
  // or the runnable will stay in the queue forever.
  if (data.mRecursionDepth <= mBaseRecursionDepth) {
    data.mRecursionDepth = mBaseRecursionDepth + 1;
  }
#endif

  mPendingIDBTransactions.AppendElement(std::move(data));
}

void CycleCollectedJSContext::DispatchToMicroTask(
    already_AddRefed aRunnable) {
  RefPtr runnable(aRunnable);

  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(runnable);

  JS::JobQueueMayNotBeEmpty(Context());

  LogMicroTaskRunnable::LogDispatch(runnable.get());
  mPendingMicroTaskRunnables.push(std::move(runnable));
}

class AsyncMutationHandler final : public mozilla::Runnable {
 public:
  AsyncMutationHandler() : mozilla::Runnable("AsyncMutationHandler") {}

  // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT.  See
  // bug 1535398.
  MOZ_CAN_RUN_SCRIPT_BOUNDARY
  NS_IMETHOD Run() override {
    CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
    if (ccjs) {
      ccjs->PerformMicroTaskCheckPoint();
    }
    return NS_OK;
  }
};

bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) {
  if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
    AfterProcessMicrotasks();
    // Nothing to do, return early.
    return false;
  }

  uint32_t currentDepth = RecursionDepth();
  if (mMicroTaskRecursionDepth >= currentDepth && !aForce) {
    // We are already executing microtasks for the current recursion depth.
    return false;
  }

  if (mTargetedMicroTaskRecursionDepth != 0 &&
      mTargetedMicroTaskRecursionDepth + mDebuggerRecursionDepth !=
          currentDepth) {
    return false;
  }

  if (NS_IsMainThread() && !nsContentUtils::IsSafeToRunScript()) {
    // Special case for main thread where DOM mutations may happen when
    // it is not safe to run scripts.
    nsContentUtils::AddScriptRunner(new AsyncMutationHandler());
    return false;
  }

  mozilla::AutoRestore restore(mMicroTaskRecursionDepth);
  MOZ_ASSERT(aForce ? currentDepth == 0 : currentDepth > 0);
  mMicroTaskRecursionDepth = currentDepth;

  AUTO_PROFILER_TRACING_MARKER("JS", "Perform microtasks", JS);

  bool didProcess = false;
  AutoSlowOperation aso;

  std::queue> suppressed;
  for (;;) {
    RefPtr runnable;
    if (!mDebuggerMicroTaskQueue.empty()) {
      runnable = std::move(mDebuggerMicroTaskQueue.front());
      mDebuggerMicroTaskQueue.pop();
    } else if (!mPendingMicroTaskRunnables.empty()) {
      runnable = std::move(mPendingMicroTaskRunnables.front());
      mPendingMicroTaskRunnables.pop();
    } else {
      break;
    }

    if (runnable->Suppressed()) {
      // Microtasks in worker shall never be suppressed.
      // Otherwise, mPendingMicroTaskRunnables will be replaced later with
      // all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly.
      MOZ_ASSERT(NS_IsMainThread());
      JS::JobQueueMayNotBeEmpty(Context());
      suppressed.push(runnable);
    } else {
      if (mPendingMicroTaskRunnables.empty() &&
          mDebuggerMicroTaskQueue.empty() && suppressed.empty()) {
        JS::JobQueueIsEmpty(Context());
      }
      didProcess = true;

      LogMicroTaskRunnable::Run log(runnable.get());
      runnable->Run(aso);
      runnable = nullptr;
    }
  }

  // Put back the suppressed microtasks so that they will be run later.
  // Note, it is possible that we end up keeping these suppressed tasks around
  // for some time, but no longer than spinning the event loop nestedly
  // (sync XHR, alert, etc.)
  mPendingMicroTaskRunnables.swap(suppressed);

  AfterProcessMicrotasks();

  return didProcess;
}

void CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint() {
  // Don't do normal microtask handling checks here, since whoever is calling
  // this method is supposed to know what they are doing.

  AutoSlowOperation aso;
  for (;;) {
    // For a debugger microtask checkpoint, we always use the debugger microtask
    // queue.
    std::queue>* microtaskQueue =
        &GetDebuggerMicroTaskQueue();

    if (microtaskQueue->empty()) {
      break;
    }

    RefPtr runnable = std::move(microtaskQueue->front());
    MOZ_ASSERT(runnable);

    LogMicroTaskRunnable::Run log(runnable.get());

    // This function can re-enter, so we remove the element before calling.
    microtaskQueue->pop();

    if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
      JS::JobQueueIsEmpty(Context());
    }
    runnable->Run(aso);
    runnable = nullptr;
  }

  AfterProcessMicrotasks();
}

NS_IMETHODIMP CycleCollectedJSContext::NotifyUnhandledRejections::Run() {
  for (size_t i = 0; i < mUnhandledRejections.Length(); ++i) {
    RefPtr& promise = mUnhandledRejections[i];
    if (!promise) {
      continue;
    }

    JS::RootingContext* cx = mCx->RootingCx();
    JS::RootedObject promiseObj(cx, promise->PromiseObj());
    MOZ_ASSERT(JS::IsPromiseObject(promiseObj));

    // Only fire unhandledrejection if the promise is still not handled;
    uint64_t promiseID = JS::GetPromiseID(promiseObj);
    if (!JS::GetPromiseIsHandled(promiseObj)) {
      if (nsCOMPtr target =
              do_QueryInterface(promise->GetParentObject())) {
        RootedDictionary init(cx);
        init.mPromise = promise;
        init.mReason = JS::GetPromiseResult(promiseObj);
        init.mCancelable = true;

        RefPtr event =
            PromiseRejectionEvent::Constructor(target, u"unhandledrejection"_ns,
                                               init);
        // We don't use the result of dispatching event here to check whether to
        // report the Promise to console.
        target->DispatchEvent(*event);
      }
    }

    if (!JS::GetPromiseIsHandled(promiseObj)) {
      DebugOnly isFound =
          mCx->mPendingUnhandledRejections.Remove(promiseID);
      MOZ_ASSERT(isFound);
    }

    // If a rejected promise is being handled in "unhandledrejection" event
    // handler, it should be removed from the table in
    // PromiseRejectionTrackerCallback.
    MOZ_ASSERT(!mCx->mPendingUnhandledRejections.Lookup(promiseID));
  }
  return NS_OK;
}

nsresult CycleCollectedJSContext::NotifyUnhandledRejections::Cancel() {
  for (size_t i = 0; i < mUnhandledRejections.Length(); ++i) {
    RefPtr& promise = mUnhandledRejections[i];
    if (!promise) {
      continue;
    }

    JS::RootedObject promiseObj(mCx->RootingCx(), promise->PromiseObj());
    mCx->mPendingUnhandledRejections.Remove(JS::GetPromiseID(promiseObj));
  }
  return NS_OK;
}

class FinalizationRegistryCleanup::CleanupRunnable
    : public DiscardableRunnable {
 public:
  explicit CleanupRunnable(FinalizationRegistryCleanup* aCleanupWork)
      : DiscardableRunnable("CleanupRunnable"), mCleanupWork(aCleanupWork) {}

  // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT.  See
  // bug 1535398.
  MOZ_CAN_RUN_SCRIPT_BOUNDARY
  NS_IMETHOD Run() override {
    mCleanupWork->DoCleanup();
    return NS_OK;
  }

 private:
  FinalizationRegistryCleanup* mCleanupWork;
};

FinalizationRegistryCleanup::FinalizationRegistryCleanup(
    CycleCollectedJSContext* aContext)
    : mContext(aContext) {}

void FinalizationRegistryCleanup::Destroy() {
  // This must happen before the CycleCollectedJSContext destructor calls
  // JS_DestroyContext().
  mCallbacks.reset();
}

void FinalizationRegistryCleanup::Init() {
  JSContext* cx = mContext->Context();
  mCallbacks.init(cx);
  JS::SetHostCleanupFinalizationRegistryCallback(cx, QueueCallback, this);
}

/* static */
void FinalizationRegistryCleanup::QueueCallback(JSFunction* aDoCleanup,
                                                JSObject* aIncumbentGlobal,
                                                void* aData) {
  FinalizationRegistryCleanup* cleanup =
      static_cast(aData);
  cleanup->QueueCallback(aDoCleanup, aIncumbentGlobal);
}

void FinalizationRegistryCleanup::QueueCallback(JSFunction* aDoCleanup,
                                                JSObject* aIncumbentGlobal) {
  bool firstCallback = mCallbacks.empty();

  MOZ_ALWAYS_TRUE(mCallbacks.append(Callback{aDoCleanup, aIncumbentGlobal}));

  if (firstCallback) {
    RefPtr cleanup = new CleanupRunnable(this);
    NS_DispatchToCurrentThread(cleanup.forget());
  }
}

void FinalizationRegistryCleanup::DoCleanup() {
  if (mCallbacks.empty()) {
    return;
  }

  JS::RootingContext* cx = mContext->RootingCx();

  JS::Rooted callbacks(cx);
  std::swap(callbacks.get(), mCallbacks.get());

  for (const Callback& callback : callbacks) {
    JS::RootedObject functionObj(
        cx, JS_GetFunctionObject(callback.mCallbackFunction));
    JS::RootedObject globalObj(cx, JS::GetNonCCWObjectGlobal(functionObj));

    nsIGlobalObject* incumbentGlobal =
        xpc::NativeGlobal(callback.mIncumbentGlobal);
    if (!incumbentGlobal) {
      continue;
    }

    RefPtr cleanupCallback(
        new FinalizationRegistryCleanupCallback(functionObj, globalObj, nullptr,
                                                incumbentGlobal));

    nsIGlobalObject* global =
        xpc::NativeGlobal(cleanupCallback->CallbackPreserveColor());
    if (global) {
      cleanupCallback->Call("FinalizationRegistryCleanup::DoCleanup");
    }
  }
}

void FinalizationRegistryCleanup::Callback::trace(JSTracer* trc) {
  JS::UnsafeTraceRoot(trc, &mCallbackFunction, "mCallbackFunction");
  JS::UnsafeTraceRoot(trc, &mIncumbentGlobal, "mIncumbentGlobal");
}

}  // namespace mozilla

Minimal test - lines (58, 72)

path: .spaces[0].spaces[0].metrics.mi.mi_original
old: 17.954319353786886
new: 96.54310340835048

path: .spaces[0].spaces[0].metrics.mi.mi_visual_studio
old: 10.49960196127888
new: 56.45795520956168

path: .spaces[0].spaces[0].metrics.mi.mi_sei
old: -7.26031241019988
new: 63.683224386649314

path: .spaces[0].spaces[0].metrics.cyclomatic.average
old: 1.2
new: 1.0

path: .spaces[0].spaces[0].metrics.cyclomatic.sum
old: 60.0
new: 1.0

path: .spaces[0].spaces[0].metrics.nom.total
old: 41.0
new: 1.0

path: .spaces[0].spaces[0].metrics.nom.functions
old: 41.0
new: 1.0

path: .spaces[0].spaces[0].metrics.nargs.average
old: 0.7073170731707317
new: 0.0

path: .spaces[0].spaces[0].metrics.nargs.sum
old: 29.0
new: 0.0

path: .spaces[0].spaces[0].metrics.loc.ploc
old: 187.0
new: 14.0

path: .spaces[0].spaces[0].metrics.loc.lloc
old: 68.0
new: 3.0

path: .spaces[0].spaces[0].metrics.loc.sloc
old: 317.0
new: 15.0

path: .spaces[0].spaces[0].metrics.loc.cloc
old: 88.0
new: 0.0

path: .spaces[0].spaces[0].metrics.loc.blank
old: 42.0
new: 1.0

path: .spaces[0].spaces[0].metrics.nexits.sum
old: 20.0
new: 0.0

path: .spaces[0].spaces[0].metrics.nexits.average
old: 0.4878048780487805
new: 0.0

path: .spaces[0].spaces[0].metrics.halstead.length
old: 1015.0
new: 68.0

path: .spaces[0].spaces[0].metrics.halstead.N2
old: 392.0
new: 30.0

path: .spaces[0].spaces[0].metrics.halstead.N1
old: 623.0
new: 38.0

path: .spaces[0].spaces[0].metrics.halstead.bugs
old: 1.7999152817759714
new: 0.05701638034984962

path: .spaces[0].spaces[0].metrics.halstead.level
old: 0.017346938775510204
new: 0.15333333333333335

path: .spaces[0].spaces[0].metrics.halstead.n1
old: 25.0
new: 10.0

path: .spaces[0].spaces[0].metrics.halstead.volume
old: 6883.08010922753
new: 343.01880011637485

path: .spaces[0].spaces[0].metrics.halstead.effort
old: 396789.32394370466
new: 2237.0791311937487

path: .spaces[0].spaces[0].metrics.halstead.n2
old: 85.0
new: 23.0

path: .spaces[0].spaces[0].metrics.halstead.purity_ratio
old: 0.6511277185380027
new: 2.0185471461497784

path: .spaces[0].spaces[0].metrics.halstead.time
old: 22043.851330205813
new: 124.28217395520826

path: .spaces[0].spaces[0].metrics.halstead.vocabulary
old: 110.0
new: 33.0

path: .spaces[0].spaces[0].metrics.halstead.difficulty
old: 57.64705882352941
new: 6.521739130434782

path: .spaces[0].spaces[0].metrics.halstead.estimated_program_length
old: 660.8946343160727
new: 137.26120593818493

path: .spaces[0].spaces[0].metrics.cognitive.average
old: 0.2682926829268293
new: 0.0

path: .spaces[0].spaces[0].metrics.cognitive.sum
old: 11.0
new: 0.0

Code

CycleCollectedJSContext::CycleCollectedJSContext()
    : mRuntime(nullptr),
      mJSContext(nullptr),
      mDoingStableStates(false),
      mTargetedMicroTaskRecursionDepth(0),
      mMicroTaskLevel(0),
      mDebuggerRecursionDepth(0),
      mMicroTaskRecursionDepth(0),
      mFinalizationRegistryCleanup(this) {
  MOZ_COUNT_CTOR(CycleCollectedJSContext);

  nsCOMPtr thread = do_GetCurrentThread();
  mOwningThread = thread.forget().downcast().take();
  MOZ_RELEASE_ASSERT(mOwningThread);
}