/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/scheduler/src/__tests__/SchedulerProfiling-test.js
529 строк
16 KB
Andrew Clark
Revert "[Tests] Reset modules by default" (#28318)
13 фев 2024, 19:39
Не верифицирован
13 фев 2024, 19:39
015ff2e
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core * @jest-environment node */ /* eslint-disable no-for-of-loops/no-for-of-loops */ 'use strict'; let Scheduler; // let runWithPriority; let ImmediatePriority; let UserBlockingPriority; let NormalPriority; let LowPriority; let IdlePriority; let scheduleCallback; let cancelCallback; // let wrapCallback; // let getCurrentPriorityLevel; // let shouldYield; let waitForAll; let waitFor; let waitForThrow; function priorityLevelToString(priorityLevel) { switch (priorityLevel) { case ImmediatePriority: return 'Immediate'; case UserBlockingPriority: return 'User-blocking'; case NormalPriority: return 'Normal'; case LowPriority: return 'Low'; case IdlePriority: return 'Idle'; default: return null; } } describe('Scheduler', () => { const {enableProfiling} = require('scheduler/src/SchedulerFeatureFlags'); if (!enableProfiling) { // The tests in this suite only apply when profiling is on it('profiling APIs are not available', () => { Scheduler = require('scheduler'); expect(Scheduler.unstable_Profiling).toBe(null); }); return; } beforeEach(() => { jest.resetModules(); jest.mock('scheduler', () => require('scheduler/unstable_mock')); Scheduler = require('scheduler'); // runWithPriority = Scheduler.unstable_runWithPriority; ImmediatePriority = Scheduler.unstable_ImmediatePriority; UserBlockingPriority = Scheduler.unstable_UserBlockingPriority; NormalPriority = Scheduler.unstable_NormalPriority; LowPriority = Scheduler.unstable_LowPriority; IdlePriority = Scheduler.unstable_IdlePriority; scheduleCallback = Scheduler.unstable_scheduleCallback; cancelCallback = Scheduler.unstable_cancelCallback; // wrapCallback = Scheduler.unstable_wrapCallback; // getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel; // shouldYield = Scheduler.unstable_shouldYield; const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; waitFor = InternalTestUtils.waitFor; waitForThrow = InternalTestUtils.waitForThrow; }); const TaskStartEvent = 1; const TaskCompleteEvent = 2; const TaskErrorEvent = 3; const TaskCancelEvent = 4; const TaskRunEvent = 5; const TaskYieldEvent = 6; const SchedulerSuspendEvent = 7; const SchedulerResumeEvent = 8; function stopProfilingAndPrintFlamegraph() { const eventBuffer = Scheduler.unstable_Profiling.stopLoggingProfilingEvents(); if (eventBuffer === null) { return '(empty profile)'; } const eventLog = new Int32Array(eventBuffer); const tasks = new Map(); const mainThreadRuns = []; let isSuspended = true; let i = 0; processLog: while (i < eventLog.length) { const instruction = eventLog[i]; const time = eventLog[i + 1]; switch (instruction) { case 0: { break processLog; } case TaskStartEvent: { const taskId = eventLog[i + 2]; const priorityLevel = eventLog[i + 3]; const task = { id: taskId, priorityLevel, label: null, start: time, end: -1, exitStatus: null, runs: [], }; tasks.set(taskId, task); i += 4; break; } case TaskCompleteEvent: { if (isSuspended) { throw Error('Task cannot Complete outside the work loop.'); } const taskId = eventLog[i + 2]; const task = tasks.get(taskId); if (task === undefined) { throw Error('Task does not exist.'); } task.end = time; task.exitStatus = 'completed'; i += 3; break; } case TaskErrorEvent: { if (isSuspended) { throw Error('Task cannot Error outside the work loop.'); } const taskId = eventLog[i + 2]; const task = tasks.get(taskId); if (task === undefined) { throw Error('Task does not exist.'); } task.end = time; task.exitStatus = 'errored'; i += 3; break; } case TaskCancelEvent: { const taskId = eventLog[i + 2]; const task = tasks.get(taskId); if (task === undefined) { throw Error('Task does not exist.'); } task.end = time; task.exitStatus = 'canceled'; i += 3; break; } case TaskRunEvent: case TaskYieldEvent: { if (isSuspended) { throw Error('Task cannot Run or Yield outside the work loop.'); } const taskId = eventLog[i + 2]; const task = tasks.get(taskId); if (task === undefined) { throw Error('Task does not exist.'); } task.runs.push(time); i += 4; break; } case SchedulerSuspendEvent: { if (isSuspended) { throw Error('Scheduler cannot Suspend outside the work loop.'); } isSuspended = true; mainThreadRuns.push(time); i += 3; break; } case SchedulerResumeEvent: { if (!isSuspended) { throw Error('Scheduler cannot Resume inside the work loop.'); } isSuspended = false; mainThreadRuns.push(time); i += 3; break; } default: { throw Error('Unknown instruction type: ' + instruction); } } } // Now we can render the tasks as a flamegraph. const labelColumnWidth = 30; // Scheduler event times are in microseconds const microsecondsPerChar = 50000; let result = ''; const mainThreadLabelColumn = '!!! Main thread '; let mainThreadTimelineColumn = ''; let isMainThreadBusy = true; for (const time of mainThreadRuns) { const index = time / microsecondsPerChar; mainThreadTimelineColumn += (isMainThreadBusy ? '█' : '░').repeat( index - mainThreadTimelineColumn.length, ); isMainThreadBusy = !isMainThreadBusy; } result += `${mainThreadLabelColumn}│${mainThreadTimelineColumn}\n`; const tasksByPriority = Array.from(tasks.values()).sort( (t1, t2) => t1.priorityLevel - t2.priorityLevel, ); for (const task of tasksByPriority) { let label = task.label; if (label === undefined) { label = 'Task'; } let labelColumn = `Task ${task.id} [${priorityLevelToString( task.priorityLevel, )}]`; labelColumn += ' '.repeat(labelColumnWidth - labelColumn.length - 1); // Add empty space up until the start mark let timelineColumn = ' '.repeat(task.start / microsecondsPerChar); let isRunning = false; for (const time of task.runs) { const index = time / microsecondsPerChar; timelineColumn += (isRunning ? '█' : '░').repeat( index - timelineColumn.length, ); isRunning = !isRunning; } const endIndex = task.end / microsecondsPerChar; timelineColumn += (isRunning ? '█' : '░').repeat( endIndex - timelineColumn.length, ); if (task.exitStatus !== 'completed') { timelineColumn += `🡐 ${task.exitStatus}`; } result += `${labelColumn}│${timelineColumn}\n`; } return '\n' + result; } it('creates a basic flamegraph', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); Scheduler.unstable_advanceTime(100); scheduleCallback( NormalPriority, () => { Scheduler.unstable_advanceTime(300); Scheduler.log('Yield 1'); scheduleCallback( UserBlockingPriority, () => { Scheduler.log('Yield 2'); Scheduler.unstable_advanceTime(300); }, {label: 'Bar'}, ); Scheduler.unstable_advanceTime(100); Scheduler.log('Yield 3'); return () => { Scheduler.log('Yield 4'); Scheduler.unstable_advanceTime(300); }; }, {label: 'Foo'}, ); await waitFor(['Yield 1', 'Yield 3']); Scheduler.unstable_advanceTime(100); await waitForAll(['Yield 2', 'Yield 4']); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │██░░░░░░░░██░░░░░░░░░░░░ Task 2 [User-blocking] │ ░░░░██████ Task 1 [Normal] │ ████████░░░░░░░░██████ `, ); }); it('marks when a task is canceled', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); const task = scheduleCallback(NormalPriority, () => { Scheduler.log('Yield 1'); Scheduler.unstable_advanceTime(300); Scheduler.log('Yield 2'); return () => { Scheduler.log('Continuation'); Scheduler.unstable_advanceTime(200); }; }); await waitFor(['Yield 1', 'Yield 2']); Scheduler.unstable_advanceTime(100); cancelCallback(task); Scheduler.unstable_advanceTime(1000); await waitForAll([]); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │░░░░░░██████████████████████ Task 1 [Normal] │██████░░🡐 canceled `, ); }); it('marks when a task errors', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); scheduleCallback(NormalPriority, () => { Scheduler.unstable_advanceTime(300); throw Error('Oops'); }); await waitForThrow('Oops'); Scheduler.unstable_advanceTime(100); Scheduler.unstable_advanceTime(1000); await waitForAll([]); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │░░░░░░██████████████████████ Task 1 [Normal] │██████🡐 errored `, ); }); it('marks when multiple tasks are canceled', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); const task1 = scheduleCallback(NormalPriority, () => { Scheduler.log('Yield 1'); Scheduler.unstable_advanceTime(300); Scheduler.log('Yield 2'); return () => { Scheduler.log('Continuation'); Scheduler.unstable_advanceTime(200); }; }); const task2 = scheduleCallback(NormalPriority, () => { Scheduler.log('Yield 3'); Scheduler.unstable_advanceTime(300); Scheduler.log('Yield 4'); return () => { Scheduler.log('Continuation'); Scheduler.unstable_advanceTime(200); }; }); await waitFor(['Yield 1', 'Yield 2']); Scheduler.unstable_advanceTime(100); cancelCallback(task1); cancelCallback(task2); // Advance more time. This should not affect the size of the main // thread row, since the Scheduler queue is empty. Scheduler.unstable_advanceTime(1000); await waitForAll([]); // The main thread row should end when the callback is cancelled. expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │░░░░░░██████████████████████ Task 1 [Normal] │██████░░🡐 canceled Task 2 [Normal] │░░░░░░░░🡐 canceled `, ); }); it('handles cancelling a task that already finished', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); const task = scheduleCallback(NormalPriority, () => { Scheduler.log('A'); Scheduler.unstable_advanceTime(1000); }); await waitForAll(['A']); cancelCallback(task); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │░░░░░░░░░░░░░░░░░░░░ Task 1 [Normal] │████████████████████ `, ); }); it('handles cancelling a task multiple times', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); scheduleCallback( NormalPriority, () => { Scheduler.log('A'); Scheduler.unstable_advanceTime(1000); }, {label: 'A'}, ); Scheduler.unstable_advanceTime(200); const task = scheduleCallback( NormalPriority, () => { Scheduler.log('B'); Scheduler.unstable_advanceTime(1000); }, {label: 'B'}, ); Scheduler.unstable_advanceTime(400); cancelCallback(task); cancelCallback(task); cancelCallback(task); await waitForAll(['A']); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │████████████░░░░░░░░░░░░░░░░░░░░ Task 1 [Normal] │░░░░░░░░░░░░████████████████████ Task 2 [Normal] │ ░░░░░░░░🡐 canceled `, ); }); it('handles delayed tasks', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); scheduleCallback( NormalPriority, () => { Scheduler.unstable_advanceTime(1000); Scheduler.log('A'); }, { delay: 1000, }, ); await waitForAll([]); Scheduler.unstable_advanceTime(1000); await waitForAll(['A']); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │████████████████████░░░░░░░░░░░░░░░░░░░░ Task 1 [Normal] │ ████████████████████ `, ); }); it('handles cancelling a delayed task', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); const task = scheduleCallback(NormalPriority, () => Scheduler.log('A'), { delay: 1000, }); cancelCallback(task); await waitForAll([]); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │ `, ); }); it('automatically stops profiling and warns if event log gets too big', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); spyOnDevAndProd(console, 'error').mockImplementation(() => {}); // Increase infinite loop guard limit const originalMaxIterations = global.__MAX_ITERATIONS__; global.__MAX_ITERATIONS__ = 120000; let taskId = 1; while (console.error.mock.calls.length === 0) { taskId++; const task = scheduleCallback(NormalPriority, () => {}); cancelCallback(task); Scheduler.unstable_flushAll(); } expect(console.error).toHaveBeenCalledTimes(1); expect(console.error.mock.calls[0][0]).toBe( "Scheduler Profiling: Event log exceeded maximum size. Don't forget " + 'to call `stopLoggingProfilingEvents()`.', ); // Should automatically clear profile expect(stopProfilingAndPrintFlamegraph()).toEqual('(empty profile)'); // Test that we can start a new profile later Scheduler.unstable_Profiling.startLoggingProfilingEvents(); scheduleCallback(NormalPriority, () => { Scheduler.unstable_advanceTime(1000); }); await waitForAll([]); // Note: The exact task id is not super important. That just how many tasks // it happens to take before the array is resized. expect(stopProfilingAndPrintFlamegraph()).toEqual(` !!! Main thread │░░░░░░░░░░░░░░░░░░░░ Task ${taskId} [Normal] │████████████████████ `); global.__MAX_ITERATIONS__ = originalMaxIterations; }); });