react source code analysis 15 scheduler&Lane

Posted by jamfrag on Mon, 03 Jan 2022 07:18:26 +0100

react source code analysis 15 scheduler&Lane

Video Explanation (efficient learning): Enter learning

When we search in the search box component like the following, we will find that the component is divided into search part and search result display list. We expect the input box to respond immediately, and the result list can have waiting time. If the data in the result list is large, we enter some text during rendering, because the priority of user input events is very high, Therefore, it is necessary to stop the rendering of the result list, which leads to the priority and scheduling between different tasks

Scheduler

We know that if our application takes a long js execution time, such as more than one frame of the device, the rendering of the device will be abnormal.

The Scheduler's main functions are time slicing and scheduling priority. React will occupy a certain js execution time when comparing differences. The Scheduler uses MessageChannel to specify a time slice before drawing by the browser. If react is not compared within the specified time, the Scheduler will forcibly hand over the execution right to the browser

Time slice

The execution time of js in one frame of the browser is as follows

requestIdleCallback can be executed when the browser is free after redrawing and rearrangement. Therefore, in order not to affect the redrawing and rearrangement, the browser can perform performance consumption calculation in requestIdleCallback. However, due to the problems of compatibility and unstable trigger time of requestIdleCallback, MessageChannel is used in the scheduler to implement requestIdleCallback, The current environment does not support MessageChannel, so setTimeout is adopted.

In the previous introduction, we know that the render phase and the commit phase will be executed after performing unitofwork. If the calculation of the cup is not completed in a frame of the browser, the js execution right will be given to the browser. In the workLoopConcurrent function, shouldYield is used to judge whether the remaining time is exhausted. In the source code, each time slice is 5ms, and this value will be adjusted according to the fps of the device.

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
function forceFrameRate(fps) {//Calculate time slice
  if (fps < 0 || fps > 125) {
    console['error'](
      'forceFrameRate takes a positive int between 0 and 125, ' +
        'forcing frame rates higher than 125 fps is not supported',
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    yieldInterval = 5;//The default time slice is 5ms
  }
}

Suspension of tasks

There is a period in the shouldYield function, so you can know that if the current time is greater than the task start time + yield interval, the task will be interrupted.

//deadline = currentTime + yieldInterval. deadline is calculated in performWorkUntilDeadline function
if (currentTime >= deadline) {
  //...
	return true
}

Scheduling priority

There are two functions in the Scheduler to create tasks with priority

  • runWithPriority: execute callback with a priority. If it is a synchronous task, the priority is ImmediateSchedulerPriority

    function unstable_runWithPriority(priorityLevel, eventHandler) {
      switch (priorityLevel) {//5 priorities
        case ImmediatePriority:
        case UserBlockingPriority:
        case NormalPriority:
        case LowPriority:
        case IdlePriority:
          break;
        default:
          priorityLevel = NormalPriority;
      }
    
      var previousPriorityLevel = currentPriorityLevel;//Save current priority
      currentPriorityLevel = priorityLevel;//priorityLevel is assigned to currentPriorityLevel
    
      try {
        return eventHandler();//Callback function
      } finally {
        currentPriorityLevel = previousPriorityLevel;//Priority before restore
      }
    }
    
  • scheduleCallback: register the callback with a priority and execute it at an appropriate time. Because it involves the calculation of expiration time, the granularity of scheduleCallback is finer than that of runWithPriority.

    • In scheduleCallback, priority means expiration time. The higher the priority, the smaller the priorityLevel, and the closer the expiration time is to the current time. var expirationTime = startTime + timeout; For example, IMMEDIATE_PRIORITY_TIMEOUT=-1, var expirationTime = startTime + (-1); It is less than the current time, so it should be executed immediately.

    • The scheduleCallback scheduling process uses a small top heap, so we can find the task with the highest priority in the complexity of O(1). If we don't know, we can consult the data. There are tasks in the small top heap of the source code, and each peek can get the task closest to the expiration time.

    • In scheduleCallback, unexpired tasks are stored in timerQueue, and expired tasks are stored in taskQueue.

      After a new newTask task is created, judge whether the newTask has expired. If it has not expired, join the timerQueue. If there is no expired task in the taskQueue at this time, and the task closest to the expiration time in the timerQueue is just a newTask, set a timer and join the taskQueue at the expiration time.

      When there are tasks in the timerQueue, the earliest expired tasks are retrieved for execution.

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();

  var startTime;//start time
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority://The higher the priority, the smaller the timeout
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;//250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  var expirationTime = startTime + timeout;//The higher the priority, the smaller the expiration time

  var newTask = {//New task
    id: taskIdCounter++,
    callback//Callback function
    priorityLevel,
    startTime,//start time
    expirationTime,//Expiration time
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {//Not expired
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);//Join timerQueue
    //There are no expired tasks in the taskQueue. The task closest to the expiration time in the timerQueue happens to be a newTask
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      //Timer. When the expiration time is reached, it will be added to taskQueue
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);//Join taskQueue
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);//Perform overdue tasks
    }
  }

  return newTask;
}

How to continue after the task is suspended

There is such a paragraph in the workLoop function

