1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  * @jest-environment node
    
  9.  */
    
  10. 
    
  11. /* eslint-disable no-for-of-loops/no-for-of-loops */
    
  12. 
    
  13. 'use strict';
    
  14. 
    
  15. let Scheduler;
    
  16. // let runWithPriority;
    
  17. let ImmediatePriority;
    
  18. let UserBlockingPriority;
    
  19. let NormalPriority;
    
  20. let LowPriority;
    
  21. let IdlePriority;
    
  22. let scheduleCallback;
    
  23. let cancelCallback;
    
  24. // let wrapCallback;
    
  25. // let getCurrentPriorityLevel;
    
  26. // let shouldYield;
    
  27. let waitForAll;
    
  28. let waitFor;
    
  29. let waitForThrow;
    
  30. 
    
  31. function priorityLevelToString(priorityLevel) {
    
  32.   switch (priorityLevel) {
    
  33.     case ImmediatePriority:
    
  34.       return 'Immediate';
    
  35.     case UserBlockingPriority:
    
  36.       return 'User-blocking';
    
  37.     case NormalPriority:
    
  38.       return 'Normal';
    
  39.     case LowPriority:
    
  40.       return 'Low';
    
  41.     case IdlePriority:
    
  42.       return 'Idle';
    
  43.     default:
    
  44.       return null;
    
  45.   }
    
  46. }
    
  47. 
    
  48. describe('Scheduler', () => {
    
  49.   const {enableProfiling} = require('scheduler/src/SchedulerFeatureFlags');
    
  50.   if (!enableProfiling) {
    
  51.     // The tests in this suite only apply when profiling is on
    
  52.     it('profiling APIs are not available', () => {
    
  53.       Scheduler = require('scheduler');
    
  54.       expect(Scheduler.unstable_Profiling).toBe(null);
    
  55.     });
    
  56.     return;
    
  57.   }
    
  58. 
    
  59.   beforeEach(() => {
    
  60.     jest.resetModules();
    
  61.     jest.mock('scheduler', () => require('scheduler/unstable_mock'));
    
  62.     Scheduler = require('scheduler');
    
  63. 
    
  64.     // runWithPriority = Scheduler.unstable_runWithPriority;
    
  65.     ImmediatePriority = Scheduler.unstable_ImmediatePriority;
    
  66.     UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
    
  67.     NormalPriority = Scheduler.unstable_NormalPriority;
    
  68.     LowPriority = Scheduler.unstable_LowPriority;
    
  69.     IdlePriority = Scheduler.unstable_IdlePriority;
    
  70.     scheduleCallback = Scheduler.unstable_scheduleCallback;
    
  71.     cancelCallback = Scheduler.unstable_cancelCallback;
    
  72.     // wrapCallback = Scheduler.unstable_wrapCallback;
    
  73.     // getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
    
  74.     // shouldYield = Scheduler.unstable_shouldYield;
    
  75. 
    
  76.     const InternalTestUtils = require('internal-test-utils');
    
  77.     waitForAll = InternalTestUtils.waitForAll;
    
  78.     waitFor = InternalTestUtils.waitFor;
    
  79.     waitForThrow = InternalTestUtils.waitForThrow;
    
  80.   });
    
  81. 
    
  82.   const TaskStartEvent = 1;
    
  83.   const TaskCompleteEvent = 2;
    
  84.   const TaskErrorEvent = 3;
    
  85.   const TaskCancelEvent = 4;
    
  86.   const TaskRunEvent = 5;
    
  87.   const TaskYieldEvent = 6;
    
  88.   const SchedulerSuspendEvent = 7;
    
  89.   const SchedulerResumeEvent = 8;
    
  90. 
    
  91.   function stopProfilingAndPrintFlamegraph() {
    
  92.     const eventBuffer =
    
  93.       Scheduler.unstable_Profiling.stopLoggingProfilingEvents();
    
  94.     if (eventBuffer === null) {
    
  95.       return '(empty profile)';
    
  96.     }
    
  97. 
    
  98.     const eventLog = new Int32Array(eventBuffer);
    
  99. 
    
  100.     const tasks = new Map();
    
  101.     const mainThreadRuns = [];
    
  102. 
    
  103.     let isSuspended = true;
    
  104.     let i = 0;
    
  105.     processLog: while (i < eventLog.length) {
    
  106.       const instruction = eventLog[i];
    
  107.       const time = eventLog[i + 1];
    
  108.       switch (instruction) {
    
  109.         case 0: {
    
  110.           break processLog;
    
  111.         }
    
  112.         case TaskStartEvent: {
    
  113.           const taskId = eventLog[i + 2];
    
  114.           const priorityLevel = eventLog[i + 3];
    
  115.           const task = {
    
  116.             id: taskId,
    
  117.             priorityLevel,
    
  118.             label: null,
    
  119.             start: time,
    
  120.             end: -1,
    
  121.             exitStatus: null,
    
  122.             runs: [],
    
  123.           };
    
  124.           tasks.set(taskId, task);
    
  125.           i += 4;
    
  126.           break;
    
  127.         }
    
  128.         case TaskCompleteEvent: {
    
  129.           if (isSuspended) {
    
  130.             throw Error('Task cannot Complete outside the work loop.');
    
  131.           }
    
  132.           const taskId = eventLog[i + 2];
    
  133.           const task = tasks.get(taskId);
    
  134.           if (task === undefined) {
    
  135.             throw Error('Task does not exist.');
    
  136.           }
    
  137.           task.end = time;
    
  138.           task.exitStatus = 'completed';
    
  139.           i += 3;
    
  140.           break;
    
  141.         }
    
  142.         case TaskErrorEvent: {
    
  143.           if (isSuspended) {
    
  144.             throw Error('Task cannot Error outside the work loop.');
    
  145.           }
    
  146.           const taskId = eventLog[i + 2];
    
  147.           const task = tasks.get(taskId);
    
  148.           if (task === undefined) {
    
  149.             throw Error('Task does not exist.');
    
  150.           }
    
  151.           task.end = time;
    
  152.           task.exitStatus = 'errored';
    
  153.           i += 3;
    
  154.           break;
    
  155.         }
    
  156.         case TaskCancelEvent: {
    
  157.           const taskId = eventLog[i + 2];
    
  158.           const task = tasks.get(taskId);
    
  159.           if (task === undefined) {
    
  160.             throw Error('Task does not exist.');
    
  161.           }
    
  162.           task.end = time;
    
  163.           task.exitStatus = 'canceled';
    
  164.           i += 3;
    
  165.           break;
    
  166.         }
    
  167.         case TaskRunEvent:
    
  168.         case TaskYieldEvent: {
    
  169.           if (isSuspended) {
    
  170.             throw Error('Task cannot Run or Yield outside the work loop.');
    
  171.           }
    
  172.           const taskId = eventLog[i + 2];
    
  173.           const task = tasks.get(taskId);
    
  174.           if (task === undefined) {
    
  175.             throw Error('Task does not exist.');
    
  176.           }
    
  177.           task.runs.push(time);
    
  178.           i += 4;
    
  179.           break;
    
  180.         }
    
  181.         case SchedulerSuspendEvent: {
    
  182.           if (isSuspended) {
    
  183.             throw Error('Scheduler cannot Suspend outside the work loop.');
    
  184.           }
    
  185.           isSuspended = true;
    
  186.           mainThreadRuns.push(time);
    
  187.           i += 3;
    
  188.           break;
    
  189.         }
    
  190.         case SchedulerResumeEvent: {
    
  191.           if (!isSuspended) {
    
  192.             throw Error('Scheduler cannot Resume inside the work loop.');
    
  193.           }
    
  194.           isSuspended = false;
    
  195.           mainThreadRuns.push(time);
    
  196.           i += 3;
    
  197.           break;
    
  198.         }
    
  199.         default: {
    
  200.           throw Error('Unknown instruction type: ' + instruction);
    
  201.         }
    
  202.       }
    
  203.     }
    
  204. 
    
  205.     // Now we can render the tasks as a flamegraph.
    
  206.     const labelColumnWidth = 30;
    
  207.     // Scheduler event times are in microseconds
    
  208.     const microsecondsPerChar = 50000;
    
  209. 
    
  210.     let result = '';
    
  211. 
    
  212.     const mainThreadLabelColumn = '!!! Main thread              ';
    
  213.     let mainThreadTimelineColumn = '';
    
  214.     let isMainThreadBusy = true;
    
  215.     for (const time of mainThreadRuns) {
    
  216.       const index = time / microsecondsPerChar;
    
  217.       mainThreadTimelineColumn += (isMainThreadBusy ? '' : '').repeat(
    
  218.         index - mainThreadTimelineColumn.length,
    
  219.       );
    
  220.       isMainThreadBusy = !isMainThreadBusy;
    
  221.     }
    
  222.     result += `${mainThreadLabelColumn}${mainThreadTimelineColumn}\n`;
    
  223. 
    
  224.     const tasksByPriority = Array.from(tasks.values()).sort(
    
  225.       (t1, t2) => t1.priorityLevel - t2.priorityLevel,
    
  226.     );
    
  227. 
    
  228.     for (const task of tasksByPriority) {
    
  229.       let label = task.label;
    
  230.       if (label === undefined) {
    
  231.         label = 'Task';
    
  232.       }
    
  233.       let labelColumn = `Task ${task.id} [${priorityLevelToString(
    
  234.         task.priorityLevel,
    
  235.       )}]`;
    
  236.       labelColumn += ' '.repeat(labelColumnWidth - labelColumn.length - 1);
    
  237. 
    
  238.       // Add empty space up until the start mark
    
  239.       let timelineColumn = ' '.repeat(task.start / microsecondsPerChar);
    
  240. 
    
  241.       let isRunning = false;
    
  242.       for (const time of task.runs) {
    
  243.         const index = time / microsecondsPerChar;
    
  244.         timelineColumn += (isRunning ? '' : '').repeat(
    
  245.           index - timelineColumn.length,
    
  246.         );
    
  247.         isRunning = !isRunning;
    
  248.       }
    
  249. 
    
  250.       const endIndex = task.end / microsecondsPerChar;
    
  251.       timelineColumn += (isRunning ? '' : '').repeat(
    
  252.         endIndex - timelineColumn.length,
    
  253.       );
    
  254. 
    
  255.       if (task.exitStatus !== 'completed') {
    
  256.         timelineColumn += `🡐 ${task.exitStatus}`;
    
  257.       }
    
  258. 
    
  259.       result += `${labelColumn}${timelineColumn}\n`;
    
  260.     }
    
  261. 
    
  262.     return '\n' + result;
    
  263.   }
    
  264. 
    
  265.   it('creates a basic flamegraph', async () => {
    
  266.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  267. 
    
  268.     Scheduler.unstable_advanceTime(100);
    
  269.     scheduleCallback(
    
  270.       NormalPriority,
    
  271.       () => {
    
  272.         Scheduler.unstable_advanceTime(300);
    
  273.         Scheduler.log('Yield 1');
    
  274.         scheduleCallback(
    
  275.           UserBlockingPriority,
    
  276.           () => {
    
  277.             Scheduler.log('Yield 2');
    
  278.             Scheduler.unstable_advanceTime(300);
    
  279.           },
    
  280.           {label: 'Bar'},
    
  281.         );
    
  282.         Scheduler.unstable_advanceTime(100);
    
  283.         Scheduler.log('Yield 3');
    
  284.         return () => {
    
  285.           Scheduler.log('Yield 4');
    
  286.           Scheduler.unstable_advanceTime(300);
    
  287.         };
    
  288.       },
    
  289.       {label: 'Foo'},
    
  290.     );
    
  291.     await waitFor(['Yield 1', 'Yield 3']);
    
  292.     Scheduler.unstable_advanceTime(100);
    
  293.     await waitForAll(['Yield 2', 'Yield 4']);
    
  294. 
    
  295.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  296.       `
    
  297. !!! Main thread              │██░░░░░░░░██░░░░░░░░░░░░
    
  298. Task 2 [User-blocking]       │        ░░░░██████
    
  299. Task 1 [Normal]              │  ████████░░░░░░░░██████
    
  300. `,
    
  301.     );
    
  302.   });
    
  303. 
    
  304.   it('marks when a task is canceled', async () => {
    
  305.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  306. 
    
  307.     const task = scheduleCallback(NormalPriority, () => {
    
  308.       Scheduler.log('Yield 1');
    
  309.       Scheduler.unstable_advanceTime(300);
    
  310.       Scheduler.log('Yield 2');
    
  311.       return () => {
    
  312.         Scheduler.log('Continuation');
    
  313.         Scheduler.unstable_advanceTime(200);
    
  314.       };
    
  315.     });
    
  316. 
    
  317.     await waitFor(['Yield 1', 'Yield 2']);
    
  318.     Scheduler.unstable_advanceTime(100);
    
  319. 
    
  320.     cancelCallback(task);
    
  321. 
    
  322.     Scheduler.unstable_advanceTime(1000);
    
  323.     await waitForAll([]);
    
  324.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  325.       `
    
  326. !!! Main thread              │░░░░░░██████████████████████
    
  327. Task 1 [Normal]              │██████░░🡐 canceled
    
  328. `,
    
  329.     );
    
  330.   });
    
  331. 
    
  332.   it('marks when a task errors', async () => {
    
  333.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  334. 
    
  335.     scheduleCallback(NormalPriority, () => {
    
  336.       Scheduler.unstable_advanceTime(300);
    
  337.       throw Error('Oops');
    
  338.     });
    
  339. 
    
  340.     await waitForThrow('Oops');
    
  341.     Scheduler.unstable_advanceTime(100);
    
  342. 
    
  343.     Scheduler.unstable_advanceTime(1000);
    
  344.     await waitForAll([]);
    
  345.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  346.       `
    
  347. !!! Main thread              │░░░░░░██████████████████████
    
  348. Task 1 [Normal]              │██████🡐 errored
    
  349. `,
    
  350.     );
    
  351.   });
    
  352. 
    
  353.   it('marks when multiple tasks are canceled', async () => {
    
  354.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  355. 
    
  356.     const task1 = scheduleCallback(NormalPriority, () => {
    
  357.       Scheduler.log('Yield 1');
    
  358.       Scheduler.unstable_advanceTime(300);
    
  359.       Scheduler.log('Yield 2');
    
  360.       return () => {
    
  361.         Scheduler.log('Continuation');
    
  362.         Scheduler.unstable_advanceTime(200);
    
  363.       };
    
  364.     });
    
  365.     const task2 = scheduleCallback(NormalPriority, () => {
    
  366.       Scheduler.log('Yield 3');
    
  367.       Scheduler.unstable_advanceTime(300);
    
  368.       Scheduler.log('Yield 4');
    
  369.       return () => {
    
  370.         Scheduler.log('Continuation');
    
  371.         Scheduler.unstable_advanceTime(200);
    
  372.       };
    
  373.     });
    
  374. 
    
  375.     await waitFor(['Yield 1', 'Yield 2']);
    
  376.     Scheduler.unstable_advanceTime(100);
    
  377. 
    
  378.     cancelCallback(task1);
    
  379.     cancelCallback(task2);
    
  380. 
    
  381.     // Advance more time. This should not affect the size of the main
    
  382.     // thread row, since the Scheduler queue is empty.
    
  383.     Scheduler.unstable_advanceTime(1000);
    
  384.     await waitForAll([]);
    
  385. 
    
  386.     // The main thread row should end when the callback is cancelled.
    
  387.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  388.       `
    
  389. !!! Main thread              │░░░░░░██████████████████████
    
  390. Task 1 [Normal]              │██████░░🡐 canceled
    
  391. Task 2 [Normal]              │░░░░░░░░🡐 canceled
    
  392. `,
    
  393.     );
    
  394.   });
    
  395. 
    
  396.   it('handles cancelling a task that already finished', async () => {
    
  397.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  398. 
    
  399.     const task = scheduleCallback(NormalPriority, () => {
    
  400.       Scheduler.log('A');
    
  401.       Scheduler.unstable_advanceTime(1000);
    
  402.     });
    
  403.     await waitForAll(['A']);
    
  404.     cancelCallback(task);
    
  405.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  406.       `
    
  407. !!! Main thread              │░░░░░░░░░░░░░░░░░░░░
    
  408. Task 1 [Normal]              │████████████████████
    
  409. `,
    
  410.     );
    
  411.   });
    
  412. 
    
  413.   it('handles cancelling a task multiple times', async () => {
    
  414.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  415. 
    
  416.     scheduleCallback(
    
  417.       NormalPriority,
    
  418.       () => {
    
  419.         Scheduler.log('A');
    
  420.         Scheduler.unstable_advanceTime(1000);
    
  421.       },
    
  422.       {label: 'A'},
    
  423.     );
    
  424.     Scheduler.unstable_advanceTime(200);
    
  425.     const task = scheduleCallback(
    
  426.       NormalPriority,
    
  427.       () => {
    
  428.         Scheduler.log('B');
    
  429.         Scheduler.unstable_advanceTime(1000);
    
  430.       },
    
  431.       {label: 'B'},
    
  432.     );
    
  433.     Scheduler.unstable_advanceTime(400);
    
  434.     cancelCallback(task);
    
  435.     cancelCallback(task);
    
  436.     cancelCallback(task);
    
  437.     await waitForAll(['A']);
    
  438.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  439.       `
    
  440. !!! Main thread              │████████████░░░░░░░░░░░░░░░░░░░░
    
  441. Task 1 [Normal]              │░░░░░░░░░░░░████████████████████
    
  442. Task 2 [Normal]              │    ░░░░░░░░🡐 canceled
    
  443. `,
    
  444.     );
    
  445.   });
    
  446. 
    
  447.   it('handles delayed tasks', async () => {
    
  448.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  449.     scheduleCallback(
    
  450.       NormalPriority,
    
  451.       () => {
    
  452.         Scheduler.unstable_advanceTime(1000);
    
  453.         Scheduler.log('A');
    
  454.       },
    
  455.       {
    
  456.         delay: 1000,
    
  457.       },
    
  458.     );
    
  459.     await waitForAll([]);
    
  460. 
    
  461.     Scheduler.unstable_advanceTime(1000);
    
  462. 
    
  463.     await waitForAll(['A']);
    
  464. 
    
  465.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  466.       `
    
  467. !!! Main thread              │████████████████████░░░░░░░░░░░░░░░░░░░░
    
  468. Task 1 [Normal]              │                    ████████████████████
    
  469. `,
    
  470.     );
    
  471.   });
    
  472. 
    
  473.   it('handles cancelling a delayed task', async () => {
    
  474.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  475.     const task = scheduleCallback(NormalPriority, () => Scheduler.log('A'), {
    
  476.       delay: 1000,
    
  477.     });
    
  478.     cancelCallback(task);
    
  479.     await waitForAll([]);
    
  480.     expect(stopProfilingAndPrintFlamegraph()).toEqual(
    
  481.       `
    
  482. !!! Main thread              │
    
  483. `,
    
  484.     );
    
  485.   });
    
  486. 
    
  487.   it('automatically stops profiling and warns if event log gets too big', async () => {
    
  488.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  489. 
    
  490.     spyOnDevAndProd(console, 'error').mockImplementation(() => {});
    
  491. 
    
  492.     // Increase infinite loop guard limit
    
  493.     const originalMaxIterations = global.__MAX_ITERATIONS__;
    
  494.     global.__MAX_ITERATIONS__ = 120000;
    
  495. 
    
  496.     let taskId = 1;
    
  497.     while (console.error.mock.calls.length === 0) {
    
  498.       taskId++;
    
  499.       const task = scheduleCallback(NormalPriority, () => {});
    
  500.       cancelCallback(task);
    
  501.       Scheduler.unstable_flushAll();
    
  502.     }
    
  503. 
    
  504.     expect(console.error).toHaveBeenCalledTimes(1);
    
  505.     expect(console.error.mock.calls[0][0]).toBe(
    
  506.       "Scheduler Profiling: Event log exceeded maximum size. Don't forget " +
    
  507.         'to call `stopLoggingProfilingEvents()`.',
    
  508.     );
    
  509. 
    
  510.     // Should automatically clear profile
    
  511.     expect(stopProfilingAndPrintFlamegraph()).toEqual('(empty profile)');
    
  512. 
    
  513.     // Test that we can start a new profile later
    
  514.     Scheduler.unstable_Profiling.startLoggingProfilingEvents();
    
  515.     scheduleCallback(NormalPriority, () => {
    
  516.       Scheduler.unstable_advanceTime(1000);
    
  517.     });
    
  518.     await waitForAll([]);
    
  519. 
    
  520.     // Note: The exact task id is not super important. That just how many tasks
    
  521.     // it happens to take before the array is resized.
    
  522.     expect(stopProfilingAndPrintFlamegraph()).toEqual(`
    
  523. !!! Main thread              │░░░░░░░░░░░░░░░░░░░░
    
  524. Task ${taskId} [Normal]          │████████████████████
    
  525. `);
    
  526. 
    
  527.     global.__MAX_ITERATIONS__ = originalMaxIterations;
    
  528.   });
    
  529. });