text: /**
   * Copyright (c) Facebook, Inc. and its affiliates.
   *
   * This source code is licensed under the MIT license found in the
   * LICENSE file in the root directory of this source tree.
   *
   * @flow
   */
  
  import type {Fiber} from './ReactFiber';
  import type {FiberRoot} from './ReactFiberRoot';
  import type {ExpirationTime} from './ReactFiberExpirationTime';
  import type {
    ReactPriorityLevel,
    SchedulerCallback,
  } from './SchedulerWithReactIntegration';
  import type {Interaction} from 'scheduler/src/Tracing';
  import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
  
  import {
    warnAboutDeprecatedLifecycles,
    enableUserTimingAPI,
    enableSuspenseServerRenderer,
    replayFailedUnitOfWorkWithInvokeGuardedCallback,
    enableProfilerTimer,
    enableSchedulerTracing,
    revertPassiveEffectsChange,
  } from 'shared/ReactFeatureFlags';
  import ReactSharedInternals from 'shared/ReactSharedInternals';
  import invariant from 'shared/invariant';
  import warning from 'shared/warning';
  
  import {
    scheduleCallback,
    cancelCallback,
    getCurrentPriorityLevel,
    runWithPriority,
    shouldYield,
    requestPaint,
    now,
    ImmediatePriority,
    UserBlockingPriority,
    NormalPriority,
    LowPriority,
    IdlePriority,
    flushSyncCallbackQueue,
    scheduleSyncCallback,
  } from './SchedulerWithReactIntegration';
  
  import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
  
  import {
    prepareForCommit,
    resetAfterCommit,
    scheduleTimeout,
    cancelTimeout,
    noTimeout,
  } from './ReactFiberHostConfig';
  
  import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
  import {
    NoMode,
    ProfileMode,
    BatchedMode,
    ConcurrentMode,
  } from './ReactTypeOfMode';
  import {
    HostRoot,
    ClassComponent,
    SuspenseComponent,
    DehydratedSuspenseComponent,
    FunctionComponent,
    ForwardRef,
    MemoComponent,
    SimpleMemoComponent,
  } from 'shared/ReactWorkTags';
  import {
    NoEffect,
    PerformedWork,
    Placement,
    Update,
    PlacementAndUpdate,
    Deletion,
    Ref,
    ContentReset,
    Snapshot,
    Callback,
    Passive,
    Incomplete,
    HostEffectMask,
  } from 'shared/ReactSideEffectTags';
  import {
    NoWork,
    Sync,
    Never,
    msToExpirationTime,
    expirationTimeToMs,
    computeInteractiveExpiration,
    computeAsyncExpiration,
    computeSuspenseExpiration,
    inferPriorityFromExpirationTime,
    LOW_PRIORITY_EXPIRATION,
    Batched,
  } from './ReactFiberExpirationTime';
  import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
  import {completeWork} from './ReactFiberCompleteWork';
  import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork';
  import {
    throwException,
    createRootErrorUpdate,
    createClassErrorUpdate,
  } from './ReactFiberThrow';
  import {
    commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
    commitLifeCycles as commitLayoutEffectOnFiber,
    commitPassiveHookEffects,
    commitPlacement,
    commitWork,
    commitDeletion,
    commitDetachRef,
    commitAttachRef,
    commitResetTextContent,
  } from './ReactFiberCommitWork';
  import {enqueueUpdate} from './ReactUpdateQueue';
  import {resetContextDependencies} from './ReactFiberNewContext';
  import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks';
  import {createCapturedValue} from './ReactCapturedValue';
  
  import {
    recordCommitTime,
    startProfilerTimer,
    stopProfilerTimerIfRunningAndRecordDelta,
  } from './ReactProfilerTimer';
  
  // DEV stuff
  import warningWithoutStack from 'shared/warningWithoutStack';
  import getComponentName from 'shared/getComponentName';
  import ReactStrictModeWarnings from './ReactStrictModeWarnings';
  import {
    phase as ReactCurrentDebugFiberPhaseInDEV,
    resetCurrentFiber as resetCurrentDebugFiberInDEV,
    setCurrentFiber as setCurrentDebugFiberInDEV,
    getStackByFiberInDevAndProd,
  } from './ReactCurrentFiber';
  import {
    recordEffect,
    recordScheduleUpdate,
    startRequestCallbackTimer,
    stopRequestCallbackTimer,
    startWorkTimer,
    stopWorkTimer,
    stopFailedWorkTimer,
    startWorkLoopTimer,
    stopWorkLoopTimer,
    startCommitTimer,
    stopCommitTimer,
    startCommitSnapshotEffectsTimer,
    stopCommitSnapshotEffectsTimer,
    startCommitHostEffectsTimer,
    stopCommitHostEffectsTimer,
    startCommitLifeCyclesTimer,
    stopCommitLifeCyclesTimer,
  } from './ReactDebugFiberPerf';
  import {
    invokeGuardedCallback,
    hasCaughtError,
    clearCaughtError,
  } from 'shared/ReactErrorUtils';
  import {onCommitRoot} from './ReactFiberDevToolsHook';
  
  const ceil = Math.ceil;
  
  const {
    ReactCurrentDispatcher,
    ReactCurrentOwner,
    ReactCurrentActingRendererSigil,
  } = ReactSharedInternals;
  
  type ExecutionContext = number;
  
  const NoContext = /*                    */ 0b000000;
  const BatchedContext = /*               */ 0b000001;
  const EventContext = /*                 */ 0b000010;
  const DiscreteEventContext = /*         */ 0b000100;
  const LegacyUnbatchedContext = /*       */ 0b001000;
  const RenderContext = /*                */ 0b010000;
  const CommitContext = /*                */ 0b100000;
  
  type RootExitStatus = 0 | 1 | 2 | 3 | 4;
  const RootIncomplete = 0;
  const RootErrored = 1;
  const RootSuspended = 2;
  const RootSuspendedWithDelay = 3;
  const RootCompleted = 4;
  
  export type Thenable = {
    then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
  };
  
  // Describes where we are in the React execution stack
  let executionContext: ExecutionContext = NoContext;
  // The root we're working on
  let workInProgressRoot: FiberRoot | null = null;
  // The fiber we're working on
  let workInProgress: Fiber | null = null;
  // The expiration time we're rendering
  let renderExpirationTime: ExpirationTime = NoWork;
  // Whether to root completed, errored, suspended, etc.
  let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
  // Most recent event time among processed updates during this render.
  // This is conceptually a time stamp but expressed in terms of an ExpirationTime
  // because we deal mostly with expiration times in the hot path, so this avoids
  // the conversion happening in the hot path.
  let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
  let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
  let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
  // If we're pinged while rendering we don't always restart immediately.
  // This flag determines if it might be worthwhile to restart if an opportunity
  // happens latere.
  let workInProgressRootHasPendingPing: boolean = false;
  // The most recent time we committed a fallback. This lets us ensure a train
  // model where we don't commit new loading states in too quick succession.
  let globalMostRecentFallbackTime: number = 0;
  const FALLBACK_THROTTLE_MS: number = 500;
  
  let nextEffect: Fiber | null = null;
  let hasUncaughtError = false;
  let firstUncaughtError = null;
  let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
  
  let rootDoesHavePassiveEffects: boolean = false;
  let rootWithPendingPassiveEffects: FiberRoot | null = null;
  let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
  
  let rootsWithPendingDiscreteUpdates: Map<
    FiberRoot,
    ExpirationTime,
  > | null = null;
  
  // Use these to prevent an infinite loop of nested updates
  const NESTED_UPDATE_LIMIT = 50;
  let nestedUpdateCount: number = 0;
  let rootWithNestedUpdates: FiberRoot | null = null;
  
  const NESTED_PASSIVE_UPDATE_LIMIT = 50;
  let nestedPassiveUpdateCount: number = 0;
  
  let interruptedBy: Fiber | null = null;
  
  // Marks the need to reschedule pending interactions at these expiration times
  // during the commit phase. This enables them to be traced across components
  // that spawn new work during render. E.g. hidden boundaries, suspended SSR
  // hydration or SuspenseList.
  let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
  
  // Expiration times are computed by adding to the current time (the start
  // time). However, if two updates are scheduled within the same event, we
  // should treat their start times as simultaneous, even if the actual clock
  // time has advanced between the first and second call.
  
  // In other words, because expiration times determine how updates are batched,
  // we want all updates of like priority that occur within the same event to
  // receive the same expiration time. Otherwise we get tearing.
  let currentEventTime: ExpirationTime = NoWork;
  
  export function requestCurrentTime() {
    if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
      // We're inside React, so it's fine to read the actual time.
      return msToExpirationTime(now());
    }
    // We're not inside React, so we may be in the middle of a browser event.
    if (currentEventTime !== NoWork) {
      // Use the same start time for all updates until we enter React again.
      return currentEventTime;
    }
    // This is the first update since React yielded. Compute a new start time.
    currentEventTime = msToExpirationTime(now());
    return currentEventTime;
  }
  
  export function computeExpirationForFiber(
    currentTime: ExpirationTime,
    fiber: Fiber,
    suspenseConfig: null | SuspenseConfig,
  ): ExpirationTime {
    const mode = fiber.mode;
    if ((mode & BatchedMode) === NoMode) {
      return Sync;
    }
  
    const priorityLevel = getCurrentPriorityLevel();
    if ((mode & ConcurrentMode) === NoMode) {
      return priorityLevel === ImmediatePriority ? Sync : Batched;
    }
  
    if ((executionContext & RenderContext) !== NoContext) {
      // Use whatever time we're already rendering
      return renderExpirationTime;
    }
  
    let expirationTime;
    if (suspenseConfig !== null) {
      // Compute an expiration time based on the Suspense timeout.
      expirationTime = computeSuspenseExpiration(
        currentTime,
        suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
      );
    } else {
      // Compute an expiration time based on the Scheduler priority.
      switch (priorityLevel) {
        case ImmediatePriority:
          expirationTime = Sync;
          break;
        case UserBlockingPriority:
          // TODO: Rename this to computeUserBlockingExpiration
          expirationTime = computeInteractiveExpiration(currentTime);
          break;
        case NormalPriority:
        case LowPriority: // TODO: Handle LowPriority
          // TODO: Rename this to... something better.
          expirationTime = computeAsyncExpiration(currentTime);
          break;
        case IdlePriority:
          expirationTime = Never;
          break;
        default:
          invariant(false, 'Expected a valid priority level');
      }
    }
  
    // If we're in the middle of rendering a tree, do not update at the same
    // expiration time that is already rendering.
    // TODO: We shouldn't have to do this if the update is on a different root.
    // Refactor computeExpirationForFiber + scheduleUpdate so we have access to
    // the root when we check for this condition.
    if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
      // This is a trick to move this update into a separate batch
      expirationTime -= 1;
    }
  
    return expirationTime;
  }
  
  let lastUniqueAsyncExpiration = NoWork;
  export function computeUniqueAsyncExpiration(): ExpirationTime {
    const currentTime = requestCurrentTime();
    let result = computeAsyncExpiration(currentTime);
    if (result <= lastUniqueAsyncExpiration) {
      // Since we assume the current time monotonically increases, we only hit
      // this branch when computeUniqueAsyncExpiration is fired multiple times
      // within a 200ms window (or whatever the async bucket size is).
      result -= 1;
    }
    lastUniqueAsyncExpiration = result;
    return result;
  }
  
  export function scheduleUpdateOnFiber(
    fiber: Fiber,
    expirationTime: ExpirationTime,
  ) {
    checkForNestedUpdates();
    warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
  
    const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
    if (root === null) {
      warnAboutUpdateOnUnmountedFiberInDEV(fiber);
      return;
    }
  
    root.pingTime = NoWork;
  
    checkForInterruption(fiber, expirationTime);
    recordScheduleUpdate();
  
    // TODO: computeExpirationForFiber also reads the priority. Pass the
    // priority as an argument to that function and this one.
    const priorityLevel = getCurrentPriorityLevel();
  
    if (expirationTime === Sync) {
      if (
        // Check if we're inside unbatchedUpdates
        (executionContext & LegacyUnbatchedContext) !== NoContext &&
        // Check if we're not already rendering
        (executionContext & (RenderContext | CommitContext)) === NoContext
      ) {
        // Register pending interactions on the root to avoid losing traced interaction data.
        schedulePendingInteractions(root, expirationTime);
  
        // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
        // root inside of batchedUpdates should be synchronous, but layout updates
        // should be deferred until the end of the batch.
        let callback = renderRoot(root, Sync, true);
        while (callback !== null) {
          callback = callback(true);
        }
      } else {
        scheduleCallbackForRoot(root, ImmediatePriority, Sync);
        if (executionContext === NoContext) {
          // Flush the synchronous work now, wnless we're already working or inside
          // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
          // scheduleCallbackForFiber to preserve the ability to schedule a callback
          // without immediately flushing it. We only do this for user-initated
          // updates, to preserve historical behavior of sync mode.
          flushSyncCallbackQueue();
        }
      }
    } else {
      scheduleCallbackForRoot(root, priorityLevel, expirationTime);
    }
  
    if (
      (executionContext & DiscreteEventContext) !== NoContext &&
      // Only updates at user-blocking priority or greater are considered
      // discrete, even inside a discrete event.
      (priorityLevel === UserBlockingPriority ||
        priorityLevel === ImmediatePriority)
    ) {
      // This is the result of a discrete event. Track the lowest priority
      // discrete update per root so we can flush them early, if needed.
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
      } else {
        const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
        if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
          rootsWithPendingDiscreteUpdates.set(root, expirationTime);
        }
      }
    }
  }
  export const scheduleWork = scheduleUpdateOnFiber;
  
  // This is split into a separate function so we can mark a fiber with pending
  // work without treating it as a typical update that originates from an event;
  // e.g. retrying a Suspense boundary isn't an update, but it does schedule work
  // on a fiber.
  function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
    // Update the source fiber's expiration time
    if (fiber.expirationTime < expirationTime) {
      fiber.expirationTime = expirationTime;
    }
    let alternate = fiber.alternate;
    if (alternate !== null && alternate.expirationTime < expirationTime) {
      alternate.expirationTime = expirationTime;
    }
    // Walk the parent path to the root and update the child expiration time.
    let node = fiber.return;
    let root = null;
    if (node === null && fiber.tag === HostRoot) {
      root = fiber.stateNode;
    } else {
      while (node !== null) {
        alternate = node.alternate;
        if (node.childExpirationTime < expirationTime) {
          node.childExpirationTime = expirationTime;
          if (
            alternate !== null &&
            alternate.childExpirationTime < expirationTime
          ) {
            alternate.childExpirationTime = expirationTime;
          }
        } else if (
          alternate !== null &&
          alternate.childExpirationTime < expirationTime
        ) {
          alternate.childExpirationTime = expirationTime;
        }
        if (node.return === null && node.tag === HostRoot) {
          root = node.stateNode;
          break;
        }
        node = node.return;
      }
    }
  
    if (root !== null) {
      // Update the first and last pending expiration times in this root
      const firstPendingTime = root.firstPendingTime;
      if (expirationTime > firstPendingTime) {
        root.firstPendingTime = expirationTime;
      }
      const lastPendingTime = root.lastPendingTime;
      if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
        root.lastPendingTime = expirationTime;
      }
    }
  
    return root;
  }
  
  // Use this function, along with runRootCallback, to ensure that only a single
  // callback per root is scheduled. It's still possible to call renderRoot
  // directly, but scheduling via this function helps avoid excessive callbacks.
  // It works by storing the callback node and expiration time on the root. When a
  // new callback comes in, it compares the expiration time to determine if it
  // should cancel the previous one. It also relies on commitRoot scheduling a
  // callback to render the next level, because that means we don't need a
  // separate callback per expiration time.
  function scheduleCallbackForRoot(
    root: FiberRoot,
    priorityLevel: ReactPriorityLevel,
    expirationTime: ExpirationTime,
  ) {
    const existingCallbackExpirationTime = root.callbackExpirationTime;
    if (existingCallbackExpirationTime < expirationTime) {
      // New callback has higher priority than the existing one.
      const existingCallbackNode = root.callbackNode;
      if (existingCallbackNode !== null) {
        cancelCallback(existingCallbackNode);
      }
      root.callbackExpirationTime = expirationTime;
  
      if (expirationTime === Sync) {
        // Sync React callbacks are scheduled on a special internal queue
        root.callbackNode = scheduleSyncCallback(
          runRootCallback.bind(
            null,
            root,
            renderRoot.bind(null, root, expirationTime),
          ),
        );
      } else {
        let options = null;
        if (expirationTime !== Never) {
          let timeout = expirationTimeToMs(expirationTime) - now();
          options = {timeout};
        }
  
        root.callbackNode = scheduleCallback(
          priorityLevel,
          runRootCallback.bind(
            null,
            root,
            renderRoot.bind(null, root, expirationTime),
          ),
          options,
        );
        if (
          enableUserTimingAPI &&
          expirationTime !== Sync &&
          (executionContext & (RenderContext | CommitContext)) === NoContext
        ) {
          // Scheduled an async callback, and we're not already working. Add an
          // entry to the flamegraph that shows we're waiting for a callback
          // to fire.
          startRequestCallbackTimer();
        }
      }
    }
  
    // Associate the current interactions with this new root+priority.
    schedulePendingInteractions(root, expirationTime);
  }
  
  function runRootCallback(root, callback, isSync) {
    const prevCallbackNode = root.callbackNode;
    let continuation = null;
    try {
      continuation = callback(isSync);
      if (continuation !== null) {
        return runRootCallback.bind(null, root, continuation);
      } else {
        return null;
      }
    } finally {
      // If the callback exits without returning a continuation, remove the
      // corresponding callback node from the root. Unless the callback node
      // has changed, which implies that it was already cancelled by a high
      // priority update.
      if (continuation === null && prevCallbackNode === root.callbackNode) {
        root.callbackNode = null;
        root.callbackExpirationTime = NoWork;
      }
    }
  }
  
  export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
    if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
      invariant(
        false,
        'work.commit(): Cannot commit while already rendering. This likely ' +
          'means you attempted to commit from inside a lifecycle method.',
      );
    }
    scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
    flushSyncCallbackQueue();
  }
  
  export function flushDiscreteUpdates() {
    // TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
    // However, `act` uses `batchedUpdates`, so there's no way to distinguish
    // those two cases. Need to fix this before exposing flushDiscreteUpdates
    // as a public API.
    if (
      (executionContext & (BatchedContext | RenderContext | CommitContext)) !==
      NoContext
    ) {
      if (__DEV__ && (executionContext & RenderContext) !== NoContext) {
        warning(
          false,
          'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
            'already rendering.',
        );
      }
      // We're already rendering, so we can't synchronously flush pending work.
      // This is probably a nested event dispatch triggered by a lifecycle/effect,
      // like `el.focus()`. Exit.
      return;
    }
    flushPendingDiscreteUpdates();
    if (!revertPassiveEffectsChange) {
      // If the discrete updates scheduled passive effects, flush them now so that
      // they fire before the next serial event.
      flushPassiveEffects();
    }
  }
  
  function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
    const firstBatch = root.firstBatch;
    if (
      firstBatch !== null &&
      firstBatch._defer &&
      firstBatch._expirationTime >= expirationTime
    ) {
      scheduleCallback(NormalPriority, () => {
        firstBatch._onComplete();
        return null;
      });
      return true;
    } else {
      return false;
    }
  }
  
  export function deferredUpdates<A>(fn: () => A): A {
    // TODO: Remove in favor of Scheduler.next
    return runWithPriority(NormalPriority, fn);
  }
  
  export function syncUpdates<A, B, C, R>(
    fn: (A, B, C) => R,
    a: A,
    b: B,
    c: C,
  ): R {
    return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c));
  }
  
  function flushPendingDiscreteUpdates() {
    if (rootsWithPendingDiscreteUpdates !== null) {
      // For each root with pending discrete updates, schedule a callback to
      // immediately flush them.
      const roots = rootsWithPendingDiscreteUpdates;
      rootsWithPendingDiscreteUpdates = null;
      roots.forEach((expirationTime, root) => {
        scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
      });
      // Now flush the immediate queue.
      flushSyncCallbackQueue();
    }
  }
  
  export function batchedUpdates<A, R>(fn: A => R, a: A): R {
    const prevExecutionContext = executionContext;
    executionContext |= BatchedContext;
    try {
      return fn(a);
    } finally {
      executionContext = prevExecutionContext;
      if (executionContext === NoContext) {
        // Flush the immediate callbacks that were scheduled during this batch
        flushSyncCallbackQueue();
      }
    }
  }
  
  export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
    const prevExecutionContext = executionContext;
    executionContext |= EventContext;
    try {
      return fn(a);
    } finally {
      executionContext = prevExecutionContext;
      if (executionContext === NoContext) {
        // Flush the immediate callbacks that were scheduled during this batch
        flushSyncCallbackQueue();
      }
    }
  }
  
  export function discreteUpdates<A, B, C, R>(
    fn: (A, B, C) => R,
    a: A,
    b: B,
    c: C,
  ): R {
    const prevExecutionContext = executionContext;
    executionContext |= DiscreteEventContext;
    try {
      // Should this
      return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
    } finally {
      executionContext = prevExecutionContext;
      if (executionContext === NoContext) {
        // Flush the immediate callbacks that were scheduled during this batch
        flushSyncCallbackQueue();
      }
    }
  }
  
  export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
    const prevExecutionContext = executionContext;
    executionContext &= ~BatchedContext;
    executionContext |= LegacyUnbatchedContext;
    try {
      return fn(a);
    } finally {
      executionContext = prevExecutionContext;
      if (executionContext === NoContext) {
        // Flush the immediate callbacks that were scheduled during this batch
        flushSyncCallbackQueue();
      }
    }
  }
  
  export function flushSync<A, R>(fn: A => R, a: A): R {
    if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
      invariant(
        false,
        'flushSync was called from inside a lifecycle method. It cannot be ' +
          'called when React is already rendering.',
      );
    }
    const prevExecutionContext = executionContext;
    executionContext |= BatchedContext;
    try {
      return runWithPriority(ImmediatePriority, fn.bind(null, a));
    } finally {
      executionContext = prevExecutionContext;
      // Flush the immediate callbacks that were scheduled during this batch.
      // Note that this will happen even if batchedUpdates is higher up
      // the stack.
      flushSyncCallbackQueue();
    }
  }
  
  export function flushControlled(fn: () => mixed): void {
    const prevExecutionContext = executionContext;
    executionContext |= BatchedContext;
    try {
      runWithPriority(ImmediatePriority, fn);
    } finally {
      executionContext = prevExecutionContext;
      if (executionContext === NoContext) {
        // Flush the immediate callbacks that were scheduled during this batch
        flushSyncCallbackQueue();
      }
    }
  }
  
  function prepareFreshStack(root, expirationTime) {
    root.finishedWork = null;
    root.finishedExpirationTime = NoWork;
  
    const timeoutHandle = root.timeoutHandle;
    if (timeoutHandle !== noTimeout) {
      // The root previous suspended and scheduled a timeout to commit a fallback
      // state. Now that we have additional work, cancel the timeout.
      root.timeoutHandle = noTimeout;
      // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
      cancelTimeout(timeoutHandle);
    }
  
    if (workInProgress !== null) {
      let interruptedWork = workInProgress.return;
      while (interruptedWork !== null) {
        unwindInterruptedWork(interruptedWork);
        interruptedWork = interruptedWork.return;
      }
    }
    workInProgressRoot = root;
    workInProgress = createWorkInProgress(root.current, null, expirationTime);
    renderExpirationTime = expirationTime;
    workInProgressRootExitStatus = RootIncomplete;
    workInProgressRootLatestProcessedExpirationTime = Sync;
    workInProgressRootLatestSuspenseTimeout = Sync;
    workInProgressRootCanSuspendUsingConfig = null;
    workInProgressRootHasPendingPing = false;
  
    if (enableSchedulerTracing) {
      spawnedWorkDuringRender = null;
    }
  
    if (__DEV__) {
      ReactStrictModeWarnings.discardPendingWarnings();
      componentsWithSuspendedDiscreteUpdates = null;
    }
  }
  
  function renderRoot(
    root: FiberRoot,
    expirationTime: ExpirationTime,
    isSync: boolean,
  ): SchedulerCallback | null {
    invariant(
      (executionContext & (RenderContext | CommitContext)) === NoContext,
      'Should not already be working.',
    );
  
    if (enableUserTimingAPI && expirationTime !== Sync) {
      const didExpire = isSync;
      stopRequestCallbackTimer(didExpire);
    }
  
    if (root.firstPendingTime < expirationTime) {
      // If there's no work left at this expiration time, exit immediately. This
      // happens when multiple callbacks are scheduled for a single root, but an
      // earlier callback flushes the work of a later one.
      return null;
    }
  
    if (isSync && root.finishedExpirationTime === expirationTime) {
      // There's already a pending commit at this expiration time.
      // TODO: This is poorly factored. This case only exists for the
      // batch.commit() API.
      return commitRoot.bind(null, root);
    }
  
    flushPassiveEffects();
  
    // If the root or expiration time have changed, throw out the existing stack
    // and prepare a fresh one. Otherwise we'll continue where we left off.
    if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
      prepareFreshStack(root, expirationTime);
      startWorkOnPendingInteractions(root, expirationTime);
    } else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
      // We could've received an update at a lower priority while we yielded.
      // We're suspended in a delayed state. Once we complete this render we're
      // just going to try to recover at the last pending time anyway so we might
      // as well start doing that eagerly.
      // Ideally we should be able to do this even for retries but we don't yet
      // know if we're going to process an update which wants to commit earlier,
      // and this path happens very early so it would happen too often. Instead,
      // for that case, we'll wait until we complete.
      if (workInProgressRootHasPendingPing) {
        // We have a ping at this expiration. Let's restart to see if we get unblocked.
        prepareFreshStack(root, expirationTime);
      } else {
        const lastPendingTime = root.lastPendingTime;
        if (lastPendingTime < expirationTime) {
          // There's lower priority work. It might be unsuspended. Try rendering
          // at that level immediately, while preserving the position in the queue.
          return renderRoot.bind(null, root, lastPendingTime);
        }
      }
    }
  
    // If we have a work-in-progress fiber, it means there's still work to do
    // in this root.
    if (workInProgress !== null) {
      const prevExecutionContext = executionContext;
      executionContext |= RenderContext;
      let prevDispatcher = ReactCurrentDispatcher.current;
      if (prevDispatcher === null) {
        // The React isomorphic package does not include a default dispatcher.
        // Instead the first renderer will lazily attach one, in order to give
        // nicer error messages.
        prevDispatcher = ContextOnlyDispatcher;
      }
      ReactCurrentDispatcher.current = ContextOnlyDispatcher;
      let prevInteractions: Set<Interaction> | null = null;
      if (enableSchedulerTracing) {
        prevInteractions = __interactionsRef.current;
        __interactionsRef.current = root.memoizedInteractions;
      }
  
      startWorkLoopTimer(workInProgress);
  
      // TODO: Fork renderRoot into renderRootSync and renderRootAsync
      if (isSync) {
        if (expirationTime !== Sync) {
          // An async update expired. There may be other expired updates on
          // this root. We should render all the expired work in a
          // single batch.
          const currentTime = requestCurrentTime();
          if (currentTime < expirationTime) {
            // Restart at the current time.
            executionContext = prevExecutionContext;
            resetContextDependencies();
            ReactCurrentDispatcher.current = prevDispatcher;
            if (enableSchedulerTracing) {
              __interactionsRef.current = ((prevInteractions: any): Set<
                Interaction,
              >);
            }
            return renderRoot.bind(null, root, currentTime);
          }
        }
      } else {
        // Since we know we're in a React event, we can clear the current
        // event time. The next update will compute a new event time.
        currentEventTime = NoWork;
      }
  
      do {
        try {
          if (isSync) {
            workLoopSync();
          } else {
            workLoop();
          }
          break;
        } catch (thrownValue) {
          // Reset module-level state that was set during the render phase.
          resetContextDependencies();
          resetHooks();
  
          const sourceFiber = workInProgress;
          if (sourceFiber === null || sourceFiber.return === null) {
            // Expected to be working on a non-root fiber. This is a fatal error
            // because there's no ancestor that can handle it; the root is
            // supposed to capture all errors that weren't caught by an error
            // boundary.
            prepareFreshStack(root, expirationTime);
            executionContext = prevExecutionContext;
            throw thrownValue;
          }
  
          if (enableProfilerTimer && sourceFiber.mode & ProfileMode) {
            // Record the time spent rendering before an error was thrown. This
            // avoids inaccurate Profiler durations in the case of a
            // suspended render.
            stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true);
          }
  
          const returnFiber = sourceFiber.return;
          throwException(
            root,
            returnFiber,
            sourceFiber,
            thrownValue,
            renderExpirationTime,
          );
          workInProgress = completeUnitOfWork(sourceFiber);
        }
      } while (true);
  
      executionContext = prevExecutionContext;
      resetContextDependencies();
      ReactCurrentDispatcher.current = prevDispatcher;
      if (enableSchedulerTracing) {
        __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
      }
  
      if (workInProgress !== null) {
        // There's still work left over. Return a continuation.
        stopInterruptedWorkLoopTimer();
        if (expirationTime !== Sync) {
          startRequestCallbackTimer();
        }
        return renderRoot.bind(null, root, expirationTime);
      }
    }
  
    // We now have a consistent tree. The next step is either to commit it, or, if
    // something suspended, wait to commit it after a timeout.
    stopFinishedWorkLoopTimer();
  
    root.finishedWork = root.current.alternate;
    root.finishedExpirationTime = expirationTime;
  
    const isLocked = resolveLocksOnRoot(root, expirationTime);
    if (isLocked) {
      // This root has a lock that prevents it from committing. Exit. If we begin
      // work on the root again, without any intervening updates, it will finish
      // without doing additional work.
      return null;
    }
  
    // Set this to null to indicate there's no in-progress render.
    workInProgressRoot = null;
  
    switch (workInProgressRootExitStatus) {
      case RootIncomplete: {
        invariant(false, 'Should have a work-in-progress.');
      }
      // Flow knows about invariant, so it compains if I add a break statement,
      // but eslint doesn't know about invariant, so it complains if I do.
      // eslint-disable-next-line no-fallthrough
      case RootErrored: {
        // An error was thrown. First check if there is lower priority work
        // scheduled on this root.
        const lastPendingTime = root.lastPendingTime;
        if (lastPendingTime < expirationTime) {
          // There's lower priority work. Before raising the error, try rendering
          // at the lower priority to see if it fixes it. Use a continuation to
          // maintain the existing priority and position in the queue.
          return renderRoot.bind(null, root, lastPendingTime);
        }
        if (!isSync) {
          // If we're rendering asynchronously, it's possible the error was
          // caused by tearing due to a mutation during an event. Try rendering
          // one more time without yiedling to events.
          prepareFreshStack(root, expirationTime);
          scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
          return null;
        }
        // If we're already rendering synchronously, commit the root in its
        // errored state.
        return commitRoot.bind(null, root);
      }
      case RootSuspended: {
        // We have an acceptable loading state. We need to figure out if we should
        // immediately commit it or wait a bit.
  
        // If we have processed new updates during this render, we may now have a
        // new loading state ready. We want to ensure that we commit that as soon as
        // possible.
        const hasNotProcessedNewUpdates =
          workInProgressRootLatestProcessedExpirationTime === Sync;
        if (hasNotProcessedNewUpdates && !isSync) {
          // If we have not processed any new updates during this pass, then this is
          // either a retry of an existing fallback state or a hidden tree.
          // Hidden trees shouldn't be batched with other work and after that's
          // fixed it can only be a retry.
          // We're going to throttle committing retries so that we don't show too
          // many loading states too quickly.
          let msUntilTimeout =
            globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
          // Don't bother with a very short suspense time.
          if (msUntilTimeout > 10) {
            if (workInProgressRootHasPendingPing) {
              // This render was pinged but we didn't get to restart earlier so try
              // restarting now instead.
              prepareFreshStack(root, expirationTime);
              return renderRoot.bind(null, root, expirationTime);
            }
            const lastPendingTime = root.lastPendingTime;
            if (lastPendingTime < expirationTime) {
              // There's lower priority work. It might be unsuspended. Try rendering
              // at that level.
              return renderRoot.bind(null, root, lastPendingTime);
            }
            // The render is suspended, it hasn't timed out, and there's no lower
            // priority work to do. Instead of committing the fallback
            // immediately, wait for more data to arrive.
            root.timeoutHandle = scheduleTimeout(
              commitRoot.bind(null, root),
              msUntilTimeout,
            );
            return null;
          }
        }
        // The work expired. Commit immediately.
        return commitRoot.bind(null, root);
      }
      case RootSuspendedWithDelay: {
        if (!isSync) {
          // We're suspended in a state that should be avoided. We'll try to avoid committing
          // it for as long as the timeouts let us.
          if (workInProgressRootHasPendingPing) {
            // This render was pinged but we didn't get to restart earlier so try
            // restarting now instead.
            prepareFreshStack(root, expirationTime);
            return renderRoot.bind(null, root, expirationTime);
          }
          const lastPendingTime = root.lastPendingTime;
          if (lastPendingTime < expirationTime) {
            // There's lower priority work. It might be unsuspended. Try rendering
            // at that level immediately.
            return renderRoot.bind(null, root, lastPendingTime);
          }
  
          let msUntilTimeout;
          if (workInProgressRootLatestSuspenseTimeout !== Sync) {
            // We have processed a suspense config whose expiration time we can use as
            // the timeout.
            msUntilTimeout =
              expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
          } else if (workInProgressRootLatestProcessedExpirationTime === Sync) {
            // This should never normally happen because only new updates cause
            // delayed states, so we should have processed something. However,
            // this could also happen in an offscreen tree.
            msUntilTimeout = 0;
          } else {
            // If we don't have a suspense config, we're going to use a heuristic to
            // determine how long we can suspend.
            const eventTimeMs: number = inferTimeFromExpirationTime(
              workInProgressRootLatestProcessedExpirationTime,
            );
            const currentTimeMs = now();
            const timeUntilExpirationMs =
              expirationTimeToMs(expirationTime) - currentTimeMs;
            let timeElapsed = currentTimeMs - eventTimeMs;
            if (timeElapsed < 0) {
              // We get this wrong some time since we estimate the time.
              timeElapsed = 0;
            }
  
            msUntilTimeout = jnd(timeElapsed) - timeElapsed;
  
            // Clamp the timeout to the expiration time.
            // TODO: Once the event time is exact instead of inferred from expiration time
            // we don't need this.
            if (timeUntilExpirationMs < msUntilTimeout) {
              msUntilTimeout = timeUntilExpirationMs;
            }
          }
  
          // Don't bother with a very short suspense time.
          if (msUntilTimeout > 10) {
            // The render is suspended, it hasn't timed out, and there's no lower
            // priority work to do. Instead of committing the fallback
            // immediately, wait for more data to arrive.
            root.timeoutHandle = scheduleTimeout(
              commitRoot.bind(null, root),
              msUntilTimeout,
            );
            return null;
          }
        }
        // The work expired. Commit immediately.
        return commitRoot.bind(null, root);
      }
      case RootCompleted: {
        // The work completed. Ready to commit.
        if (
          !isSync &&
          workInProgressRootLatestProcessedExpirationTime !== Sync &&
          workInProgressRootCanSuspendUsingConfig !== null
        ) {
          // If we have exceeded the minimum loading delay, which probably
          // means we have shown a spinner already, we might have to suspend
          // a bit longer to ensure that the spinner is shown for enough time.
          const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
            workInProgressRootLatestProcessedExpirationTime,
            expirationTime,
            workInProgressRootCanSuspendUsingConfig,
          );
          if (msUntilTimeout > 10) {
            root.timeoutHandle = scheduleTimeout(
              commitRoot.bind(null, root),
              msUntilTimeout,
            );
            return null;
          }
        }
        return commitRoot.bind(null, root);
      }
      default: {
        invariant(false, 'Unknown root exit status.');
      }
    }
  }
  
  export function markCommitTimeOfFallback() {
    globalMostRecentFallbackTime = now();
  }
  
  export function markRenderEventTimeAndConfig(
    expirationTime: ExpirationTime,
    suspenseConfig: null | SuspenseConfig,
  ): void {
    if (
      expirationTime < workInProgressRootLatestProcessedExpirationTime &&
      expirationTime > Never
    ) {
      workInProgressRootLatestProcessedExpirationTime = expirationTime;
    }
    if (suspenseConfig !== null) {
      if (
        expirationTime < workInProgressRootLatestSuspenseTimeout &&
        expirationTime > Never
      ) {
        workInProgressRootLatestSuspenseTimeout = expirationTime;
        // Most of the time we only have one config and getting wrong is not bad.
        workInProgressRootCanSuspendUsingConfig = suspenseConfig;
      }
    }
  }
  
  export function renderDidSuspend(): void {
    if (workInProgressRootExitStatus === RootIncomplete) {
      workInProgressRootExitStatus = RootSuspended;
    }
  }
  
  export function renderDidSuspendDelayIfPossible(): void {
    if (
      workInProgressRootExitStatus === RootIncomplete ||
      workInProgressRootExitStatus === RootSuspended
    ) {
      workInProgressRootExitStatus = RootSuspendedWithDelay;
    }
  }
  
  export function renderDidError() {
    if (workInProgressRootExitStatus !== RootCompleted) {
      workInProgressRootExitStatus = RootErrored;
    }
  }
  
  function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number {
    // We don't know exactly when the update was scheduled, but we can infer an
    // approximate start time from the expiration time.
    const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
    return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
  }
  
  function inferTimeFromExpirationTimeWithSuspenseConfig(
    expirationTime: ExpirationTime,
    suspenseConfig: SuspenseConfig,
  ): number {
    // We don't know exactly when the update was scheduled, but we can infer an
    // approximate start time from the expiration time by subtracting the timeout
    // that was added to the event time.
    const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
    return (
      earliestExpirationTimeMs -
      (suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION)
    );
  }
  
  function workLoopSync() {
    // Already timed out, so perform work without checking if we need to yield.
    while (workInProgress !== null) {
      workInProgress = performUnitOfWork(workInProgress);
    }
  }
  
  function workLoop() {
    // Perform work until Scheduler asks us to yield
    while (workInProgress !== null && !shouldYield()) {
      workInProgress = performUnitOfWork(workInProgress);
    }
  }
  
  function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
    // The current, flushed, state of this fiber is the alternate. Ideally
    // nothing should rely on this, but relying on it here means that we don't
    // need an additional field on the work in progress.
    const current = unitOfWork.alternate;
  
    startWorkTimer(unitOfWork);
    setCurrentDebugFiberInDEV(unitOfWork);
  
    let next;
    if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
      startProfilerTimer(unitOfWork);
      next = beginWork(current, unitOfWork, renderExpirationTime);
      stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
    } else {
      next = beginWork(current, unitOfWork, renderExpirationTime);
    }
  
    resetCurrentDebugFiberInDEV();
    unitOfWork.memoizedProps = unitOfWork.pendingProps;
    if (next === null) {
      // If this doesn't spawn new work, complete the current work.
      next = completeUnitOfWork(unitOfWork);
    }
  
    ReactCurrentOwner.current = null;
    return next;
  }
  
  function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
    // Attempt to complete the current unit of work, then move to the next
    // sibling. If there are no more siblings, return to the parent fiber.
    workInProgress = unitOfWork;
    do {
      // The current, flushed, state of this fiber is the alternate. Ideally
      // nothing should rely on this, but relying on it here means that we don't
      // need an additional field on the work in progress.
      const current = workInProgress.alternate;
      const returnFiber = workInProgress.return;
  
      // Check if the work completed or if something threw.
      if ((workInProgress.effectTag & Incomplete) === NoEffect) {
        setCurrentDebugFiberInDEV(workInProgress);
        let next;
        if (
          !enableProfilerTimer ||
          (workInProgress.mode & ProfileMode) === NoMode
        ) {
          next = completeWork(current, workInProgress, renderExpirationTime);
        } else {
          startProfilerTimer(workInProgress);
          next = completeWork(current, workInProgress, renderExpirationTime);
          // Update render duration assuming we didn't error.
          stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
        }
        stopWorkTimer(workInProgress);
        resetCurrentDebugFiberInDEV();
        resetChildExpirationTime(workInProgress);
  
        if (next !== null) {
          // Completing this fiber spawned new work. Work on that next.
          return next;
        }
  
        if (
          returnFiber !== null &&
          // Do not append effects to parents if a sibling failed to complete
          (returnFiber.effectTag & Incomplete) === NoEffect
        ) {
          // Append all the effects of the subtree and this fiber onto the effect
          // list of the parent. The completion order of the children affects the
          // side-effect order.
          if (returnFiber.firstEffect === null) {
            returnFiber.firstEffect = workInProgress.firstEffect;
          }
          if (workInProgress.lastEffect !== null) {
            if (returnFiber.lastEffect !== null) {
              returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
            }
            returnFiber.lastEffect = workInProgress.lastEffect;
          }
  
          // If this fiber had side-effects, we append it AFTER the children's
          // side-effects. We can perform certain side-effects earlier if needed,
          // by doing multiple passes over the effect list. We don't want to
          // schedule our own side-effect on our own list because if end up
          // reusing children we'll schedule this effect onto itself since we're
          // at the end.
          const effectTag = workInProgress.effectTag;
  
          // Skip both NoWork and PerformedWork tags when creating the effect
          // list. PerformedWork effect is read by React DevTools but shouldn't be
          // committed.
          if (effectTag > PerformedWork) {
            if (returnFiber.lastEffect !== null) {
              returnFiber.lastEffect.nextEffect = workInProgress;
            } else {
              returnFiber.firstEffect = workInProgress;
            }
            returnFiber.lastEffect = workInProgress;
          }
        }
      } else {
        // This fiber did not complete because something threw. Pop values off
        // the stack without entering the complete phase. If this is a boundary,
        // capture values if possible.
        const next = unwindWork(workInProgress, renderExpirationTime);
  
        // Because this fiber did not complete, don't reset its expiration time.
  
        if (
          enableProfilerTimer &&
          (workInProgress.mode & ProfileMode) !== NoMode
        ) {
          // Record the render duration for the fiber that errored.
          stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
  
          // Include the time spent working on failed children before continuing.
          let actualDuration = workInProgress.actualDuration;
          let child = workInProgress.child;
          while (child !== null) {
            actualDuration += child.actualDuration;
            child = child.sibling;
          }
          workInProgress.actualDuration = actualDuration;
        }
  
        if (next !== null) {
          // If completing this work spawned new work, do that next. We'll come
          // back here again.
          // Since we're restarting, remove anything that is not a host effect
          // from the effect tag.
          // TODO: The name stopFailedWorkTimer is misleading because Suspense
          // also captures and restarts.
          stopFailedWorkTimer(workInProgress);
          next.effectTag &= HostEffectMask;
          return next;
        }
        stopWorkTimer(workInProgress);
  
        if (returnFiber !== null) {
          // Mark the parent fiber as incomplete and clear its effect list.
          returnFiber.firstEffect = returnFiber.lastEffect = null;
          returnFiber.effectTag |= Incomplete;
        }
      }
  
      const siblingFiber = workInProgress.sibling;
      if (siblingFiber !== null) {
        // If there is more work to do in this returnFiber, do that next.
        return siblingFiber;
      }
      // Otherwise, return to the parent
      workInProgress = returnFiber;
    } while (workInProgress !== null);
  
    // We've reached the root.
    if (workInProgressRootExitStatus === RootIncomplete) {
      workInProgressRootExitStatus = RootCompleted;
    }
    return null;
  }
  
  function resetChildExpirationTime(completedWork: Fiber) {
    if (
      renderExpirationTime !== Never &&
      completedWork.childExpirationTime === Never
    ) {
      // The children of this component are hidden. Don't bubble their
      // expiration times.
      return;
    }
  
    let newChildExpirationTime = NoWork;
  
    // Bubble up the earliest expiration time.
    if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
      // In profiling mode, resetChildExpirationTime is also used to reset
      // profiler durations.
      let actualDuration = completedWork.actualDuration;
      let treeBaseDuration = completedWork.selfBaseDuration;
  
      // When a fiber is cloned, its actualDuration is reset to 0. This value will
      // only be updated if work is done on the fiber (i.e. it doesn't bailout).
      // When work is done, it should bubble to the parent's actualDuration. If
      // the fiber has not been cloned though, (meaning no work was done), then
      // this value will reflect the amount of time spent working on a previous
      // render. In that case it should not bubble. We determine whether it was
      // cloned by comparing the child pointer.
      const shouldBubbleActualDurations =
        completedWork.alternate === null ||
        completedWork.child !== completedWork.alternate.child;
  
      let child = completedWork.child;
      while (child !== null) {
        const childUpdateExpirationTime = child.expirationTime;
        const childChildExpirationTime = child.childExpirationTime;
        if (childUpdateExpirationTime > newChildExpirationTime) {
          newChildExpirationTime = childUpdateExpirationTime;
        }
        if (childChildExpirationTime > newChildExpirationTime) {
          newChildExpirationTime = childChildExpirationTime;
        }
        if (shouldBubbleActualDurations) {
          actualDuration += child.actualDuration;
        }
        treeBaseDuration += child.treeBaseDuration;
        child = child.sibling;
      }
      completedWork.actualDuration = actualDuration;
      completedWork.treeBaseDuration = treeBaseDuration;
    } else {
      let child = completedWork.child;
      while (child !== null) {
        const childUpdateExpirationTime = child.expirationTime;
        const childChildExpirationTime = child.childExpirationTime;
        if (childUpdateExpirationTime > newChildExpirationTime) {
          newChildExpirationTime = childUpdateExpirationTime;
        }
        if (childChildExpirationTime > newChildExpirationTime) {
          newChildExpirationTime = childChildExpirationTime;
        }
        child = child.sibling;
      }
    }
  
    completedWork.childExpirationTime = newChildExpirationTime;
  }
  
  function commitRoot(root) {
    runWithPriority(ImmediatePriority, commitRootImpl.bind(null, root));
    // If there are passive effects, schedule a callback to flush them. This goes
    // outside commitRootImpl so that it inherits the priority of the render.
    if (rootWithPendingPassiveEffects !== null) {
      const priorityLevel = getCurrentPriorityLevel();
      scheduleCallback(priorityLevel, () => {
        flushPassiveEffects();
        return null;
      });
    }
    return null;
  }
  
  function commitRootImpl(root) {
    flushPassiveEffects();
    flushRenderPhaseStrictModeWarningsInDEV();
    flushSuspensePriorityWarningInDEV();
  
    invariant(
      (executionContext & (RenderContext | CommitContext)) === NoContext,
      'Should not already be working.',
    );
  
    const finishedWork = root.finishedWork;
    const expirationTime = root.finishedExpirationTime;
    if (finishedWork === null) {
      return null;
    }
    root.finishedWork = null;
    root.finishedExpirationTime = NoWork;
  
    invariant(
      finishedWork !== root.current,
      'Cannot commit the same tree as before. This error is likely caused by ' +
        'a bug in React. Please file an issue.',
    );
  
    // commitRoot never returns a continuation; it always finishes synchronously.
    // So we can clear these now to allow a new callback to be scheduled.
    root.callbackNode = null;
    root.callbackExpirationTime = NoWork;
  
    startCommitTimer();
  
    // Update the first and last pending times on this root. The new first
    // pending time is whatever is left on the root fiber.
    const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
    const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
    const firstPendingTimeBeforeCommit =
      childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
        ? childExpirationTimeBeforeCommit
        : updateExpirationTimeBeforeCommit;
    root.firstPendingTime = firstPendingTimeBeforeCommit;
    if (firstPendingTimeBeforeCommit < root.lastPendingTime) {
      // This usually means we've finished all the work, but it can also happen
      // when something gets downprioritized during render, like a hidden tree.
      root.lastPendingTime = firstPendingTimeBeforeCommit;
    }
  
    if (root === workInProgressRoot) {
      // We can reset these now that they are finished.
      workInProgressRoot = null;
      workInProgress = null;
      renderExpirationTime = NoWork;
    } else {
      // This indicates that the last root we worked on is not the same one that
      // we're committing now. This most commonly happens when a suspended root
      // times out.
    }
  
    // Get the list of effects.
    let firstEffect;
    if (finishedWork.effectTag > PerformedWork) {
      // A fiber's effect list consists only of its children, not itself. So if
      // the root has an effect, we need to add it to the end of the list. The
      // resulting list is the set that would belong to the root's parent, if it
      // had one; that is, all the effects in the tree including the root.
      if (finishedWork.lastEffect !== null) {
        finishedWork.lastEffect.nextEffect = finishedWork;
        firstEffect = finishedWork.firstEffect;
      } else {
        firstEffect = finishedWork;
      }
    } else {
      // There is no effect on the root.
      firstEffect = finishedWork.firstEffect;
    }
  
    if (firstEffect !== null) {
      const prevExecutionContext = executionContext;
      executionContext |= CommitContext;
      let prevInteractions: Set<Interaction> | null = null;
      if (enableSchedulerTracing) {
        prevInteractions = __interactionsRef.current;
        __interactionsRef.current = root.memoizedInteractions;
      }
  
      // Reset this to null before calling lifecycles
      ReactCurrentOwner.current = null;
  
      // The commit phase is broken into several sub-phases. We do a separate pass
      // of the effect list for each phase: all mutation effects come before all
      // layout effects, and so on.
  
      // The first phase a "before mutation" phase. We use this phase to read the
      // state of the host tree right before we mutate it. This is where
      // getSnapshotBeforeUpdate is called.
      startCommitSnapshotEffectsTimer();
      prepareForCommit(root.containerInfo);
      nextEffect = firstEffect;
      do {
        if (__DEV__) {
          invokeGuardedCallback(null, commitBeforeMutationEffects, null);
          if (hasCaughtError()) {
            invariant(nextEffect !== null, 'Should be working on an effect.');
            const error = clearCaughtError();
            captureCommitPhaseError(nextEffect, error);
            nextEffect = nextEffect.nextEffect;
          }
        } else {
          try {
            commitBeforeMutationEffects();
          } catch (error) {
            invariant(nextEffect !== null, 'Should be working on an effect.');
            captureCommitPhaseError(nextEffect, error);
            nextEffect = nextEffect.nextEffect;
          }
        }
      } while (nextEffect !== null);
      stopCommitSnapshotEffectsTimer();
  
      if (enableProfilerTimer) {
        // Mark the current commit time to be shared by all Profilers in this
        // batch. This enables them to be grouped later.
        recordCommitTime();
      }
  
      // The next phase is the mutation phase, where we mutate the host tree.
      startCommitHostEffectsTimer();
      nextEffect = firstEffect;
      do {
        if (__DEV__) {
          invokeGuardedCallback(null, commitMutationEffects, null);
          if (hasCaughtError()) {
            invariant(nextEffect !== null, 'Should be working on an effect.');
            const error = clearCaughtError();
            captureCommitPhaseError(nextEffect, error);
            nextEffect = nextEffect.nextEffect;
          }
        } else {
          try {
            commitMutationEffects();
          } catch (error) {
            invariant(nextEffect !== null, 'Should be working on an effect.');
            captureCommitPhaseError(nextEffect, error);
            nextEffect = nextEffect.nextEffect;
          }
        }
      } while (nextEffect !== null);
      stopCommitHostEffectsTimer();
      resetAfterCommit(root.containerInfo);
  
      // The work-in-progress tree is now the current tree. This must come after
      // the mutation phase, so that the previous tree is still current during
      // componentWillUnmount, but before the layout phase, so that the finished
      // work is current during componentDidMount/Update.
      root.current = finishedWork;
  
      // The next phase is the layout phase, where we call effects that read
      // the host tree after it's been mutated. The idiomatic use case for this is
      // layout, but class component lifecycles also fire here for legacy reasons.
      startCommitLifeCyclesTimer();
      nextEffect = firstEffect;
      do {
        if (__DEV__) {
          invokeGuardedCallback(
            null,
            commitLayoutEffects,
            null,
            root,
            expirationTime,
          );
          if (hasCaughtError()) {
            invariant(nextEffect !== null, 'Should be working on an effect.');
            const error = clearCaughtError();
            captureCommitPhaseError(nextEffect, error);
            nextEffect = nextEffect.nextEffect;
          }
        } else {
          try {
            commitLayoutEffects(root, expirationTime);
          } catch (error) {
            invariant(nextEffect !== null, 'Should be working on an effect.');
            captureCommitPhaseError(nextEffect, error);
            nextEffect = nextEffect.nextEffect;
          }
        }
      } while (nextEffect !== null);
      stopCommitLifeCyclesTimer();
  
      nextEffect = null;
  
      // Tell Scheduler to yield at the end of the frame, so the browser has an
      // opportunity to paint.
      requestPaint();
  
      if (enableSchedulerTracing) {
        __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
      }
      executionContext = prevExecutionContext;
    } else {
      // No effects.
      root.current = finishedWork;
      // Measure these anyway so the flamegraph explicitly shows that there were
      // no effects.
      // TODO: Maybe there's a better way to report this.
      startCommitSnapshotEffectsTimer();
      stopCommitSnapshotEffectsTimer();
      if (enableProfilerTimer) {
        recordCommitTime();
      }
      startCommitHostEffectsTimer();
      stopCommitHostEffectsTimer();
      startCommitLifeCyclesTimer();
      stopCommitLifeCyclesTimer();
    }
  
    stopCommitTimer();
  
    const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
  
    if (rootDoesHavePassiveEffects) {
      // This commit has passive effects. Stash a reference to them. But don't
      // schedule a callback until after flushing layout work.
      rootDoesHavePassiveEffects = false;
      rootWithPendingPassiveEffects = root;
      pendingPassiveEffectsExpirationTime = expirationTime;
    }
  
    // Check if there's remaining work on this root
    const remainingExpirationTime = root.firstPendingTime;
    if (remainingExpirationTime !== NoWork) {
      const currentTime = requestCurrentTime();
      const priorityLevel = inferPriorityFromExpirationTime(
        currentTime,
        remainingExpirationTime,
      );
  
      if (enableSchedulerTracing) {
        if (spawnedWorkDuringRender !== null) {
          const expirationTimes = spawnedWorkDuringRender;
          spawnedWorkDuringRender = null;
          for (let i = 0; i < expirationTimes.length; i++) {
            scheduleInteractions(
              root,
              expirationTimes[i],
              root.memoizedInteractions,
            );
          }
        }
      }
  
      scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
    } else {
      // If there's no remaining work, we can clear the set of already failed
      // error boundaries.
      legacyErrorBoundariesThatAlreadyFailed = null;
    }
  
    if (enableSchedulerTracing) {
      if (!rootDidHavePassiveEffects) {
        // If there are no passive effects, then we can complete the pending interactions.
        // Otherwise, we'll wait until after the passive effects are flushed.
        // Wait to do this until after remaining work has been scheduled,
        // so that we don't prematurely signal complete for interactions when there's e.g. hidden work.
        finishPendingInteractions(root, expirationTime);
      }
    }
  
    onCommitRoot(finishedWork.stateNode, expirationTime);
  
    if (remainingExpirationTime === Sync) {
      // Count the number of times the root synchronously re-renders without
      // finishing. If there are too many, it indicates an infinite update loop.
      if (root === rootWithNestedUpdates) {
        nestedUpdateCount++;
      } else {
        nestedUpdateCount = 0;
        rootWithNestedUpdates = root;
      }
    } else {
      nestedUpdateCount = 0;
    }
  
    if (hasUncaughtError) {
      hasUncaughtError = false;
      const error = firstUncaughtError;
      firstUncaughtError = null;
      throw error;
    }
  
    if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
      // This is a legacy edge case. We just committed the initial mount of
      // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
      // synchronously, but layout updates should be deferred until the end
      // of the batch.
      return null;
    }
  
    // If layout work was scheduled, flush it now.
    flushSyncCallbackQueue();
    return null;
  }
  
  function commitBeforeMutationEffects() {
    while (nextEffect !== null) {
      if ((nextEffect.effectTag & Snapshot) !== NoEffect) {
        setCurrentDebugFiberInDEV(nextEffect);
        recordEffect();
  
        const current = nextEffect.alternate;
        commitBeforeMutationEffectOnFiber(current, nextEffect);
  
        resetCurrentDebugFiberInDEV();
      }
      nextEffect = nextEffect.nextEffect;
    }
  }
  
  function commitMutationEffects() {
    // TODO: Should probably move the bulk of this function to commitWork.
    while (nextEffect !== null) {
      setCurrentDebugFiberInDEV(nextEffect);
  
      const effectTag = nextEffect.effectTag;
  
      if (effectTag & ContentReset) {
        commitResetTextContent(nextEffect);
      }
  
      if (effectTag & Ref) {
        const current = nextEffect.alternate;
        if (current !== null) {
          commitDetachRef(current);
        }
      }
  
      // The following switch statement is only concerned about placement,
      // updates, and deletions. To avoid needing to add a case for every possible
      // bitmap value, we remove the secondary effects from the effect tag and
      // switch on that value.
      let primaryEffectTag = effectTag & (Placement | Update | Deletion);
      switch (primaryEffectTag) {
        case Placement: {
          commitPlacement(nextEffect);
          // Clear the "placement" from effect tag so that we know that this is
          // inserted, before any life-cycles like componentDidMount gets called.
          // TODO: findDOMNode doesn't rely on this any more but isMounted does
          // and isMounted is deprecated anyway so we should be able to kill this.
          nextEffect.effectTag &= ~Placement;
          break;
        }
        case PlacementAndUpdate: {
          // Placement
          commitPlacement(nextEffect);
          // Clear the "placement" from effect tag so that we know that this is
          // inserted, before any life-cycles like componentDidMount gets called.
          nextEffect.effectTag &= ~Placement;
  
          // Update
          const current = nextEffect.alternate;
          commitWork(current, nextEffect);
          break;
        }
        case Update: {
          const current = nextEffect.alternate;
          commitWork(current, nextEffect);
          break;
        }
        case Deletion: {
          commitDeletion(nextEffect);
          break;
        }
      }
  
      // TODO: Only record a mutation effect if primaryEffectTag is non-zero.
      recordEffect();
  
      resetCurrentDebugFiberInDEV();
      nextEffect = nextEffect.nextEffect;
    }
  }
  
  function commitLayoutEffects(
    root: FiberRoot,
    committedExpirationTime: ExpirationTime,
  ) {
    // TODO: Should probably move the bulk of this function to commitWork.
    while (nextEffect !== null) {
      setCurrentDebugFiberInDEV(nextEffect);
  
      const effectTag = nextEffect.effectTag;
  
      if (effectTag & (Update | Callback)) {
        recordEffect();
        const current = nextEffect.alternate;
        commitLayoutEffectOnFiber(
          root,
          current,
          nextEffect,
          committedExpirationTime,
        );
      }
  
      if (effectTag & Ref) {
        recordEffect();
        commitAttachRef(nextEffect);
      }
  
      if (effectTag & Passive) {
        rootDoesHavePassiveEffects = true;
      }
  
      resetCurrentDebugFiberInDEV();
      nextEffect = nextEffect.nextEffect;
    }
  }
  
  export function flushPassiveEffects() {
    if (rootWithPendingPassiveEffects === null) {
      return false;
    }
    const root = rootWithPendingPassiveEffects;
    const expirationTime = pendingPassiveEffectsExpirationTime;
    rootWithPendingPassiveEffects = null;
    pendingPassiveEffectsExpirationTime = NoWork;
  
    let prevInteractions: Set<Interaction> | null = null;
    if (enableSchedulerTracing) {
      prevInteractions = __interactionsRef.current;
      __interactionsRef.current = root.memoizedInteractions;
    }
  
    invariant(
      (executionContext & (RenderContext | CommitContext)) === NoContext,
      'Cannot flush passive effects while already rendering.',
    );
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;
  
    // Note: This currently assumes there are no passive effects on the root
    // fiber, because the root is not part of its own effect list. This could
    // change in the future.
    let effect = root.current.firstEffect;
    while (effect !== null) {
      if (__DEV__) {
        setCurrentDebugFiberInDEV(effect);
        invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
        if (hasCaughtError()) {
          invariant(effect !== null, 'Should be working on an effect.');
          const error = clearCaughtError();
          captureCommitPhaseError(effect, error);
        }
        resetCurrentDebugFiberInDEV();
      } else {
        try {
          commitPassiveHookEffects(effect);
        } catch (error) {
          invariant(effect !== null, 'Should be working on an effect.');
          captureCommitPhaseError(effect, error);
        }
      }
      effect = effect.nextEffect;
    }
  
    if (enableSchedulerTracing) {
      __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
      finishPendingInteractions(root, expirationTime);
    }
  
    executionContext = prevExecutionContext;
    flushSyncCallbackQueue();
  
    // If additional passive effects were scheduled, increment a counter. If this
    // exceeds the limit, we'll fire a warning.
    nestedPassiveUpdateCount =
      rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1;
  
    return true;
  }
  
  export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean {
    return (
      legacyErrorBoundariesThatAlreadyFailed !== null &&
      legacyErrorBoundariesThatAlreadyFailed.has(instance)
    );
  }
  
  export function markLegacyErrorBoundaryAsFailed(instance: mixed) {
    if (legacyErrorBoundariesThatAlreadyFailed === null) {
      legacyErrorBoundariesThatAlreadyFailed = new Set([instance]);
    } else {
      legacyErrorBoundariesThatAlreadyFailed.add(instance);
    }
  }
  
  function prepareToThrowUncaughtError(error: mixed) {
    if (!hasUncaughtError) {
      hasUncaughtError = true;
      firstUncaughtError = error;
    }
  }
  export const onUncaughtError = prepareToThrowUncaughtError;
  
  function captureCommitPhaseErrorOnRoot(
    rootFiber: Fiber,
    sourceFiber: Fiber,
    error: mixed,
  ) {
    const errorInfo = createCapturedValue(error, sourceFiber);
    const update = createRootErrorUpdate(rootFiber, errorInfo, Sync);
    enqueueUpdate(rootFiber, update);
    const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
    if (root !== null) {
      scheduleCallbackForRoot(root, ImmediatePriority, Sync);
    }
  }
  
  export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
    if (sourceFiber.tag === HostRoot) {
      // Error was thrown at the root. There is no parent, so the root
      // itself should capture it.
      captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
      return;
    }
  
    let fiber = sourceFiber.return;
    while (fiber !== null) {
      if (fiber.tag === HostRoot) {
        captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
        return;
      } else if (fiber.tag === ClassComponent) {
        const ctor = fiber.type;
        const instance = fiber.stateNode;
        if (
          typeof ctor.getDerivedStateFromError === 'function' ||
          (typeof instance.componentDidCatch === 'function' &&
            !isAlreadyFailedLegacyErrorBoundary(instance))
        ) {
          const errorInfo = createCapturedValue(error, sourceFiber);
          const update = createClassErrorUpdate(
            fiber,
            errorInfo,
            // TODO: This is always sync
            Sync,
          );
          enqueueUpdate(fiber, update);
          const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
          if (root !== null) {
            scheduleCallbackForRoot(root, ImmediatePriority, Sync);
          }
          return;
        }
      }
      fiber = fiber.return;
    }
  }
  
  export function pingSuspendedRoot(
    root: FiberRoot,
    thenable: Thenable,
    suspendedTime: ExpirationTime,
  ) {
    const pingCache = root.pingCache;
    if (pingCache !== null) {
      // The thenable resolved, so we no longer need to memoize, because it will
      // never be thrown again.
      pingCache.delete(thenable);
    }
  
    if (workInProgressRoot === root && renderExpirationTime === suspendedTime) {
      // Received a ping at the same priority level at which we're currently
      // rendering. We might want to restart this render. This should mirror
      // the logic of whether or not a root suspends once it completes.
  
      // TODO: If we're rendering sync either due to Sync, Batched or expired,
      // we should probably never restart.
  
      // If we're suspended with delay, we'll always suspend so we can always
      // restart. If we're suspended without any updates, it might be a retry.
      // If it's early in the retry we can restart. We can't know for sure
      // whether we'll eventually process an update during this render pass,
      // but it's somewhat unlikely that we get to a ping before that, since
      // getting to the root most update is usually very fast.
      if (
        workInProgressRootExitStatus === RootSuspendedWithDelay ||
        (workInProgressRootExitStatus === RootSuspended &&
          workInProgressRootLatestProcessedExpirationTime === Sync &&
          now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
      ) {
        // Restart from the root. Don't need to schedule a ping because
        // we're already working on this tree.
        prepareFreshStack(root, renderExpirationTime);
      } else {
        // Even though we can't restart right now, we might get an
        // opportunity later. So we mark this render as having a ping.
        workInProgressRootHasPendingPing = true;
      }
      return;
    }
  
    const lastPendingTime = root.lastPendingTime;
    if (lastPendingTime < suspendedTime) {
      // The root is no longer suspended at this time.
      return;
    }
  
    const pingTime = root.pingTime;
    if (pingTime !== NoWork && pingTime < suspendedTime) {
      // There's already a lower priority ping scheduled.
      return;
    }
  
    // Mark the time at which this ping was scheduled.
    root.pingTime = suspendedTime;
  
    if (root.finishedExpirationTime === suspendedTime) {
      // If there's a pending fallback waiting to commit, throw it away.
      root.finishedExpirationTime = NoWork;
      root.finishedWork = null;
    }
  
    const currentTime = requestCurrentTime();
    const priorityLevel = inferPriorityFromExpirationTime(
      currentTime,
      suspendedTime,
    );
    scheduleCallbackForRoot(root, priorityLevel, suspendedTime);
  }
  
  export function retryTimedOutBoundary(boundaryFiber: Fiber) {
    // The boundary fiber (a Suspense component or SuspenseList component)
    // previously was rendered in its fallback state. One of the promises that
    // suspended it has resolved, which means at least part of the tree was
    // likely unblocked. Try rendering again, at a new expiration time.
    const currentTime = requestCurrentTime();
    const suspenseConfig = null; // Retries don't carry over the already committed update.
    const retryTime = computeExpirationForFiber(
      currentTime,
      boundaryFiber,
      suspenseConfig,
    );
    // TODO: Special case idle priority?
    const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime);
    const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
    if (root !== null) {
      scheduleCallbackForRoot(root, priorityLevel, retryTime);
    }
  }
  
  export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) {
    let retryCache: WeakSet<Thenable> | Set<Thenable> | null;
    if (enableSuspenseServerRenderer) {
      switch (boundaryFiber.tag) {
        case SuspenseComponent:
          retryCache = boundaryFiber.stateNode;
          break;
        case DehydratedSuspenseComponent:
          retryCache = boundaryFiber.memoizedState;
          break;
        default:
          invariant(
            false,
            'Pinged unknown suspense boundary type. ' +
              'This is probably a bug in React.',
          );
      }
    } else {
      retryCache = boundaryFiber.stateNode;
    }
  
    if (retryCache !== null) {
      // The thenable resolved, so we no longer need to memoize, because it will
      // never be thrown again.
      retryCache.delete(thenable);
    }
  
    retryTimedOutBoundary(boundaryFiber);
  }
  
  // Computes the next Just Noticeable Difference (JND) boundary.
  // The theory is that a person can't tell the difference between small differences in time.
  // Therefore, if we wait a bit longer than necessary that won't translate to a noticeable
  // difference in the experience. However, waiting for longer might mean that we can avoid
  // showing an intermediate loading state. The longer we have already waited, the harder it
  // is to tell small differences in time. Therefore, the longer we've already waited,
  // the longer we can wait additionally. At some point we have to give up though.
  // We pick a train model where the next boundary commits at a consistent schedule.
  // These particular numbers are vague estimates. We expect to adjust them based on research.
  function jnd(timeElapsed: number) {
    return timeElapsed < 120
      ? 120
      : timeElapsed < 480
        ? 480
        : timeElapsed < 1080
          ? 1080
          : timeElapsed < 1920
            ? 1920
            : timeElapsed < 3000
              ? 3000
              : timeElapsed < 4320
                ? 4320
                : ceil(timeElapsed / 1960) * 1960;
  }
  
  function computeMsUntilSuspenseLoadingDelay(
    mostRecentEventTime: ExpirationTime,
    committedExpirationTime: ExpirationTime,
    suspenseConfig: SuspenseConfig,
  ) {
    const busyMinDurationMs = (suspenseConfig.busyMinDurationMs: any) | 0;
    if (busyMinDurationMs <= 0) {
      return 0;
    }
    const busyDelayMs = (suspenseConfig.busyDelayMs: any) | 0;
  
    // Compute the time until this render pass would expire.
    const currentTimeMs: number = now();
    const eventTimeMs: number = inferTimeFromExpirationTimeWithSuspenseConfig(
      mostRecentEventTime,
      suspenseConfig,
    );
    const timeElapsed = currentTimeMs - eventTimeMs;
    if (timeElapsed <= busyDelayMs) {
      // If we haven't yet waited longer than the initial delay, we don't
      // have to wait any additional time.
      return 0;
    }
    const msUntilTimeout = busyDelayMs + busyMinDurationMs - timeElapsed;
    // This is the value that is passed to `setTimeout`.
    return msUntilTimeout;
  }
  
  function checkForNestedUpdates() {
    if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
      nestedUpdateCount = 0;
      rootWithNestedUpdates = null;
      invariant(
        false,
        'Maximum update depth exceeded. This can happen when a component ' +
          'repeatedly calls setState inside componentWillUpdate or ' +
          'componentDidUpdate. React limits the number of nested updates to ' +
          'prevent infinite loops.',
      );
    }
  
    if (__DEV__) {
      if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) {
        nestedPassiveUpdateCount = 0;
        warning(
          false,
          'Maximum update depth exceeded. This can happen when a component ' +
            "calls setState inside useEffect, but useEffect either doesn't " +
            'have a dependency array, or one of the dependencies changes on ' +
            'every render.',
        );
      }
    }
  }
  
  function flushRenderPhaseStrictModeWarningsInDEV() {
    if (__DEV__) {
      ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings();
      ReactStrictModeWarnings.flushLegacyContextWarning();
  
      if (warnAboutDeprecatedLifecycles) {
        ReactStrictModeWarnings.flushPendingDeprecationWarnings();
      }
    }
  }
  
  function stopFinishedWorkLoopTimer() {
    const didCompleteRoot = true;
    stopWorkLoopTimer(interruptedBy, didCompleteRoot);
    interruptedBy = null;
  }
  
  function stopInterruptedWorkLoopTimer() {
    // TODO: Track which fiber caused the interruption.
    const didCompleteRoot = false;
    stopWorkLoopTimer(interruptedBy, didCompleteRoot);
    interruptedBy = null;
  }
  
  function checkForInterruption(
    fiberThatReceivedUpdate: Fiber,
    updateExpirationTime: ExpirationTime,
  ) {
    if (
      enableUserTimingAPI &&
      workInProgressRoot !== null &&
      updateExpirationTime > renderExpirationTime
    ) {
      interruptedBy = fiberThatReceivedUpdate;
    }
  }
  
  let didWarnStateUpdateForUnmountedComponent: Set<string> | null = null;
  function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
    if (__DEV__) {
      const tag = fiber.tag;
      if (
        tag !== HostRoot &&
        tag !== ClassComponent &&
        tag !== FunctionComponent &&
        tag !== ForwardRef &&
        tag !== MemoComponent &&
        tag !== SimpleMemoComponent
      ) {
        // Only warn for user-defined components, not internal ones like Suspense.
        return;
      }
      // We show the whole stack but dedupe on the top component's name because
      // the problematic code almost always lies inside that component.
      const componentName = getComponentName(fiber.type) || 'ReactComponent';
      if (didWarnStateUpdateForUnmountedComponent !== null) {
        if (didWarnStateUpdateForUnmountedComponent.has(componentName)) {
          return;
        }
        didWarnStateUpdateForUnmountedComponent.add(componentName);
      } else {
        didWarnStateUpdateForUnmountedComponent = new Set([componentName]);
      }
      warningWithoutStack(
        false,
        "Can't perform a React state update on an unmounted component. This " +
          'is a no-op, but it indicates a memory leak in your application. To ' +
          'fix, cancel all subscriptions and asynchronous tasks in %s.%s',
        tag === ClassComponent
          ? 'the componentWillUnmount method'
          : 'a useEffect cleanup function',
        getStackByFiberInDevAndProd(fiber),
      );
    }
  }
  
  let beginWork;
  if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
    let dummyFiber = null;
    beginWork = (current, unitOfWork, expirationTime) => {
      // If a component throws an error, we replay it again in a synchronously
      // dispatched event, so that the debugger will treat it as an uncaught
      // error See ReactErrorUtils for more information.
  
      // Before entering the begin phase, copy the work-in-progress onto a dummy
      // fiber. If beginWork throws, we'll use this to reset the state.
      const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
        dummyFiber,
        unitOfWork,
      );
      try {
        return originalBeginWork(current, unitOfWork, expirationTime);
      } catch (originalError) {
        if (
          originalError !== null &&
          typeof originalError === 'object' &&
          typeof originalError.then === 'function'
        ) {
          // Don't replay promises. Treat everything else like an error.
          throw originalError;
        }
  
        // Keep this code in sync with renderRoot; any changes here must have
        // corresponding changes there.
        resetContextDependencies();
        resetHooks();
  
        // Unwind the failed stack frame
        unwindInterruptedWork(unitOfWork);
  
        // Restore the original properties of the fiber.
        assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
  
        if (enableProfilerTimer && unitOfWork.mode & ProfileMode) {
          // Reset the profiler timer.
          startProfilerTimer(unitOfWork);
        }
  
        // Run beginWork again.
        invokeGuardedCallback(
          null,
          originalBeginWork,
          null,
          current,
          unitOfWork,
          expirationTime,
        );
  
        if (hasCaughtError()) {
          const replayError = clearCaughtError();
          // `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`.
          // Rethrow this error instead of the original one.
          throw replayError;
        } else {
          // This branch is reachable if the render phase is impure.
          throw originalError;
        }
      }
    };
  } else {
    beginWork = originalBeginWork;
  }
  
  let didWarnAboutUpdateInRender = false;
  let didWarnAboutUpdateInGetChildContext = false;
  function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
    if (__DEV__) {
      if (fiber.tag === ClassComponent) {
        switch (ReactCurrentDebugFiberPhaseInDEV) {
          case 'getChildContext':
            if (didWarnAboutUpdateInGetChildContext) {
              return;
            }
            warningWithoutStack(
              false,
              'setState(...): Cannot call setState() inside getChildContext()',
            );
            didWarnAboutUpdateInGetChildContext = true;
            break;
          case 'render':
            if (didWarnAboutUpdateInRender) {
              return;
            }
            warningWithoutStack(
              false,
              'Cannot update during an existing state transition (such as ' +
                'within `render`). Render methods should be a pure function of ' +
                'props and state.',
            );
            didWarnAboutUpdateInRender = true;
            break;
        }
      }
    }
  }
  
  // We export a simple object here to be used by a renderer/test-utils
  // as the value of ReactCurrentActingRendererSigil.current
  // This identity lets us identify (ha!) when the wrong renderer's act()
  // wraps anothers' updates/effects
  export const ReactActingRendererSigil = {};
  
  export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
    if (__DEV__) {
      if (
        ReactCurrentActingRendererSigil.current !== null &&
        ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
      ) {
        warningWithoutStack(
          false,
          "It looks like you're using the wrong act() around your test interactions.\n" +
            'Be sure to use the matching version of act() corresponding to your renderer:\n\n' +
            '// for react-dom:\n' +
            "import {act} from 'react-dom/test-utils';\n" +
            '//...\n' +
            'act(() => ...);\n\n' +
            '// for react-test-renderer:\n' +
            "import TestRenderer from 'react-test-renderer';\n" +
            'const {act} = TestRenderer;\n' +
            '//...\n' +
            'act(() => ...);' +
            '%s',
          getStackByFiberInDevAndProd(fiber),
        );
      }
    }
  }
  
  function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
    if (__DEV__) {
      if (
        executionContext === NoContext &&
        ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
      ) {
        warningWithoutStack(
          false,
          'An update to %s inside a test was not wrapped in act(...).\n\n' +
            'When testing, code that causes React state updates should be ' +
            'wrapped into act(...):\n\n' +
            'act(() => {\n' +
            '  /* fire events that update state */\n' +
            '});\n' +
            '/* assert on the output */\n\n' +
            "This ensures that you're testing the behavior the user would see " +
            'in the browser.' +
            ' Learn more at https://fb.me/react-wrap-tests-with-act' +
            '%s',
          getComponentName(fiber.type),
          getStackByFiberInDevAndProd(fiber),
        );
      }
    }
  }
  
  export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
  
  let componentsWithSuspendedDiscreteUpdates = null;
  export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
    if (__DEV__) {
      if (
        (sourceFiber.mode & ConcurrentMode) !== NoEffect &&
        // Check if we're currently rendering a discrete update. Ideally, all we
        // would need to do is check the current priority level. But we currently
        // have no rigorous way to distinguish work that was scheduled at user-
        // blocking priority from work that expired a bit and was "upgraded" to
        // a higher priority. That's because we don't schedule separate callbacks
        // for every level, only the highest priority level per root. The priority
        // of subsequent levels is inferred from the expiration time, but this is
        // an imprecise heuristic.
        //
        // However, we do store the last discrete pending update per root. So we
        // can reliably compare to that one. (If we broaden this warning to include
        // high pri updates that aren't discrete, then this won't be sufficient.)
        //
        // My rationale is that it's better for this warning to have false
        // negatives than false positives.
        rootsWithPendingDiscreteUpdates !== null &&
        workInProgressRoot !== null &&
        renderExpirationTime ===
          rootsWithPendingDiscreteUpdates.get(workInProgressRoot)
      ) {
        // Add the component name to a set.
        const componentName = getComponentName(sourceFiber.type);
        if (componentsWithSuspendedDiscreteUpdates === null) {
          componentsWithSuspendedDiscreteUpdates = new Set([componentName]);
        } else {
          componentsWithSuspendedDiscreteUpdates.add(componentName);
        }
      }
    }
  }
  
  function flushSuspensePriorityWarningInDEV() {
    if (__DEV__) {
      if (componentsWithSuspendedDiscreteUpdates !== null) {
        const componentNames = [];
        componentsWithSuspendedDiscreteUpdates.forEach(name => {
          componentNames.push(name);
        });
        componentsWithSuspendedDiscreteUpdates = null;
  
        // TODO: A more helpful version of this message could include the names of
        // the component that were updated, not the ones that suspended. To do
        // that we'd need to track all the components that updated during this
        // render, perhaps using the same mechanism as `markRenderEventTime`.
        warningWithoutStack(
          false,
          'The following components suspended during a user-blocking update: %s' +
            '\n\n' +
            'Updates triggered by user interactions (e.g. click events) are ' +
            'considered user-blocking by default. They should not suspend. ' +
            'Updates that can afford to take a bit longer should be wrapped ' +
            'with `Scheduler.next` (or an equivalent abstraction). This ' +
            'typically includes any update that shows new content, like ' +
            'a navigation.' +
            '\n\n' +
            'Generally, you should split user interactions into at least two ' +
            'seprate updates: a user-blocking update to provide immediate ' +
            'feedback, and another update to perform the actual change.',
          // TODO: Add link to React docs with more information, once it exists
          componentNames.sort().join(', '),
        );
      }
    }
  }
  
  function computeThreadID(root, expirationTime) {
    // Interaction threads are unique per root and expiration time.
    return expirationTime * 1000 + root.interactionThreadID;
  }
  
  export function markSpawnedWork(expirationTime: ExpirationTime) {
    if (!enableSchedulerTracing) {
      return;
    }
    if (spawnedWorkDuringRender === null) {
      spawnedWorkDuringRender = [expirationTime];
    } else {
      spawnedWorkDuringRender.push(expirationTime);
    }
  }
  
  function scheduleInteractions(root, expirationTime, interactions) {
    if (!enableSchedulerTracing) {
      return;
    }
  
    if (interactions.size > 0) {
      const pendingInteractionMap = root.pendingInteractionMap;
      const pendingInteractions = pendingInteractionMap.get(expirationTime);
      if (pendingInteractions != null) {
        interactions.forEach(interaction => {
          if (!pendingInteractions.has(interaction)) {
            // Update the pending async work count for previously unscheduled interaction.
            interaction.__count++;
          }
  
          pendingInteractions.add(interaction);
        });
      } else {
        pendingInteractionMap.set(expirationTime, new Set(interactions));
  
        // Update the pending async work count for the current interactions.
        interactions.forEach(interaction => {
          interaction.__count++;
        });
      }
  
      const subscriber = __subscriberRef.current;
      if (subscriber !== null) {
        const threadID = computeThreadID(root, expirationTime);
        subscriber.onWorkScheduled(interactions, threadID);
      }
    }
  }
  
  function schedulePendingInteractions(root, expirationTime) {
    // This is called when work is scheduled on a root.
    // It associates the current interactions with the newly-scheduled expiration.
    // They will be restored when that expiration is later committed.
    if (!enableSchedulerTracing) {
      return;
    }
  
    scheduleInteractions(root, expirationTime, __interactionsRef.current);
  }
  
  function startWorkOnPendingInteractions(root, expirationTime) {
    // This is called when new work is started on a root.
    if (!enableSchedulerTracing) {
      return;
    }
  
    // Determine which interactions this batch of work currently includes, So that
    // we can accurately attribute time spent working on it, And so that cascading
    // work triggered during the render phase will be associated with it.
    const interactions: Set<Interaction> = new Set();
    root.pendingInteractionMap.forEach(
      (scheduledInteractions, scheduledExpirationTime) => {
        if (scheduledExpirationTime >= expirationTime) {
          scheduledInteractions.forEach(interaction =>
            interactions.add(interaction),
          );
        }
      },
    );
  
    // Store the current set of interactions on the FiberRoot for a few reasons:
    // We can re-use it in hot functions like renderRoot() without having to
    // recalculate it. We will also use it in commitWork() to pass to any Profiler
    // onRender() hooks. This also provides DevTools with a way to access it when
    // the onCommitRoot() hook is called.
    root.memoizedInteractions = interactions;
  
    if (interactions.size > 0) {
      const subscriber = __subscriberRef.current;
      if (subscriber !== null) {
        const threadID = computeThreadID(root, expirationTime);
        try {
          subscriber.onWorkStarted(interactions, threadID);
        } catch (error) {
          // If the subscriber throws, rethrow it in a separate task
          scheduleCallback(ImmediatePriority, () => {
            throw error;
          });
        }
      }
    }
  }
  
  function finishPendingInteractions(root, committedExpirationTime) {
    if (!enableSchedulerTracing) {
      return;
    }
  
    const earliestRemainingTimeAfterCommit = root.firstPendingTime;
  
    let subscriber;
  
    try {
      subscriber = __subscriberRef.current;
      if (subscriber !== null && root.memoizedInteractions.size > 0) {
        const threadID = computeThreadID(root, committedExpirationTime);
        subscriber.onWorkStopped(root.memoizedInteractions, threadID);
      }
    } catch (error) {
      // If the subscriber throws, rethrow it in a separate task
      scheduleCallback(ImmediatePriority, () => {
        throw error;
      });
    } finally {
      // Clear completed interactions from the pending Map.
      // Unless the render was suspended or cascading work was scheduled,
      // In which case– leave pending interactions until the subsequent render.
      const pendingInteractionMap = root.pendingInteractionMap;
      pendingInteractionMap.forEach(
        (scheduledInteractions, scheduledExpirationTime) => {
          // Only decrement the pending interaction count if we're done.
          // If there's still work at the current priority,
          // That indicates that we are waiting for suspense data.
          if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) {
            pendingInteractionMap.delete(scheduledExpirationTime);
  
            scheduledInteractions.forEach(interaction => {
              interaction.__count--;
  
              if (subscriber !== null && interaction.__count === 0) {
                try {
                  subscriber.onInteractionScheduledWorkCompleted(interaction);
                } catch (error) {
                  // If the subscriber throws, rethrow it in a separate task
                  scheduleCallback(ImmediatePriority, () => {
                    throw error;
                  });
                }
              }
            });
          }
        },
      );
    }
  }
  