const continuationCallback = callback(didUserCallbackTimeout);//A callback is a scheduled callback
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {//Judge the return value type after callback execution
  currentTask.callback = continuationCallback;//If it is a function type, it will be assigned to currenttask callback
  markTaskYield(currentTask, currentTime);
} else {
  if (enableProfiling) {
    markTaskCompleted(currentTask, currentTime);
    currentTask.isQueued = false;
  }
  if (currentTask === peek(taskQueue)) {
    pop(taskQueue);//If it is a function type, it is deleted from taskQueue
  }
}
advanceTimers(currentTime);

At the end of the performcurrentworkonroot function, there is a judgment that if the callbackNode is equal to the originalCallbackNode, the execution of the task will be resumed

if (root.callbackNode === originalCallbackNode) {
  // The task node scheduled for this root is the same one that's
  // currently executed. Need to return a continuation.
  return performConcurrentWorkOnRoot.bind(null, root);
}

Lane

Lane and Scheduler are two sets of priority mechanisms. Compared with lane, lane has finer priority granularity. Lane means lane. Like a racing car, when a task obtains priority, it always gives priority to the track in the inner circle. The priority represented by Lane has the following characteristics.

  • It can represent the priority of different batches

    It can be seen from the code that each priority is a 31 bit binary number. 1 means that the position can be used, and 0 means that the position cannot be used. From the first priority NoLanes to offscreen lane, the priority is reduced. The lower the priority, the more the number of 1 (the more cars in the outer ring of the race), that is, the priority with multiple 1 is the same batch.

    export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
    export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;
    
    export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
    export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;
    
    export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
    const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;
    
    const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
    const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;
    
    export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000;
    export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;
    
    const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
    const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;
    
    const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
    
    export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;
    
    export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;
    
    const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;
    
    export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
    const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;
    
    export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;
    
  • High performance of priority calculation

    For example, it is possible to judge whether there is an intersection between the lane s represented by a and b by binary bitwise AND

    export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
      return (a & b) !== NoLanes;
    }
    

How do task s in the Lane model get priority (the initial track of the car)

The task obtains the track from the high priority lanes. This process occurs in the findUpdateLane function. If there are no available lanes in the high priority, it will drop to the low priority lanes to find them. Pickarbitrrylane will call getHighestPriorityLane to obtain the highest priority in a batch of lanes, That is, the rightmost bit is obtained through lanes & - lanes

export function findUpdateLane(
  lanePriority: LanePriority,
  wipLanes: Lanes,
): Lane {
  switch (lanePriority) {
    //...
    case DefaultLanePriority: {
      let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);//Find the next lane with the highest priority
      if (lane === NoLane) {//The lanes of the last level are full. Drop to transition lanes and continue to look for available tracks
        lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
        if (lane === NoLane) {//Transition lanes are full, too
          lane = pickArbitraryLane(DefaultLanes);//Start with DefaultLanes
        }
      }
      return lane;
    }
  }
}

How does the high priority in the Lane model jump in the queue (racing to grab the track)

In the Lane model, if a low priority task is executed and a high priority task is triggered during scheduling, the high priority task interrupts the low priority task. At this time, the low priority task should be cancelled first, because the low priority task may have been in progress for some time and part of the Fiber tree has been built, Therefore, you need to restore the Fiber tree. This process occurs in the function prepareFreshStack, which initializes the built Fiber tree

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;//Callback of setState that has been called before
  //...
	if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    //If the callback priority of the new setState is equal to that of the previous setState, it will enter the logic of batchedUpdate
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    //If the two callback priorities are inconsistent, they will be interrupted by the high priority task. First cancel the current low priority task
    cancelCallback(existingCallbackNode);
  }
	//Starting point of scheduling render phase
	newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root),
  );
	//...
}

function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
	//...
  //Reassign and initialize variables such as workInProgressRoot
  workInProgressRoot = root;
  workInProgress = createWorkInProgress(root.current, null);
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootIncomplete;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
	//...
}

How to solve the hunger problem in the Lane model (the last car has to reach the finish line)

In the process of scheduling priority, markstarvedlanes asexpired will be called to traverse pendingLanes (the lane contained in the unexecuted task). If there is no expiration time, an expiration time will be calculated. If it is expired, it will be added to root.expiredLanes, and then expiredLanes will be returned first when getNextLane function is called next time

export function markStarvedLanesAsExpired(
  root: FiberRoot,
  currentTime: number,
): void {

  const pendingLanes = root.pendingLanes;
  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;
  const expirationTimes = root.expirationTimes;

  let lanes = pendingLanes;
  while (lanes > 0) {//Traversal lanes
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;

    const expirationTime = expirationTimes[index];
    if (expirationTime === NoTimestamp) {

      if (
        (lane & suspendedLanes) === NoLanes ||
        (lane & pingedLanes) !== NoLanes
      ) {
        expirationTimes[index] = computeExpirationTime(lane, currentTime);//Calculate expiration time
      }
    } else if (expirationTime <= currentTime) {//Out of date
      root.expiredLanes |= lane;//Add the currently traversed lane in expiredLanes
    }

    lanes &= ~lane;
  }
}

export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
 	//...
  if (expiredLanes !== NoLanes) {
    nextLanes = expiredLanes;
    nextLanePriority = return_highestLanePriority = SyncLanePriority;//Return expired lane first
  } else {
  //...
    }
  return nextLanes;
}

The following figure is more intuitive. With the passage of time, low priority tasks will be cut in the queue and eventually become high priority tasks

Topics: Front-end React