diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index b57dd8fb54..8fdab3e563 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,6 +1,6 @@ { "packages": ["packages/react", "packages/react-dom", "packages/scheduler"], - "buildCommand": "build --type=NODE react/index,react-dom/index,react-dom/server,react-dom/test-utils,scheduler/index,scheduler/unstable_no_dom,scheduler/tracing,react/jsx-runtime,react/jsx-dev-runtime", + "buildCommand": "build --type=NODE react/index,react-dom/index,react-dom/server,react-dom/test-utils,scheduler/index,scheduler/unstable_no_dom,react/jsx-runtime,react/jsx-dev-runtime", "publishDirectory": { "react": "build/node_modules/react", "react-dom": "build/node_modules/react-dom", diff --git a/fixtures/tracing/index.html b/fixtures/tracing/index.html deleted file mode 100644 index 61b2af1a43..0000000000 --- a/fixtures/tracing/index.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - Test tracing UMD - - - -

Test tracing UMD

-

- This fixture tests that the new tracing API is accessible via UMD build using the UMD shim. - It does not exhaustively test API functionality, only that the forwarded methods can be called. -

-

- Before running the tests below, check the console to make sure there are no errors. -

-

- Tests - -

-
    -
  1. - Test scheduler API -
  2. -
  3. - Test tracing API -
  4. -
  5. - Test tracing subscriptions API -
  6. -
  7. - Test end-to-end integration -
  8. -
- - - - - - - - - - - diff --git a/fixtures/tracing/script.js b/fixtures/tracing/script.js deleted file mode 100644 index 72a86c618d..0000000000 --- a/fixtures/tracing/script.js +++ /dev/null @@ -1,206 +0,0 @@ -function runTest(listItem, callback) { - try { - callback(); - listItem.className = 'correct'; - listItem.setAttribute('data-value', 'All checks pass'); - } catch (error) { - listItem.className = 'incorrect'; - listItem.setAttribute('data-value', error); - } -} - -function runAllTests() { - try { - checkSchedulerAPI(); - } finally { - try { - checkSchedulerTracingAPI(); - } finally { - try { - checkSchedulerTracingSubscriptionsAPI(); - } finally { - checkEndToEndIntegration(); - } - } - } -} - -function checkSchedulerAPI() { - runTest(document.getElementById('checkSchedulerAPI'), () => { - if ( - typeof Scheduler === 'undefined' || - typeof Scheduler.unstable_now !== 'function' || - typeof Scheduler.unstable_scheduleCallback !== 'function' || - typeof Scheduler.unstable_cancelCallback !== 'function' - ) { - throw 'API is not defined'; - } - - const abs = Math.abs(Scheduler.unstable_now() - performance.now()); - if (typeof abs !== 'number' || Number.isNaN(abs) || abs > 5) { - throw 'API does not work'; - } - - // There is no real way to verify that the two APIs are connected. - }); -} - -function checkSchedulerTracingAPI() { - runTest(document.getElementById('checkSchedulerTracingAPI'), () => { - if ( - typeof SchedulerTracing === 'undefined' || - typeof SchedulerTracing.unstable_clear !== 'function' || - typeof SchedulerTracing.unstable_getCurrent !== 'function' || - typeof SchedulerTracing.unstable_getThreadID !== 'function' || - typeof SchedulerTracing.unstable_trace !== 'function' || - typeof SchedulerTracing.unstable_wrap !== 'function' - ) { - throw 'API is not defined'; - } - - try { - let interactionsSet; - SchedulerTracing.unstable_trace('test', 123, () => { - interactionsSet = SchedulerTracing.unstable_getCurrent(); - }); - if (interactionsSet.size !== 1) { - throw null; - } - const interaction = Array.from(interactionsSet)[0]; - if (interaction.name !== 'test' || interaction.timestamp !== 123) { - throw null; - } - } catch (error) { - throw 'API does not work'; - } - - const ForwardedSchedulerTracing = - React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing; - - if ( - SchedulerTracing.unstable_getThreadID() === - ForwardedSchedulerTracing.unstable_getThreadID() - ) { - throw 'API forwarding is broken'; - } - }); -} - -function checkSchedulerTracingSubscriptionsAPI() { - runTest( - document.getElementById('checkSchedulerTracingSubscriptionsAPI'), - () => { - if ( - typeof SchedulerTracing === 'undefined' || - typeof SchedulerTracing.unstable_subscribe !== 'function' || - typeof SchedulerTracing.unstable_unsubscribe !== 'function' - ) { - throw 'API is not defined'; - } - - const onInteractionScheduledWorkCompletedCalls = []; - const onInteractionTracedCalls = []; - const onWorkCanceledCalls = []; - const onWorkScheduledCalls = []; - const onWorkStartedCalls = []; - const onWorkStoppedCalls = []; - const subscriber = { - onInteractionScheduledWorkCompleted: (...args) => - onInteractionScheduledWorkCompletedCalls.push(args), - onInteractionTraced: (...args) => onInteractionTracedCalls.push(args), - onWorkCanceled: (...args) => onWorkCanceledCalls.push(args), - onWorkScheduled: (...args) => onWorkScheduledCalls.push(args), - onWorkStarted: (...args) => onWorkStartedCalls.push(args), - onWorkStopped: (...args) => onWorkStoppedCalls.push(args), - }; - - try { - SchedulerTracing.unstable_subscribe(subscriber); - SchedulerTracing.unstable_trace('foo', 123, () => {}); - SchedulerTracing.unstable_unsubscribe(subscriber); - if (onInteractionTracedCalls.length !== 1) { - throw null; - } - const interaction = onInteractionTracedCalls[0][0]; - if (interaction.name !== 'foo' || interaction.timestamp !== 123) { - throw null; - } - SchedulerTracing.unstable_trace('bar', 456, () => {}); - if (onInteractionTracedCalls.length !== 1) { - throw null; - } - } catch (error) { - throw 'API does not forward methods'; - } - - const ForwardedSchedulerTracing = - React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED - .SchedulerTracing; - - try { - ForwardedSchedulerTracing.unstable_subscribe(subscriber); - SchedulerTracing.unstable_trace('foo', 123, () => {}); - ForwardedSchedulerTracing.unstable_trace('bar', 456, () => {}); - SchedulerTracing.unstable_unsubscribe(subscriber); - if (onInteractionTracedCalls.length !== 3) { - throw null; - } - const interactionFoo = onInteractionTracedCalls[1][0]; - const interactionBar = onInteractionTracedCalls[2][0]; - if ( - interactionFoo.name !== 'foo' || - interactionFoo.timestamp !== 123 || - interactionBar.name !== 'bar' || - interactionBar.timestamp !== 456 - ) { - throw null; - } - ForwardedSchedulerTracing.unstable_trace('baz', 789, () => {}); - if (onInteractionTracedCalls.length !== 3) { - throw null; - } - } catch (error) { - throw 'API forwarding is broken'; - } - } - ); -} - -function checkEndToEndIntegration() { - runTest(document.getElementById('checkEndToEndIntegration'), () => { - try { - const onRenderCalls = []; - const onRender = (...args) => onRenderCalls.push(args); - const container = document.createElement('div'); - - SchedulerTracing.unstable_trace('render', 123, () => { - ReactDOM.render( - React.createElement( - React.Profiler, - {id: 'profiler', onRender}, - React.createElement('div', null, 'hi') - ), - container - ); - }); - - if (container.textContent !== 'hi') { - throw null; - } - - if (onRenderCalls.length !== 1) { - throw null; - } - const call = onRenderCalls[0]; - if (call.length !== 7) { - throw null; - } - const interaction = Array.from(call[6])[0]; - if (interaction.name !== 'render' || interaction.timestamp !== 123) { - throw null; - } - } catch (error) { - throw 'End to end integration is broken'; - } - }); -} diff --git a/fixtures/tracing/test.html b/fixtures/tracing/test.html deleted file mode 100644 index df09e7b62d..0000000000 --- a/fixtures/tracing/test.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Test tracing UMD - - - -
- - - - - - - - diff --git a/fixtures/tracing/test.js b/fixtures/tracing/test.js deleted file mode 100644 index 18753a8193..0000000000 --- a/fixtures/tracing/test.js +++ /dev/null @@ -1,101 +0,0 @@ -const {createElement, Component, Suspense} = React; -const {createRoot} = ReactDOM; -const { - unstable_subscribe: subscribe, - unstable_trace: trace, - unstable_wrap: wrap, -} = SchedulerTracing; - -const createLogger = (backgroundColor, color, enabled) => ( - message, - ...args -) => { - if (enabled === false) return; - console.groupCollapsed( - `%c${message}`, - `background-color: ${backgroundColor}; color: ${color}; padding: 2px 4px;`, - ...args - ); - console.log( - new Error('stack').stack - .split('\n') - .slice(2) - .join('\n') - ); - console.groupEnd(); -}; - -window.log = { - app: createLogger('#37474f', '#fff'), - interaction: createLogger('#6a1b9a', '#fff'), - react: createLogger('#ff5722', '#fff'), - tracing: createLogger('#2962ff', '#fff'), - work: createLogger('#e1bee7', '#000'), -}; - -// Fake suspense -const resolvedValues = {}; -const read = key => { - if (!resolvedValues[key]) { - log.app(`Suspending for "${key}" ...`); - throw new Promise( - wrap(resolve => { - setTimeout( - wrap(() => { - log.app(`Loaded "${key}" ...`); - resolvedValues[key] = true; - resolve(key); - }), - 1000 - ); - }) - ); - } - return key; -}; - -const TestApp = () => - createElement( - Suspense, - {fallback: createElement(PlaceholderText)}, - createElement(SuspendingChild, {text: 'foo'}), - createElement(SuspendingChild, {text: 'bar'}), - createElement(SuspendingChild, {text: 'baz'}) - ); - -const PlaceholderText = () => 'Loading ...'; - -const SuspendingChild = ({text}) => { - const resolvedValue = read(text); - return resolvedValue; -}; - -subscribe({ - onInteractionScheduledWorkCompleted: interaction => - log.interaction( - 'onInteractionScheduledWorkCompleted', - JSON.stringify(interaction) - ), - onInteractionTraced: interaction => - log.interaction('onInteractionTraced', JSON.stringify(interaction)), - onWorkCanceled: interactions => - log.work('onWorkCanceled', JSON.stringify(Array.from(interactions))), - onWorkScheduled: interactions => - log.work('onWorkScheduled', JSON.stringify(Array.from(interactions))), - onWorkStarted: interactions => - log.work('onWorkStarted', JSON.stringify(Array.from(interactions))), - onWorkStopped: interactions => - log.work('onWorkStopped', JSON.stringify(Array.from(interactions))), -}); - -const element = document.getElementById('root'); -trace('initial_render', performance.now(), () => { - const root = createRoot(element); - log.app('render()'); - root.render( - createElement(TestApp), - wrap(() => { - log.app('committed'); - }) - ); -}); diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap index 9b94c71e6e..ad1d487ea7 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -39,7 +39,6 @@ Object { 3 => 1, 5 => 1, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 16, @@ -87,7 +86,6 @@ Object { 3 => 3, 4 => 2, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 15, @@ -124,7 +122,6 @@ Object { 5 => 3, 3 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 18, @@ -188,7 +185,6 @@ Object { 4 => 1, 5 => 1, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 12, @@ -254,7 +250,6 @@ Object { 2 => 10, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 25, @@ -302,7 +297,6 @@ Object { 2 => 10, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 35, @@ -341,7 +335,6 @@ Object { 2 => 10, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 45, @@ -451,7 +444,6 @@ Object { 1, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 12, @@ -556,7 +548,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 25, @@ -625,7 +616,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 35, @@ -676,7 +666,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 45, @@ -693,8 +682,6 @@ Object { ], "displayName": "Parent", "initialTreeBaseDurations": Array [], - "interactionCommits": Array [], - "interactions": Array [], "operations": Array [ Array [ 1, @@ -950,7 +937,6 @@ Object { 1, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 11, @@ -1037,7 +1023,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 22, @@ -1142,7 +1127,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 35, @@ -1159,8 +1143,6 @@ Object { ], "displayName": "Parent", "initialTreeBaseDurations": Array [], - "interactionCommits": Array [], - "interactions": Array [], "operations": Array [ Array [ 1, @@ -1352,7 +1334,6 @@ Object { 2 => 10, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 13, @@ -1397,7 +1378,6 @@ Object { 2 => 10, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 34, @@ -1433,7 +1413,6 @@ Object { 2 => 10, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 44, @@ -1456,8 +1435,6 @@ Object { 4 => 1, 5 => 1, }, - "interactionCommits": Map {}, - "interactions": Map {}, "operations": Array [ Array [ 1, @@ -1619,7 +1596,6 @@ Object { 13 => 0, 14 => 1, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 24, @@ -1636,8 +1612,6 @@ Object { ], "displayName": "Parent", "initialTreeBaseDurations": Map {}, - "interactionCommits": Map {}, - "interactions": Map {}, "operations": Array [ Array [ 1, @@ -1712,7 +1686,6 @@ Object { "effectDuration": null, "fiberActualDurations": Map {}, "fiberSelfDurations": Map {}, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 34, @@ -1734,8 +1707,6 @@ Object { 8 => 0, 9 => 1, }, - "interactionCommits": Map {}, - "interactions": Map {}, "operations": Array [ Array [ 1, @@ -1890,7 +1861,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 13, @@ -1959,7 +1929,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 34, @@ -2010,7 +1979,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 44, @@ -2048,8 +2016,6 @@ Object { 1, ], ], - "interactionCommits": Array [], - "interactions": Array [], "operations": Array [ Array [ 1, @@ -2256,7 +2222,6 @@ Object { 1, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 24, @@ -2273,8 +2238,6 @@ Object { ], "displayName": "Parent", "initialTreeBaseDurations": Array [], - "interactionCommits": Array [], - "interactions": Array [], "operations": Array [ Array [ 1, @@ -2346,7 +2309,6 @@ Object { "effectDuration": null, "fiberActualDurations": Array [], "fiberSelfDurations": Array [], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 34, @@ -2380,8 +2342,6 @@ Object { 1, ], ], - "interactionCommits": Array [], - "interactions": Array [], "operations": Array [ Array [ 1, @@ -2470,7 +2430,6 @@ Object { 1 => 0, 2 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, @@ -2487,8 +2446,6 @@ Object { ], "displayName": "Suspense", "initialTreeBaseDurations": Map {}, - "interactionCommits": Map {}, - "interactions": Map {}, "operations": Array [ Array [ 1, @@ -2548,7 +2505,6 @@ Object { 2 => 0, 3 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, @@ -2589,7 +2545,6 @@ Object { 2 => 0, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2624,7 +2579,6 @@ Object { "fiberSelfDurations": Map { 3 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2659,7 +2613,6 @@ Object { "fiberSelfDurations": Map { 3 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2698,7 +2651,6 @@ Object { 2 => 0, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2762,7 +2714,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, @@ -2821,7 +2772,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2862,7 +2812,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2903,7 +2852,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2960,7 +2908,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -2977,8 +2924,6 @@ Object { ], "displayName": "Component", "initialTreeBaseDurations": Array [], - "interactionCommits": Array [], - "interactions": Array [], "operations": Array [ Array [ 1, @@ -3125,7 +3070,6 @@ Object { 6 => 0, 7 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, @@ -3206,7 +3150,6 @@ Object { 3 => 0, 2 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -3283,7 +3226,6 @@ Object { 2 => 0, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -3361,7 +3303,6 @@ Object { 2 => 0, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -3438,7 +3379,6 @@ Object { 2 => 0, 1 => 0, }, - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -3574,7 +3514,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, @@ -3703,7 +3642,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -3834,7 +3772,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -3966,7 +3903,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -4097,7 +4033,6 @@ Object { 0, ], ], - "interactionIDs": Array [], "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, @@ -4114,8 +4049,6 @@ Object { ], "displayName": "LegacyContextProvider", "initialTreeBaseDurations": Array [], - "interactionCommits": Array [], - "interactions": Array [], "operations": Array [ Array [ 1, @@ -4325,341 +4258,3 @@ Object { "version": 5, } `; - -exports[`ProfilingCache should report every traced interaction: Interactions 1`] = ` -Array [ - Object { - "__count": 1, - "id": 0, - "name": "mount: one child", - "timestamp": 0, - }, - Object { - "__count": 0, - "id": 1, - "name": "update: two children", - "timestamp": 11, - }, -] -`; - -exports[`ProfilingCache should report every traced interaction: imported data 1`] = ` -Object { - "dataForRoots": Array [ - Object { - "commitData": Array [ - Object { - "changeDescriptions": Array [ - Array [ - 2, - Object { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - ], - Array [ - 3, - Object { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - ], - Array [ - 4, - Object { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - ], - ], - "duration": 11, - "effectDuration": null, - "fiberActualDurations": Array [ - Array [ - 1, - 11, - ], - Array [ - 2, - 11, - ], - Array [ - 3, - 0, - ], - Array [ - 4, - 1, - ], - ], - "fiberSelfDurations": Array [ - Array [ - 1, - 0, - ], - Array [ - 2, - 10, - ], - Array [ - 3, - 0, - ], - Array [ - 4, - 1, - ], - ], - "interactionIDs": Array [ - 0, - ], - "passiveEffectDuration": null, - "priorityLevel": "Normal", - "timestamp": 11, - "updaters": Array [ - Object { - "displayName": "Anonymous", - "hocDisplayNames": null, - "id": 1, - "key": null, - "type": 11, - }, - ], - }, - Object { - "changeDescriptions": Array [ - Array [ - 3, - Object { - "context": null, - "didHooksChange": false, - "isFirstMount": false, - "props": Array [], - "state": null, - }, - ], - Array [ - 5, - Object { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - ], - Array [ - 2, - Object { - "context": null, - "didHooksChange": false, - "isFirstMount": false, - "props": Array [ - "count", - ], - "state": null, - }, - ], - ], - "duration": 11, - "effectDuration": null, - "fiberActualDurations": Array [ - Array [ - 3, - 0, - ], - Array [ - 5, - 1, - ], - Array [ - 2, - 11, - ], - Array [ - 1, - 11, - ], - ], - "fiberSelfDurations": Array [ - Array [ - 3, - 0, - ], - Array [ - 5, - 1, - ], - Array [ - 2, - 10, - ], - Array [ - 1, - 0, - ], - ], - "interactionIDs": Array [ - 1, - ], - "passiveEffectDuration": null, - "priorityLevel": "Immediate", - "timestamp": 22, - "updaters": Array [ - Object { - "displayName": "Anonymous", - "hocDisplayNames": null, - "id": 1, - "key": null, - "type": 11, - }, - ], - }, - ], - "displayName": "Parent", - "initialTreeBaseDurations": Array [], - "interactionCommits": Array [ - Array [ - 0, - Array [ - 0, - ], - ], - Array [ - 1, - Array [ - 1, - ], - ], - ], - "interactions": Array [ - Array [ - 0, - Object { - "__count": 1, - "id": 0, - "name": "mount: one child", - "timestamp": 0, - }, - ], - Array [ - 1, - Object { - "__count": 0, - "id": 1, - "name": "update: two children", - "timestamp": 11, - }, - ], - ], - "operations": Array [ - Array [ - 1, - 1, - 15, - 6, - 80, - 97, - 114, - 101, - 110, - 116, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 48, - 1, - 1, - 11, - 1, - 1, - 4, - 1, - 11000, - 1, - 2, - 5, - 1, - 0, - 1, - 0, - 4, - 2, - 11000, - 1, - 3, - 5, - 2, - 2, - 2, - 3, - 4, - 3, - 0, - 1, - 4, - 8, - 2, - 2, - 2, - 0, - 4, - 4, - 1000, - ], - Array [ - 1, - 1, - 8, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 49, - 1, - 5, - 5, - 2, - 2, - 1, - 2, - 4, - 5, - 1000, - 4, - 2, - 12000, - 3, - 2, - 3, - 3, - 5, - 4, - 4, - 1, - 12000, - ], - ], - "rootID": 1, - "snapshots": Array [], - }, - ], - "version": 5, -} -`; diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCharts-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCharts-test.js.snap index 5a497bafb6..e118ef9da3 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCharts-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCharts-test.js.snap @@ -255,48 +255,6 @@ Object { } `; -exports[`profiling charts interactions should contain valid data: Interactions 1`] = ` -Object { - "interactions": Array [ - Object { - "__count": 1, - "id": 0, - "name": "mount", - "timestamp": 0, - }, - Object { - "__count": 0, - "id": 1, - "name": "update", - "timestamp": 15, - }, - ], - "lastInteractionTime": 25, - "maxCommitDuration": 15, -} -`; - -exports[`profiling charts interactions should contain valid data: Interactions 2`] = ` -Object { - "interactions": Array [ - Object { - "__count": 1, - "id": 0, - "name": "mount", - "timestamp": 0, - }, - Object { - "__count": 0, - "id": 1, - "name": "update", - "timestamp": 15, - }, - ], - "lastInteractionTime": 25, - "maxCommitDuration": 15, -} -`; - exports[`profiling charts ranked chart should contain valid data: 0: CommitTree 1`] = ` Object { "nodes": Map { diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index 84311c0d87..0b7273054c 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -16,7 +16,6 @@ describe('ProfilingCache', () => { let React; let ReactDOM; let Scheduler; - let SchedulerTracing; let TestRenderer: ReactTestRenderer; let bridge: FrontendBridge; let store: Store; @@ -35,7 +34,6 @@ describe('ProfilingCache', () => { React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); TestRenderer = utils.requireTestRenderer(); }); @@ -622,79 +620,6 @@ describe('ProfilingCache', () => { } }); - it('should report every traced interaction', () => { - const Parent = ({count}) => { - Scheduler.unstable_advanceTime(10); - const children = new Array(count) - .fill(true) - .map((_, index) => ); - return ( - - {children} - - - ); - }; - const Child = ({duration}) => { - Scheduler.unstable_advanceTime(duration); - return null; - }; - const MemoizedChild = React.memo(Child); - - const container = document.createElement('div'); - - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => - SchedulerTracing.unstable_trace( - 'mount: one child', - Scheduler.unstable_now(), - () => ReactDOM.render(, container), - ), - ); - utils.act(() => - SchedulerTracing.unstable_trace( - 'update: two children', - Scheduler.unstable_now(), - () => ReactDOM.render(, container), - ), - ); - utils.act(() => store.profilerStore.stopProfiling()); - - let interactions = null; - - function Validator({previousInteractions, rootID}) { - interactions = store.profilerStore.profilingCache.getInteractionsChartData( - { - rootID, - }, - ).interactions; - if (previousInteractions != null) { - expect(interactions).toEqual(previousInteractions); - } else { - expect(interactions).toMatchSnapshot('Interactions'); - } - return null; - } - - const rootID = store.roots[0]; - - utils.act(() => - TestRenderer.create( - , - ), - ); - - expect(interactions).not.toBeNull(); - - utils.exportImportHelper(bridge, store); - - utils.act(() => - TestRenderer.create( - , - ), - ); - }); - it('should handle unexpectedly shallow suspense trees', () => { const container = document.createElement('div'); diff --git a/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js b/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js index 802a742157..b16e38f88c 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js @@ -14,7 +14,6 @@ describe('profiling charts', () => { let React; let ReactDOM; let Scheduler; - let SchedulerTracing; let TestRenderer: TestRendererType; let store: Store; let utils; @@ -30,7 +29,6 @@ describe('profiling charts', () => { React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); TestRenderer = utils.requireTestRenderer(); }); @@ -56,18 +54,8 @@ describe('profiling charts', () => { const container = document.createElement('div'); utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => - SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => - ReactDOM.render(, container), - ), - ); - utils.act(() => - SchedulerTracing.unstable_trace( - 'update', - Scheduler.unstable_now(), - () => ReactDOM.render(, container), - ), - ); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); utils.act(() => store.profilerStore.stopProfiling()); let renderFinished = false; @@ -132,18 +120,8 @@ describe('profiling charts', () => { const container = document.createElement('div'); utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => - SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => - ReactDOM.render(, container), - ), - ); - utils.act(() => - SchedulerTracing.unstable_trace( - 'update', - Scheduler.unstable_now(), - () => ReactDOM.render(, container), - ), - ); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); utils.act(() => store.profilerStore.stopProfiling()); let renderFinished = false; @@ -181,69 +159,4 @@ describe('profiling charts', () => { } }); }); - - describe('interactions', () => { - it('should contain valid data', () => { - const Parent = (_: {||}) => { - Scheduler.unstable_advanceTime(10); - return ( - - - - - - ); - }; - - // Memoize children to verify that chart doesn't include in the update. - const Child = React.memo(function Child({duration}) { - Scheduler.unstable_advanceTime(duration); - return null; - }); - - const container = document.createElement('div'); - - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => - SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => - ReactDOM.render(, container), - ), - ); - utils.act(() => - SchedulerTracing.unstable_trace( - 'update', - Scheduler.unstable_now(), - () => ReactDOM.render(, container), - ), - ); - utils.act(() => store.profilerStore.stopProfiling()); - - let renderFinished = false; - - function Validator({commitIndex, rootID}) { - const chartData = store.profilerStore.profilingCache.getInteractionsChartData( - { - rootID, - }, - ); - expect(chartData).toMatchSnapshot('Interactions'); - renderFinished = true; - return null; - } - - const rootID = store.roots[0]; - - for (let commitIndex = 0; commitIndex < 2; commitIndex++) { - renderFinished = false; - - utils.act(() => { - TestRenderer.create( - , - ); - }); - - expect(renderFinished).toBe(true); - } - }); - }); }); diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 807bbde0c7..c1172bfbe1 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -102,7 +102,6 @@ import type { SerializedElement, WorkTagMap, } from './types'; -import type {Interaction} from 'react-devtools-shared/src/devtools/views/Profiler/types'; import type { ComponentFilter, ElementType, @@ -2136,6 +2135,22 @@ export function attach( // We don't patch any methods so there is no cleanup. } + function rootSupportsProfiling(root) { + if (root.memoizedInteractions != null) { + // v16 builds include this field for the scheduler/tracing API. + return true; + } else if ( + root.current != null && + root.current.hasOwnProperty('treeBaseDuration') + ) { + // The scheduler/tracing API was removed in v17 though + // so we need to check a non-root Fiber. + return true; + } else { + return false; + } + } + function flushInitialOperations() { const localPendingOperationsQueue = pendingOperationsQueue; @@ -2161,21 +2176,14 @@ export function attach( currentRootID = getFiberID(getPrimaryFiber(root.current)); setRootPseudoKey(currentRootID, root.current); - // Checking root.memoizedInteractions handles multi-renderer edge-case- - // where some v16 renderers support profiling and others don't. - if (isProfiling && root.memoizedInteractions != null) { - // If profiling is active, store commit time and duration, and the current interactions. + // Handle multi-renderer edge-case where only some v16 renderers support profiling. + if (isProfiling && rootSupportsProfiling(root)) { + // If profiling is active, store commit time and duration. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { changeDescriptions: recordChangeDescriptions ? new Map() : null, durations: [], commitTime: getCurrentTime() - profilingStartTime, - interactions: Array.from(root.memoizedInteractions).map( - (interaction: Interaction) => ({ - ...interaction, - timestamp: interaction.timestamp - profilingStartTime, - }), - ), maxActualDuration: 0, priorityLevel: null, updaters: getUpdatersList(root), @@ -2205,8 +2213,7 @@ export function attach( } function handlePostCommitFiberRoot(root) { - const isProfilingSupported = root.memoizedInteractions != null; - if (isProfiling && isProfilingSupported) { + if (isProfiling && rootSupportsProfiling(root)) { if (currentCommitProfilingMetadata !== null) { const {effectDuration, passiveEffectDuration} = getEffectDurations( root, @@ -2233,23 +2240,16 @@ export function attach( traceUpdatesForNodes.clear(); } - // Checking root.memoizedInteractions handles multi-renderer edge-case- - // where some v16 renderers support profiling and others don't. - const isProfilingSupported = root.memoizedInteractions != null; + // Handle multi-renderer edge-case where only some v16 renderers support profiling. + const isProfilingSupported = rootSupportsProfiling(root); if (isProfiling && isProfilingSupported) { - // If profiling is active, store commit time and duration, and the current interactions. + // If profiling is active, store commit time and duration. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { changeDescriptions: recordChangeDescriptions ? new Map() : null, durations: [], commitTime: getCurrentTime() - profilingStartTime, - interactions: Array.from(root.memoizedInteractions).map( - (interaction: Interaction) => ({ - ...interaction, - timestamp: interaction.timestamp - profilingStartTime, - }), - ), maxActualDuration: 0, priorityLevel: priorityLevel == null ? null : formatPriorityLevel(priorityLevel), @@ -3386,7 +3386,6 @@ export function attach( commitTime: number, durations: Array, effectDuration: number | null, - interactions: Array, maxActualDuration: number, passiveEffectDuration: number | null, priorityLevel: string | null, @@ -3419,8 +3418,6 @@ export function attach( (commitProfilingMetadata, rootID) => { const commitData: Array = []; const initialTreeBaseDurations: Array<[number, number]> = []; - const allInteractions: Map = new Map(); - const interactionCommits: Map> = new Map(); const displayName = (displayNamesByRootID !== null && displayNamesByRootID.get(rootID)) || @@ -3444,7 +3441,6 @@ export function attach( changeDescriptions, durations, effectDuration, - interactions, maxActualDuration, passiveEffectDuration, priorityLevel, @@ -3452,23 +3448,6 @@ export function attach( updaters, } = commitProfilingData; - const interactionIDs: Array = []; - - interactions.forEach(interaction => { - if (!allInteractions.has(interaction.id)) { - allInteractions.set(interaction.id, interaction); - } - - interactionIDs.push(interaction.id); - - const commitIndices = interactionCommits.get(interaction.id); - if (commitIndices != null) { - commitIndices.push(commitIndex); - } else { - interactionCommits.set(interaction.id, [commitIndex]); - } - }); - const fiberActualDurations: Array<[number, number]> = []; const fiberSelfDurations: Array<[number, number]> = []; for (let i = 0; i < durations.length; i += 3) { @@ -3486,7 +3465,6 @@ export function attach( effectDuration, fiberActualDurations, fiberSelfDurations, - interactionIDs, passiveEffectDuration, priorityLevel, timestamp: commitTime, @@ -3498,8 +3476,6 @@ export function attach( commitData, displayName, initialTreeBaseDurations, - interactionCommits: Array.from(interactionCommits.entries()), - interactions: Array.from(allInteractions.entries()), rootID, }); }, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 466a5b7fa2..342931c250 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -14,7 +14,6 @@ import type { ComponentFilter, ElementType, } from 'react-devtools-shared/src/types'; -import type {Interaction} from 'react-devtools-shared/src/devtools/views/Profiler/types'; import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; type BundleType = @@ -165,7 +164,6 @@ export type CommitDataBackend = {| fiberActualDurations: Array<[number, number]>, // Tuple of fiber ID and computed "self" duration fiberSelfDurations: Array<[number, number]>, - interactionIDs: Array, // Only available in certain (newer) React builds, passiveEffectDuration: number | null, priorityLevel: string | null, @@ -178,9 +176,6 @@ export type ProfilingDataForRootBackend = {| displayName: string, // Tuple of Fiber ID and base duration initialTreeBaseDurations: Array<[number, number]>, - // Tuple of Interaction ID and commit indices - interactionCommits: Array<[number, Array]>, - interactions: Array<[number, Interaction]>, rootID: number, |}; diff --git a/packages/react-devtools-shared/src/devtools/ProfilingCache.js b/packages/react-devtools-shared/src/devtools/ProfilingCache.js index 03e156cdf0..96e236b143 100644 --- a/packages/react-devtools-shared/src/devtools/ProfilingCache.js +++ b/packages/react-devtools-shared/src/devtools/ProfilingCache.js @@ -16,10 +16,6 @@ import { getChartData as getFlamegraphChartData, invalidateChartData as invalidateFlamegraphChartData, } from 'react-devtools-shared/src/devtools/views/Profiler/FlamegraphChartBuilder'; -import { - getChartData as getInteractionsChartData, - invalidateChartData as invalidateInteractionsChartData, -} from 'react-devtools-shared/src/devtools/views/Profiler/InteractionsChartBuilder'; import { getChartData as getRankedChartData, invalidateChartData as invalidateRankedChartData, @@ -27,7 +23,6 @@ import { import type {CommitTree} from 'react-devtools-shared/src/devtools/views/Profiler/types'; import type {ChartData as FlamegraphChartData} from 'react-devtools-shared/src/devtools/views/Profiler/FlamegraphChartBuilder'; -import type {ChartData as InteractionsChartData} from 'react-devtools-shared/src/devtools/views/Profiler/InteractionsChartBuilder'; import type {ChartData as RankedChartData} from 'react-devtools-shared/src/devtools/views/Profiler/RankedChartBuilder'; export default class ProfilingCache { @@ -92,16 +87,6 @@ export default class ProfilingCache { rootID, }); - getInteractionsChartData = ({ - rootID, - }: {| - rootID: number, - |}): InteractionsChartData => - getInteractionsChartData({ - profilerStore: this._profilerStore, - rootID, - }); - getRankedChartData = ({ commitIndex, commitTree, @@ -123,7 +108,6 @@ export default class ProfilingCache { invalidateCommitTrees(); invalidateFlamegraphChartData(); - invalidateInteractionsChartData(); invalidateRankedChartData(); } } diff --git a/packages/react-devtools-shared/src/devtools/views/Icon.js b/packages/react-devtools-shared/src/devtools/views/Icon.js index e879acdbfa..ffa297610b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Icon.js +++ b/packages/react-devtools-shared/src/devtools/views/Icon.js @@ -19,7 +19,6 @@ export type IconType = | 'error' | 'facebook' | 'flame-chart' - | 'interactions' | 'profiler' | 'ranked-chart' | 'search' @@ -59,9 +58,6 @@ export default function Icon({className = '', type}: Props) { case 'flame-chart': pathData = PATH_FLAME_CHART; break; - case 'interactions': - pathData = PATH_INTERACTIONS; - break; case 'profiler': pathData = PATH_PROFILER; break; @@ -138,14 +134,6 @@ const PATH_FLAME_CHART = ` 13.3541667,19.4702042 C13.3541667,20.1226027 12.7851952,20.6514763 12.0833333,20.6514763 Z `; -const PATH_INTERACTIONS = ` - M23 8c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2 - 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55 - 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02 - 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55 - 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2z -`; - const PATH_PROFILER = 'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z'; const PATH_SEARCH = ` diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.css b/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.css deleted file mode 100644 index 62784d3333..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.css +++ /dev/null @@ -1,43 +0,0 @@ -.Interaction, -.SelectedInteraction { - display: flex; - align-items: center; - padding: 0 0.25rem; - border-bottom: 1px solid var(--color-border); -} - -.Interaction:hover { - background-color: var(--color-background-hover); -} - -.SelectedInteraction { - background-color: var(--color-background-hover); -} - -.Name { - white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; -} - -.Timeline { - position: relative; - height: 100%; -} - -.InteractionLine { - position: absolute; - height: 3px; - background-color: var(--color-commit-did-not-render-fill); - color: var(--color-commit-did-not-render-fill-text); - border-radius: 0.125rem; -} - -.CommitBox { - position: absolute; - width: var(--interaction-commit-size); - height: var(--interaction-commit-size); - background-color: var(--color-commit-did-not-render-fill); - color: var(--color-commit-did-not-render-fill-text); - cursor: pointer; -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.js b/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.js deleted file mode 100644 index b720ca3e0d..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * 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 * as React from 'react'; -import {memo, useCallback} from 'react'; -import {areEqual} from 'react-window'; -import {getGradientColor} from './utils'; - -import styles from './InteractionListItem.css'; - -import type {ItemData} from './Interactions'; - -type Props = { - data: ItemData, - index: number, - style: Object, - ... -}; - -function InteractionListItem({data: itemData, index, style}: Props) { - const { - chartData, - dataForRoot, - labelWidth, - scaleX, - selectedInteractionID, - selectCommitIndex, - selectInteraction, - selectTab, - } = itemData; - - const {commitData, interactionCommits} = dataForRoot; - const {interactions, lastInteractionTime, maxCommitDuration} = chartData; - - const interaction = interactions[index]; - if (interaction == null) { - throw Error(`Could not find interaction #${index}`); - } - - const handleClick = useCallback(() => { - selectInteraction(interaction.id); - }, [interaction, selectInteraction]); - - const commits = interactionCommits.get(interaction.id) || []; - - const startTime = interaction.timestamp; - const stopTime = lastInteractionTime; - - const viewCommit = (commitIndex: number) => { - selectTab('flame-chart'); - selectCommitIndex(commitIndex); - }; - - return ( -
-
- {interaction.name} -
-
- {commits.map(commitIndex => ( -
viewCommit(commitIndex)} - style={{ - backgroundColor: getGradientColor( - Math.min( - 1, - Math.max( - 0, - commitData[commitIndex].duration / maxCommitDuration, - ), - ) || 0, - ), - left: labelWidth + scaleX(commitData[commitIndex].timestamp, 0), - }} - /> - ))} -
- ); -} - -export default memo(InteractionListItem, areEqual); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.css b/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.css deleted file mode 100644 index 81fd9da918..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.css +++ /dev/null @@ -1,9 +0,0 @@ -.Container { - width: 100%; - flex: 1; - padding: 0.5rem; -} - -.FocusTarget:focus { - outline: none; -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.js deleted file mode 100644 index b39d973053..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * 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 * as React from 'react'; -import {useCallback, useContext, useMemo} from 'react'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import {FixedSizeList} from 'react-window'; -import {ProfilerContext} from './ProfilerContext'; -import InteractionListItem from './InteractionListItem'; -import NoInteractions from './NoInteractions'; -import {StoreContext} from '../context'; -import {scale} from './utils'; - -import styles from './Interactions.css'; - -import type {ProfilingDataForRootFrontend} from './types'; -import type {ChartData} from './InteractionsChartBuilder'; -import type {TabID} from './ProfilerContext'; - -export type ItemData = {| - chartData: ChartData, - dataForRoot: ProfilingDataForRootFrontend, - labelWidth: number, - scaleX: (value: number, fallbackValue: number) => number, - selectedInteractionID: number | null, - selectCommitIndex: (id: number | null) => void, - selectInteraction: (id: number | null) => void, - selectTab: (id: TabID) => void, -|}; - -export default function InteractionsAutoSizer(_: {||}) { - return ( -
- - {({height, width}) => } - -
- ); -} - -function Interactions({height, width}: {|height: number, width: number|}) { - const { - rootID, - selectedInteractionID, - selectInteraction, - selectCommitIndex, - selectTab, - } = useContext(ProfilerContext); - const {profilerStore} = useContext(StoreContext); - const {profilingCache} = profilerStore; - - const dataForRoot = profilerStore.getDataForRoot(((rootID: any): number)); - - const chartData = profilingCache.getInteractionsChartData({ - rootID: ((rootID: any): number), - }); - - const {interactions} = chartData; - - const handleKeyDown = useCallback( - event => { - let index; - switch (event.key) { - case 'ArrowDown': - index = interactions.findIndex( - interaction => interaction.id === selectedInteractionID, - ); - selectInteraction(Math.min(interactions.length - 1, index + 1)); - event.stopPropagation(); - break; - case 'ArrowUp': - index = interactions.findIndex( - interaction => interaction.id === selectedInteractionID, - ); - selectInteraction(Math.max(0, index - 1)); - event.stopPropagation(); - break; - default: - break; - } - }, - [interactions, selectedInteractionID, selectInteraction], - ); - - const itemData = useMemo(() => { - const interactionCommitSize = parseInt( - getComputedStyle((document.body: any)).getPropertyValue( - '--interaction-commit-size', - ), - 10, - ); - const interactionLabelWidth = parseInt( - getComputedStyle((document.body: any)).getPropertyValue( - '--interaction-label-width', - ), - 10, - ); - - const labelWidth = Math.min(interactionLabelWidth, width / 5); - const timelineWidth = width - labelWidth - interactionCommitSize; - - return { - chartData, - dataForRoot, - labelWidth, - scaleX: scale(0, chartData.lastInteractionTime, 0, timelineWidth), - selectedInteractionID, - selectCommitIndex, - selectInteraction, - selectTab, - }; - }, [ - chartData, - dataForRoot, - selectedInteractionID, - selectCommitIndex, - selectInteraction, - selectTab, - width, - ]); - - // If a commit contains no fibers with an actualDuration > 0, - // Display a fallback message. - if (interactions.length === 0) { - return ; - } - - return ( -
- - {InteractionListItem} - -
- ); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionsChartBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionsChartBuilder.js deleted file mode 100644 index 90ebfade4c..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionsChartBuilder.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 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 ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore'; - -import type {Interaction} from './types'; - -export type ChartData = {| - interactions: Array, - lastInteractionTime: number, - maxCommitDuration: number, -|}; - -const cachedChartData: Map = new Map(); - -export function getChartData({ - profilerStore, - rootID, -}: {| - profilerStore: ProfilerStore, - rootID: number, -|}): ChartData { - if (cachedChartData.has(rootID)) { - return ((cachedChartData.get(rootID): any): ChartData); - } - - const dataForRoot = profilerStore.getDataForRoot(rootID); - if (dataForRoot == null) { - throw Error(`Could not find profiling data for root "${rootID}"`); - } - - const {commitData, interactions} = dataForRoot; - - const lastInteractionTime = - commitData.length > 0 ? commitData[commitData.length - 1].timestamp : 0; - - let maxCommitDuration = 0; - - commitData.forEach(commitDatum => { - maxCommitDuration = Math.max(maxCommitDuration, commitDatum.duration); - }); - - const chartData = { - interactions: Array.from(interactions.values()), - lastInteractionTime, - maxCommitDuration, - }; - - cachedChartData.set(rootID, chartData); - - return chartData; -} - -export function invalidateChartData(): void { - cachedChartData.clear(); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.css b/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.css deleted file mode 100644 index 6c6872058a..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.css +++ /dev/null @@ -1,16 +0,0 @@ -.NoInteractions { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.Header { - font-size: var(--font-size-sans-large); -} - -.Link { - color: var(--color-button); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.js b/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.js deleted file mode 100644 index c2f1f65480..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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 * as React from 'react'; - -import styles from './NoInteractions.css'; - -export default function NoInteractions({ - height, - width, -}: {| - height: number, - width: number, -|}) { - return ( -
-

No interactions were recorded.

-

- - Learn more about the interaction tracing API here. - -

-
- ); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 2b4e117d66..58008334bc 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -15,14 +15,12 @@ import TabBar from '../TabBar'; import ClearProfilingDataButton from './ClearProfilingDataButton'; import CommitFlamegraph from './CommitFlamegraph'; import CommitRanked from './CommitRanked'; -import Interactions from './Interactions'; import RootSelector from './RootSelector'; import RecordToggle from './RecordToggle'; import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; import SnapshotSelector from './SnapshotSelector'; import SidebarCommitInfo from './SidebarCommitInfo'; -import SidebarInteractions from './SidebarInteractions'; import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo'; import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle'; @@ -53,9 +51,6 @@ function Profiler(_: {||}) { case 'ranked-chart': view = ; break; - case 'interactions': - view = ; - break; default: break; } @@ -72,9 +67,6 @@ function Profiler(_: {||}) { let sidebar = null; if (!isProfiling && !isProcessingData && didRecordCommits) { switch (selectedTabID) { - case 'interactions': - sidebar = ; - break; case 'flame-chart': case 'ranked-chart': // TRICKY @@ -148,12 +140,6 @@ const tabs = [ label: 'Ranked', title: 'Ranked chart', }, - { - id: 'interactions', - icon: 'interactions', - label: 'Interactions', - title: 'Profiled interactions', - }, ]; const NoProfilingData = () => ( diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js index ac3b58f187..48f9aa11ee 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js @@ -19,7 +19,7 @@ import {StoreContext} from '../context'; import type {ProfilingDataFrontend} from './types'; -export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions'; +export type TabID = 'flame-chart' | 'ranked-chart'; export type Context = {| // Which tab is selected in the Profiler UI? @@ -64,10 +64,6 @@ export type Context = {| selectedFiberID: number | null, selectedFiberName: string | null, selectFiber: (id: number | null, name: string | null) => void, - - // Which interaction is currently selected in the Interactions graph? - selectedInteractionID: number | null, - selectInteraction: (id: number | null) => void, |}; const ProfilerContext = createContext(((null: any): Context)); @@ -216,9 +212,6 @@ function ProfilerContextController({children}: Props) { null, ); const [selectedTabID, selectTab] = useState('flame-chart'); - const [selectedInteractionID, selectInteraction] = useState( - null, - ); if (isProfiling) { batchedUpdates(() => { @@ -229,9 +222,6 @@ function ProfilerContextController({children}: Props) { selectFiberID(null); selectFiberName(null); } - if (selectedInteractionID !== null) { - selectInteraction(null); - } }); } @@ -262,9 +252,6 @@ function ProfilerContextController({children}: Props) { selectedFiberID, selectedFiberName, selectFiber, - - selectedInteractionID, - selectInteraction, }), [ selectedTabID, @@ -293,9 +280,6 @@ function ProfilerContextController({children}: Props) { selectedFiberID, selectedFiberName, selectFiber, - - selectedInteractionID, - selectInteraction, ], ); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.css b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.css index d82f10b3d7..1fe2d298e9 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.css @@ -23,33 +23,6 @@ margin: 0 0 0.5rem; } -.NoInteractions { - color: var(--color-dim); -} - -.Interactions { - margin: 0 0 0.5rem; -} -.NoInteractions, -.Interaction { - display: block; - width: 100%; - text-align: left; - background: none; - border: none; - padding: 0.25rem 0.5rem; - color: var(--color-text); -} -.Interaction:focus, -.Interaction:hover { - outline: none; - background-color: var(--color-background-hover); -} - -.NoInteractions { - color: var(--color-dim); -} - .Label { overflow: hidden; text-overflow: ellipsis; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js index dcfaf7d68b..6619fb7c78 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js @@ -20,12 +20,7 @@ import styles from './SidebarCommitInfo.css'; export type Props = {||}; export default function SidebarCommitInfo(_: Props) { - const { - selectedCommitIndex, - rootID, - selectInteraction, - selectTab, - } = useContext(ProfilerContext); + const {selectedCommitIndex, rootID} = useContext(ProfilerContext); const {profilerStore} = useContext(StoreContext); @@ -33,22 +28,15 @@ export default function SidebarCommitInfo(_: Props) { return
Nothing selected
; } - const {interactions} = profilerStore.getDataForRoot(rootID); const { duration, effectDuration, - interactionIDs, passiveEffectDuration, priorityLevel, timestamp, updaters, } = profilerStore.getCommitData(rootID, selectedCommitIndex); - const viewInteraction = interactionID => { - selectTab('interactions'); - selectInteraction(interactionID); - }; - const hasCommitPhaseDurations = effectDuration !== null || passiveEffectDuration !== null; @@ -120,29 +108,6 @@ export default function SidebarCommitInfo(_: Props) { )} - -
  • - : -
    - {interactionIDs.length === 0 ? ( -
    (none)
    - ) : null} - {interactionIDs.map(interactionID => { - const interaction = interactions.get(interactionID); - if (interaction == null) { - throw Error(`Invalid interaction "${interactionID}"`); - } - return ( - - ); - })} -
    -
  • diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.css b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.css deleted file mode 100644 index c5643dfcd7..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.css +++ /dev/null @@ -1,55 +0,0 @@ -.Toolbar { - height: 2.25rem; - padding: 0 0.5rem; - flex: 0 0 auto; - display: flex; - align-items: center; -} - -.Content { - padding: 0.5rem; - user-select: none; - border-top: 1px solid var(--color-border); - overflow: auto; -} - -.Name { - font-size: var(--font-size-sans-large); - white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; -} - -.NothingSelected { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - color: var(--color-dim); -} - -.Commits { - font-weight: bold; -} - -.List { - list-style: none; - margin: 0; - padding: 0; -} - -.ListItem { - display: flex; - flex-direction: row; - align-items: center; - padding: 0.25rem 0.5rem; -} -.ListItem:hover { - background-color: var(--color-background-hover); -} - -.CommitBox { - width: 20px; - height: 20px; - margin-right: 0.5rem; -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.js deleted file mode 100644 index 186ec24250..0000000000 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * 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 * as React from 'react'; -import {Fragment, useContext} from 'react'; -import {ProfilerContext} from './ProfilerContext'; -import {formatDuration, formatTime} from './utils'; -import {StoreContext} from '../context'; -import {getGradientColor} from './utils'; - -import styles from './SidebarInteractions.css'; - -export type Props = {||}; - -export default function SidebarInteractions(_: Props) { - const { - selectedInteractionID, - rootID, - selectCommitIndex, - selectTab, - } = useContext(ProfilerContext); - - const {profilerStore} = useContext(StoreContext); - const {profilingCache} = profilerStore; - - if (selectedInteractionID === null) { - return
    Nothing selected
    ; - } - - const {interactionCommits, interactions} = profilerStore.getDataForRoot( - ((rootID: any): number), - ); - const interaction = interactions.get(selectedInteractionID); - if (interaction == null) { - throw Error( - `Could not find interaction by selected interaction id "${selectedInteractionID}"`, - ); - } - - const {maxCommitDuration} = profilingCache.getInteractionsChartData({ - rootID: ((rootID: any): number), - }); - - const viewCommit = (commitIndex: number) => { - selectTab('flame-chart'); - selectCommitIndex(commitIndex); - }; - - const listItems: Array = []; - const commitIndices = interactionCommits.get(selectedInteractionID); - if (commitIndices != null) { - commitIndices.forEach(commitIndex => { - const {duration, timestamp} = profilerStore.getCommitData( - ((rootID: any): number), - commitIndex, - ); - - listItems.push( -
  • viewCommit(commitIndex)}> -
    -
    - timestamp: {formatTime(timestamp)}s -
    - duration: {formatDuration(duration)}ms -
    -
  • , - ); - }); - } - - return ( - -
    -
    - {interaction.name} -
    -
    -
    -
    Commits:
    -
      {listItems}
    -
    -
    - ); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/constants.js b/packages/react-devtools-shared/src/devtools/views/Profiler/constants.js index fd6b4365f2..547642620a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/constants.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/constants.js @@ -8,7 +8,5 @@ */ export const barWidthThreshold = 2; -export const interactionCommitSize = 10; -export const interactionLabelWidth = 200; export const maxBarWidth = 30; export const minBarWidth = 5; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/types.js b/packages/react-devtools-shared/src/devtools/views/Profiler/types.js index 90c56b3529..c3889d47c2 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/types.js @@ -26,12 +26,6 @@ export type CommitTree = {| rootID: number, |}; -export type Interaction = {| - id: number, - name: string, - timestamp: number, -|}; - export type SnapshotNode = {| id: number, children: Array, @@ -69,9 +63,6 @@ export type CommitDataFrontend = {| // Fibers that did not render will not have entries in this Map. fiberSelfDurations: Map, - // Which interactions (IDs) were associated with this commit. - interactionIDs: Array, - // How long was the passive commit phase? // Note that not all builds of React expose this property. passiveEffectDuration: number | null, @@ -98,12 +89,6 @@ export type ProfilingDataForRootFrontend = {| // This info can be used along with commitOperations to reconstruct the tree for any commit. initialTreeBaseDurations: Map, - // All interactions recorded (for this root) during the current session. - interactionCommits: Map>, - - // All interactions recorded (for this root) during the current session. - interactions: Map, - // List of tree mutation that occur during profiling. // These mutations can be used along with initial snapshots to reconstruct the tree for any commit. operations: Array>, @@ -131,7 +116,6 @@ export type CommitDataExport = {| fiberActualDurations: Array<[number, number]>, // Tuple of fiber ID and computed "self" duration fiberSelfDurations: Array<[number, number]>, - interactionIDs: Array, passiveEffectDuration: number | null, priorityLevel: string | null, timestamp: number, @@ -143,9 +127,6 @@ export type ProfilingDataForRootExport = {| displayName: string, // Tuple of Fiber ID and base duration initialTreeBaseDurations: Array<[number, number]>, - // Tuple of Interaction ID and commit indices - interactionCommits: Array<[number, Array]>, - interactions: Array<[number, Interaction]>, operations: Array>, rootID: number, snapshots: Array<[number, SnapshotNode]>, diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js b/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js index cace5aaf2d..2594f255af 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js @@ -43,14 +43,7 @@ export function prepareProfilingDataFrontendFromBackendAndStore( dataBackends.forEach(dataBackend => { dataBackend.dataForRoots.forEach( - ({ - commitData, - displayName, - initialTreeBaseDurations, - interactionCommits, - interactions, - rootID, - }) => { + ({commitData, displayName, initialTreeBaseDurations, rootID}) => { const operations = operationsByRootID.get(rootID); if (operations == null) { throw Error( @@ -66,12 +59,7 @@ export function prepareProfilingDataFrontendFromBackendAndStore( } // Do not filter empty commits from the profiler data! - // We used to do this, but it was error prone (see #18798). - // A commit may appear to be empty (no actual durations) because of component filters, - // but filtering these empty commits causes interaction commit indices to be off by N. - // This not only corrupts the resulting data, but also potentially causes runtime errors. - // - // For that matter, hiding "empty" commits might cause confusion too. + // Hiding "empty" commits might cause confusion too. // A commit *did happen* even if none of the components the Profiler is showing were involved. const convertedCommitData = commitData.map( (commitDataBackend, commitIndex) => ({ @@ -85,7 +73,6 @@ export function prepareProfilingDataFrontendFromBackendAndStore( commitDataBackend.fiberActualDurations, ), fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations), - interactionIDs: commitDataBackend.interactionIDs, passiveEffectDuration: commitDataBackend.passiveEffectDuration, priorityLevel: commitDataBackend.priorityLevel, timestamp: commitDataBackend.timestamp, @@ -113,8 +100,6 @@ export function prepareProfilingDataFrontendFromBackendAndStore( commitData: convertedCommitData, displayName, initialTreeBaseDurations: new Map(initialTreeBaseDurations), - interactionCommits: new Map(interactionCommits), - interactions: new Map(interactions), operations, rootID, snapshots, @@ -144,8 +129,6 @@ export function prepareProfilingDataFrontendFromExport( commitData, displayName, initialTreeBaseDurations, - interactionCommits, - interactions, operations, rootID, snapshots, @@ -158,7 +141,6 @@ export function prepareProfilingDataFrontendFromExport( effectDuration, fiberActualDurations, fiberSelfDurations, - interactionIDs, passiveEffectDuration, priorityLevel, timestamp, @@ -170,7 +152,6 @@ export function prepareProfilingDataFrontendFromExport( effectDuration, fiberActualDurations: new Map(fiberActualDurations), fiberSelfDurations: new Map(fiberSelfDurations), - interactionIDs, passiveEffectDuration, priorityLevel, timestamp, @@ -179,8 +160,6 @@ export function prepareProfilingDataFrontendFromExport( ), displayName, initialTreeBaseDurations: new Map(initialTreeBaseDurations), - interactionCommits: new Map(interactionCommits), - interactions: new Map(interactions), operations, rootID, snapshots: new Map(snapshots), @@ -201,8 +180,6 @@ export function prepareProfilingDataExport( commitData, displayName, initialTreeBaseDurations, - interactionCommits, - interactions, operations, rootID, snapshots, @@ -215,7 +192,6 @@ export function prepareProfilingDataExport( effectDuration, fiberActualDurations, fiberSelfDurations, - interactionIDs, passiveEffectDuration, priorityLevel, timestamp, @@ -229,7 +205,6 @@ export function prepareProfilingDataExport( effectDuration, fiberActualDurations: Array.from(fiberActualDurations.entries()), fiberSelfDurations: Array.from(fiberSelfDurations.entries()), - interactionIDs, passiveEffectDuration, priorityLevel, timestamp, @@ -240,8 +215,6 @@ export function prepareProfilingDataExport( initialTreeBaseDurations: Array.from( initialTreeBaseDurations.entries(), ), - interactionCommits: Array.from(interactionCommits.entries()), - interactions: Array.from(interactions.entries()), operations, rootID, snapshots: Array.from(snapshots.entries()), diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index e703e01184..6d03a9519a 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -211,8 +211,4 @@ Courier, monospace; --font-family-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; - - /* Constant values shared between JS and CSS */ - --interaction-commit-size: 10px; - --interaction-label-width: 200px; } diff --git a/packages/react-devtools-shell/src/app/InteractionTracing/index.js b/packages/react-devtools-shell/src/app/InteractionTracing/index.js deleted file mode 100644 index aab8e77e90..0000000000 --- a/packages/react-devtools-shell/src/app/InteractionTracing/index.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 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 * as React from 'react'; -import { - Fragment, - useCallback, - useLayoutEffect, - useEffect, - useState, -} from 'react'; -import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; -import { - unstable_trace as trace, - unstable_wrap as wrap, -} from 'scheduler/tracing'; - -function sleep(ms) { - const start = performance.now(); - let now; - do { - now = performance.now(); - } while (now - ms < start); -} - -export default function InteractionTracing() { - const [count, setCount] = useState(0); - const [shouldCascade, setShouldCascade] = useState(false); - - const handleUpdate = useCallback(() => { - trace('count', performance.now(), () => { - setTimeout( - wrap(() => { - setCount(count + 1); - }), - count * 100, - ); - }); - }, [count]); - - const handleCascadingUpdate = useCallback(() => { - trace('cascade', performance.now(), () => { - setTimeout( - wrap(() => { - batchedUpdates(() => { - setCount(count + 1); - setShouldCascade(true); - }); - }), - count * 100, - ); - }); - }, [count]); - - const handleMultiple = useCallback(() => { - trace('first', performance.now(), () => { - trace('second', performance.now(), () => { - setTimeout( - wrap(() => { - setCount(count + 1); - }), - count * 100, - ); - }); - }); - }, [count]); - - useEffect(() => { - if (shouldCascade) { - setTimeout( - wrap(() => { - setShouldCascade(false); - }), - count * 100, - ); - } - }, [count, shouldCascade]); - - useLayoutEffect(() => { - sleep(150); - }); - - useEffect(() => { - sleep(300); - }); - - return ( - -

    Interaction Tracing

    - - - -
    - ); -} diff --git a/packages/react-devtools-shell/src/app/index.js b/packages/react-devtools-shell/src/app/index.js index f5683b69a8..51f00c75a5 100644 --- a/packages/react-devtools-shell/src/app/index.js +++ b/packages/react-devtools-shell/src/app/index.js @@ -14,7 +14,6 @@ import ElementTypes from './ElementTypes'; import Hydration from './Hydration'; import InlineWarnings from './InlineWarnings'; import InspectableElements from './InspectableElements'; -import InteractionTracing from './InteractionTracing'; import ReactNativeWeb from './ReactNativeWeb'; import ToDoList from './ToDoList'; import Toggle from './Toggle'; @@ -48,7 +47,6 @@ function mountHelper(App) { function mountTestApp() { mountHelper(ToDoList); - mountHelper(InteractionTracing); mountHelper(InspectableElements); mountHelper(Hydration); mountHelper(ElementTypes); diff --git a/packages/react-devtools/OVERVIEW.md b/packages/react-devtools/OVERVIEW.md index ee73da31c6..0c8d47da26 100644 --- a/packages/react-devtools/OVERVIEW.md +++ b/packages/react-devtools/OVERVIEW.md @@ -286,7 +286,6 @@ When profiling begins, the frontend takes a snapshot/copy of each root. This sna When profiling begins, the backend records the base durations of each fiber currently in the tree. While profiling is in progress, the backend also stores some information about each commit, including: * Commit time and duration * Which elements were rendered during that commit -* Which interactions (if any) were part of the commit * Which props and state changed (if enabled in profiler settings) This information will eventually be required by the frontend in order to render its profiling graphs, but it will not be sent across the bridge until profiling has completed (to minimize the performance impact of profiling). diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js index 242743d2f3..6ce9981fde 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -10,7 +10,6 @@ let React; let ReactDOM; let ReactTestUtils; -let SchedulerTracing; let Scheduler; let act; let container; @@ -119,7 +118,6 @@ function runActTests(label, render, unmount, rerender) { React = require('react'); ReactDOM = require('react-dom'); ReactTestUtils = require('react-dom/test-utils'); - SchedulerTracing = require('scheduler/tracing'); Scheduler = require('scheduler'); act = ReactTestUtils.act; container = document.createElement('div'); @@ -497,87 +495,6 @@ function runActTests(label, render, unmount, rerender) { }); }); - describe('interaction tracing', () => { - if (__DEV__) { - it('should correctly trace interactions for sync roots', () => { - let expectedInteraction; - - const Component = jest.fn(() => { - expect(expectedInteraction).toBeDefined(); - - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expect(interactions).toContain(expectedInteraction); - - return null; - }); - - act(() => { - SchedulerTracing.unstable_trace( - 'mount traced inside act', - performance.now(), - () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - render(, container); - }, - ); - }); - - act(() => { - SchedulerTracing.unstable_trace( - 'update traced inside act', - performance.now(), - () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - rerender(); - }, - ); - }); - - const secondContainer = document.createElement('div'); - - SchedulerTracing.unstable_trace( - 'mount traced outside act', - performance.now(), - () => { - act(() => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - render(, secondContainer); - }); - }, - ); - - SchedulerTracing.unstable_trace( - 'update traced outside act', - performance.now(), - () => { - act(() => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - rerender(); - }); - }, - ); - - expect(Component).toHaveBeenCalledTimes( - label === 'legacy mode' ? 4 : 8, - ); - unmount(secondContainer); - }); - } - }); - describe('error propagation', () => { it('propagates errors - sync', () => { let err; diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index f876158b67..28dd03158b 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1684,84 +1684,4 @@ describe('ReactUpdates', () => { expect(container.textContent).toBe('1000'); }); } - - if (__DEV__) { - it('should properly trace interactions within batched updates', () => { - const SchedulerTracing = require('scheduler/tracing'); - - let expectedInteraction; - - const container = document.createElement('div'); - - const Component = jest.fn(() => { - expect(expectedInteraction).toBeDefined(); - - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expect(interactions).toContain(expectedInteraction); - - return null; - }); - - ReactDOM.unstable_batchedUpdates(() => { - SchedulerTracing.unstable_trace( - 'mount traced inside a batched update', - 1, - () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - ReactDOM.render(, container); - }, - ); - }); - - ReactDOM.unstable_batchedUpdates(() => { - SchedulerTracing.unstable_trace( - 'update traced inside a batched update', - 2, - () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - ReactDOM.render(, container); - }, - ); - }); - - const secondContainer = document.createElement('div'); - - SchedulerTracing.unstable_trace( - 'mount traced outside a batched update', - 3, - () => { - ReactDOM.unstable_batchedUpdates(() => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - ReactDOM.render(, secondContainer); - }); - }, - ); - - SchedulerTracing.unstable_trace( - 'update traced outside a batched update', - 4, - () => { - ReactDOM.unstable_batchedUpdates(() => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - expectedInteraction = Array.from(interactions)[0]; - - ReactDOM.render(, container); - }); - }, - ); - - expect(Component).toHaveBeenCalledTimes(4); - }); - } }); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 325fbac1f2..2dfc676657 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -78,7 +78,6 @@ import { disableModulePatternComponents, enableProfilerCommitHooks, enableProfilerTimer, - enableSchedulerTracing, enableSuspenseServerRenderer, warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, @@ -200,7 +199,6 @@ import { isSimpleFunctionComponent, } from './ReactFiber.new'; import { - markSpawnedWork, retryDehydratedSuspenseBoundary, scheduleUpdateOnFiber, renderDidSuspendDelayIfPossible, @@ -211,7 +209,6 @@ import { RetryAfterError, NoContext, } from './ReactFiberWorkLoop.new'; -import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import {setWorkInProgressVersion} from './ReactMutableSource.new'; import { requestCacheFromPool, @@ -635,9 +632,6 @@ function updateOffscreenComponent( } // Schedule this fiber to re-render at offscreen priority. Then bailout. - if (enableSchedulerTracing) { - markSpawnedWork((OffscreenLane: Lane)); - } workInProgress.lanes = workInProgress.childLanes = laneToLanes( OffscreenLane, ); @@ -1939,9 +1933,6 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { // RetryLane even if it's the one currently rendering since we're leaving // it behind on this node. workInProgress.lanes = SomeRetryLane; - if (enableSchedulerTracing) { - markSpawnedWork(SomeRetryLane); - } return fallbackFragment; } else { return mountSuspensePrimaryChildren( @@ -2397,17 +2388,11 @@ function mountDehydratedSuspenseComponent( // time. This will mean that Suspense timeouts are slightly shifted to later than // they should be. // Schedule a normal pri update to render this content. - if (enableSchedulerTracing) { - markSpawnedWork(DefaultHydrationLane); - } workInProgress.lanes = laneToLanes(DefaultHydrationLane); } else { // We'll continue hydrating the rest at offscreen priority since we'll already // be showing the right content coming from the server, it is no rush. workInProgress.lanes = laneToLanes(OffscreenLane); - if (enableSchedulerTracing) { - markSpawnedWork(OffscreenLane); - } } return null; } @@ -2520,10 +2505,7 @@ function updateDehydratedSuspenseComponent( // Leave the child in place. I.e. the dehydrated fragment. workInProgress.child = current.child; // Register a callback to retry this boundary once the server has sent the result. - let retry = retryDehydratedSuspenseBoundary.bind(null, current); - if (enableSchedulerTracing) { - retry = Schedule_tracing_wrap(retry); - } + const retry = retryDehydratedSuspenseBoundary.bind(null, current); registerSuspenseInstanceRetry(suspenseInstance, retry); return null; } else { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 9cac49a046..7c9df820b9 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -78,7 +78,6 @@ import { disableModulePatternComponents, enableProfilerCommitHooks, enableProfilerTimer, - enableSchedulerTracing, enableSuspenseServerRenderer, warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, @@ -200,7 +199,6 @@ import { isSimpleFunctionComponent, } from './ReactFiber.old'; import { - markSpawnedWork, retryDehydratedSuspenseBoundary, scheduleUpdateOnFiber, renderDidSuspendDelayIfPossible, @@ -211,7 +209,6 @@ import { RetryAfterError, NoContext, } from './ReactFiberWorkLoop.old'; -import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import {setWorkInProgressVersion} from './ReactMutableSource.old'; import { requestCacheFromPool, @@ -635,9 +632,6 @@ function updateOffscreenComponent( } // Schedule this fiber to re-render at offscreen priority. Then bailout. - if (enableSchedulerTracing) { - markSpawnedWork((OffscreenLane: Lane)); - } workInProgress.lanes = workInProgress.childLanes = laneToLanes( OffscreenLane, ); @@ -1939,9 +1933,6 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { // RetryLane even if it's the one currently rendering since we're leaving // it behind on this node. workInProgress.lanes = SomeRetryLane; - if (enableSchedulerTracing) { - markSpawnedWork(SomeRetryLane); - } return fallbackFragment; } else { return mountSuspensePrimaryChildren( @@ -2397,17 +2388,11 @@ function mountDehydratedSuspenseComponent( // time. This will mean that Suspense timeouts are slightly shifted to later than // they should be. // Schedule a normal pri update to render this content. - if (enableSchedulerTracing) { - markSpawnedWork(DefaultHydrationLane); - } workInProgress.lanes = laneToLanes(DefaultHydrationLane); } else { // We'll continue hydrating the rest at offscreen priority since we'll already // be showing the right content coming from the server, it is no rush. workInProgress.lanes = laneToLanes(OffscreenLane); - if (enableSchedulerTracing) { - markSpawnedWork(OffscreenLane); - } } return null; } @@ -2520,10 +2505,7 @@ function updateDehydratedSuspenseComponent( // Leave the child in place. I.e. the dehydrated fragment. workInProgress.child = current.child; // Register a callback to retry this boundary once the server has sent the result. - let retry = retryDehydratedSuspenseBoundary.bind(null, current); - if (enableSchedulerTracing) { - retry = Schedule_tracing_wrap(retry); - } + const retry = retryDehydratedSuspenseBoundary.bind(null, current); registerSuspenseInstanceRetry(suspenseInstance, retry); return null; } else { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 1e1a3f0b98..46f1929456 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -25,9 +25,7 @@ import type {Wakeable} from 'shared/ReactTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; -import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { - enableSchedulerTracing, enableProfilerTimer, enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, @@ -641,17 +639,7 @@ export function commitPassiveEffectDurations( } if (typeof onPostCommit === 'function') { - if (enableSchedulerTracing) { - onPostCommit( - id, - phase, - passiveEffectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onPostCommit(id, phase, passiveEffectDuration, commitTime); - } + onPostCommit(id, phase, passiveEffectDuration, commitTime); } // Bubble times to the next nearest ancestor Profiler. @@ -919,46 +907,24 @@ function commitLayoutEffectOnFiber( } if (typeof onRender === 'function') { - if (enableSchedulerTracing) { - onRender( - finishedWork.memoizedProps.id, - phase, - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onRender( - finishedWork.memoizedProps.id, - phase, - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - ); - } + onRender( + finishedWork.memoizedProps.id, + phase, + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + ); } if (enableProfilerCommitHooks) { if (typeof onCommit === 'function') { - if (enableSchedulerTracing) { - onCommit( - finishedWork.memoizedProps.id, - phase, - effectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onCommit( - finishedWork.memoizedProps.id, - phase, - effectDuration, - commitTime, - ); - } + onCommit( + finishedWork.memoizedProps.id, + phase, + effectDuration, + commitTime, + ); } // Schedule a passive effect for this Profiler to call onPostCommit hooks. @@ -2105,13 +2071,8 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) { } wakeables.forEach(wakeable => { // Memoize using the boundary fiber to prevent redundant listeners. - let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); + const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); if (!retryCache.has(wakeable)) { - if (enableSchedulerTracing) { - if (wakeable.__reactDoNotTraceInteractions !== true) { - retry = Schedule_tracing_wrap(retry); - } - } retryCache.add(wakeable); if (enableUpdaterTracking) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index f631c0131a..f45a44083e 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -25,9 +25,7 @@ import type {Wakeable} from 'shared/ReactTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; -import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { - enableSchedulerTracing, enableProfilerTimer, enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, @@ -641,17 +639,7 @@ export function commitPassiveEffectDurations( } if (typeof onPostCommit === 'function') { - if (enableSchedulerTracing) { - onPostCommit( - id, - phase, - passiveEffectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onPostCommit(id, phase, passiveEffectDuration, commitTime); - } + onPostCommit(id, phase, passiveEffectDuration, commitTime); } // Bubble times to the next nearest ancestor Profiler. @@ -919,46 +907,24 @@ function commitLayoutEffectOnFiber( } if (typeof onRender === 'function') { - if (enableSchedulerTracing) { - onRender( - finishedWork.memoizedProps.id, - phase, - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onRender( - finishedWork.memoizedProps.id, - phase, - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - ); - } + onRender( + finishedWork.memoizedProps.id, + phase, + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + ); } if (enableProfilerCommitHooks) { if (typeof onCommit === 'function') { - if (enableSchedulerTracing) { - onCommit( - finishedWork.memoizedProps.id, - phase, - effectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onCommit( - finishedWork.memoizedProps.id, - phase, - effectDuration, - commitTime, - ); - } + onCommit( + finishedWork.memoizedProps.id, + phase, + effectDuration, + commitTime, + ); } // Schedule a passive effect for this Profiler to call onPostCommit hooks. @@ -2105,13 +2071,8 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) { } wakeables.forEach(wakeable => { // Memoize using the boundary fiber to prevent redundant listeners. - let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); + const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); if (!retryCache.has(wakeable)) { - if (enableSchedulerTracing) { - if (wakeable.__reactDoNotTraceInteractions !== true) { - retry = Schedule_tracing_wrap(retry); - } - } retryCache.add(wakeable); if (enableUpdaterTracking) { diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index f366ccdf88..e11e15bac8 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -118,7 +118,6 @@ import { getIsHydrating, } from './ReactFiberHydrationContext.new'; import { - enableSchedulerTracing, enableSuspenseCallback, enableSuspenseServerRenderer, enableScopeAPI, @@ -127,7 +126,6 @@ import { enableSuspenseLayoutEffectSemantics, } from 'shared/ReactFeatureFlags'; import { - markSpawnedWork, renderDidSuspend, renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, @@ -981,9 +979,6 @@ function completeWork( 'This is probably a bug in React.', ); prepareToHydrateHostSuspenseInstance(workInProgress); - if (enableSchedulerTracing) { - markSpawnedWork(OffscreenLane); - } bubbleProperties(workInProgress); if (enableProfilerTimer) { if ((workInProgress.mode & ProfileMode) !== NoMode) { @@ -1259,9 +1254,6 @@ function completeWork( // We can use any RetryLane even if it's the one currently rendering // since we're leaving it behind on this node. workInProgress.lanes = SomeRetryLane; - if (enableSchedulerTracing) { - markSpawnedWork(SomeRetryLane); - } } } else { cutOffTailIfNeeded(renderState, false); @@ -1320,9 +1312,6 @@ function completeWork( // We can use any RetryLane even if it's the one currently rendering // since we're leaving it behind on this node. workInProgress.lanes = SomeRetryLane; - if (enableSchedulerTracing) { - markSpawnedWork(SomeRetryLane); - } } } if (renderState.isBackwards) { diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 8bfd7d3fe9..ba6200d364 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -118,7 +118,6 @@ import { getIsHydrating, } from './ReactFiberHydrationContext.old'; import { - enableSchedulerTracing, enableSuspenseCallback, enableSuspenseServerRenderer, enableScopeAPI, @@ -127,7 +126,6 @@ import { enableSuspenseLayoutEffectSemantics, } from 'shared/ReactFeatureFlags'; import { - markSpawnedWork, renderDidSuspend, renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, @@ -981,9 +979,6 @@ function completeWork( 'This is probably a bug in React.', ); prepareToHydrateHostSuspenseInstance(workInProgress); - if (enableSchedulerTracing) { - markSpawnedWork(OffscreenLane); - } bubbleProperties(workInProgress); if (enableProfilerTimer) { if ((workInProgress.mode & ProfileMode) !== NoMode) { @@ -1259,9 +1254,6 @@ function completeWork( // We can use any RetryLane even if it's the one currently rendering // since we're leaving it behind on this node. workInProgress.lanes = SomeRetryLane; - if (enableSchedulerTracing) { - markSpawnedWork(SomeRetryLane); - } } } else { cutOffTailIfNeeded(renderState, false); @@ -1320,9 +1312,6 @@ function completeWork( // We can use any RetryLane even if it's the one currently rendering // since we're leaving it behind on this node. workInProgress.lanes = SomeRetryLane; - if (enableSchedulerTracing) { - markSpawnedWork(SomeRetryLane); - } } } if (renderState.isBackwards) { diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 728769261c..e82262b573 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -20,14 +20,12 @@ import { createLaneMap, } from './ReactFiberLane.new'; import { - enableSchedulerTracing, enableSuspenseCallback, enableCache, enableProfilerCommitHooks, enableProfilerTimer, enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; -import {unstable_getThreadID} from 'scheduler/tracing'; import {initializeUpdateQueue} from './ReactUpdateQueue.new'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; @@ -66,11 +64,6 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.mutableSourceEagerHydrationData = null; } - if (enableSchedulerTracing) { - this.interactionThreadID = unstable_getThreadID(); - this.memoizedInteractions = new Set(); - this.pendingInteractionMap = new Map(); - } if (enableSuspenseCallback) { this.hydrationCallbacks = null; } diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 7f338a4dbb..20c3cdaa6c 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -20,14 +20,12 @@ import { createLaneMap, } from './ReactFiberLane.old'; import { - enableSchedulerTracing, enableSuspenseCallback, enableCache, enableProfilerCommitHooks, enableProfilerTimer, enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; -import {unstable_getThreadID} from 'scheduler/tracing'; import {initializeUpdateQueue} from './ReactUpdateQueue.old'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; @@ -66,11 +64,6 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.mutableSourceEagerHydrationData = null; } - if (enableSchedulerTracing) { - this.interactionThreadID = unstable_getThreadID(); - this.memoizedInteractions = new Set(); - this.pendingInteractionMap = new Map(); - } if (enableSuspenseCallback) { this.hydrationCallbacks = null; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index ddf72f428d..0b15fb13a2 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -10,7 +10,6 @@ import type {Thenable, Wakeable} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; -import type {Interaction} from 'scheduler/src/Tracing'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {StackCursor} from './ReactFiberStack.new'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; @@ -24,7 +23,6 @@ import { enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, enableProfilerNestedUpdateScheduledHook, - enableSchedulerTracing, warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, enableDebugTracing, @@ -83,8 +81,6 @@ import { // The scheduler is imported here *only* to detect whether it's been mocked import * as Scheduler from 'scheduler'; -import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; - import { resetAfterCommit, scheduleTimeout, @@ -349,13 +345,6 @@ let rootWithNestedUpdates: FiberRoot | null = null; const NESTED_PASSIVE_UPDATE_LIMIT = 50; let nestedPassiveUpdateCount: number = 0; -// Marks the need to reschedule pending interactions at these lanes -// 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. -// TODO: Can use a bitmask instead of an array -let spawnedWorkDuringRender: null | Array = null; - // If two updates are scheduled within the same event, we should treat their // event times as simultaneous, even if the actual clock time has advanced // between the first and second call. @@ -496,11 +485,7 @@ export function scheduleUpdateOnFiber( if (current.tag === Profiler) { const {id, onNestedUpdateScheduled} = current.memoizedProps; if (typeof onNestedUpdateScheduled === 'function') { - if (enableSchedulerTracing) { - onNestedUpdateScheduled(id, root.memoizedInteractions); - } else { - onNestedUpdateScheduled(id); - } + onNestedUpdateScheduled(id); } } current = current.return; @@ -543,16 +528,12 @@ export function scheduleUpdateOnFiber( // 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, lane); - // 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. performSyncWorkOnRoot(root); } else { ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, lane); if ( executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode @@ -569,7 +550,6 @@ export function scheduleUpdateOnFiber( } else { // Schedule other updates after in case the callback is sync. ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, lane); } return root; @@ -1269,10 +1249,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) { enqueueInterleavedUpdates(); - if (enableSchedulerTracing) { - spawnedWorkDuringRender = null; - } - if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); } @@ -1357,21 +1333,6 @@ function popDispatcher(prevDispatcher) { ReactCurrentDispatcher.current = prevDispatcher; } -function pushInteractions(root) { - if (enableSchedulerTracing) { - const prevInteractions: Set | null = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - return prevInteractions; - } - return null; -} - -function popInteractions(prevInteractions) { - if (enableSchedulerTracing) { - __interactionsRef.current = prevInteractions; - } -} - export function markCommitTimeOfFallback() { globalMostRecentFallbackTime = now(); } @@ -1454,11 +1415,8 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } prepareFreshStack(root, lanes); - startWorkOnPendingInteractions(root, lanes); } - const prevInteractions = pushInteractions(root); - if (__DEV__) { if (enableDebugTracing) { logRenderStarted(lanes); @@ -1478,9 +1436,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } } while (true); resetContextDependencies(); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } executionContext = prevExecutionContext; popDispatcher(prevDispatcher); @@ -1546,11 +1501,8 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { resetRenderTimer(); prepareFreshStack(root, lanes); - startWorkOnPendingInteractions(root, lanes); } - const prevInteractions = pushInteractions(root); - if (__DEV__) { if (enableDebugTracing) { logRenderStarted(lanes); @@ -1570,9 +1522,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } } while (true); resetContextDependencies(); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } popDispatcher(prevDispatcher); executionContext = prevExecutionContext; @@ -1874,7 +1823,6 @@ function commitRootImpl(root, renderPriorityLevel) { const prevExecutionContext = executionContext; executionContext |= CommitContext; - const prevInteractions = pushInteractions(root); // Reset this to null before calling lifecycles ReactCurrentOwner.current = null; @@ -1947,9 +1895,6 @@ function commitRootImpl(root, renderPriorityLevel) { // opportunity to paint. requestPaint(); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } executionContext = prevExecutionContext; // Reset the priority to the previous non-sync value. @@ -1980,22 +1925,7 @@ function commitRootImpl(root, renderPriorityLevel) { remainingLanes = root.pendingLanes; // Check if there's remaining work on this root - if (remainingLanes !== NoLanes) { - if (enableSchedulerTracing) { - if (spawnedWorkDuringRender !== null) { - const expirationTimes = spawnedWorkDuringRender; - spawnedWorkDuringRender = null; - for (let i = 0; i < expirationTimes.length; i++) { - scheduleInteractions( - root, - expirationTimes[i], - root.memoizedInteractions, - ); - } - } - schedulePendingInteractions(root, remainingLanes); - } - } else { + if (remainingLanes === NoLanes) { // If there's no remaining work, we can clear the set of already failed // error boundaries. legacyErrorBoundariesThatAlreadyFailed = null; @@ -2007,16 +1937,6 @@ function commitRootImpl(root, renderPriorityLevel) { } } - 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, lanes); - } - } - if (includesSomeLane(remainingLanes, (SyncLane: Lane))) { if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { markNestedUpdateScheduled(); @@ -2177,7 +2097,6 @@ function flushPassiveEffectsImpl() { const prevExecutionContext = executionContext; executionContext |= CommitContext; - const prevInteractions = pushInteractions(root); commitPassiveUnmountEffects(root.current); commitPassiveMountEffects(root, root.current); @@ -2192,11 +2111,6 @@ function flushPassiveEffectsImpl() { } } - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - finishPendingInteractions(root, lanes); - } - if (__DEV__) { isFlushingPassiveEffects = false; } @@ -2271,7 +2185,6 @@ function captureCommitPhaseErrorOnRoot( if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, SyncLane); } } @@ -2318,7 +2231,6 @@ export function captureCommitPhaseError( if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, SyncLane); } return; } @@ -2390,7 +2302,6 @@ export function pingSuspendedRoot( } ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, pingedLanes); } function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { @@ -2409,7 +2320,6 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { if (root !== null) { markRootUpdated(root, retryLane, eventTime); ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, retryLane); } } @@ -3008,167 +2918,6 @@ export function warnIfUnmockedScheduler(fiber: Fiber) { } } -function computeThreadID(root: FiberRoot, lane: Lane | Lanes) { - // Interaction threads are unique per root and expiration time. - // NOTE: Intentionally unsound cast. All that matters is that it's a number - // and it represents a batch of work. Could make a helper function instead, - // but meh this is fine for now. - return (lane: any) * 1000 + root.interactionThreadID; -} - -export function markSpawnedWork(lane: Lane | Lanes) { - if (!enableSchedulerTracing) { - return; - } - if (spawnedWorkDuringRender === null) { - spawnedWorkDuringRender = [lane]; - } else { - spawnedWorkDuringRender.push(lane); - } -} - -function scheduleInteractions( - root: FiberRoot, - lane: Lane | Lanes, - interactions: Set, -) { - if (!enableSchedulerTracing) { - return; - } - - if (interactions.size > 0) { - const pendingInteractionMap = root.pendingInteractionMap; - const pendingInteractions = pendingInteractionMap.get(lane); - 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(lane, 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, lane); - subscriber.onWorkScheduled(interactions, threadID); - } - } -} - -function schedulePendingInteractions(root: FiberRoot, lane: Lane | Lanes) { - // 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, lane, __interactionsRef.current); -} - -function startWorkOnPendingInteractions(root: FiberRoot, lanes: Lanes) { - // 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 = new Set(); - root.pendingInteractionMap.forEach((scheduledInteractions, scheduledLane) => { - if (includesSomeLane(lanes, scheduledLane)) { - 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 performConcurrentWorkOnRoot() - // 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, lanes); - try { - subscriber.onWorkStarted(interactions, threadID); - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediateSchedulerPriority, () => { - throw error; - }); - } - } - } -} - -function finishPendingInteractions(root, committedLanes) { - if (!enableSchedulerTracing) { - return; - } - - const remainingLanesAfterCommit = root.pendingLanes; - - let subscriber; - - try { - subscriber = __subscriberRef.current; - if (subscriber !== null && root.memoizedInteractions.size > 0) { - // FIXME: More than one lane can finish in a single commit. - const threadID = computeThreadID(root, committedLanes); - subscriber.onWorkStopped(root.memoizedInteractions, threadID); - } - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediateSchedulerPriority, () => { - 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, lane) => { - // 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 (!includesSomeLane(remainingLanesAfterCommit, lane)) { - pendingInteractionMap.delete(lane); - - 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(ImmediateSchedulerPriority, () => { - throw error; - }); - } - } - }); - } - }); - } -} - // `act` testing API // // TODO: This is mostly a copy-paste from the legacy `act`, which does not have diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 3b0b196e11..7b03e4cc74 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -10,7 +10,6 @@ import type {Thenable, Wakeable} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.old'; -import type {Interaction} from 'scheduler/src/Tracing'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; import type {StackCursor} from './ReactFiberStack.old'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; @@ -24,7 +23,6 @@ import { enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, enableProfilerNestedUpdateScheduledHook, - enableSchedulerTracing, warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, enableDebugTracing, @@ -83,8 +81,6 @@ import { // The scheduler is imported here *only* to detect whether it's been mocked import * as Scheduler from 'scheduler'; -import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; - import { resetAfterCommit, scheduleTimeout, @@ -349,13 +345,6 @@ let rootWithNestedUpdates: FiberRoot | null = null; const NESTED_PASSIVE_UPDATE_LIMIT = 50; let nestedPassiveUpdateCount: number = 0; -// Marks the need to reschedule pending interactions at these lanes -// 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. -// TODO: Can use a bitmask instead of an array -let spawnedWorkDuringRender: null | Array = null; - // If two updates are scheduled within the same event, we should treat their // event times as simultaneous, even if the actual clock time has advanced // between the first and second call. @@ -496,11 +485,7 @@ export function scheduleUpdateOnFiber( if (current.tag === Profiler) { const {id, onNestedUpdateScheduled} = current.memoizedProps; if (typeof onNestedUpdateScheduled === 'function') { - if (enableSchedulerTracing) { - onNestedUpdateScheduled(id, root.memoizedInteractions); - } else { - onNestedUpdateScheduled(id); - } + onNestedUpdateScheduled(id); } } current = current.return; @@ -543,16 +528,12 @@ export function scheduleUpdateOnFiber( // 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, lane); - // 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. performSyncWorkOnRoot(root); } else { ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, lane); if ( executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode @@ -569,7 +550,6 @@ export function scheduleUpdateOnFiber( } else { // Schedule other updates after in case the callback is sync. ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, lane); } return root; @@ -1269,10 +1249,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) { enqueueInterleavedUpdates(); - if (enableSchedulerTracing) { - spawnedWorkDuringRender = null; - } - if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); } @@ -1357,21 +1333,6 @@ function popDispatcher(prevDispatcher) { ReactCurrentDispatcher.current = prevDispatcher; } -function pushInteractions(root) { - if (enableSchedulerTracing) { - const prevInteractions: Set | null = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - return prevInteractions; - } - return null; -} - -function popInteractions(prevInteractions) { - if (enableSchedulerTracing) { - __interactionsRef.current = prevInteractions; - } -} - export function markCommitTimeOfFallback() { globalMostRecentFallbackTime = now(); } @@ -1454,11 +1415,8 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } prepareFreshStack(root, lanes); - startWorkOnPendingInteractions(root, lanes); } - const prevInteractions = pushInteractions(root); - if (__DEV__) { if (enableDebugTracing) { logRenderStarted(lanes); @@ -1478,9 +1436,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } } while (true); resetContextDependencies(); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } executionContext = prevExecutionContext; popDispatcher(prevDispatcher); @@ -1546,11 +1501,8 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { resetRenderTimer(); prepareFreshStack(root, lanes); - startWorkOnPendingInteractions(root, lanes); } - const prevInteractions = pushInteractions(root); - if (__DEV__) { if (enableDebugTracing) { logRenderStarted(lanes); @@ -1570,9 +1522,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } } while (true); resetContextDependencies(); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } popDispatcher(prevDispatcher); executionContext = prevExecutionContext; @@ -1874,7 +1823,6 @@ function commitRootImpl(root, renderPriorityLevel) { const prevExecutionContext = executionContext; executionContext |= CommitContext; - const prevInteractions = pushInteractions(root); // Reset this to null before calling lifecycles ReactCurrentOwner.current = null; @@ -1947,9 +1895,6 @@ function commitRootImpl(root, renderPriorityLevel) { // opportunity to paint. requestPaint(); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } executionContext = prevExecutionContext; // Reset the priority to the previous non-sync value. @@ -1980,22 +1925,7 @@ function commitRootImpl(root, renderPriorityLevel) { remainingLanes = root.pendingLanes; // Check if there's remaining work on this root - if (remainingLanes !== NoLanes) { - if (enableSchedulerTracing) { - if (spawnedWorkDuringRender !== null) { - const expirationTimes = spawnedWorkDuringRender; - spawnedWorkDuringRender = null; - for (let i = 0; i < expirationTimes.length; i++) { - scheduleInteractions( - root, - expirationTimes[i], - root.memoizedInteractions, - ); - } - } - schedulePendingInteractions(root, remainingLanes); - } - } else { + if (remainingLanes === NoLanes) { // If there's no remaining work, we can clear the set of already failed // error boundaries. legacyErrorBoundariesThatAlreadyFailed = null; @@ -2007,16 +1937,6 @@ function commitRootImpl(root, renderPriorityLevel) { } } - 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, lanes); - } - } - if (includesSomeLane(remainingLanes, (SyncLane: Lane))) { if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { markNestedUpdateScheduled(); @@ -2177,7 +2097,6 @@ function flushPassiveEffectsImpl() { const prevExecutionContext = executionContext; executionContext |= CommitContext; - const prevInteractions = pushInteractions(root); commitPassiveUnmountEffects(root.current); commitPassiveMountEffects(root, root.current); @@ -2192,11 +2111,6 @@ function flushPassiveEffectsImpl() { } } - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - finishPendingInteractions(root, lanes); - } - if (__DEV__) { isFlushingPassiveEffects = false; } @@ -2271,7 +2185,6 @@ function captureCommitPhaseErrorOnRoot( if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, SyncLane); } } @@ -2318,7 +2231,6 @@ export function captureCommitPhaseError( if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, SyncLane); } return; } @@ -2390,7 +2302,6 @@ export function pingSuspendedRoot( } ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, pingedLanes); } function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { @@ -2409,7 +2320,6 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { if (root !== null) { markRootUpdated(root, retryLane, eventTime); ensureRootIsScheduled(root, eventTime); - schedulePendingInteractions(root, retryLane); } } @@ -3008,167 +2918,6 @@ export function warnIfUnmockedScheduler(fiber: Fiber) { } } -function computeThreadID(root: FiberRoot, lane: Lane | Lanes) { - // Interaction threads are unique per root and expiration time. - // NOTE: Intentionally unsound cast. All that matters is that it's a number - // and it represents a batch of work. Could make a helper function instead, - // but meh this is fine for now. - return (lane: any) * 1000 + root.interactionThreadID; -} - -export function markSpawnedWork(lane: Lane | Lanes) { - if (!enableSchedulerTracing) { - return; - } - if (spawnedWorkDuringRender === null) { - spawnedWorkDuringRender = [lane]; - } else { - spawnedWorkDuringRender.push(lane); - } -} - -function scheduleInteractions( - root: FiberRoot, - lane: Lane | Lanes, - interactions: Set, -) { - if (!enableSchedulerTracing) { - return; - } - - if (interactions.size > 0) { - const pendingInteractionMap = root.pendingInteractionMap; - const pendingInteractions = pendingInteractionMap.get(lane); - 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(lane, 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, lane); - subscriber.onWorkScheduled(interactions, threadID); - } - } -} - -function schedulePendingInteractions(root: FiberRoot, lane: Lane | Lanes) { - // 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, lane, __interactionsRef.current); -} - -function startWorkOnPendingInteractions(root: FiberRoot, lanes: Lanes) { - // 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 = new Set(); - root.pendingInteractionMap.forEach((scheduledInteractions, scheduledLane) => { - if (includesSomeLane(lanes, scheduledLane)) { - 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 performConcurrentWorkOnRoot() - // 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, lanes); - try { - subscriber.onWorkStarted(interactions, threadID); - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediateSchedulerPriority, () => { - throw error; - }); - } - } - } -} - -function finishPendingInteractions(root, committedLanes) { - if (!enableSchedulerTracing) { - return; - } - - const remainingLanesAfterCommit = root.pendingLanes; - - let subscriber; - - try { - subscriber = __subscriberRef.current; - if (subscriber !== null && root.memoizedInteractions.size > 0) { - // FIXME: More than one lane can finish in a single commit. - const threadID = computeThreadID(root, committedLanes); - subscriber.onWorkStopped(root.memoizedInteractions, threadID); - } - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediateSchedulerPriority, () => { - 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, lane) => { - // 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 (!includesSomeLane(remainingLanesAfterCommit, lane)) { - pendingInteractionMap.delete(lane); - - 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(ImmediateSchedulerPriority, () => { - throw error; - }); - } - } - }); - } - }); - } -} - // `act` testing API // // TODO: This is mostly a copy-paste from the legacy `act`, which does not have diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 6d7d38232e..104a9ef728 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -24,7 +24,6 @@ import type {Lane, Lanes, LaneMap} from './ReactFiberLane.old'; import type {RootTag} from './ReactRootTags'; import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig'; import type {Wakeable} from 'shared/ReactTypes'; -import type {Interaction} from 'scheduler/src/Tracing'; import type {Cache} from './ReactFiberCacheComponent.old'; // Unwind Circular: moved from ReactFiberHooks.old @@ -240,16 +239,6 @@ type BaseFiberRootProperties = {| pooledCacheLanes: Lanes, |}; -// The following attributes are only used by interaction tracing builds. -// They enable interactions to be associated with their async work, -// And expose interaction metadata to the React DevTools Profiler plugin. -// Note that these attributes are only defined when the enableSchedulerTracing flag is enabled. -type ProfilingOnlyFiberRootProperties = {| - interactionThreadID: number, - memoizedInteractions: Set, - pendingInteractionMap: Map>, -|}; - // The following attributes are only used by DevTools and are only present in DEV builds. // They enable DevTools Profiler UI to show which Fiber(s) scheduled a given commit. type UpdaterTrackingOnlyFiberRootProperties = {| @@ -270,12 +259,9 @@ type SuspenseCallbackOnlyFiberRootProperties = {| // Exported FiberRoot type includes all properties, // To avoid requiring potentially error-prone :any casts throughout the project. -// Profiling properties are only safe to access in profiling builds (when enableSchedulerTracing is true). // The types are defined separately within this file to ensure they stay in sync. -// (We don't have to use an inline :any cast when enableSchedulerTracing is disabled.) export type FiberRoot = { ...BaseFiberRootProperties, - ...ProfilingOnlyFiberRootProperties, ...SuspenseCallbackOnlyFiberRootProperties, ...UpdaterTrackingOnlyFiberRootProperties, ... diff --git a/packages/react-reconciler/src/Scheduler.js b/packages/react-reconciler/src/Scheduler.js index f590f284a9..dabb5baaf6 100644 --- a/packages/react-reconciler/src/Scheduler.js +++ b/packages/react-reconciler/src/Scheduler.js @@ -12,9 +12,6 @@ // because Rollup would use dynamic dispatch for CommonJS interop named imports. // When we switch to ESM, we can delete this module. import * as Scheduler from 'scheduler'; -import {__interactionsRef} from 'scheduler/tracing'; -import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; -import invariant from 'shared/invariant'; export const scheduleCallback = Scheduler.unstable_scheduleCallback; export const cancelCallback = Scheduler.unstable_cancelCallback; @@ -28,19 +25,4 @@ export const UserBlockingPriority = Scheduler.unstable_UserBlockingPriority; export const NormalPriority = Scheduler.unstable_NormalPriority; export const LowPriority = Scheduler.unstable_LowPriority; export const IdlePriority = Scheduler.unstable_IdlePriority; - -if (enableSchedulerTracing) { - // Provide explicit error message when production+profiling bundle of e.g. - // react-dom is used with production (non-profiling) bundle of - // scheduler/tracing - invariant( - __interactionsRef != null && __interactionsRef.current != null, - 'It is not supported to run the profiling version of a renderer (for ' + - 'example, `react-dom/profiling`) without also replacing the ' + - '`scheduler/tracing` module with `scheduler/tracing-profiling`. Your ' + - 'bundler might have a setting for aliasing both modules. Learn more at ' + - 'https://reactjs.org/link/profiling', - ); -} - export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null; diff --git a/packages/scheduler/tracing.js b/packages/react-reconciler/src/__mocks__/scheduler/tracing.js similarity index 59% rename from packages/scheduler/tracing.js rename to packages/react-reconciler/src/__mocks__/scheduler/tracing.js index e3001a8ecf..c9f74c6bb2 100644 --- a/packages/scheduler/tracing.js +++ b/packages/react-reconciler/src/__mocks__/scheduler/tracing.js @@ -7,7 +7,5 @@ * @flow */ -'use strict'; - -export * from './src/Tracing'; -export * from './src/TracingSubscriptions'; +// This placeholder is to support a legacy/regression test in ReactFresh-test. +// Without this mock, jest.mock('scheduler/tracing') throws. diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 07d3f7ff52..27d09697fc 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -18,7 +18,6 @@ let readText; let resolveText; let ReactNoop; let Scheduler; -let SchedulerTracing; let Suspense; let useState; let useReducer; @@ -43,7 +42,6 @@ describe('ReactHooksWithNoopRenderer', () => { React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); useState = React.useState; useReducer = React.useReducer; useEffect = React.useEffect; @@ -1797,71 +1795,6 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); }); - // @gate enableSchedulerTracing - it('does not flush non-discrete passive effects when flushing sync (with tracing)', () => { - const onInteractionScheduledWorkCompleted = jest.fn(); - const onWorkCanceled = jest.fn(); - SchedulerTracing.unstable_subscribe({ - onInteractionScheduledWorkCompleted, - onInteractionTraced: jest.fn(), - onWorkCanceled, - onWorkScheduled: jest.fn(), - onWorkStarted: jest.fn(), - onWorkStopped: jest.fn(), - }); - - let _updateCount; - function Counter(props) { - const [count, updateCount] = useState(0); - _updateCount = updateCount; - useEffect(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - tracingEvent, - ]); - Scheduler.unstable_yieldValue(`Will set count to 1`); - updateCount(1); - }, []); - return ; - } - - const tracingEvent = {id: 0, name: 'hello', timestamp: 0}; - // we explicitly wait for missing act() warnings here since - // it's a lot harder to simulate this condition inside an act scope - expect(() => { - SchedulerTracing.unstable_trace( - tracingEvent.name, - tracingEvent.timestamp, - () => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - }, - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }).toErrorDev(['An update to Counter ran an effect']); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0); - - // A flush sync doesn't cause the passive effects to fire. - act(() => { - ReactNoop.flushSync(() => { - _updateCount(2); - }); - }); - - expect(Scheduler).toHaveYielded([ - 'Will set count to 1', - 'Count: 2', - 'Count: 1', - ]); - - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect(onWorkCanceled).toHaveBeenCalledTimes(0); - }); - it( 'in legacy mode, useEffect is deferred and updates finish synchronously ' + '(in a single batch)', diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index ae3f286f54..e0fcab7a77 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -2,7 +2,6 @@ let React; let ReactTestRenderer; let ReactFeatureFlags; let Scheduler; -let SchedulerTracing; let ReactCache; let Suspense; let act; @@ -18,12 +17,10 @@ describe('ReactSuspense', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; - ReactFeatureFlags.enableSchedulerTracing = true; React = require('react'); ReactTestRenderer = require('react-test-renderer'); act = ReactTestRenderer.unstable_concurrentAct; Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); ReactCache = require('react-cache'); Suspense = React.Suspense; @@ -1271,76 +1268,6 @@ describe('ReactSuspense', () => { ]); }); - it('should call onInteractionScheduledWorkCompleted after suspending', () => { - const subscriber = { - onInteractionScheduledWorkCompleted: jest.fn(), - onInteractionTraced: jest.fn(), - onWorkCanceled: jest.fn(), - onWorkScheduled: jest.fn(), - onWorkStarted: jest.fn(), - onWorkStopped: jest.fn(), - }; - SchedulerTracing.unstable_subscribe(subscriber); - SchedulerTracing.unstable_trace('test', performance.now(), () => { - function App() { - return ( - }> - - - - - ); - } - - const root = ReactTestRenderer.create(null); - root.update(); - - expect(Scheduler).toHaveYielded([ - 'Suspend! [A]', - 'Suspend! [B]', - 'Suspend! [C]', - 'Loading...', - ]); - - // Resolve A - jest.advanceTimersByTime(1000); - - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushUntilNextPaint([ - 'A', - // The promises for B and C have now been thrown twice - 'Suspend! [B]', - 'Suspend! [C]', - ]); - - // Resolve B - jest.advanceTimersByTime(1000); - - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - expect(Scheduler).toFlushUntilNextPaint([ - // Even though the promise for B was thrown twice, we should only - // re-render once. - 'B', - // The promise for C has now been thrown three times - 'Suspend! [C]', - ]); - - // Resolve C - jest.advanceTimersByTime(1000); - - expect(Scheduler).toHaveYielded(['Promise resolved [C]']); - expect(Scheduler).toFlushAndYield([ - // Even though the promise for C was thrown three times, we should only - // re-render once. - 'C', - ]); - }); - - expect( - subscriber.onInteractionScheduledWorkCompleted, - ).toHaveBeenCalledTimes(1); - }); - it('#14162', () => { const {lazy} = React; diff --git a/packages/react-reconciler/src/__tests__/ReactTracing-test.internal.js b/packages/react-reconciler/src/__tests__/ReactTracing-test.internal.js deleted file mode 100644 index 3815ec8487..0000000000 --- a/packages/react-reconciler/src/__tests__/ReactTracing-test.internal.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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. - * - * @emails react-core - * @jest-environment node - */ - -'use strict'; - -describe('ReactTracing', () => { - it('should error if profiling renderer and non-profiling scheduler/tracing bundles are combined', () => { - jest.resetModules(); - - const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSchedulerTracing = false; - - require('scheduler/tracing'); - - ReactFeatureFlags.enableSchedulerTracing = true; - - expect(() => require('react-dom')).toThrow( - 'Learn more at https://reactjs.org/link/profiling', - ); - }); -}); diff --git a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js index 8d762f87b8..248223e349 100644 --- a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js @@ -292,91 +292,6 @@ describe('updaters', () => { done(); }); - // @gate experimental - it('traces interaction through hidden subtree', async () => { - const { - FunctionComponent, - HostRoot, - } = require('react-reconciler/src/ReactWorkTags'); - - // Note: This is based on a similar component we use in www. We can delete once - // the extra div wrapper is no longer necessary. - function LegacyHiddenDiv({children, mode}) { - return ( - - ); - } - - const Child = () => { - const [didMount, setDidMount] = React.useState(false); - Scheduler.unstable_yieldValue('Child'); - React.useEffect(() => { - if (didMount) { - Scheduler.unstable_yieldValue('Child:update'); - } else { - Scheduler.unstable_yieldValue('Child:mount'); - setDidMount(true); - } - }, [didMount]); - return
    ; - }; - - const App = () => { - Scheduler.unstable_yieldValue('App'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('App:mount'); - }, []); - return ( - - - - ); - }; - - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - await ReactTestUtils.act(async () => { - root.render(); - }); - - // TODO: There are 4 commits here instead of 3 - // because this update was scheduled at idle priority, - // and idle updates are slightly higher priority than offscreen work. - // So it takes two render passes to finish it. - // The onCommit hook is called even after the no-op bailout update. - expect(Scheduler).toHaveYielded([ - 'App', - 'onCommitRoot', - 'App:mount', - - 'Child', - 'onCommitRoot', - 'Child:mount', - - 'onCommitRoot', - - 'Child', - 'onCommitRoot', - 'Child:update', - ]); - expect(allSchedulerTypes).toEqual([ - // Initial render - [null], - // Offscreen update - [], - // Child passive effect - [Child], - // Offscreen update - [], - ]); - expect(allSchedulerTags).toEqual([[HostRoot], [], [FunctionComponent], []]); - }); - // @gate experimental it('should cover error handling', async () => { let triggerError = null; diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js index b8c47e93d6..fbd81d9576 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js @@ -25,7 +25,6 @@ function loadModules() { jest.useFakeTimers(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSchedulerTracing = true; ReactFeatureFlags.enableProfilerTimer = true; React = require('react'); diff --git a/packages/react-refresh/package.json b/packages/react-refresh/package.json index 590bb19dea..4f5f3c31c5 100644 --- a/packages/react-refresh/package.json +++ b/packages/react-refresh/package.json @@ -28,6 +28,7 @@ }, "devDependencies": { "react-16-8": "npm:react@16.8.0", - "react-dom-16-8": "npm:react-dom@16.8.0" + "react-dom-16-8": "npm:react-dom@16.8.0", + "scheduler-0-13": "npm:scheduler@0.13.0" } } \ No newline at end of file diff --git a/packages/react-refresh/src/__tests__/ReactFresh-test.js b/packages/react-refresh/src/__tests__/ReactFresh-test.js index 3fa7a61442..1f83a84d83 100644 --- a/packages/react-refresh/src/__tests__/ReactFresh-test.js +++ b/packages/react-refresh/src/__tests__/ReactFresh-test.js @@ -3844,6 +3844,10 @@ describe('ReactFresh', () => { // Redirect all React/ReactDOM requires to v16.8.0 // This version predates Fast Refresh support. + jest.mock('scheduler', () => jest.requireActual('scheduler-0-13')); + jest.mock('scheduler/tracing', () => + jest.requireActual('scheduler-0-13/tracing'), + ); jest.mock('react', () => jest.requireActual('react-16-8')); jest.mock('react-dom', () => jest.requireActual('react-dom-16-8')); diff --git a/packages/react/index.js b/packages/react/index.js index 5319bb80be..094d0fcd23 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -28,11 +28,6 @@ export type ElementConfig = React$ElementConfig; export type ElementRef = React$ElementRef; export type Config = React$Config; export type ChildrenArray<+T> = $ReadOnlyArray> | T; -export type Interaction = { - name: string, - timestamp: number, - ... -}; // Export all exports so that they're available in tests. // We can't use export * from in Flow for some reason. diff --git a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js deleted file mode 100644 index 1a026d7324..0000000000 --- a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js +++ /dev/null @@ -1,922 +0,0 @@ -/** - * 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. - * - * @emails react-core - */ - -'use strict'; - -let React; -let ReactDOM; -let ReactDOMServer; -let ReactFeatureFlags; -let Scheduler; -let SchedulerTracing; -let TestUtils; -let act; -let onInteractionScheduledWorkCompleted; -let onInteractionTraced; -let onWorkCanceled; -let onWorkScheduled; -let onWorkStarted; -let onWorkStopped; -let IdleEventPriority; -let ContinuousEventPriority; - -function loadModules() { - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - - ReactFeatureFlags.enableProfilerTimer = true; - ReactFeatureFlags.enableSchedulerTracing = true; - ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; - - React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMServer = require('react-dom/server'); - Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); - TestUtils = require('react-dom/test-utils'); - - IdleEventPriority = require('react-reconciler/constants').IdleEventPriority; - ContinuousEventPriority = require('react-reconciler/constants') - .ContinuousEventPriority; - - act = TestUtils.unstable_concurrentAct; - - onInteractionScheduledWorkCompleted = jest.fn(); - onInteractionTraced = jest.fn(); - onWorkCanceled = jest.fn(); - onWorkScheduled = jest.fn(); - onWorkStarted = jest.fn(); - onWorkStopped = jest.fn(); - - // Verify interaction subscriber methods are called as expected. - SchedulerTracing.unstable_subscribe({ - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled, - onWorkScheduled, - onWorkStarted, - onWorkStopped, - }); -} - -// Note: This is based on a similar component we use in www. We can delete once -// the extra div wrapper is no longer necessary. -function LegacyHiddenDiv({children, mode}) { - return ( - - ); -} - -describe('ReactDOMTracing', () => { - beforeEach(() => { - jest.resetModules(); - - loadModules(); - }); - - describe('interaction tracing', () => { - describe('hidden', () => { - // @gate experimental - it('traces interaction through hidden subtree', () => { - const Child = () => { - const [didMount, setDidMount] = React.useState(false); - Scheduler.unstable_yieldValue('Child'); - React.useEffect(() => { - if (didMount) { - Scheduler.unstable_yieldValue('Child:update'); - } else { - Scheduler.unstable_yieldValue('Child:mount'); - setDidMount(true); - } - }, [didMount]); - return
    ; - }; - - const App = () => { - Scheduler.unstable_yieldValue('App'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('App:mount'); - }, []); - return ( - - - - ); - }; - - let interaction; - - const onRender = jest.fn(); - - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - act(() => { - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - root.render( - - - , - ); - }); - } else { - root.render( - - - , - ); - } - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onRender).toHaveBeenCalledTimes(1); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - - expect(Scheduler).toFlushAndYield(['Child', 'Child:update']); - }); - }); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - - if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) { - expect(onRender).toHaveBeenCalledTimes(3); - } else { - // TODO: This is 4 instead of 3 because this update was scheduled at - // idle priority, and idle updates are slightly higher priority than - // offscreen work. So it takes two render passes to finish it. Profiler - // calls `onRender` for the first render even though everything - // bails out. - expect(onRender).toHaveBeenCalledTimes(4); - } - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - }); - - // @gate experimental - it('traces interaction through hidden subtree when there is other pending traced work', () => { - const Child = () => { - Scheduler.unstable_yieldValue('Child'); - return
    ; - }; - - let wrapped = null; - - const App = () => { - Scheduler.unstable_yieldValue('App'); - React.useEffect(() => { - wrapped = SchedulerTracing.unstable_wrap(() => {}); - Scheduler.unstable_yieldValue('App:mount'); - }, []); - return ( - - - - ); - }; - - let interaction; - - const onRender = jest.fn(); - - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - - act(() => { - root.render( - - - , - ); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - - expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onRender).toHaveBeenCalledTimes(1); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - - expect(wrapped).not.toBeNull(); - - expect(Scheduler).toFlushAndYield(['Child']); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - }); - }); - - wrapped(); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - // @gate experimental - it('traces interaction through hidden subtree that schedules more idle/never work', () => { - const Child = () => { - const [didMount, setDidMount] = React.useState(false); - Scheduler.unstable_yieldValue('Child'); - React.useLayoutEffect(() => { - if (didMount) { - Scheduler.unstable_yieldValue('Child:update'); - } else { - Scheduler.unstable_yieldValue('Child:mount'); - ReactDOM.unstable_runWithPriority(IdleEventPriority, () => - setDidMount(true), - ); - } - }, [didMount]); - return
    ; - }; - - const App = () => { - Scheduler.unstable_yieldValue('App'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('App:mount'); - }, []); - return ( - - - - ); - }; - - let interaction; - - const onRender = jest.fn(); - - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - act(() => { - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - root.render( - - - , - ); - }); - } else { - root.render( - - - , - ); - } - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - - expect(Scheduler).toFlushAndYieldThrough(['App', 'App:mount']); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onRender).toHaveBeenCalledTimes(1); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - - expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - - expect(Scheduler).toFlushAndYield(['Child', 'Child:update']); - }); - }); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - - // TODO: This is 4 instead of 3 because this update was scheduled at - // idle priority, and idle updates are slightly higher priority than - // offscreen work. So it takes two render passes to finish it. Profiler - // calls `onRender` for the first render even though everything - // bails out. - expect(onRender).toHaveBeenCalledTimes(4); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - }); - - // @gate experimental - it('does not continue interactions across pre-existing idle work', () => { - const Child = () => { - Scheduler.unstable_yieldValue('Child'); - return
    ; - }; - - let update = null; - - const WithHiddenWork = () => { - Scheduler.unstable_yieldValue('WithHiddenWork'); - return ( - - - - ); - }; - - const Updater = () => { - Scheduler.unstable_yieldValue('Updater'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('Updater:effect'); - }); - - const setCount = React.useState(0)[1]; - update = () => { - setCount(current => current + 1); - }; - - return
    ; - }; - - const App = () => { - Scheduler.unstable_yieldValue('App'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('App:effect'); - }); - - return ( - <> - - - - ); - }; - - const onRender = jest.fn(); - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - - // Schedule some idle work without any interactions. - act(() => { - root.render( - - - , - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'App', - 'WithHiddenWork', - 'Updater', - 'Updater:effect', - 'App:effect', - ]); - expect(update).not.toBeNull(); - - // Trace a higher-priority update. - let interaction = null; - SchedulerTracing.unstable_trace('update', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - update(); - }); - expect(interaction).not.toBeNull(); - expect(onRender).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - - // Ensure the traced interaction completes without being attributed to the pre-existing idle work. - expect(Scheduler).toFlushAndYieldThrough([ - 'Updater', - 'Updater:effect', - ]); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - - // Complete low-priority work and ensure no lingering interaction. - expect(Scheduler).toFlushAndYield(['Child']); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect(onRender).toHaveBeenCalledTimes(3); - expect(onRender).toHaveLastRenderedWithInteractions(new Set([])); - }); - }); - - // @gate experimental - it('should properly trace interactions when there is work of interleaved priorities', () => { - const Child = () => { - Scheduler.unstable_yieldValue('Child'); - return
    ; - }; - - let scheduleUpdate = null; - let scheduleUpdateWithHidden = null; - - const MaybeHiddenWork = () => { - const [flag, setFlag] = React.useState(false); - scheduleUpdateWithHidden = () => setFlag(true); - Scheduler.unstable_yieldValue('MaybeHiddenWork'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('MaybeHiddenWork:effect'); - }); - return flag ? ( - - - - ) : null; - }; - - const Updater = () => { - Scheduler.unstable_yieldValue('Updater'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('Updater:effect'); - }); - - const setCount = React.useState(0)[1]; - scheduleUpdate = () => setCount(current => current + 1); - - return
    ; - }; - - const App = () => { - Scheduler.unstable_yieldValue('App'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('App:effect'); - }); - - return ( - <> - - - - ); - }; - - const onRender = jest.fn(); - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - - act(() => { - root.render( - - - , - ); - expect(Scheduler).toFlushAndYield([ - 'App', - 'MaybeHiddenWork', - 'Updater', - 'MaybeHiddenWork:effect', - 'Updater:effect', - 'App:effect', - ]); - expect(scheduleUpdate).not.toBeNull(); - expect(scheduleUpdateWithHidden).not.toBeNull(); - expect(onRender).toHaveBeenCalledTimes(1); - - // schedule traced high-pri update and a (non-traced) low-pri update. - let interaction = null; - SchedulerTracing.unstable_trace('update', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - ReactDOM.unstable_runWithPriority(ContinuousEventPriority, () => - scheduleUpdateWithHidden(), - ); - }); - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - scheduleUpdate(); - }); - } else { - scheduleUpdate(); - } - expect(interaction).not.toBeNull(); - expect(onRender).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - - // high-pri update should leave behind idle work and should not complete the interaction - expect(Scheduler).toFlushAndYieldThrough([ - 'MaybeHiddenWork', - 'MaybeHiddenWork:effect', - ]); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - - // low-pri update should not have the interaction - expect(Scheduler).toFlushAndYieldThrough([ - 'Updater', - 'Updater:effect', - ]); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onRender).toHaveBeenCalledTimes(3); - expect(onRender).toHaveLastRenderedWithInteractions(new Set([])); - - // idle work should complete the interaction - expect(Scheduler).toFlushAndYield(['Child']); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - expect(onRender).toHaveBeenCalledTimes(4); - expect(onRender).toHaveLastRenderedWithInteractions( - new Set([interaction]), - ); - }); - }); - - // @gate experimental - it('should properly trace interactions through a multi-pass SuspenseList render', () => { - const SuspenseList = React.SuspenseList; - const Suspense = React.Suspense; - function Text({text}) { - Scheduler.unstable_yieldValue(text); - React.useEffect(() => { - Scheduler.unstable_yieldValue('Commit ' + text); - }); - return {text}; - } - function App() { - return ( - - }> - - - }> - - - }> - - - - ); - } - - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - - let interaction; - - act(() => { - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - // This render is only CPU bound. Nothing suspends. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - root.render(); - }); - } else { - root.render(); - } - }); - - expect(Scheduler).toFlushAndYieldThrough(['A']); - - Scheduler.unstable_advanceTime(200); - jest.advanceTimersByTime(200); - - expect(Scheduler).toFlushAndYieldThrough(['B']); - - Scheduler.unstable_advanceTime(300); - jest.advanceTimersByTime(300); - - // Time has now elapsed for so long that we're just going to give up - // rendering the rest of the content. So that we can at least show - // something. - expect(Scheduler).toFlushAndYieldThrough([ - 'Loading C', - 'Commit A', - 'Commit B', - 'Commit Loading C', - ]); - - // Schedule an unrelated low priority update that shouldn't be included - // in the previous interaction. This is meant to ensure that we don't - // rely on the whole tree completing to cover up bugs. - ReactDOM.unstable_runWithPriority(IdleEventPriority, () => { - root.render(); - }); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - // Then we do a second pass to commit the last item. - expect(Scheduler).toFlushAndYieldThrough(['C', 'Commit C']); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - }); - }); - - describe('hydration', () => { - // @gate experimental - it('traces interaction across hydration', () => { - const ref = React.createRef(); - - function Child() { - return 'Hello'; - } - - function App() { - return ( -
    - - - -
    - ); - } - - // Render the final HTML. - const finalHTML = ReactDOMServer.renderToString(); - - const container = document.createElement('div'); - container.innerHTML = finalHTML; - - let interaction; - - const root = ReactDOM.createRoot(container, {hydrate: true}); - - // Hydrate it. - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - - root.render(); - }); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(ref.current).not.toBe(null); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - // @gate experimental - it('traces interaction across suspended hydration', async () => { - let suspend = false; - let resolve; - const promise = new Promise( - resolvePromise => (resolve = resolvePromise), - ); - const ref = React.createRef(); - - function Child() { - if (suspend) { - throw promise; - } else { - return 'Hello'; - } - } - - function App() { - return ( -
    - - - - - -
    - ); - } - - // Render the final HTML. - // Don't suspend on the server. - const finalHTML = ReactDOMServer.renderToString(); - - const container = document.createElement('div'); - container.innerHTML = finalHTML; - - let interaction; - - const root = ReactDOM.createRoot(container, {hydrate: true}); - - // Start hydrating but simulate blocking for suspense data. - suspend = true; - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - - root.render(); - }); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(ref.current).toBe(null); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - // Resolving the promise should continue hydration - suspend = false; - resolve(); - await promise; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(ref.current).not.toBe(null); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - // @gate experimental - it('traces interaction across suspended hydration from server', async () => { - // Copied from ReactDOMHostConfig.js - const SUSPENSE_START_DATA = '$'; - const SUSPENSE_PENDING_START_DATA = '$?'; - - const ref = React.createRef(); - - function App() { - return ( - - Hello - - ); - } - - const container = document.createElement('div'); - - // Render the final HTML. - const finalHTML = ReactDOMServer.renderToString(); - - // Replace the marker with a pending state. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping - const escapedMarker = SUSPENSE_START_DATA.replace( - /[.*+\-?^${}()|[\]\\]/g, - '\\$&', - ); - container.innerHTML = finalHTML.replace( - new RegExp(escapedMarker, 'g'), - SUSPENSE_PENDING_START_DATA, - ); - - let interaction; - - const root = ReactDOM.createRoot(container, {hydrate: true}); - - // Start hydrating but simulate blocking for suspense data from the server. - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - - root.render(); - }); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(ref.current).toBe(null); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - // Unblock rendering, pretend the content is injected by the server. - const startNode = container.childNodes[0]; - expect(startNode).not.toBe(null); - expect(startNode.nodeType).toBe(Node.COMMENT_NODE); - - startNode.textContent = SUSPENSE_START_DATA; - startNode._reactRetry(); - - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(ref.current).not.toBe(null); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - // @gate experimental - it('traces interaction across client-rendered hydration', () => { - let suspend = false; - const promise = new Promise(() => {}); - const ref = React.createRef(); - - function Child() { - if (suspend) { - throw promise; - } else { - return 'Hello'; - } - } - - function App() { - return ( -
    - - - - - -
    - ); - } - - // Render the final HTML. - suspend = true; - const finalHTML = ReactDOMServer.renderToString(); - - const container = document.createElement('div'); - container.innerHTML = finalHTML; - - let interaction; - - const root = ReactDOM.createRoot(container, {hydrate: true}); - - // Hydrate without suspending to fill in the client-rendered content. - suspend = false; - SchedulerTracing.unstable_trace('initialization', 0, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - - root.render(); - }); - - expect(onWorkStopped).toHaveBeenCalledTimes(1); - - // Advance time a bit so that we get into a new expiration bucket. - Scheduler.unstable_advanceTime(300); - jest.advanceTimersByTime(300); - - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(ref.current).not.toBe(null); - - // We should've had two commits that was traced. - // First one that hydrates the parent, and then one that hydrates - // the boundary at higher than Never priority. - expect(onWorkStopped).toHaveBeenCalledTimes(3); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - }); - }); -}); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 02c7541e38..bac44d736e 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -14,23 +14,15 @@ let React; let ReactFeatureFlags; let ReactNoop; let Scheduler; -let ReactCache; let ReactTestRenderer; let ReactTestRendererAct; -let SchedulerTracing; let AdvanceTime; -let AsyncText; -let ComponentWithPassiveEffect; -let Text; -let TextResource; -let resourcePromise; function loadModules({ enableProfilerTimer = true, enableProfilerCommitHooks = true, enableProfilerNestedUpdatePhase = true, enableProfilerNestedUpdateScheduledHook = false, - enableSchedulerTracing = true, replayFailedUnitOfWorkWithInvokeGuardedCallback = false, useNoopRenderer = false, } = {}) { @@ -40,13 +32,10 @@ function loadModules({ ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks; ReactFeatureFlags.enableProfilerNestedUpdatePhase = enableProfilerNestedUpdatePhase; ReactFeatureFlags.enableProfilerNestedUpdateScheduledHook = enableProfilerNestedUpdateScheduledHook; - ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback; React = require('react'); Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); - ReactCache = require('react-cache'); if (useNoopRenderer) { ReactNoop = require('react-noop-renderer'); @@ -72,4960 +61,2774 @@ function loadModules({ return this.props.children || null; } }; - - resourcePromise = null; - - TextResource = ReactCache.unstable_createResource( - ([text, ms = 0]) => { - resourcePromise = new Promise((resolve, reject) => - setTimeout(() => { - Scheduler.unstable_yieldValue(`Promise resolved [${text}]`); - resolve(text); - }, ms), - ); - return resourcePromise; - }, - ([text, ms]) => text, - ); - - AsyncText = ({ms, text}) => { - try { - TextResource.read([text, ms]); - Scheduler.unstable_yieldValue(`AsyncText [${text}]`); - return text; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.unstable_yieldValue(`Suspend [${text}]`); - } else { - Scheduler.unstable_yieldValue(`Error [${text}]`); - } - throw promise; - } - }; - - Text = ({text}) => { - Scheduler.unstable_yieldValue(`Text [${text}]`); - return text; - }; - - ComponentWithPassiveEffect = () => { - // Intentionally schedule a passive effect so the onPostCommit hook will be called. - React.useEffect(() => {}); - return null; - }; } describe('Profiler', () => { describe('works in profiling and non-profiling bundles', () => { - [true, false].forEach(enableSchedulerTracing => { - [true, false].forEach(enableProfilerTimer => { - describe(`enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - } enableProfilerTimer:${ - enableProfilerTimer ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); + [true, false].forEach(enableProfilerTimer => { + describe(`enableProfilerTimer:${ + enableProfilerTimer ? 'enabled' : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); - loadModules({enableSchedulerTracing, enableProfilerTimer}); - }); - - // This will throw in production too, - // But the test is only interested in verifying the DEV error message. - if (__DEV__ && enableProfilerTimer) { - it('should warn if required params are missing', () => { - expect(() => { - ReactTestRenderer.create(); - }).toErrorDev( - 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', - { - withoutStack: true, - }, - ); - }); - } - - it('should support an empty Profiler (with no children)', () => { - // As root - expect( - ReactTestRenderer.create( - , - ).toJSON(), - ).toMatchSnapshot(); - - // As non-root - expect( - ReactTestRenderer.create( -
    - -
    , - ).toJSON(), - ).toMatchSnapshot(); - }); - - it('should render children', () => { - const FunctionComponent = ({label}) => {label}; - const renderer = ReactTestRenderer.create( -
    - outside span - - inside span - - -
    , - ); - expect(renderer.toJSON()).toMatchSnapshot(); - }); - - it('should support nested Profilers', () => { - const FunctionComponent = ({label}) =>
    {label}
    ; - class ClassComponent extends React.Component { - render() { - return {this.props.label}; - } - } - const renderer = ReactTestRenderer.create( - - - - - inner span - - , - ); - expect(renderer.toJSON()).toMatchSnapshot(); - }); - }); - }); - }); - }); - - [true, false].forEach(enableSchedulerTracing => { - describe(`onRender enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({ - enableSchedulerTracing, - }); - }); - - it('should handle errors thrown', () => { - const callback = jest.fn(id => { - if (id === 'throw') { - throw Error('expected'); - } + loadModules({enableProfilerTimer}); }); - let didMount = false; - class ClassComponent extends React.Component { - componentDidMount() { - didMount = true; - } - render() { - return this.props.children; - } - } - - // Errors thrown from onRender should not break the commit phase, - // Or prevent other lifecycles from being called. - expect(() => - ReactTestRenderer.create( - - - -
    - - - , - ), - ).toThrow('expected'); - expect(didMount).toBe(true); - expect(callback).toHaveBeenCalledTimes(2); - }); - - // @gate experimental || !enableSyncDefaultUpdates - it('is not invoked until the commit phase', () => { - const callback = jest.fn(); - - const Yield = ({value}) => { - Scheduler.unstable_yieldValue(value); - return null; - }; - - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - ReactTestRenderer.create( - - - - , + // This will throw in production too, + // But the test is only interested in verifying the DEV error message. + if (__DEV__ && enableProfilerTimer) { + it('should warn if required params are missing', () => { + expect(() => { + ReactTestRenderer.create(); + }).toErrorDev( + 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', { - unstable_isConcurrent: true, + withoutStack: true, }, ); }); - } else { - ReactTestRenderer.create( - - - - , - { - unstable_isConcurrent: true, - }, - ); } - // Times are logged until a render is committed. - expect(Scheduler).toFlushAndYieldThrough(['first']); - expect(callback).toHaveBeenCalledTimes(0); - expect(Scheduler).toFlushAndYield(['last']); - expect(callback).toHaveBeenCalledTimes(1); - }); - - it('does not record times for components outside of Profiler tree', () => { - // Mock the Scheduler module so we can track how many times the current - // time is read - jest.mock('scheduler', obj => { - const ActualScheduler = require.requireActual( - 'scheduler/unstable_mock', - ); - return { - ...ActualScheduler, - unstable_now: function mockUnstableNow() { - ActualScheduler.unstable_yieldValue('read current time'); - return ActualScheduler.unstable_now(); - }, - }; - }); - - jest.resetModules(); - - loadModules({enableSchedulerTracing}); - - // Clear yields in case the current time is read during initialization. - Scheduler.unstable_clearYields(); - - ReactTestRenderer.create( -
    - - - - - -
    , - ); - - // TODO: unstable_now is called by more places than just the profiler. - // Rewrite this test so it's less fragile. - expect(Scheduler).toHaveYielded([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); - - // Restore original mock - jest.mock('scheduler', () => - require.requireActual('scheduler/unstable_mock'), - ); - }); - - it('does not report work done on a sibling', () => { - const callback = jest.fn(); - - const DoesNotUpdate = React.memo( - function DoesNotUpdateInner() { - Scheduler.unstable_advanceTime(10); - return null; - }, - () => true, - ); - - let updateProfilerSibling; - - function ProfilerSibling() { - const [count, setCount] = React.useState(0); - updateProfilerSibling = () => setCount(count + 1); - return null; - } - - function App() { - return ( - - - - - - - ); - } - - const renderer = ReactTestRenderer.create(); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(10); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(0); // start time - expect(call[5]).toBe(10); // commit time - expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - callback.mockReset(); - - Scheduler.unstable_advanceTime(20); // 10 -> 30 - - renderer.update(); - - if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) { - // None of the Profiler's subtree was rendered because App bailed out before the Profiler. - // So we expect onRender not to be called. - expect(callback).not.toHaveBeenCalled(); - } else { - // Updating a parent reports a re-render, - // since React technically did a little bit of work between the Profiler and the bailed out subtree. - // This is not optimal but it's how the old reconciler fork works. - expect(callback).toHaveBeenCalledTimes(1); - - call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(0); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(30); // start time - expect(call[5]).toBe(30); // commit time - expect(call[6]).toEqual( - enableSchedulerTracing ? new Set() : undefined, - ); // interaction events - - callback.mockReset(); - } - - Scheduler.unstable_advanceTime(20); // 30 -> 50 - - // Updating a sibling should not report a re-render. - ReactTestRendererAct(updateProfilerSibling); - - expect(callback).not.toHaveBeenCalled(); - }); - - it('logs render times for both mount and update', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - let [call] = callback.mock.calls; - - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(10); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(15); // commit time - expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - callback.mockReset(); - - Scheduler.unstable_advanceTime(20); // 15 -> 35 - - renderer.update( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - [call] = callback.mock.calls; - - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(10); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(35); // start time - expect(call[5]).toBe(45); // commit time - expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - callback.mockReset(); - - Scheduler.unstable_advanceTime(20); // 45 -> 65 - - renderer.update( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - [call] = callback.mock.calls; - - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(4); // actual time - expect(call[3]).toBe(4); // base time - expect(call[4]).toBe(65); // start time - expect(call[5]).toBe(69); // commit time - expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('includes render times of nested Profilers in their parent times', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - // Callbacks bubble (reverse order). - const [childCall, parentCall] = callback.mock.calls; - expect(childCall[0]).toBe('child'); - expect(parentCall[0]).toBe('parent'); - - // Parent times should include child times - expect(childCall[2]).toBe(20); // actual time - expect(childCall[3]).toBe(20); // base time - expect(childCall[4]).toBe(15); // start time - expect(childCall[5]).toBe(35); // commit time - expect(parentCall[2]).toBe(30); // actual time - expect(parentCall[3]).toBe(30); // base time - expect(parentCall[4]).toBe(5); // start time - expect(parentCall[5]).toBe(35); // commit time - }); - - it('traces sibling Profilers separately', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - const [firstCall, secondCall] = callback.mock.calls; - expect(firstCall[0]).toBe('first'); - expect(secondCall[0]).toBe('second'); - - // Parent times should include child times - expect(firstCall[2]).toBe(20); // actual time - expect(firstCall[3]).toBe(20); // base time - expect(firstCall[4]).toBe(5); // start time - expect(firstCall[5]).toBe(30); // commit time - expect(secondCall[2]).toBe(5); // actual time - expect(secondCall[3]).toBe(5); // base time - expect(secondCall[4]).toBe(25); // start time - expect(secondCall[5]).toBe(30); // commit time - }); - - it('does not include time spent outside of profile root', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - const [call] = callback.mock.calls; - expect(call[0]).toBe('test'); - expect(call[2]).toBe(5); // actual time - expect(call[3]).toBe(5); // base time - expect(call[4]).toBe(25); // start time - expect(call[5]).toBe(50); // commit time - }); - - it('is not called when blocked by sCU false', () => { - const callback = jest.fn(); - - let instance; - class Updater extends React.Component { - state = {}; - render() { - instance = this; - return this.props.children; - } - } - - const renderer = ReactTestRenderer.create( - - - -
    - - - , - ); - - // All profile callbacks are called for initial render - expect(callback).toHaveBeenCalledTimes(2); - - callback.mockReset(); - - renderer.unstable_flushSync(() => { - instance.setState({ - count: 1, - }); - }); - - // Only call onRender for paths that have re-rendered. - // Since the Updater's props didn't change, - // React does not re-render its children. - expect(callback).toHaveBeenCalledTimes(1); - expect(callback.mock.calls[0][0]).toBe('outer'); - }); - - it('decreases actual time but not base time when sCU prevents an update', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - Scheduler.unstable_advanceTime(30); // 28 -> 58 - - renderer.update( - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - const [mountCall, updateCall] = callback.mock.calls; - - expect(mountCall[1]).toBe('mount'); - expect(mountCall[2]).toBe(23); // actual time - expect(mountCall[3]).toBe(23); // base time - expect(mountCall[4]).toBe(5); // start time - expect(mountCall[5]).toBe(28); // commit time - - expect(updateCall[1]).toBe('update'); - expect(updateCall[2]).toBe(4); // actual time - expect(updateCall[3]).toBe(17); // base time - expect(updateCall[4]).toBe(58); // start time - expect(updateCall[5]).toBe(62); // commit time - }); - - it('includes time spent in render phase lifecycles', () => { - class WithLifecycles extends React.Component { - state = {}; - static getDerivedStateFromProps() { - Scheduler.unstable_advanceTime(3); - return null; - } - shouldComponentUpdate() { - Scheduler.unstable_advanceTime(7); - return true; - } - render() { - Scheduler.unstable_advanceTime(5); - return null; - } - } - - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - , - ); - - Scheduler.unstable_advanceTime(15); // 13 -> 28 - - renderer.update( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - const [mountCall, updateCall] = callback.mock.calls; - - expect(mountCall[1]).toBe('mount'); - expect(mountCall[2]).toBe(8); // actual time - expect(mountCall[3]).toBe(8); // base time - expect(mountCall[4]).toBe(5); // start time - expect(mountCall[5]).toBe(13); // commit time - - expect(updateCall[1]).toBe('update'); - expect(updateCall[2]).toBe(15); // actual time - expect(updateCall[3]).toBe(15); // base time - expect(updateCall[4]).toBe(28); // start time - expect(updateCall[5]).toBe(43); // commit time - }); - - it('should clear nested-update flag when multiple cascading renders are scheduled', () => { - loadModules({ - enableSchedulerTracing, - useNoopRenderer: true, - }); - - function Component() { - const [didMount, setDidMount] = React.useState(false); - const [didMountAndUpdate, setDidMountAndUpdate] = React.useState( - false, - ); - - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - - React.useEffect(() => { - if (didMount && !didMountAndUpdate) { - setDidMountAndUpdate(true); - } - }, [didMount, didMountAndUpdate]); - - Scheduler.unstable_yieldValue(`${didMount}:${didMountAndUpdate}`); - - return null; - } - - const onRender = jest.fn(); - - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - expect(Scheduler).toHaveYielded([ - 'false:false', - 'true:false', - 'true:true', - ]); - - expect(onRender).toHaveBeenCalledTimes(3); - expect(onRender.mock.calls[0][1]).toBe('mount'); - expect(onRender.mock.calls[1][1]).toBe('nested-update'); - expect(onRender.mock.calls[2][1]).toBe('update'); - }); - - // @gate experimental - it('is properly distinguish updates and nested-updates when there is more than sync remaining work', () => { - loadModules({ - enableSchedulerTracing, - useNoopRenderer: true, - }); - - function Component() { - const [didMount, setDidMount] = React.useState(false); - - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - Scheduler.unstable_yieldValue(didMount); - return didMount; - } - - const onRender = jest.fn(); - - // Schedule low-priority work. - React.unstable_startTransition(() => - ReactNoop.render( - - - , - ), - ); - - // Flush sync work with a nested update - ReactNoop.flushSync(() => { - ReactNoop.render( - - - , - ); - }); - expect(Scheduler).toHaveYielded([false, true]); - - // Verify that the nested update inside of the sync work is appropriately tagged. - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender.mock.calls[0][1]).toBe('mount'); - expect(onRender.mock.calls[1][1]).toBe('nested-update'); - }); - - describe('with regard to interruptions', () => { - // @gate experimental || !enableSyncDefaultUpdates - it('should accumulate actual time after a scheduling interruptions', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - // Render partially, but run out of time before completing. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - }); - } else { + it('should support an empty Profiler (with no children)', () => { + // As root + expect( ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:2']); - expect(callback).toHaveBeenCalledTimes(0); + , + ).toJSON(), + ).toMatchSnapshot(); - // Resume render for remaining children. - expect(Scheduler).toFlushAndYield(['Yield:3']); - - // Verify that logged times include both durations above. - expect(callback).toHaveBeenCalledTimes(1); - const [call] = callback.mock.calls; - expect(call[2]).toBe(5); // actual time - expect(call[3]).toBe(5); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(10); // commit time - }); - - // @gate experimental || !enableSyncDefaultUpdates - it('should not include time between frames', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - // Render partially, but don't finish. - // This partial render should take 5ms of simulated time. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - ReactTestRenderer.create( - - - - - - - , - {unstable_isConcurrent: true}, - ); - }); - } else { + // As non-root + expect( ReactTestRenderer.create( - - - - - - - , - {unstable_isConcurrent: true}, - ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(50); // 10 -> 60 - - // Flush the remaining work, - // Which should take an additional 10ms of simulated time. - expect(Scheduler).toFlushAndYield(['Yield:10', 'Yield:17']); - expect(callback).toHaveBeenCalledTimes(2); - - const [innerCall, outerCall] = callback.mock.calls; - - // Verify that the actual time includes all work times, - // But not the time that elapsed between frames. - expect(innerCall[0]).toBe('inner'); - expect(innerCall[2]).toBe(17); // actual time - expect(innerCall[3]).toBe(17); // base time - expect(innerCall[4]).toBe(70); // start time - expect(innerCall[5]).toBe(87); // commit time - expect(outerCall[0]).toBe('outer'); - expect(outerCall[2]).toBe(32); // actual time - expect(outerCall[3]).toBe(32); // base time - expect(outerCall[4]).toBe(5); // start time - expect(outerCall[5]).toBe(87); // commit time +
    + +
    , + ).toJSON(), + ).toMatchSnapshot(); }); - // @gate experimental || !enableSyncDefaultUpdates - it('should report the expected times when a high-pri update replaces a mount in-progress', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - // Render a partially update, but don't finish. - // This partial render should take 10ms of simulated time. - let renderer; - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - }); - } else { - renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:10']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 15 -> 115 - - // Interrupt with higher priority work. - // The interrupted work simulates an additional 5ms of time. - renderer.unstable_flushSync(() => { - renderer.update( - - - , - ); - }); - expect(Scheduler).toHaveYielded(['Yield:5']); - - // The initial work was thrown away in this case, - // So the actual and base times should only include the final rendered tree times. - expect(callback).toHaveBeenCalledTimes(1); - const call = callback.mock.calls[0]; - expect(call[2]).toBe(5); // actual time - expect(call[3]).toBe(5); // base time - expect(call[4]).toBe(115); // start time - expect(call[5]).toBe(120); // commit time - - callback.mockReset(); - - // Verify no more unexpected callbacks from low priority work - expect(Scheduler).toFlushWithoutYielding(); - expect(callback).toHaveBeenCalledTimes(0); - }); - - // @gate experimental || !enableSyncDefaultUpdates - it('should report the expected times when a high-priority update replaces a low-priority update', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - + it('should render children', () => { + const FunctionComponent = ({label}) => {label}; const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, +
    + outside span + + inside span + + +
    , ); - - // Render everything initially. - // This should take 21 seconds of actual and base time. - expect(Scheduler).toFlushAndYield(['Yield:6', 'Yield:15']); - expect(callback).toHaveBeenCalledTimes(1); - let call = callback.mock.calls[0]; - expect(call[2]).toBe(21); // actual time - expect(call[3]).toBe(21); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(26); // commit time - - callback.mockReset(); - - Scheduler.unstable_advanceTime(30); // 26 -> 56 - - // Render a partially update, but don't finish. - // This partial render should take 3ms of simulated time. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - renderer.update( - - - - - , - ); - }); - } else { - renderer.update( - - - - - , - ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:3']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 59 -> 159 - - // Render another 5ms of simulated time. - expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 164 -> 264 - - // Interrupt with higher priority work. - // The interrupted work simulates an additional 11ms of time. - renderer.unstable_flushSync(() => { - renderer.update( - - - , - ); - }); - expect(Scheduler).toHaveYielded(['Yield:11']); - - // The actual time should include only the most recent render, - // Because this lets us avoid a lot of commit phase reset complexity. - // The base time includes only the final rendered tree times. - expect(callback).toHaveBeenCalledTimes(1); - call = callback.mock.calls[0]; - expect(call[2]).toBe(11); // actual time - expect(call[3]).toBe(11); // base time - expect(call[4]).toBe(264); // start time - expect(call[5]).toBe(275); // commit time - - // Verify no more unexpected callbacks from low priority work - expect(Scheduler).toFlushAndYield([]); - expect(callback).toHaveBeenCalledTimes(1); + expect(renderer.toJSON()).toMatchSnapshot(); }); - // @gate experimental || !enableSyncDefaultUpdates - it('should report the expected times when a high-priority update interrupts a low-priority update', () => { - const callback = jest.fn(); - - const Yield = ({renderTime}) => { - Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); - return null; - }; - - let first; - class FirstComponent extends React.Component { - state = {renderTime: 1}; + it('should support nested Profilers', () => { + const FunctionComponent = ({label}) =>
    {label}
    ; + class ClassComponent extends React.Component { render() { - first = this; - Scheduler.unstable_advanceTime(this.state.renderTime); - Scheduler.unstable_yieldValue( - 'FirstComponent:' + this.state.renderTime, - ); - return ; + return {this.props.label}; } } - let second; - class SecondComponent extends React.Component { - state = {renderTime: 2}; - render() { - second = this; - Scheduler.unstable_advanceTime(this.state.renderTime); - Scheduler.unstable_yieldValue( - 'SecondComponent:' + this.state.renderTime, - ); - return ; - } - } - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - - // Render everything initially. - // This simulates a total of 14ms of actual render time. - // The base render time is also 14ms for the initial render. - expect(Scheduler).toFlushAndYield([ - 'FirstComponent:1', - 'Yield:4', - 'SecondComponent:2', - 'Yield:7', - ]); - expect(callback).toHaveBeenCalledTimes(1); - let call = callback.mock.calls[0]; - expect(call[2]).toBe(14); // actual time - expect(call[3]).toBe(14); // base time - expect(call[4]).toBe(5); // start time - expect(call[5]).toBe(19); // commit time - - callback.mockClear(); - - Scheduler.unstable_advanceTime(100); // 19 -> 119 - - // Render a partially update, but don't finish. - // This partial render will take 10ms of actual render time. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - first.setState({renderTime: 10}); - }); - } else { - first.setState({renderTime: 10}); - } - expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']); - expect(callback).toHaveBeenCalledTimes(0); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 129 -> 229 - - // Interrupt with higher priority work. - // This simulates a total of 37ms of actual render time. - renderer.unstable_flushSync(() => second.setState({renderTime: 30})); - expect(Scheduler).toHaveYielded(['SecondComponent:30', 'Yield:7']); - - // The actual time should include only the most recent render (37ms), - // Because this greatly simplifies the commit phase logic. - // The base time should include the more recent times for the SecondComponent subtree, - // As well as the original times for the FirstComponent subtree. - expect(callback).toHaveBeenCalledTimes(1); - call = callback.mock.calls[0]; - expect(call[2]).toBe(37); // actual time - expect(call[3]).toBe(42); // base time - expect(call[4]).toBe(229); // start time - expect(call[5]).toBe(266); // commit time - - callback.mockClear(); - - // Simulate time moving forward while frame is paused. - Scheduler.unstable_advanceTime(100); // 266 -> 366 - - // Resume the original low priority update, with rebased state. - // This simulates a total of 14ms of actual render time, - // And does not include the original (interrupted) 10ms. - // The tree contains 42ms of base render time at this point, - // Reflecting the most recent (longer) render durations. - // TODO: This actual time should decrease by 10ms once the scheduler supports resuming. - expect(Scheduler).toFlushAndYield(['FirstComponent:10', 'Yield:4']); - expect(callback).toHaveBeenCalledTimes(1); - call = callback.mock.calls[0]; - expect(call[2]).toBe(14); // actual time - expect(call[3]).toBe(51); // base time - expect(call[4]).toBe(366); // start time - expect(call[5]).toBe(380); // commit time - }); - - [true, false].forEach( - replayFailedUnitOfWorkWithInvokeGuardedCallback => { - describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${ - replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 'enabled' - : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({ - replayFailedUnitOfWorkWithInvokeGuardedCallback, - }); - }); - - it('should accumulate actual time after an error handled by componentDidCatch()', () => { - const callback = jest.fn(); - - const ThrowsError = ({unused}) => { - Scheduler.unstable_advanceTime(3); - throw Error('expected error'); - }; - - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - Scheduler.unstable_advanceTime(2); - return this.state.error === null ? ( - this.props.children - ) : ( - - ); - } - } - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - // Callbacks bubble (reverse order). - const [mountCall, updateCall] = callback.mock.calls; - - // The initial mount only includes the ErrorBoundary (which takes 2) - // But it spends time rendering all of the failed subtree also. - expect(mountCall[1]).toBe('mount'); - // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError) - // We don't count the time spent in replaying the failed unit of work (ThrowsError) - expect(mountCall[2]).toBe(14); - // base time includes: 2 (ErrorBoundary) - // Since the tree is empty for the initial commit - expect(mountCall[3]).toBe(2); - // start time - expect(mountCall[4]).toBe(5); - // commit time: 5 initially + 14 of work - // Add an additional 3 (ThrowsError) if we replayed the failed work - expect(mountCall[5]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 22 - : 19, - ); - - // The update includes the ErrorBoundary and its fallback child - expect(updateCall[1]).toBe('nested-update'); - // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) - expect(updateCall[2]).toBe(22); - // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) - expect(updateCall[3]).toBe(22); - // start time - expect(updateCall[4]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 22 - : 19, - ); - // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime) - // Add an additional 3 (ThrowsError) if we replayed the failed work - expect(updateCall[5]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 44 - : 41, - ); - }); - - it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => { - const callback = jest.fn(); - - const ThrowsError = ({unused}) => { - Scheduler.unstable_advanceTime(10); - throw Error('expected error'); - }; - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - Scheduler.unstable_advanceTime(2); - return this.state.error === null ? ( - this.props.children - ) : ( - - ); - } - } - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - ReactTestRenderer.create( - - - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - // Callbacks bubble (reverse order). - const [mountCall] = callback.mock.calls; - - // The initial mount includes the ErrorBoundary's error state, - // But it also spends actual time rendering UI that fails and isn't included. - expect(mountCall[1]).toBe('mount'); - // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError) - // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime) - // We don't count the time spent in replaying the failed unit of work (ThrowsError) - expect(mountCall[2]).toBe(39); - // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) - expect(mountCall[3]).toBe(22); - // start time - expect(mountCall[4]).toBe(5); - // commit time - expect(mountCall[5]).toBe( - __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback - ? 54 - : 44, - ); - }); - - it('should reset the fiber stack correct after a "complete" phase error', () => { - jest.resetModules(); - - loadModules({ - useNoopRenderer: true, - replayFailedUnitOfWorkWithInvokeGuardedCallback, - }); - - // Simulate a renderer error during the "complete" phase. - // This mimics behavior like React Native's View/Text nesting validation. - ReactNoop.render( - - hi - , - ); - expect(Scheduler).toFlushAndThrow('Error in host config.'); - - // A similar case we've seen caused by an invariant in ReactDOM. - // It didn't reproduce without a host component inside. - ReactNoop.render( - - - hi - - , - ); - expect(Scheduler).toFlushAndThrow('Error in host config.'); - - // So long as the profiler timer's fiber stack is reset correctly, - // Subsequent renders should not error. - ReactNoop.render( - - hi - , - ); - expect(Scheduler).toFlushWithoutYielding(); - }); - }); - }, - ); - }); - - it('reflects the most recently rendered id value', () => { - const callback = jest.fn(); - - Scheduler.unstable_advanceTime(5); // 0 -> 5 - - const renderer = ReactTestRenderer.create( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - Scheduler.unstable_advanceTime(20); // 7 -> 27 - - renderer.update( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - const [mountCall, updateCall] = callback.mock.calls; - - expect(mountCall[0]).toBe('one'); - expect(mountCall[1]).toBe('mount'); - expect(mountCall[2]).toBe(2); // actual time - expect(mountCall[3]).toBe(2); // base time - expect(mountCall[4]).toBe(5); // start time - - expect(updateCall[0]).toBe('two'); - expect(updateCall[1]).toBe('update'); - expect(updateCall[2]).toBe(1); // actual time - expect(updateCall[3]).toBe(1); // base time - expect(updateCall[4]).toBe(27); // start time - }); - - it('should not be called until after mutations', () => { - let classComponentMounted = false; - const callback = jest.fn( - (id, phase, actualDuration, baseDuration, startTime, commitTime) => { - // Don't call this hook until after mutations - expect(classComponentMounted).toBe(true); - // But the commit time should reflect pre-mutation - expect(commitTime).toBe(2); - }, - ); - - class ClassComponent extends React.Component { - componentDidMount() { - Scheduler.unstable_advanceTime(5); - classComponentMounted = true; - } - render() { - Scheduler.unstable_advanceTime(2); - return null; - } - } - - ReactTestRenderer.create( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - }); - }); - - describe(`onCommit enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({ - enableSchedulerTracing, - }); - }); - - it('should report time spent in layout effects and commit lifecycles', () => { - const callback = jest.fn(); - - const ComponentWithEffects = () => { - React.useLayoutEffect(() => { - Scheduler.unstable_advanceTime(10); - return () => { - Scheduler.unstable_advanceTime(100); - }; - }, []); - React.useLayoutEffect(() => { - Scheduler.unstable_advanceTime(1000); - return () => { - Scheduler.unstable_advanceTime(10000); - }; - }); - React.useEffect(() => { - // This passive effect is here to verify that its time isn't reported. - Scheduler.unstable_advanceTime(5); - return () => { - Scheduler.unstable_advanceTime(7); - }; - }); - return null; - }; - - class ComponentWithCommitHooks extends React.Component { - componentDidMount() { - Scheduler.unstable_advanceTime(100000); - } - componentDidUpdate() { - Scheduler.unstable_advanceTime(1000000); - } - render() { - return null; - } - } - - Scheduler.unstable_advanceTime(1); - - const renderer = ReactTestRenderer.create( - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(101010); // durations - expect(call[3]).toBe(1); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - Scheduler.unstable_advanceTime(1); - - renderer.update( - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('update-test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(1011000); // durations - expect(call[3]).toBe(101017); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - Scheduler.unstable_advanceTime(1); - - renderer.update( - , - ); - - expect(callback).toHaveBeenCalledTimes(3); - - call = callback.mock.calls[2]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('unmount-test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(10100); // durations - expect(call[3]).toBe(1112030); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should report time spent in layout effects and commit lifecycles with cascading renders', () => { - const callback = jest.fn(); - - const ComponentWithEffects = ({shouldCascade}) => { - const [didCascade, setDidCascade] = React.useState(false); - Scheduler.unstable_advanceTime(100000000); - React.useLayoutEffect(() => { - if (shouldCascade && !didCascade) { - setDidCascade(true); - } - Scheduler.unstable_advanceTime(didCascade ? 30 : 10); - return () => { - Scheduler.unstable_advanceTime(100); - }; - }, [didCascade, shouldCascade]); - return null; - }; - - class ComponentWithCommitHooks extends React.Component { - state = { - didCascade: false, - }; - componentDidMount() { - Scheduler.unstable_advanceTime(1000); - } - componentDidUpdate() { - Scheduler.unstable_advanceTime(10000); - if (this.props.shouldCascade && !this.state.didCascade) { - this.setState({didCascade: true}); - } - } - render() { - Scheduler.unstable_advanceTime(1000000000); - return null; - } - } - - Scheduler.unstable_advanceTime(1); - - const renderer = ReactTestRenderer.create( - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(2); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(1010); // durations - expect(call[3]).toBe(1100000001); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); - expect(call[1]).toBe('nested-update'); - expect(call[2]).toBe(130); // durations - expect(call[3]).toBe(1200001011); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - Scheduler.unstable_advanceTime(1); - - renderer.update( - - - - , - ); - - expect(callback).toHaveBeenCalledTimes(4); - - call = callback.mock.calls[2]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('update-test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(10130); // durations - expect(call[3]).toBe(2300001142); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - call = callback.mock.calls[3]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('update-test'); - expect(call[1]).toBe('nested-update'); - expect(call[2]).toBe(10000); // durations - expect(call[3]).toBe(3300011272); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should include time spent in ref callbacks', () => { - const callback = jest.fn(); - - const refSetter = ref => { - if (ref !== null) { - Scheduler.unstable_advanceTime(10); - } else { - Scheduler.unstable_advanceTime(100); - } - }; - - class ClassComponent extends React.Component { - render() { - return null; - } - } - - const Component = () => { - Scheduler.unstable_advanceTime(1000); - return ; - }; - - Scheduler.unstable_advanceTime(1); - - const renderer = ReactTestRenderer.create( - - - , - ); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(10); // durations - expect(call[3]).toBe(1001); // commit start time (before mutations or effects) - - callback.mockClear(); - - renderer.update(); - - expect(callback).toHaveBeenCalledTimes(1); - - call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(100); // durations - expect(call[3]).toBe(1011); // commit start time (before mutations or effects) - }); - - it('should bubble time spent in layout effects to higher profilers', () => { - const callback = jest.fn(); - - const ComponentWithEffects = ({ - cleanupDuration, - duration, - setCountRef, - }) => { - const setCount = React.useState(0)[1]; - if (setCountRef != null) { - setCountRef.current = setCount; - } - React.useLayoutEffect(() => { - Scheduler.unstable_advanceTime(duration); - return () => { - Scheduler.unstable_advanceTime(cleanupDuration); - }; - }); - Scheduler.unstable_advanceTime(1); - return null; - }; - - const setCountRef = React.createRef(null); - - let renderer = null; - ReactTestRendererAct(() => { - renderer = ReactTestRenderer.create( - - - - - - + + + + + inner span , ); - }); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root-mount'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(1010); // durations - expect(call[3]).toBe(2); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - ReactTestRendererAct(() => setCountRef.current(count => count + 1)); - - expect(callback).toHaveBeenCalledTimes(2); - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root-mount'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(110); // durations - expect(call[3]).toBe(1013); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - ReactTestRendererAct(() => { - renderer.update( - - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(3); - - call = callback.mock.calls[2]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root-update'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(1100); // durations - expect(call[3]).toBe(1124); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should properly report time in layout effects even when there are errors', () => { - const callback = jest.fn(); - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - return this.state.error === null - ? this.props.children - : this.props.fallback; - } - } - - const ComponentWithEffects = ({ - cleanupDuration, - duration, - effectDuration, - shouldThrow, - }) => { - React.useLayoutEffect(() => { - Scheduler.unstable_advanceTime(effectDuration); - if (shouldThrow) { - throw Error('expected'); - } - return () => { - Scheduler.unstable_advanceTime(cleanupDuration); - }; - }); - Scheduler.unstable_advanceTime(duration); - return null; - }; - - Scheduler.unstable_advanceTime(1); - - // Test an error that happens during an effect - - ReactTestRendererAct(() => { - ReactTestRenderer.create( - - - }> - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(2); - - let call = callback.mock.calls[0]; - - // Initial render (with error) - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(100100); // durations - expect(call[3]).toBe(10011); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - call = callback.mock.calls[1]; - - // Cleanup render from error boundary - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('nested-update'); - expect(call[2]).toBe(100000000); // durations - expect(call[3]).toBe(10110111); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should properly report time in layout effect cleanup functions even when there are errors', () => { - const callback = jest.fn(); - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - return this.state.error === null - ? this.props.children - : this.props.fallback; - } - } - - const ComponentWithEffects = ({ - cleanupDuration, - duration, - effectDuration, - shouldThrow = false, - }) => { - React.useLayoutEffect(() => { - Scheduler.unstable_advanceTime(effectDuration); - return () => { - Scheduler.unstable_advanceTime(cleanupDuration); - if (shouldThrow) { - throw Error('expected'); - } - }; - }); - Scheduler.unstable_advanceTime(duration); - return null; - }; - - Scheduler.unstable_advanceTime(1); - - let renderer = null; - - ReactTestRendererAct(() => { - renderer = ReactTestRenderer.create( - - - }> - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - // Initial render - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(100100); // durations - expect(call[3]).toBe(10011); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - callback.mockClear(); - - // Test an error that happens during an cleanup function - - ReactTestRendererAct(() => { - renderer.update( - - - }> - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(2); - - call = callback.mock.calls[0]; - - // Update (that throws) - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(1101100); // durations - expect(call[3]).toBe(120121); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - call = callback.mock.calls[1]; - - // Cleanup render from error boundary - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('nested-update'); - expect(call[2]).toBe(100001000); // durations - expect(call[3]).toBe(11221221); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - if (enableSchedulerTracing) { - it('should report interactions that were active', () => { - const callback = jest.fn(); - - const ComponentWithEffects = () => { - const [didMount, setDidMount] = React.useState(false); - React.useLayoutEffect(() => { - Scheduler.unstable_advanceTime(didMount ? 1000 : 100); - if (!didMount) { - setDidMount(true); - } - return () => { - Scheduler.unstable_advanceTime(10000); - }; - }, [didMount]); - Scheduler.unstable_advanceTime(10); - return null; - }; - - const interaction = { - id: 0, - name: 'mount', - timestamp: Scheduler.unstable_now(), - }; - - Scheduler.unstable_advanceTime(1); - - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - - , - ); - }, - ); - - expect(callback).toHaveBeenCalledTimes(2); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[4]).toMatchInteractions([interaction]); - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('nested-update'); - expect(call[4]).toMatchInteractions([interaction]); - }); - } - }); - - describe(`onPostCommit enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({ - enableSchedulerTracing, + expect(renderer.toJSON()).toMatchSnapshot(); }); }); - - it('should report time spent in passive effects', () => { - const callback = jest.fn(); - - const ComponentWithEffects = () => { - React.useLayoutEffect(() => { - // This layout effect is here to verify that its time isn't reported. - Scheduler.unstable_advanceTime(5); - return () => { - Scheduler.unstable_advanceTime(7); - }; - }); - React.useEffect(() => { - Scheduler.unstable_advanceTime(10); - return () => { - Scheduler.unstable_advanceTime(100); - }; - }, []); - React.useEffect(() => { - Scheduler.unstable_advanceTime(1000); - return () => { - Scheduler.unstable_advanceTime(10000); - }; - }); - return null; - }; - - Scheduler.unstable_advanceTime(1); - - let renderer; - ReactTestRendererAct(() => { - renderer = ReactTestRenderer.create( - - - , - ); - }); - Scheduler.unstable_flushAll(); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(1010); // durations - expect(call[3]).toBe(1); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - Scheduler.unstable_advanceTime(1); - - ReactTestRendererAct(() => { - renderer.update( - - - , - ); - }); - Scheduler.unstable_flushAll(); - - expect(callback).toHaveBeenCalledTimes(2); - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('update-test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(11000); // durations - expect(call[3]).toBe(1017); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - Scheduler.unstable_advanceTime(1); - - ReactTestRendererAct(() => { - renderer.update( - , - ); - }); - Scheduler.unstable_flushAll(); - - expect(callback).toHaveBeenCalledTimes(3); - - call = callback.mock.calls[2]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('unmount-test'); - expect(call[1]).toBe('update'); - // TODO (bvaughn) The duration reported below should be 10100, but is 0 - // by the time the passive effect is flushed its parent Fiber pointer is gone. - // If we refactor to preserve the unmounted Fiber tree we could fix this. - // The current implementation would require too much extra overhead to track this. - expect(call[2]).toBe(0); // durations - expect(call[3]).toBe(12030); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should report time spent in passive effects with cascading renders', () => { - const callback = jest.fn(); - - const ComponentWithEffects = () => { - const [didMount, setDidMount] = React.useState(false); - Scheduler.unstable_advanceTime(1000); - React.useEffect(() => { - if (!didMount) { - setDidMount(true); - } - Scheduler.unstable_advanceTime(didMount ? 30 : 10); - return () => { - Scheduler.unstable_advanceTime(100); - }; - }, [didMount]); - return null; - }; - - Scheduler.unstable_advanceTime(1); - - ReactTestRendererAct(() => { - ReactTestRenderer.create( - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(2); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(10); // durations - expect(call[3]).toBe(1001); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('mount-test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(130); // durations - expect(call[3]).toBe(2011); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should bubble time spent in effects to higher profilers', () => { - const callback = jest.fn(); - - const ComponentWithEffects = ({ - cleanupDuration, - duration, - setCountRef, - }) => { - const setCount = React.useState(0)[1]; - if (setCountRef != null) { - setCountRef.current = setCount; - } - React.useEffect(() => { - Scheduler.unstable_advanceTime(duration); - return () => { - Scheduler.unstable_advanceTime(cleanupDuration); - }; - }); - Scheduler.unstable_advanceTime(1); - return null; - }; - - const setCountRef = React.createRef(null); - - let renderer = null; - ReactTestRendererAct(() => { - renderer = ReactTestRenderer.create( - - - - - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root-mount'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(1010); // durations - expect(call[3]).toBe(2); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - ReactTestRendererAct(() => setCountRef.current(count => count + 1)); - - expect(callback).toHaveBeenCalledTimes(2); - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root-mount'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(110); // durations - expect(call[3]).toBe(1013); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - ReactTestRendererAct(() => { - renderer.update( - - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(3); - - call = callback.mock.calls[2]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root-update'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(1100); // durations - expect(call[3]).toBe(1124); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should properly report time in passive effects even when there are errors', () => { - const callback = jest.fn(); - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - return this.state.error === null - ? this.props.children - : this.props.fallback; - } - } - - const ComponentWithEffects = ({ - cleanupDuration, - duration, - effectDuration, - shouldThrow, - }) => { - React.useEffect(() => { - Scheduler.unstable_advanceTime(effectDuration); - if (shouldThrow) { - throw Error('expected'); - } - return () => { - Scheduler.unstable_advanceTime(cleanupDuration); - }; - }); - Scheduler.unstable_advanceTime(duration); - return null; - }; - - Scheduler.unstable_advanceTime(1); - - // Test an error that happens during an effect - - ReactTestRendererAct(() => { - ReactTestRenderer.create( - - - }> - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(2); - - let call = callback.mock.calls[0]; - - // Initial render (with error) - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(100100); // durations - expect(call[3]).toBe(10011); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - call = callback.mock.calls[1]; - - // Cleanup render from error boundary - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(100000000); // durations - expect(call[3]).toBe(10110111); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - it('should properly report time in passive effect cleanup functions even when there are errors', () => { - const callback = jest.fn(); - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - return this.state.error === null - ? this.props.children - : this.props.fallback; - } - } - - const ComponentWithEffects = ({ - cleanupDuration, - duration, - effectDuration, - shouldThrow = false, - id, - }) => { - React.useEffect(() => { - Scheduler.unstable_advanceTime(effectDuration); - return () => { - Scheduler.unstable_advanceTime(cleanupDuration); - if (shouldThrow) { - throw Error('expected'); - } - }; - }); - Scheduler.unstable_advanceTime(duration); - return null; - }; - - Scheduler.unstable_advanceTime(1); - - let renderer = null; - - ReactTestRendererAct(() => { - renderer = ReactTestRenderer.create( - - - }> - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(1); - - let call = callback.mock.calls[0]; - - // Initial render - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[2]).toBe(100100); // durations - expect(call[3]).toBe(10011); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - callback.mockClear(); - - // Test an error that happens during an cleanup function - - ReactTestRendererAct(() => { - renderer.update( - - - }> - - - - , - ); - }); - - expect(callback).toHaveBeenCalledTimes(2); - - call = callback.mock.calls[0]; - - // Update (that throws) - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('update'); - // We continue flushing pending effects even if one throws. - expect(call[2]).toBe(1101100); // durations - expect(call[3]).toBe(120121); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - - call = callback.mock.calls[1]; - - // Cleanup render from error boundary - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(100000000); // durations - // The commit time varies because the above duration time varies - expect(call[3]).toBe(11221221); // commit start time (before mutations or effects) - expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events - }); - - if (enableSchedulerTracing) { - it('should report interactions that were active', () => { - const callback = jest.fn(); - - const ComponentWithEffects = () => { - const [didMount, setDidMount] = React.useState(false); - React.useEffect(() => { - Scheduler.unstable_advanceTime(didMount ? 1000 : 100); - if (!didMount) { - setDidMount(true); - } - return () => { - Scheduler.unstable_advanceTime(10000); - }; - }, [didMount]); - Scheduler.unstable_advanceTime(10); - return null; - }; - - const interaction = { - id: 0, - name: 'mount', - timestamp: Scheduler.unstable_now(), - }; - - Scheduler.unstable_advanceTime(1); - - ReactTestRendererAct(() => { - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - - , - ); - }, - ); - }); - - expect(callback).toHaveBeenCalledTimes(2); - - let call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('mount'); - expect(call[4]).toMatchInteractions([interaction]); - - call = callback.mock.calls[1]; - - expect(call).toHaveLength(enableSchedulerTracing ? 5 : 4); - expect(call[0]).toBe('root'); - expect(call[1]).toBe('update'); - expect(call[4]).toMatchInteractions([interaction]); - }); - } - }); - - describe(`onNestedUpdateScheduled enableSchedulerTracing:${ - enableSchedulerTracing ? 'enabled' : 'disabled' - }`, () => { - beforeEach(() => { - jest.resetModules(); - - loadModules({ - enableProfilerNestedUpdateScheduledHook: true, - enableSchedulerTracing, - useNoopRenderer: true, - }); - }); - - it('is not called when the legacy render API is used to schedule an update', () => { - const onNestedUpdateScheduled = jest.fn(); - - ReactNoop.renderLegacySyncRoot( - -
    initial
    -
    , - ); - - ReactNoop.renderLegacySyncRoot( - -
    update
    -
    , - ); - - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - }); - - it('is not called when the root API is used to schedule an update', () => { - const onNestedUpdateScheduled = jest.fn(); - - ReactNoop.render( - -
    initial
    -
    , - ); - - ReactNoop.render( - -
    update
    -
    , - ); - - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - }); - - it('is called when a function component schedules an update during a layout effect', () => { - function Component() { - const [didMount, setDidMount] = React.useState(false); - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); - return didMount; - } - - const interactionCreation = { - id: 0, - name: 'creation event', - timestamp: Scheduler.unstable_now(), - }; - - const onNestedUpdateScheduled = jest.fn(); - - SchedulerTracing.unstable_trace( - interactionCreation.name, - Scheduler.unstable_now(), - () => { - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - }, - ); - - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); - expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); - expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(onNestedUpdateScheduled.mock.calls[0][1]).toMatchInteractions([ - interactionCreation, - ]); - } - }); - - it('is called when a function component schedules a batched update during a layout effect', () => { - function Component() { - const [didMount, setDidMount] = React.useState(false); - React.useLayoutEffect(() => { - ReactNoop.batchedUpdates(() => { - setDidMount(true); - }); - }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); - return didMount; - } - - const onNestedUpdateScheduled = jest.fn(); - const onRender = jest.fn(); - - ReactNoop.render( - - - , - ); - expect(Scheduler).toFlushAndYield([ - 'Component:false', - 'Component:true', - ]); - - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender.mock.calls[0][1]).toBe('mount'); - expect(onRender.mock.calls[1][1]).toBe('nested-update'); - - expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); - expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('root'); - }); - - it('bubbles up and calls all ancestor Profilers', () => { - function Component() { - const [didMount, setDidMount] = React.useState(false); - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); - return didMount; - } - const onNestedUpdateScheduledOne = jest.fn(); - const onNestedUpdateScheduledTwo = jest.fn(); - const onNestedUpdateScheduledThree = jest.fn(); - - ReactNoop.act(() => { - ReactNoop.render( - - - <> - - - - - , - ); - }); - - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); - expect(onNestedUpdateScheduledOne).toHaveBeenCalledTimes(1); - expect(onNestedUpdateScheduledOne.mock.calls[0][0]).toBe('one'); - expect(onNestedUpdateScheduledTwo).toHaveBeenCalledTimes(1); - expect(onNestedUpdateScheduledTwo.mock.calls[0][0]).toBe('two'); - expect(onNestedUpdateScheduledThree).not.toHaveBeenCalled(); - }); - - it('is not called when an update is scheduled for another doort during a layout effect', () => { - const setStateRef = React.createRef(null); - - function ComponentRootOne() { - const [state, setState] = React.useState(false); - setStateRef.current = setState; - Scheduler.unstable_yieldValue(`ComponentRootOne:${state}`); - return state; - } - - function ComponentRootTwo() { - React.useLayoutEffect(() => { - setStateRef.current(true); - }, []); - Scheduler.unstable_yieldValue('ComponentRootTwo'); - return null; - } - - const interactionCreation = { - id: 0, - name: 'creation event', - timestamp: Scheduler.unstable_now(), - }; - - const onNestedUpdateScheduled = jest.fn(); - - SchedulerTracing.unstable_trace( - interactionCreation.name, - Scheduler.unstable_now(), - () => { - ReactNoop.act(() => { - ReactNoop.renderToRootWithID( - - - , - 1, - ); - - ReactNoop.renderToRootWithID( - - - , - 2, - ); - }); - }, - ); - - expect(Scheduler).toHaveYielded([ - 'ComponentRootOne:false', - 'ComponentRootTwo', - 'ComponentRootOne:true', - ]); - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - }); - - it('is not called when a function component schedules an update during a passive effect', () => { - function Component() { - const [didMount, setDidMount] = React.useState(false); - React.useEffect(() => { - setDidMount(true); - }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); - return didMount; - } - - const onNestedUpdateScheduled = jest.fn(); - - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - }); - - it('is not called when a function component schedules an update outside of render', () => { - const updateFnRef = React.createRef(null); - - function Component() { - const [state, setState] = React.useState(false); - updateFnRef.current = () => setState(true); - Scheduler.unstable_yieldValue(`Component:${state}`); - return state; - } - - const onNestedUpdateScheduled = jest.fn(); - - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - expect(Scheduler).toHaveYielded(['Component:false']); - - ReactNoop.act(() => { - updateFnRef.current(); - }); - expect(Scheduler).toHaveYielded(['Component:true']); - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - }); - - it('it is not called when a component schedules an update during render', () => { - function Component() { - const [state, setState] = React.useState(false); - if (state === false) { - setState(true); - } - Scheduler.unstable_yieldValue(`Component:${state}`); - return state; - } - - const onNestedUpdateScheduled = jest.fn(); - - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - }); - - it('it is called when a component schedules an update from a ref callback', () => { - function Component({mountChild}) { - const [refAttached, setRefAttached] = React.useState(false); - const [refDetached, setRefDetached] = React.useState(false); - const refSetter = React.useCallback(ref => { - if (ref !== null) { - setRefAttached(true); - } else { - setRefDetached(true); - } - }, []); - Scheduler.unstable_yieldValue( - `Component:${refAttached}:${refDetached}`, - ); - return mountChild ?
    : null; - } - - const onNestedUpdateScheduled = jest.fn(); - - const interactionCreation = { - id: 0, - name: 'creation event', - timestamp: Scheduler.unstable_now(), - }; - - SchedulerTracing.unstable_trace( - interactionCreation.name, - Scheduler.unstable_now(), - () => { - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - }, - ); - - expect(Scheduler).toHaveYielded([ - 'Component:false:false', - 'Component:true:false', - ]); - expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); - expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(onNestedUpdateScheduled.mock.calls[0][1]).toMatchInteractions([ - interactionCreation, - ]); - } - - const interactionUpdate = { - id: 1, - name: 'update event', - timestamp: Scheduler.unstable_now(), - }; - - SchedulerTracing.unstable_trace( - interactionUpdate.name, - Scheduler.unstable_now(), - () => { - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - }, - ); - - expect(Scheduler).toHaveYielded([ - 'Component:true:false', - 'Component:true:true', - ]); - expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(2); - expect(onNestedUpdateScheduled.mock.calls[1][0]).toBe('test'); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(onNestedUpdateScheduled.mock.calls[1][1]).toMatchInteractions([ - interactionUpdate, - ]); - } - }); - - it('is called when a class component schedules an update from the componentDidMount lifecycles', () => { - class Component extends React.Component { - state = { - value: false, - }; - componentDidMount() { - this.setState({value: true}); - } - render() { - const {value} = this.state; - Scheduler.unstable_yieldValue(`Component:${value}`); - return value; - } - } - - const interactionCreation = { - id: 0, - name: 'creation event', - timestamp: Scheduler.unstable_now(), - }; - - const onNestedUpdateScheduled = jest.fn(); - - SchedulerTracing.unstable_trace( - interactionCreation.name, - Scheduler.unstable_now(), - () => { - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - }, - ); - - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); - expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); - expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(onNestedUpdateScheduled.mock.calls[0][1]).toMatchInteractions([ - interactionCreation, - ]); - } - }); - - it('is called when a class component schedules an update from the componentDidUpdate lifecycles', () => { - class Component extends React.Component { - state = { - nestedUpdateSheduled: false, - }; - componentDidUpdate(prevProps, prevState) { - if ( - this.props.scheduleNestedUpdate && - !this.state.nestedUpdateSheduled - ) { - this.setState({nestedUpdateSheduled: true}); - } - } - render() { - const {scheduleNestedUpdate} = this.props; - const {nestedUpdateSheduled} = this.state; - Scheduler.unstable_yieldValue( - `Component:${scheduleNestedUpdate}:${nestedUpdateSheduled}`, - ); - return nestedUpdateSheduled; - } - } - - const onNestedUpdateScheduled = jest.fn(); - - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - expect(Scheduler).toHaveYielded(['Component:false:false']); - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - - const interactionCreation = { - id: 0, - name: 'creation event', - timestamp: Scheduler.unstable_now(), - }; - - SchedulerTracing.unstable_trace( - interactionCreation.name, - Scheduler.unstable_now(), - () => { - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - }, - ); - - expect(Scheduler).toHaveYielded([ - 'Component:true:false', - 'Component:true:true', - ]); - expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); - expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(onNestedUpdateScheduled.mock.calls[0][1]).toMatchInteractions([ - interactionCreation, - ]); - } - }); - - it('is not called when a class component schedules an update outside of render', () => { - const updateFnRef = React.createRef(null); - - class Component extends React.Component { - state = { - value: false, - }; - render() { - const {value} = this.state; - updateFnRef.current = () => this.setState({value: true}); - Scheduler.unstable_yieldValue(`Component:${value}`); - return value; - } - } - - const onNestedUpdateScheduled = jest.fn(); - - ReactNoop.act(() => { - ReactNoop.render( - - - , - ); - }); - expect(Scheduler).toHaveYielded(['Component:false']); - - ReactNoop.act(() => { - updateFnRef.current(); - }); - expect(Scheduler).toHaveYielded(['Component:true']); - expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - }); - - // TODO Add hydration tests to ensure we don't have false positives called. }); }); +}); - describe('interaction tracing', () => { - let onInteractionScheduledWorkCompleted; - let onInteractionTraced; - let onWorkCanceled; - let onWorkScheduled; - let onWorkStarted; - let onWorkStopped; - let throwInOnInteractionScheduledWorkCompleted; - let throwInOnWorkScheduled; - let throwInOnWorkStarted; - let throwInOnWorkStopped; +describe(`onRender`, () => { + beforeEach(() => { + jest.resetModules(); - const getWorkForReactThreads = mockFn => - mockFn.mock.calls.filter(([interactions, threadID]) => threadID > 0); + loadModules(); + }); - function loadModulesForTracing(params) { - jest.resetModules(); + it('should handle errors thrown', () => { + const callback = jest.fn(id => { + if (id === 'throw') { + throw Error('expected'); + } + }); - loadModules({ - enableSchedulerTracing: true, - ...params, - }); - - throwInOnInteractionScheduledWorkCompleted = false; - throwInOnWorkScheduled = false; - throwInOnWorkStarted = false; - throwInOnWorkStopped = false; - - onInteractionScheduledWorkCompleted = jest.fn(() => { - if (throwInOnInteractionScheduledWorkCompleted) { - throw Error('Expected error onInteractionScheduledWorkCompleted'); - } - }); - onInteractionTraced = jest.fn(); - onWorkCanceled = jest.fn(); - onWorkScheduled = jest.fn(() => { - if (throwInOnWorkScheduled) { - throw Error('Expected error onWorkScheduled'); - } - }); - onWorkStarted = jest.fn(() => { - if (throwInOnWorkStarted) { - throw Error('Expected error onWorkStarted'); - } - }); - onWorkStopped = jest.fn(() => { - if (throwInOnWorkStopped) { - throw Error('Expected error onWorkStopped'); - } - }); - - // Verify interaction subscriber methods are called as expected. - SchedulerTracing.unstable_subscribe({ - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled, - onWorkScheduled, - onWorkStarted, - onWorkStopped, - }); + let didMount = false; + class ClassComponent extends React.Component { + componentDidMount() { + didMount = true; + } + render() { + return this.props.children; + } } - beforeEach(() => loadModulesForTracing()); + // Errors thrown from onRender should not break the commit phase, + // Or prevent other lifecycles from being called. + expect(() => + ReactTestRenderer.create( + + + +
    + + + , + ), + ).toThrow('expected'); + expect(didMount).toBe(true); + expect(callback).toHaveBeenCalledTimes(2); + }); - describe('error handling', () => { - it('should cover errors thrown in onWorkScheduled', () => { - function Component({children}) { - Scheduler.unstable_yieldValue('Component:' + children); - return children; - } + // @gate experimental || !enableSyncDefaultUpdates + it('is not invoked until the commit phase', () => { + const callback = jest.fn(); - let renderer; + const Yield = ({value}) => { + Scheduler.unstable_yieldValue(value); + return null; + }; - // Errors that happen inside of a subscriber should throw, - throwInOnWorkScheduled = true; - expect(() => { - SchedulerTracing.unstable_trace( - 'event', - Scheduler.unstable_now(), - () => { - renderer = ReactTestRenderer.create(fail, { - unstable_isConcurrent: true, - }); - }, - ); - }).toThrow('Expected error onWorkScheduled'); - expect(Scheduler).toFlushAndYield(['Component:fail']); - throwInOnWorkScheduled = false; - expect(onWorkScheduled).toHaveBeenCalled(); - - // But should not leave React in a broken state for subsequent renders. - renderer = ReactTestRenderer.create(succeed, { + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.unstable_startTransition(() => { + ReactTestRenderer.create( + + + + , + { + unstable_isConcurrent: true, + }, + ); + }); + } else { + ReactTestRenderer.create( + + + + , + { unstable_isConcurrent: true, - }); - expect(Scheduler).toFlushAndYield(['Component:succeed']); - const tree = renderer.toTree(); - expect(tree.type).toBe(Component); - expect(tree.props.children).toBe('succeed'); - }); + }, + ); + } - it('should cover errors thrown in onWorkStarted', () => { - function Component({children}) { - Scheduler.unstable_yieldValue('Component:' + children); - return children; - } + // Times are logged until a render is committed. + expect(Scheduler).toFlushAndYieldThrough(['first']); + expect(callback).toHaveBeenCalledTimes(0); + expect(Scheduler).toFlushAndYield(['last']); + expect(callback).toHaveBeenCalledTimes(1); + }); - let renderer; - SchedulerTracing.unstable_trace( - 'event', - Scheduler.unstable_now(), - () => { - renderer = ReactTestRenderer.create(text, { - unstable_isConcurrent: true, - }); - }, - ); - onWorkStarted.mockClear(); + it('does not record times for components outside of Profiler tree', () => { + // Mock the Scheduler module so we can track how many times the current + // time is read + jest.mock('scheduler', obj => { + const ActualScheduler = require.requireActual('scheduler/unstable_mock'); + return { + ...ActualScheduler, + unstable_now: function mockUnstableNow() { + ActualScheduler.unstable_yieldValue('read current time'); + return ActualScheduler.unstable_now(); + }, + }; + }); - // Errors that happen inside of a subscriber should throw, - throwInOnWorkStarted = true; - expect(Scheduler).toFlushAndThrow('Expected error onWorkStarted'); - throwInOnWorkStarted = false; - // Rendering was interrupted by the error that was thrown, then - // continued and finished in the next task. - expect(Scheduler).toHaveYielded(['Component:text']); - expect(onWorkStarted).toHaveBeenCalled(); - const tree = renderer.toTree(); - expect(tree.type).toBe(Component); - expect(tree.props.children).toBe('text'); - }); + jest.resetModules(); - it('should cover errors thrown in onWorkStopped', () => { - function Component({children}) { - Scheduler.unstable_yieldValue('Component:' + children); - return children; - } + loadModules(); - let renderer; - SchedulerTracing.unstable_trace( - 'event', - Scheduler.unstable_now(), - () => { - renderer = ReactTestRenderer.create(text, { - unstable_isConcurrent: true, - }); - }, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + // Clear yields in case the current time is read during initialization. + Scheduler.unstable_clearYields(); - // Errors that happen in an on-stopped callback, - throwInOnWorkStopped = true; - expect(() => { - expect(Scheduler).toFlushAndYield(['Component:text']); - }).toThrow('Expected error onWorkStopped'); - throwInOnWorkStopped = false; - expect(onWorkStopped).toHaveBeenCalledTimes(2); + ReactTestRenderer.create( +
    + + + + + +
    , + ); - // Should still commit the update, - const tree = renderer.toTree(); - expect(tree.type).toBe(Component); - expect(tree.props.children).toBe('text'); + // TODO: unstable_now is called by more places than just the profiler. + // Rewrite this test so it's less fragile. + expect(Scheduler).toHaveYielded([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); - // And still call onInteractionScheduledWorkCompleted if the interaction is finished. - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - }); + // Restore original mock + jest.mock('scheduler', () => + require.requireActual('scheduler/unstable_mock'), + ); + }); - it('should cover errors thrown in onInteractionScheduledWorkCompleted', () => { - function Component({children}) { - Scheduler.unstable_yieldValue('Component:' + children); - return children; - } + it('does not report work done on a sibling', () => { + const callback = jest.fn(); - const eventOne = { - id: 0, - name: 'event one', - timestamp: Scheduler.unstable_now(), - }; - const eventTwo = { - id: 1, - name: 'event two', - timestamp: Scheduler.unstable_now(), - }; + const DoesNotUpdate = React.memo( + function DoesNotUpdateInner() { + Scheduler.unstable_advanceTime(10); + return null; + }, + () => true, + ); - SchedulerTracing.unstable_trace( - eventOne.name, - Scheduler.unstable_now(), - () => { - SchedulerTracing.unstable_trace( - eventTwo.name, - Scheduler.unstable_now(), - () => { - ReactTestRenderer.create(text, { - unstable_isConcurrent: true, - }); - }, - ); - }, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + let updateProfilerSibling; - throwInOnInteractionScheduledWorkCompleted = true; - expect(() => { - expect(Scheduler).toFlushAndYield(['Component:text']); - }).toThrow('Expected error onInteractionScheduledWorkCompleted'); + function ProfilerSibling() { + const [count, setCount] = React.useState(0); + updateProfilerSibling = () => setCount(count + 1); + return null; + } - // Even though an error is thrown for one completed interaction, - // The completed callback should be called for all completed interactions. - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); + function App() { + return ( + + + + + + + ); + } + + const renderer = ReactTestRenderer.create(); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(6); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(10); // actual time + expect(call[3]).toBe(10); // base time + expect(call[4]).toBe(0); // start time + expect(call[5]).toBe(10); // commit time + + callback.mockReset(); + + Scheduler.unstable_advanceTime(20); // 10 -> 30 + + renderer.update(); + + if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) { + // None of the Profiler's subtree was rendered because App bailed out before the Profiler. + // So we expect onRender not to be called. + expect(callback).not.toHaveBeenCalled(); + } else { + // Updating a parent reports a re-render, + // since React technically did a little bit of work between the Profiler and the bailed out subtree. + // This is not optimal but it's how the old reconciler fork works. + expect(callback).toHaveBeenCalledTimes(1); + + call = callback.mock.calls[0]; + + expect(call).toHaveLength(6); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(0); // actual time + expect(call[3]).toBe(10); // base time + expect(call[4]).toBe(30); // start time + expect(call[5]).toBe(30); // commit time + + callback.mockReset(); + } + + Scheduler.unstable_advanceTime(20); // 30 -> 50 + + // Updating a sibling should not report a re-render. + ReactTestRendererAct(updateProfilerSibling); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('logs render times for both mount and update', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + let [call] = callback.mock.calls; + + expect(call).toHaveLength(6); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(10); // actual time + expect(call[3]).toBe(10); // base time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(15); // commit time + + callback.mockReset(); + + Scheduler.unstable_advanceTime(20); // 15 -> 35 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + [call] = callback.mock.calls; + + expect(call).toHaveLength(6); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(10); // actual time + expect(call[3]).toBe(10); // base time + expect(call[4]).toBe(35); // start time + expect(call[5]).toBe(45); // commit time + + callback.mockReset(); + + Scheduler.unstable_advanceTime(20); // 45 -> 65 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + [call] = callback.mock.calls; + + expect(call).toHaveLength(6); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(4); // actual time + expect(call[3]).toBe(4); // base time + expect(call[4]).toBe(65); // start time + expect(call[5]).toBe(69); // commit time + }); + + it('includes render times of nested Profilers in their parent times', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + ReactTestRenderer.create( + + + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + // Callbacks bubble (reverse order). + const [childCall, parentCall] = callback.mock.calls; + expect(childCall[0]).toBe('child'); + expect(parentCall[0]).toBe('parent'); + + // Parent times should include child times + expect(childCall[2]).toBe(20); // actual time + expect(childCall[3]).toBe(20); // base time + expect(childCall[4]).toBe(15); // start time + expect(childCall[5]).toBe(35); // commit time + expect(parentCall[2]).toBe(30); // actual time + expect(parentCall[3]).toBe(30); // base time + expect(parentCall[4]).toBe(5); // start time + expect(parentCall[5]).toBe(35); // commit time + }); + + it('traces sibling Profilers separately', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + ReactTestRenderer.create( + + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [firstCall, secondCall] = callback.mock.calls; + expect(firstCall[0]).toBe('first'); + expect(secondCall[0]).toBe('second'); + + // Parent times should include child times + expect(firstCall[2]).toBe(20); // actual time + expect(firstCall[3]).toBe(20); // base time + expect(firstCall[4]).toBe(5); // start time + expect(firstCall[5]).toBe(30); // commit time + expect(secondCall[2]).toBe(5); // actual time + expect(secondCall[3]).toBe(5); // base time + expect(secondCall[4]).toBe(25); // start time + expect(secondCall[5]).toBe(30); // commit time + }); + + it('does not include time spent outside of profile root', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + ReactTestRenderer.create( + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + const [call] = callback.mock.calls; + expect(call[0]).toBe('test'); + expect(call[2]).toBe(5); // actual time + expect(call[3]).toBe(5); // base time + expect(call[4]).toBe(25); // start time + expect(call[5]).toBe(50); // commit time + }); + + it('is not called when blocked by sCU false', () => { + const callback = jest.fn(); + + let instance; + class Updater extends React.Component { + state = {}; + render() { + instance = this; + return this.props.children; + } + } + + const renderer = ReactTestRenderer.create( + + + +
    + + + , + ); + + // All profile callbacks are called for initial render + expect(callback).toHaveBeenCalledTimes(2); + + callback.mockReset(); + + renderer.unstable_flushSync(() => { + instance.setState({ + count: 1, }); }); - it('should properly trace work scheduled during the begin render phase', () => { - const callback = jest.fn(); - let wrapped; - const Component = jest.fn(() => { - wrapped = SchedulerTracing.unstable_wrap(callback); + // Only call onRender for paths that have re-rendered. + // Since the Updater's props didn't change, + // React does not re-render its children. + expect(callback).toHaveBeenCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('outer'); + }); + + it('decreases actual time but not base time when sCU prevents an update', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + Scheduler.unstable_advanceTime(30); // 28 -> 58 + + renderer.update( + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(23); // actual time + expect(mountCall[3]).toBe(23); // base time + expect(mountCall[4]).toBe(5); // start time + expect(mountCall[5]).toBe(28); // commit time + + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(4); // actual time + expect(updateCall[3]).toBe(17); // base time + expect(updateCall[4]).toBe(58); // start time + expect(updateCall[5]).toBe(62); // commit time + }); + + it('includes time spent in render phase lifecycles', () => { + class WithLifecycles extends React.Component { + state = {}; + static getDerivedStateFromProps() { + Scheduler.unstable_advanceTime(3); return null; - }); + } + shouldComponentUpdate() { + Scheduler.unstable_advanceTime(7); + return true; + } + render() { + Scheduler.unstable_advanceTime(5); + return null; + } + } - let interaction; - SchedulerTracing.unstable_trace('event', Scheduler.unstable_now(), () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - interaction = Array.from(interactions)[0]; - ReactTestRenderer.create(); - }); + const callback = jest.fn(); - expect(Component).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(callback).not.toHaveBeenCalled(); + Scheduler.unstable_advanceTime(5); // 0 -> 5 - wrapped(); + const renderer = ReactTestRenderer.create( + + + , + ); + + Scheduler.unstable_advanceTime(15); // 13 -> 28 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(8); // actual time + expect(mountCall[3]).toBe(8); // base time + expect(mountCall[4]).toBe(5); // start time + expect(mountCall[5]).toBe(13); // commit time + + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(15); // actual time + expect(updateCall[3]).toBe(15); // base time + expect(updateCall[4]).toBe(28); // start time + expect(updateCall[5]).toBe(43); // commit time + }); + + it('should clear nested-update flag when multiple cascading renders are scheduled', () => { + loadModules({ + useNoopRenderer: true, + }); + + function Component() { + const [didMount, setDidMount] = React.useState(false); + const [didMountAndUpdate, setDidMountAndUpdate] = React.useState(false); + + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + + React.useEffect(() => { + if (didMount && !didMountAndUpdate) { + setDidMountAndUpdate(true); + } + }, [didMount, didMountAndUpdate]); + + Scheduler.unstable_yieldValue(`${didMount}:${didMountAndUpdate}`); + + return null; + } + + const onRender = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + expect(Scheduler).toHaveYielded(['false:false', 'true:false', 'true:true']); + + expect(onRender).toHaveBeenCalledTimes(3); + expect(onRender.mock.calls[0][1]).toBe('mount'); + expect(onRender.mock.calls[1][1]).toBe('nested-update'); + expect(onRender.mock.calls[2][1]).toBe('update'); + }); + + // @gate experimental + it('is properly distinguish updates and nested-updates when there is more than sync remaining work', () => { + loadModules({ + useNoopRenderer: true, + }); + + function Component() { + const [didMount, setDidMount] = React.useState(false); + + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + Scheduler.unstable_yieldValue(didMount); + return didMount; + } + + const onRender = jest.fn(); + + // Schedule low-priority work. + React.unstable_startTransition(() => + ReactNoop.render( + + + , + ), + ); + + // Flush sync work with a nested update + ReactNoop.flushSync(() => { + ReactNoop.render( + + + , + ); + }); + expect(Scheduler).toHaveYielded([false, true]); + + // Verify that the nested update inside of the sync work is appropriately tagged. + expect(onRender).toHaveBeenCalledTimes(2); + expect(onRender.mock.calls[0][1]).toBe('mount'); + expect(onRender.mock.calls[1][1]).toBe('nested-update'); + }); + + describe('with regard to interruptions', () => { + // @gate experimental || !enableSyncDefaultUpdates + it('should accumulate actual time after a scheduling interruptions', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); + return null; + }; + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + // Render partially, but run out of time before completing. + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.unstable_startTransition(() => { + ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + }); + } else { + ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + } + expect(Scheduler).toFlushAndYieldThrough(['Yield:2']); + expect(callback).toHaveBeenCalledTimes(0); + + // Resume render for remaining children. + expect(Scheduler).toFlushAndYield(['Yield:3']); + + // Verify that logged times include both durations above. expect(callback).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); + const [call] = callback.mock.calls; + expect(call[2]).toBe(5); // actual time + expect(call[3]).toBe(5); // base time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(10); // commit time }); // @gate experimental || !enableSyncDefaultUpdates - it('should associate traced events with their subsequent commits', () => { - let instance = null; + it('should not include time between frames', () => { + const callback = jest.fn(); - const Yield = ({duration = 10, value}) => { - Scheduler.unstable_advanceTime(duration); - Scheduler.unstable_yieldValue(value); + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); return null; }; - class Example extends React.Component { - state = { - count: 0, - }; - render() { - instance = this; - return ( - - - {this.state.count} - - - + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + // Render partially, but don't finish. + // This partial render should take 5ms of simulated time. + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.unstable_startTransition(() => { + ReactTestRenderer.create( + + + + + + + , + {unstable_isConcurrent: true}, ); - } - } - - ReactTestRendererAct(() => { - Scheduler.unstable_advanceTime(1); - - const interactionCreation = { - id: 0, - name: 'creation event', - timestamp: Scheduler.unstable_now(), - }; - - const onPostCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onPostCommit'); }); - let renderer; - SchedulerTracing.unstable_trace( - interactionCreation.name, - Scheduler.unstable_now(), - () => { - renderer = ReactTestRenderer.create( - - - , - { - unstable_isConcurrent: true, - }, - ); - }, + } else { + ReactTestRenderer.create( + + + + + + + , + {unstable_isConcurrent: true}, ); + } + expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); + expect(callback).toHaveBeenCalledTimes(0); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interactionCreation, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(50); // 10 -> 60 - // The scheduler/tracing package will notify of work started for the default thread, - // But React shouldn't notify until it's been flushed. - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); + // Flush the remaining work, + // Which should take an additional 10ms of simulated time. + expect(Scheduler).toFlushAndYield(['Yield:10', 'Yield:17']); + expect(callback).toHaveBeenCalledTimes(2); - // Work may have been scheduled multiple times. - // We only care that the subscriber was notified at least once. - // As for the thread ID- the actual value isn't important, only that there was one. - expect(onWorkScheduled).toHaveBeenCalled(); - expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([ - interactionCreation, - ]); - expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true); + const [innerCall, outerCall] = callback.mock.calls; - // Mount - expect(Scheduler).toFlushAndYield(['first', 'last', 'onPostCommit']); - expect(onPostCommit).toHaveBeenCalledTimes(1); - let call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[4]).toMatchInteractions([interactionCreation]); - } - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionCreation); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStarted)[0][0], - ).toMatchInteractions([interactionCreation]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStopped)[0][0], - ).toMatchInteractions([interactionCreation]); - - onPostCommit.mockClear(); - onWorkScheduled.mockClear(); - onWorkStarted.mockClear(); - onWorkStopped.mockClear(); - - Scheduler.unstable_advanceTime(3); - - let didRunCallback = false; - - const interactionOne = { - id: 1, - name: 'initial event', - timestamp: Scheduler.unstable_now(), - }; - SchedulerTracing.unstable_trace( - interactionOne.name, - Scheduler.unstable_now(), - () => { - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - instance.setState({count: 1}); - - // Update state again to verify our traced interaction isn't registered twice - instance.setState({count: 2}); - }); - } else { - instance.setState({count: 1}); - - // Update state again to verify our traced interaction isn't registered twice - instance.setState({count: 2}); - } - - // The scheduler/tracing package will notify of work started for the default thread, - // But React shouldn't notify until it's been flushed. - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - // Work may have been scheduled multiple times. - // We only care that the subscriber was notified at least once. - // As for the thread ID- the actual value isn't important, only that there was one. - expect(onWorkScheduled).toHaveBeenCalled(); - expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([ - interactionOne, - ]); - expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true); - - expect(Scheduler).toFlushAndYieldThrough(['first']); - expect(onPostCommit).not.toHaveBeenCalled(); - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interactionOne, - ); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes( - 1, - ); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStarted)[0][0], - ).toMatchInteractions([interactionOne]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - expect(Scheduler).toFlushAndYield(['last', 'onPostCommit']); - expect(onPostCommit).toHaveBeenCalledTimes(1); - - call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[4]).toMatchInteractions([interactionOne]); - } - - didRunCallback = true; - - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStarted)[0][0], - ).toMatchInteractions([interactionOne]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStopped)[0][0], - ).toMatchInteractions([interactionOne]); - }, - ); - - expect(didRunCallback).toBe(true); - - onPostCommit.mockClear(); - onWorkScheduled.mockClear(); - onWorkStarted.mockClear(); - onWorkStopped.mockClear(); - - Scheduler.unstable_advanceTime(17); - - // Verify that updating state again does not re-log our interaction. - instance.setState({count: 3}); - expect(Scheduler).toFlushAndYield(['first', 'last', 'onPostCommit']); - - expect(onPostCommit).toHaveBeenCalledTimes(1); - call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[4]).toMatchInteractions([]); - } - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionOne); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - onPostCommit.mockClear(); - - Scheduler.unstable_advanceTime(3); - - // Verify that root updates are also associated with traced events. - const interactionTwo = { - id: 2, - name: 'root update event', - timestamp: Scheduler.unstable_now(), - }; - SchedulerTracing.unstable_trace( - interactionTwo.name, - Scheduler.unstable_now(), - () => { - renderer.update( - - - , - ); - }, - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(3); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interactionTwo, - ); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - - // The scheduler/tracing package will notify of work started for the default thread, - // But React shouldn't notify until it's been flushed. - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - // Work may have been scheduled multiple times. - // We only care that the subscriber was notified at least once. - // As for the thread ID- the actual value isn't important, only that there was one. - expect(onWorkScheduled).toHaveBeenCalled(); - expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([ - interactionTwo, - ]); - expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true); - - expect(Scheduler).toFlushAndYield(['first', 'last', 'onPostCommit']); - - expect(onPostCommit).toHaveBeenCalledTimes(1); - call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - if (ReactFeatureFlags.enableSchedulerTracing) { - expect(call[4]).toMatchInteractions([interactionTwo]); - } - - expect(onInteractionTraced).toHaveBeenCalledTimes(3); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionTwo); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStarted)[0][0], - ).toMatchInteractions([interactionTwo]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStopped)[0][0], - ).toMatchInteractions([interactionTwo]); - }); + // Verify that the actual time includes all work times, + // But not the time that elapsed between frames. + expect(innerCall[0]).toBe('inner'); + expect(innerCall[2]).toBe(17); // actual time + expect(innerCall[3]).toBe(17); // base time + expect(innerCall[4]).toBe(70); // start time + expect(innerCall[5]).toBe(87); // commit time + expect(outerCall[0]).toBe('outer'); + expect(outerCall[2]).toBe(32); // actual time + expect(outerCall[3]).toBe(32); // base time + expect(outerCall[4]).toBe(5); // start time + expect(outerCall[5]).toBe(87); // commit time }); - it('should not mark an interaction complete while passive effects are outstanding', () => { - const onCommit = jest.fn(); - const onPostCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onPostCommit'); - }); + // @gate experimental || !enableSyncDefaultUpdates + it('should report the expected times when a high-pri update replaces a mount in-progress', () => { + const callback = jest.fn(); - const ComponentWithEffects = () => { - React.useEffect(() => { - Scheduler.unstable_yieldValue('passive effect'); - }); - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('layout effect'); - }); - Scheduler.unstable_yieldValue('render'); + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); return null; }; - SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => { - ReactTestRenderer.create( - - + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + // Render a partially update, but don't finish. + // This partial render should take 10ms of simulated time. + let renderer; + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.unstable_startTransition(() => { + renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + }); + } else { + renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + } + expect(Scheduler).toFlushAndYieldThrough(['Yield:10']); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 15 -> 115 + + // Interrupt with higher priority work. + // The interrupted work simulates an additional 5ms of time. + renderer.unstable_flushSync(() => { + renderer.update( + + , ); }); + expect(Scheduler).toHaveYielded(['Yield:5']); - expect(Scheduler).toHaveYielded(['render', 'layout effect']); + // The initial work was thrown away in this case, + // So the actual and base times should only include the final rendered tree times. + expect(callback).toHaveBeenCalledTimes(1); + const call = callback.mock.calls[0]; + expect(call[2]).toBe(5); // actual time + expect(call[3]).toBe(5); // base time + expect(call[4]).toBe(115); // start time + expect(call[5]).toBe(120); // commit time - expect(onCommit).toHaveBeenCalled(); - expect(onPostCommit).not.toHaveBeenCalled(); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + callback.mockReset(); - expect(Scheduler).toFlushAndYield(['passive effect', 'onPostCommit']); + // Verify no more unexpected callbacks from low priority work + expect(Scheduler).toFlushWithoutYielding(); + expect(callback).toHaveBeenCalledTimes(0); + }); - expect(onCommit).toHaveBeenCalled(); - expect(onPostCommit).toHaveBeenCalled(); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + // @gate experimental || !enableSyncDefaultUpdates + it('should report the expected times when a high-priority update replaces a low-priority update', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); + return null; + }; + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); + + // Render everything initially. + // This should take 21 seconds of actual and base time. + expect(Scheduler).toFlushAndYield(['Yield:6', 'Yield:15']); + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(21); // actual time + expect(call[3]).toBe(21); // base time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(26); // commit time + + callback.mockReset(); + + Scheduler.unstable_advanceTime(30); // 26 -> 56 + + // Render a partially update, but don't finish. + // This partial render should take 3ms of simulated time. + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.unstable_startTransition(() => { + renderer.update( + + + + + , + ); + }); + } else { + renderer.update( + + + + + , + ); + } + expect(Scheduler).toFlushAndYieldThrough(['Yield:3']); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 59 -> 159 + + // Render another 5ms of simulated time. + expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 164 -> 264 + + // Interrupt with higher priority work. + // The interrupted work simulates an additional 11ms of time. + renderer.unstable_flushSync(() => { + renderer.update( + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Yield:11']); + + // The actual time should include only the most recent render, + // Because this lets us avoid a lot of commit phase reset complexity. + // The base time includes only the final rendered tree times. + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(11); // actual time + expect(call[3]).toBe(11); // base time + expect(call[4]).toBe(264); // start time + expect(call[5]).toBe(275); // commit time + + // Verify no more unexpected callbacks from low priority work + expect(Scheduler).toFlushAndYield([]); + expect(callback).toHaveBeenCalledTimes(1); }); // @gate experimental || !enableSyncDefaultUpdates it('should report the expected times when a high-priority update interrupts a low-priority update', () => { - const onPostCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onPostCommit'); - }); + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); + return null; + }; let first; class FirstComponent extends React.Component { - state = {count: 0}; + state = {renderTime: 1}; render() { first = this; - Scheduler.unstable_yieldValue('FirstComponent'); - return ; + Scheduler.unstable_advanceTime(this.state.renderTime); + Scheduler.unstable_yieldValue( + 'FirstComponent:' + this.state.renderTime, + ); + return ; } } let second; class SecondComponent extends React.Component { - state = {count: 0}; + state = {renderTime: 2}; render() { second = this; - Scheduler.unstable_yieldValue('SecondComponent'); - return ; + Scheduler.unstable_advanceTime(this.state.renderTime); + Scheduler.unstable_yieldValue( + 'SecondComponent:' + this.state.renderTime, + ); + return ; } } - Scheduler.unstable_advanceTime(5); + Scheduler.unstable_advanceTime(5); // 0 -> 5 - ReactTestRendererAct(() => { - const renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isConcurrent: true}, + ); - // Initial mount. - expect(Scheduler).toFlushAndYield([ - 'FirstComponent', - 'SecondComponent', - 'onPostCommit', - ]); + // Render everything initially. + // This simulates a total of 14ms of actual render time. + // The base render time is also 14ms for the initial render. + expect(Scheduler).toFlushAndYield([ + 'FirstComponent:1', + 'Yield:4', + 'SecondComponent:2', + 'Yield:7', + ]); + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(14); // actual time + expect(call[3]).toBe(14); // base time + expect(call[4]).toBe(5); // start time + expect(call[5]).toBe(19); // commit time - expect(onInteractionTraced).not.toHaveBeenCalled(); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + callback.mockClear(); - onPostCommit.mockClear(); + Scheduler.unstable_advanceTime(100); // 19 -> 119 - Scheduler.unstable_advanceTime(100); + // Render a partially update, but don't finish. + // This partial render will take 10ms of actual render time. + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.unstable_startTransition(() => { + first.setState({renderTime: 10}); + }); + } else { + first.setState({renderTime: 10}); + } + expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']); + expect(callback).toHaveBeenCalledTimes(0); - const interactionLowPri = { - id: 0, - name: 'lowPri', - timestamp: Scheduler.unstable_now(), - }; + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 129 -> 229 - SchedulerTracing.unstable_trace( - interactionLowPri.name, - Scheduler.unstable_now(), - () => { - // Render a partially update, but don't finish. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - first.setState({count: 1}); - }); - } else { - first.setState({count: 1}); - } + // Interrupt with higher priority work. + // This simulates a total of 37ms of actual render time. + renderer.unstable_flushSync(() => second.setState({renderTime: 30})); + expect(Scheduler).toHaveYielded(['SecondComponent:30', 'Yield:7']); - expect(onWorkScheduled).toHaveBeenCalled(); - expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([ - interactionLowPri, - ]); + // The actual time should include only the most recent render (37ms), + // Because this greatly simplifies the commit phase logic. + // The base time should include the more recent times for the SecondComponent subtree, + // As well as the original times for the FirstComponent subtree. + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(37); // actual time + expect(call[3]).toBe(42); // base time + expect(call[4]).toBe(229); // start time + expect(call[5]).toBe(266); // commit time - expect(Scheduler).toFlushAndYieldThrough(['FirstComponent']); - expect(onPostCommit).not.toHaveBeenCalled(); + callback.mockClear(); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interactionLowPri, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStarted)[0][0], - ).toMatchInteractions([interactionLowPri]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); + // Simulate time moving forward while frame is paused. + Scheduler.unstable_advanceTime(100); // 266 -> 366 - Scheduler.unstable_advanceTime(100); - - const interactionHighPri = { - id: 1, - name: 'highPri', - timestamp: Scheduler.unstable_now(), - }; - - // Interrupt with higher priority work. - // This simulates a total of 37ms of actual render time. - renderer.unstable_flushSync(() => { - SchedulerTracing.unstable_trace( - interactionHighPri.name, - Scheduler.unstable_now(), - () => { - second.setState({count: 1}); - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect( - onInteractionTraced, - ).toHaveBeenLastNotifiedOfInteraction(interactionHighPri); - expect( - onInteractionScheduledWorkCompleted, - ).not.toHaveBeenCalled(); - - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - }, - ); - }); - - expect(Scheduler).toHaveYielded([ - 'SecondComponent', - 'onPostCommit', - ]); - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes( - 1, - ); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionHighPri); - - // Verify the high priority update was associated with the high priority event. - expect(onPostCommit).toHaveBeenCalledTimes(1); - let call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing - ? [interactionLowPri, interactionHighPri] - : [], - ); - - onPostCommit.mockClear(); - - Scheduler.unstable_advanceTime(100); - - // Resume the original low priority update, with rebased state. - // Verify the low priority update was retained. - expect(Scheduler).toFlushAndYield([ - 'FirstComponent', - 'onPostCommit', - ]); - expect(onPostCommit).toHaveBeenCalledTimes(1); - call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing - ? [interactionLowPri] - : [], - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes( - 1, - ); - - // Work might be started multiple times before being completed. - // This is okay; it's part of the scheduler/tracing contract. - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(3); - expect( - getWorkForReactThreads(onWorkStarted)[1][0], - ).toMatchInteractions([interactionLowPri, interactionHighPri]); - expect( - getWorkForReactThreads(onWorkStarted)[2][0], - ).toMatchInteractions([interactionLowPri]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2); - expect( - getWorkForReactThreads(onWorkStopped)[0][0], - ).toMatchInteractions([interactionLowPri, interactionHighPri]); - expect( - getWorkForReactThreads(onWorkStopped)[1][0], - ).toMatchInteractions([interactionLowPri]); - }, - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionLowPri); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(3); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2); - }); + // Resume the original low priority update, with rebased state. + // This simulates a total of 14ms of actual render time, + // And does not include the original (interrupted) 10ms. + // The tree contains 42ms of base render time at this point, + // Reflecting the most recent (longer) render durations. + // TODO: This actual time should decrease by 10ms once the scheduler supports resuming. + expect(Scheduler).toFlushAndYield(['FirstComponent:10', 'Yield:4']); + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(14); // actual time + expect(call[3]).toBe(51); // base time + expect(call[4]).toBe(366); // start time + expect(call[5]).toBe(380); // commit time }); - it('should trace work spawned by a commit phase lifecycle and setState callback', () => { - let instance; - class Example extends React.Component { - state = { - count: 0, - }; - componentDidMount() { - Scheduler.unstable_advanceTime(10); // Advance timer to keep commits separate - this.setState({count: 1}); // Intentional cascading update - } - componentDidUpdate(prevProps, prevState) { - if (this.state.count === 2 && prevState.count === 1) { - Scheduler.unstable_advanceTime(10); // Advance timer to keep commits separate - this.setState({count: 3}); // Intentional cascading update - } - } - render() { - instance = this; - Scheduler.unstable_yieldValue('Example:' + this.state.count); - return ; - } - } + [true, false].forEach(replayFailedUnitOfWorkWithInvokeGuardedCallback => { + describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${ + replayFailedUnitOfWorkWithInvokeGuardedCallback ? 'enabled' : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); - const interactionOne = { - id: 0, - name: 'componentDidMount test', - timestamp: Scheduler.unstable_now(), - }; - - // Initial mount. - const onPostCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onPostCommit'); - }); - - ReactTestRendererAct(() => { - let firstCommitTime = Scheduler.unstable_now(); - SchedulerTracing.unstable_trace( - interactionOne.name, - Scheduler.unstable_now(), - () => { - ReactTestRenderer.create( - - - , - {unstable_isConcurrent: true}, - ); - }, - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interactionOne, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - expect(Scheduler).toFlushAndYield([ - 'Example:0', - 'onPostCommit', - 'Example:1', - 'onPostCommit', - ]); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionOne); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(2); - expect( - getWorkForReactThreads(onWorkStarted)[0][0], - ).toMatchInteractions([interactionOne]); - expect( - getWorkForReactThreads(onWorkStarted)[1][0], - ).toMatchInteractions([interactionOne]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2); - expect( - getWorkForReactThreads(onWorkStopped)[0][0], - ).toMatchInteractions([interactionOne]); - expect( - getWorkForReactThreads(onWorkStopped)[1][0], - ).toMatchInteractions([interactionOne]); - - expect(onPostCommit).toHaveBeenCalledTimes(2); - let call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(firstCommitTime); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionOne] : [], - ); - call = onPostCommit.mock.calls[1]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionOne] : [], - ); - - onPostCommit.mockClear(); - - const interactionTwo = { - id: 1, - name: 'componentDidUpdate test', - timestamp: Scheduler.unstable_now(), - }; - - // Cause an traced, async update - SchedulerTracing.unstable_trace( - interactionTwo.name, - Scheduler.unstable_now(), - () => { - instance.setState({count: 2}); - }, - ); - expect(onPostCommit).not.toHaveBeenCalled(); - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interactionTwo, - ); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(2); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(2); - - Scheduler.unstable_advanceTime(5); - - // Flush async work (outside of traced scope) - // This will cause an intentional cascading update from did-update - firstCommitTime = Scheduler.unstable_now(); - expect(Scheduler).toFlushAndYield([ - 'Example:2', - 'onPostCommit', - 'Example:3', - 'onPostCommit', - ]); - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionTwo); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(4); - expect( - getWorkForReactThreads(onWorkStarted)[2][0], - ).toMatchInteractions([interactionTwo]); - expect( - getWorkForReactThreads(onWorkStarted)[3][0], - ).toMatchInteractions([interactionTwo]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(4); - expect( - getWorkForReactThreads(onWorkStopped)[2][0], - ).toMatchInteractions([interactionTwo]); - expect( - getWorkForReactThreads(onWorkStopped)[3][0], - ).toMatchInteractions([interactionTwo]); - - // Verify the cascading commit is associated with the origin event - expect(onPostCommit).toHaveBeenCalledTimes(2); - call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(firstCommitTime); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionTwo] : [], - ); - call = onPostCommit.mock.calls[1]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionTwo] : [], - ); - - onPostCommit.mockClear(); - - const interactionThree = { - id: 2, - name: 'setState callback test', - timestamp: Scheduler.unstable_now(), - }; - - // Cause a cascading update from the setState callback - function callback() { - instance.setState({count: 6}); - } - SchedulerTracing.unstable_trace( - interactionThree.name, - Scheduler.unstable_now(), - () => { - instance.setState({count: 5}, callback); - }, - ); - expect(onPostCommit).not.toHaveBeenCalled(); - - expect(onInteractionTraced).toHaveBeenCalledTimes(3); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interactionThree, - ); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(4); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(4); - - // Flush async work (outside of traced scope) - // This will cause an intentional cascading update from the setState callback - firstCommitTime = Scheduler.unstable_now(); - expect(Scheduler).toFlushAndYield([ - 'Example:5', - 'onPostCommit', - 'Example:6', - 'onPostCommit', - ]); - - expect(onInteractionTraced).toHaveBeenCalledTimes(3); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(3); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interactionThree); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(6); - expect( - getWorkForReactThreads(onWorkStarted)[4][0], - ).toMatchInteractions([interactionThree]); - expect( - getWorkForReactThreads(onWorkStarted)[5][0], - ).toMatchInteractions([interactionThree]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(6); - expect( - getWorkForReactThreads(onWorkStopped)[4][0], - ).toMatchInteractions([interactionThree]); - expect( - getWorkForReactThreads(onWorkStopped)[5][0], - ).toMatchInteractions([interactionThree]); - - // Verify the cascading commit is associated with the origin event - expect(onPostCommit).toHaveBeenCalledTimes(2); - call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(firstCommitTime); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionThree] : [], - ); - call = onPostCommit.mock.calls[1]; - expect(call[0]).toEqual('test'); - expect(call[3]).toEqual(Scheduler.unstable_now()); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interactionThree] : [], - ); - }); - }); - - it('should trace interactions associated with a parent component state update', () => { - const onPostCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onPostCommit'); - }); - let parentInstance = null; - - class Child extends React.Component { - render() { - Scheduler.unstable_yieldValue('Child:' + this.props.count); - return ; - } - } - - class Parent extends React.Component { - state = { - count: 0, - }; - render() { - parentInstance = this; - return ( - - - - ); - } - } - - Scheduler.unstable_advanceTime(1); - - ReactTestRendererAct(() => { - ReactTestRenderer.create(, { - unstable_isConcurrent: true, + loadModules({ + replayFailedUnitOfWorkWithInvokeGuardedCallback, + }); }); - expect(Scheduler).toFlushAndYield(['Child:0', 'onPostCommit']); - onPostCommit.mockClear(); - const interaction = { - id: 0, - name: 'parent interaction', - timestamp: Scheduler.unstable_now(), - }; + it('should accumulate actual time after an error handled by componentDidCatch()', () => { + const callback = jest.fn(); - SchedulerTracing.unstable_trace( - interaction.name, - Scheduler.unstable_now(), - () => { - parentInstance.setState({count: 1}); - }, - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - expect(onPostCommit).not.toHaveBeenCalled(); - expect(Scheduler).toFlushAndYield(['Child:1', 'onPostCommit']); - expect(onPostCommit).toHaveBeenCalledTimes(1); - const call = onPostCommit.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStarted)[0][0], - ).toMatchInteractions([interaction]); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(1); - expect( - getWorkForReactThreads(onWorkStopped)[0][0], - ).toMatchInteractions([interaction]); - }); - }); - - describe('suspense', () => { - function awaitableAdvanceTimers(ms) { - jest.advanceTimersByTime(ms); - // Wait until the end of the current tick - // We cannot use a timer since we're faking them - return Promise.resolve().then(() => {}); - } - - it('traces both the temporary placeholder and the finished render for an interaction', async () => { - loadModulesForTracing({useNoopRenderer: true}); - - const interaction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - const monkey = React.createRef(); - class Monkey extends React.Component { - render() { - Scheduler.unstable_yieldValue('Monkey'); - return null; - } - } - - const onCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onCommit'); - }); - await ReactNoop.act(async () => { - SchedulerTracing.unstable_trace( - interaction.name, - Scheduler.unstable_now(), - () => { - ReactNoop.render( - - }> - - - - - - , - ); - }, - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - expect(Scheduler).toFlushAndYield([ - 'Suspend [Async]', - 'Text [Loading...]', - 'Text [Sync]', - 'Monkey', - 'onCommit', - ]); - // Should have committed the placeholder. - expect(ReactNoop.getChildrenAsJSX()).toEqual('Loading...Sync'); - expect(onCommit).toHaveBeenCalledTimes(1); - - let call = onCommit.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - // An unrelated update in the middle shouldn't affect things... - monkey.current.forceUpdate(); - expect(Scheduler).toFlushAndYield(['Monkey', 'onCommit']); - expect(onCommit).toHaveBeenCalledTimes(2); - - // Once the promise resolves, we render the suspended view - await awaitableAdvanceTimers(20000); - expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); - expect(Scheduler).toFlushAndYield(['AsyncText [Async]', 'onCommit']); - expect(ReactNoop.getChildrenAsJSX()).toEqual('AsyncSync'); - expect(onCommit).toHaveBeenCalledTimes(3); - - call = onCommit.mock.calls[2]; - expect(call[0]).toEqual('test-profiler'); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - }); - - it('does not prematurely complete for suspended sync renders', async () => { - const interaction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - const onPostCommit = jest.fn(() => - Scheduler.unstable_yieldValue('onPostCommit'), - ); - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - }> - - - - , - ); - }, - ); - - expect(Scheduler).toHaveYielded(['Suspend [loaded]', 'Text [loading]']); - - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - jest.runAllTimers(); - await resourcePromise; - - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushUntilNextPaint([ - 'onPostCommit', - 'AsyncText [loaded]', - ]); - expect(Scheduler).toFlushAndYield(['onPostCommit']); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - it('traces cascading work after suspended sync renders', async () => { - let wrappedCascadingFn; - class AsyncComponentWithCascadingWork extends React.Component { - state = { - hasMounted: false, + const ThrowsError = ({unused}) => { + Scheduler.unstable_advanceTime(3); + throw Error('expected error'); }; - componentDidMount() { - wrappedCascadingFn = SchedulerTracing.unstable_wrap(() => { - this.setState({hasMounted: true}); - }); - } - - render() { - Scheduler.unstable_yieldValue('render'); - const {ms, text} = this.props; - TextResource.read([text, ms]); - return {this.state.hasMounted}; - } - } - - const interaction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - const onPostCommit = jest.fn(() => - Scheduler.unstable_yieldValue('onPostCommit'), - ); - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - - }> - - - , - ); - }, - ); - - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - expect(Scheduler).toHaveYielded(['render', 'Text [loading]']); - - jest.runAllTimers(); - await resourcePromise; - - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushUntilNextPaint(['onPostCommit', 'render']); - - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - wrappedCascadingFn(); - expect(Scheduler).toHaveYielded(['onPostCommit', 'render']); - - // The new reconciler does not call onPostCommit again - // because the resolved suspended subtree doesn't contain any passive effects. - // If or its decendents had a passive effect, - // onPostCommit would be called again. - if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) { - expect(Scheduler).toFlushAndYield([]); - } else { - expect(Scheduler).toFlushAndYield(['onPostCommit']); - } - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - it('does not prematurely complete for suspended renders that have exceeded their deadline', async () => { - const interaction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - const onCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onCommit'); - }); - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - }> - - - , - { - unstable_isConcurrent: true, - }, - ); - }, - ); - - Scheduler.unstable_advanceTime(400); - await awaitableAdvanceTimers(400); - - expect(Scheduler).toFlushAndYield([ - 'Suspend [loaded]', - 'Text [loading]', - 'onCommit', - ]); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - Scheduler.unstable_advanceTime(500); - await awaitableAdvanceTimers(500); - - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushAndYield(['AsyncText [loaded]', 'onCommit']); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - it('decrements interaction count correctly if suspense loads before placeholder is shown', async () => { - const interaction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - const onCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onCommit'); - }); - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - }> - - - , - {unstable_isConcurrent: true}, - ); - }, - ); - expect(Scheduler).toFlushAndYield([ - 'Suspend [loaded]', - 'Text [loading]', - 'onCommit', - ]); - - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(100); - await resourcePromise; - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushAndYield(['AsyncText [loaded]', 'onCommit']); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - it('handles high-pri renderers between suspended and resolved (sync) trees', async () => { - const initialRenderInteraction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - const onPostCommit = jest.fn(() => - Scheduler.unstable_yieldValue('onPostCommit'), - ); - let renderer; - SchedulerTracing.unstable_trace( - initialRenderInteraction.name, - initialRenderInteraction.timestamp, - () => { - renderer = ReactTestRenderer.create( - - - }> - - - - , - ); - }, - ); - expect(renderer.toJSON()).toEqual(['loading', 'initial']); - expect(Scheduler).toHaveYielded([ - 'Suspend [loaded]', - 'Text [loading]', - 'Text [initial]', - ]); - expect(Scheduler).toFlushAndYield(['onPostCommit']); - - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onPostCommit).toHaveBeenCalledTimes(1); - expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([ - initialRenderInteraction, - ]); - onPostCommit.mockClear(); - - const highPriUpdateInteraction = { - id: 1, - name: 'hiPriUpdate', - timestamp: Scheduler.unstable_now(), - }; - - const originalPromise = resourcePromise; - - renderer.unstable_flushSync(() => { - SchedulerTracing.unstable_trace( - highPriUpdateInteraction.name, - highPriUpdateInteraction.timestamp, - () => { - renderer.update( - - - }> - - - - , - ); - }, - ); - }); - expect(renderer.toJSON()).toEqual(['loading', 'updated']); - expect(Scheduler).toHaveYielded([ - 'Suspend [loaded]', - 'Text [loading]', - 'Text [updated]', - ]); - expect(Scheduler).toFlushAndYield(['onPostCommit']); - - expect(onPostCommit).toHaveBeenCalledTimes(1); - expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([ - highPriUpdateInteraction, - ]); - onPostCommit.mockClear(); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted.mock.calls[0][0], - ).toMatchInteraction(highPriUpdateInteraction); - onInteractionScheduledWorkCompleted.mockClear(); - - Scheduler.unstable_advanceTime(100); - jest.advanceTimersByTime(100); - await originalPromise; - - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushUntilNextPaint(['AsyncText [loaded]']); - expect(renderer.toJSON()).toEqual(['loaded', 'updated']); - expect(Scheduler).toFlushAndYield(['onPostCommit']); - - expect(onPostCommit).toHaveBeenCalledTimes(1); - expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([ - initialRenderInteraction, - ]); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted.mock.calls[0][0], - ).toMatchInteraction(initialRenderInteraction); - }); - - // @gate experimental || !enableSyncDefaultUpdates - it('handles high-pri renderers between suspended and resolved (async) trees', async () => { - // Set up an initial shell. We need to set this up before the test sceanrio - // because we want initial render to suspend on navigation to the initial state. - const renderer = ReactTestRenderer.create( - {}}> - } /> - , - {unstable_isConcurrent: true}, - ); - expect(Scheduler).toFlushAndYield([]); - - const initialRenderInteraction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - const onCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onCommit'); - }); - SchedulerTracing.unstable_trace( - initialRenderInteraction.name, - initialRenderInteraction.timestamp, - () => { - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.unstable_startTransition(() => { - renderer.update( - - }> - - - - , - ); - }); - } else { - renderer.update( - - }> - - - - , + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + Scheduler.unstable_advanceTime(2); + return this.state.error === null ? ( + this.props.children + ) : ( + ); } - }, - ); - expect(Scheduler).toFlushAndYield([ - 'Suspend [loaded]', - 'Text [loading]', - 'Text [initial]', - ]); + } - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onCommit).not.toHaveBeenCalled(); + Scheduler.unstable_advanceTime(5); // 0 -> 5 - Scheduler.unstable_advanceTime(50); - jest.advanceTimersByTime(50); + ReactTestRenderer.create( + + + + + + , + ); - const highPriUpdateInteraction = { - id: 1, - name: 'hiPriUpdate', - timestamp: Scheduler.unstable_now(), - }; + expect(callback).toHaveBeenCalledTimes(2); - const originalPromise = resourcePromise; + // Callbacks bubble (reverse order). + const [mountCall, updateCall] = callback.mock.calls; - renderer.unstable_flushSync(() => { - SchedulerTracing.unstable_trace( - highPriUpdateInteraction.name, - highPriUpdateInteraction.timestamp, - () => { - renderer.update( - - }> - - - - , - ); - }, + // The initial mount only includes the ErrorBoundary (which takes 2) + // But it spends time rendering all of the failed subtree also. + expect(mountCall[1]).toBe('mount'); + // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError) + // We don't count the time spent in replaying the failed unit of work (ThrowsError) + expect(mountCall[2]).toBe(14); + // base time includes: 2 (ErrorBoundary) + // Since the tree is empty for the initial commit + expect(mountCall[3]).toBe(2); + // start time + expect(mountCall[4]).toBe(5); + // commit time: 5 initially + 14 of work + // Add an additional 3 (ThrowsError) if we replayed the failed work + expect(mountCall[5]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 22 + : 19, + ); + + // The update includes the ErrorBoundary and its fallback child + expect(updateCall[1]).toBe('nested-update'); + // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(updateCall[2]).toBe(22); + // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(updateCall[3]).toBe(22); + // start time + expect(updateCall[4]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 22 + : 19, + ); + // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime) + // Add an additional 3 (ThrowsError) if we replayed the failed work + expect(updateCall[5]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 44 + : 41, ); }); - expect(Scheduler).toHaveYielded([ - 'Suspend [loaded]', - 'Text [loading]', - 'Text [updated]', - 'onCommit', - ]); - expect(renderer.toJSON()).toEqual(['loading', 'updated']); - expect(onCommit).toHaveBeenCalledTimes(1); - expect(onCommit.mock.calls[0][4]).toMatchInteractions([ - highPriUpdateInteraction, - ]); - onCommit.mockClear(); + it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => { + const callback = jest.fn(); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0); + const ThrowsError = ({unused}) => { + Scheduler.unstable_advanceTime(10); + throw Error('expected error'); + }; - Scheduler.unstable_advanceTime(50); - jest.advanceTimersByTime(50); - await originalPromise; - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushAndYield(['AsyncText [loaded]', 'onCommit']); - expect(renderer.toJSON()).toEqual(['loaded', 'updated']); - - expect(onCommit).toHaveBeenCalledTimes(1); - expect(onCommit.mock.calls[0][4]).toMatchInteractions([ - highPriUpdateInteraction, - ]); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - expect( - onInteractionScheduledWorkCompleted.mock.calls[0][0], - ).toMatchInteraction(initialRenderInteraction); - expect( - onInteractionScheduledWorkCompleted.mock.calls[1][0], - ).toMatchInteraction(highPriUpdateInteraction); - }); - - it('does not trace Promises flagged with __reactDoNotTraceInteractions', async () => { - loadModulesForTracing({useNoopRenderer: true}); - - const interaction = { - id: 0, - name: 'initial render', - timestamp: Scheduler.unstable_now(), - }; - - AsyncText = ({ms, text}) => { - try { - TextResource.read([text, ms]); - Scheduler.unstable_yieldValue(`AsyncText [${text}]`); - return text; - } catch (promise) { - promise.__reactDoNotTraceInteractions = true; - - if (typeof promise.then === 'function') { - Scheduler.unstable_yieldValue(`Suspend [${text}]`); - } else { - Scheduler.unstable_yieldValue(`Error [${text}]`); + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + Scheduler.unstable_advanceTime(2); + return this.state.error === null ? ( + this.props.children + ) : ( + + ); } - throw promise; } - }; - const onCommit = jest.fn(() => { - Scheduler.unstable_yieldValue('onCommit'); - }); - SchedulerTracing.unstable_trace( - interaction.name, - Scheduler.unstable_now(), - () => { - ReactNoop.render( - - }> - - - - , - ); - }, - ); + Scheduler.unstable_advanceTime(5); // 0 -> 5 - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - expect(Scheduler).toFlushAndYield([ - 'Suspend [Async]', - 'Text [Loading...]', - 'Text [Sync]', - 'onCommit', - ]); - // Should have committed the placeholder. - expect(ReactNoop.getChildrenAsJSX()).toEqual('Loading...Sync'); - expect(onCommit).toHaveBeenCalledTimes(1); - - let call = onCommit.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[4]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); - - // The interaction is now complete. - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - - // Once the promise resolves, we render the suspended view - await awaitableAdvanceTimers(20000); - expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); - expect(Scheduler).toFlushAndYield(['AsyncText [Async]', 'onCommit']); - expect(ReactNoop.getChildrenAsJSX()).toEqual('AsyncSync'); - expect(onCommit).toHaveBeenCalledTimes(2); - - // No interactions should be associated with this update. - call = onCommit.mock.calls[1]; - expect(call[0]).toEqual('test-profiler'); - expect(call[4]).toMatchInteractions([]); - }); - - it('should properly report base duration wrt suspended subtrees', async () => { - loadModulesForTracing({useNoopRenderer: true}); - - const onRender = jest.fn(); - - let resolve = null; - const promise = new Promise(_resolve => { - resolve = _resolve; - }); - - function Other() { - Scheduler.unstable_advanceTime(1); - Scheduler.unstable_yieldValue('Other'); - return
    Other
    ; - } - - function Fallback() { - Scheduler.unstable_advanceTime(8); - Scheduler.unstable_yieldValue('Fallback'); - return
    Fallback
    ; - } - - let shouldSuspend = false; - function Suspender() { - Scheduler.unstable_advanceTime(15); - if (shouldSuspend) { - Scheduler.unstable_yieldValue('Suspender!'); - throw promise; - } - Scheduler.unstable_yieldValue('Suspender'); - return
    Suspender
    ; - } - - function App() { - return ( - - - }> - - - + ReactTestRenderer.create( + + + + + + , ); - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Other', 'Suspender']); - expect(ReactNoop).toMatchRenderedOutput( - <> -
    Other
    -
    Suspender
    - , - ); - expect(onRender).toHaveBeenCalledTimes(1); - expect(onRender.mock.calls[0][2]).toBe(1 + 15); // actual - expect(onRender.mock.calls[0][3]).toBe(1 + 15); // base + expect(callback).toHaveBeenCalledTimes(1); - shouldSuspend = true; - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Other', 'Suspender!', 'Fallback']); - await awaitableAdvanceTimers(20000); - expect(ReactNoop).toMatchRenderedOutput( - <> -
    Other
    - -
    Fallback
    - , - ); - expect(onRender).toHaveBeenCalledTimes(2); - expect(onRender.mock.calls[1][2]).toBe(1 + 15 + 8); // actual - expect(onRender.mock.calls[1][3]).toBe(1 + 8); // base + // Callbacks bubble (reverse order). + const [mountCall] = callback.mock.calls; - shouldSuspend = false; - resolve(); - await promise; - expect(Scheduler).toFlushAndYield(['Suspender']); - expect(ReactNoop).toMatchRenderedOutput( - <> -
    Other
    -
    Suspender
    - , - ); - expect(onRender).toHaveBeenCalledTimes(3); - expect(onRender.mock.calls[2][2]).toBe(15); // actual - expect(onRender.mock.calls[2][3]).toBe(1 + 15); // base - }); + // The initial mount includes the ErrorBoundary's error state, + // But it also spends actual time rendering UI that fails and isn't included. + expect(mountCall[1]).toBe('mount'); + // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError) + // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime) + // We don't count the time spent in replaying the failed unit of work (ThrowsError) + expect(mountCall[2]).toBe(39); + // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(mountCall[3]).toBe(22); + // start time + expect(mountCall[4]).toBe(5); + // commit time + expect(mountCall[5]).toBe( + __DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback + ? 54 + : 44, + ); + }); - if (__DEV__) { - it('double invoking does not disconnect wrapped async work', () => { - ReactFeatureFlags.enableStrictEffects = true; - ReactFeatureFlags.createRootStrictEffectsByDefault = true; + it('should reset the fiber stack correct after a "complete" phase error', () => { + jest.resetModules(); - const callback = jest.fn(() => { - const wrappedInteractions = SchedulerTracing.unstable_getCurrent(); - // Expect wrappedInteractions and interactions to be the same set. - expect(wrappedInteractions).toMatchInteractions([interaction]); + loadModules({ + useNoopRenderer: true, + replayFailedUnitOfWorkWithInvokeGuardedCallback, }); - const Component = jest.fn(() => { - React.useEffect(() => { - setTimeout(SchedulerTracing.unstable_wrap(callback), 0); - }); - React.useLayoutEffect(() => { - setTimeout(SchedulerTracing.unstable_wrap(callback), 0); - }); - - return null; - }); - - let interaction; - SchedulerTracing.unstable_trace( - 'event', - Scheduler.unstable_now(), - () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - interaction = Array.from(interactions)[0]; - ReactTestRendererAct(() => { - ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - }); - }, + // Simulate a renderer error during the "complete" phase. + // This mimics behavior like React Native's View/Text nesting validation. + ReactNoop.render( + + hi + , ); - Scheduler.unstable_flushAll(); + expect(Scheduler).toFlushAndThrow('Error in host config.'); - jest.runAllTimers(); + // A similar case we've seen caused by an invariant in ReactDOM. + // It didn't reproduce without a host component inside. + ReactNoop.render( + + + hi + + , + ); + expect(Scheduler).toFlushAndThrow('Error in host config.'); - expect(callback).toHaveBeenCalledTimes(4); // 2x per effect - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + // So long as the profiler timer's fiber stack is reset correctly, + // Subsequent renders should not error. + ReactNoop.render( + + hi + , + ); + expect(Scheduler).toFlushWithoutYielding(); }); - } + }); }); }); + + it('reflects the most recently rendered id value', () => { + const callback = jest.fn(); + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const renderer = ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + Scheduler.unstable_advanceTime(20); // 7 -> 27 + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[0]).toBe('one'); + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(2); // actual time + expect(mountCall[3]).toBe(2); // base time + expect(mountCall[4]).toBe(5); // start time + + expect(updateCall[0]).toBe('two'); + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(1); // actual time + expect(updateCall[3]).toBe(1); // base time + expect(updateCall[4]).toBe(27); // start time + }); + + it('should not be called until after mutations', () => { + let classComponentMounted = false; + const callback = jest.fn( + (id, phase, actualDuration, baseDuration, startTime, commitTime) => { + // Don't call this hook until after mutations + expect(classComponentMounted).toBe(true); + // But the commit time should reflect pre-mutation + expect(commitTime).toBe(2); + }, + ); + + class ClassComponent extends React.Component { + componentDidMount() { + Scheduler.unstable_advanceTime(5); + classComponentMounted = true; + } + render() { + Scheduler.unstable_advanceTime(2); + return null; + } + } + + ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + }); +}); + +describe(`onCommit`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules(); + }); + + it('should report time spent in layout effects and commit lifecycles', () => { + const callback = jest.fn(); + + const ComponentWithEffects = () => { + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(10); + return () => { + Scheduler.unstable_advanceTime(100); + }; + }, []); + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(1000); + return () => { + Scheduler.unstable_advanceTime(10000); + }; + }); + React.useEffect(() => { + // This passive effect is here to verify that its time isn't reported. + Scheduler.unstable_advanceTime(5); + return () => { + Scheduler.unstable_advanceTime(7); + }; + }); + return null; + }; + + class ComponentWithCommitHooks extends React.Component { + componentDidMount() { + Scheduler.unstable_advanceTime(100000); + } + componentDidUpdate() { + Scheduler.unstable_advanceTime(1000000); + } + render() { + return null; + } + } + + Scheduler.unstable_advanceTime(1); + + const renderer = ReactTestRenderer.create( + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('mount-test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(101010); // durations + expect(call[3]).toBe(1); // commit start time (before mutations or effects) + + Scheduler.unstable_advanceTime(1); + + renderer.update( + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + call = callback.mock.calls[1]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('update-test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(1011000); // durations + expect(call[3]).toBe(101017); // commit start time (before mutations or effects) + + Scheduler.unstable_advanceTime(1); + + renderer.update(); + + expect(callback).toHaveBeenCalledTimes(3); + + call = callback.mock.calls[2]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('unmount-test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(10100); // durations + expect(call[3]).toBe(1112030); // commit start time (before mutations or effects) + }); + + it('should report time spent in layout effects and commit lifecycles with cascading renders', () => { + const callback = jest.fn(); + + const ComponentWithEffects = ({shouldCascade}) => { + const [didCascade, setDidCascade] = React.useState(false); + Scheduler.unstable_advanceTime(100000000); + React.useLayoutEffect(() => { + if (shouldCascade && !didCascade) { + setDidCascade(true); + } + Scheduler.unstable_advanceTime(didCascade ? 30 : 10); + return () => { + Scheduler.unstable_advanceTime(100); + }; + }, [didCascade, shouldCascade]); + return null; + }; + + class ComponentWithCommitHooks extends React.Component { + state = { + didCascade: false, + }; + componentDidMount() { + Scheduler.unstable_advanceTime(1000); + } + componentDidUpdate() { + Scheduler.unstable_advanceTime(10000); + if (this.props.shouldCascade && !this.state.didCascade) { + this.setState({didCascade: true}); + } + } + render() { + Scheduler.unstable_advanceTime(1000000000); + return null; + } + } + + Scheduler.unstable_advanceTime(1); + + const renderer = ReactTestRenderer.create( + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('mount-test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(1010); // durations + expect(call[3]).toBe(1100000001); // commit start time (before mutations or effects) + + call = callback.mock.calls[1]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('mount-test'); + expect(call[1]).toBe('nested-update'); + expect(call[2]).toBe(130); // durations + expect(call[3]).toBe(1200001011); // commit start time (before mutations or effects) + + Scheduler.unstable_advanceTime(1); + + renderer.update( + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(4); + + call = callback.mock.calls[2]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('update-test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(10130); // durations + expect(call[3]).toBe(2300001142); // commit start time (before mutations or effects) + + call = callback.mock.calls[3]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('update-test'); + expect(call[1]).toBe('nested-update'); + expect(call[2]).toBe(10000); // durations + expect(call[3]).toBe(3300011272); // commit start time (before mutations or effects) + }); + + it('should include time spent in ref callbacks', () => { + const callback = jest.fn(); + + const refSetter = ref => { + if (ref !== null) { + Scheduler.unstable_advanceTime(10); + } else { + Scheduler.unstable_advanceTime(100); + } + }; + + class ClassComponent extends React.Component { + render() { + return null; + } + } + + const Component = () => { + Scheduler.unstable_advanceTime(1000); + return ; + }; + + Scheduler.unstable_advanceTime(1); + + const renderer = ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(10); // durations + expect(call[3]).toBe(1001); // commit start time (before mutations or effects) + + callback.mockClear(); + + renderer.update(); + + expect(callback).toHaveBeenCalledTimes(1); + + call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(100); // durations + expect(call[3]).toBe(1011); // commit start time (before mutations or effects) + }); + + it('should bubble time spent in layout effects to higher profilers', () => { + const callback = jest.fn(); + + const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => { + const setCount = React.useState(0)[1]; + if (setCountRef != null) { + setCountRef.current = setCount; + } + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(duration); + return () => { + Scheduler.unstable_advanceTime(cleanupDuration); + }; + }); + Scheduler.unstable_advanceTime(1); + return null; + }; + + const setCountRef = React.createRef(null); + + let renderer = null; + ReactTestRendererAct(() => { + renderer = ReactTestRenderer.create( + + + + + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root-mount'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(1010); // durations + expect(call[3]).toBe(2); // commit start time (before mutations or effects) + + ReactTestRendererAct(() => setCountRef.current(count => count + 1)); + + expect(callback).toHaveBeenCalledTimes(2); + + call = callback.mock.calls[1]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root-mount'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(110); // durations + expect(call[3]).toBe(1013); // commit start time (before mutations or effects) + + ReactTestRendererAct(() => { + renderer.update( + + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(3); + + call = callback.mock.calls[2]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root-update'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(1100); // durations + expect(call[3]).toBe(1124); // commit start time (before mutations or effects) + }); + + it('should properly report time in layout effects even when there are errors', () => { + const callback = jest.fn(); + + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + return this.state.error === null + ? this.props.children + : this.props.fallback; + } + } + + const ComponentWithEffects = ({ + cleanupDuration, + duration, + effectDuration, + shouldThrow, + }) => { + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(effectDuration); + if (shouldThrow) { + throw Error('expected'); + } + return () => { + Scheduler.unstable_advanceTime(cleanupDuration); + }; + }); + Scheduler.unstable_advanceTime(duration); + return null; + }; + + Scheduler.unstable_advanceTime(1); + + // Test an error that happens during an effect + + ReactTestRendererAct(() => { + ReactTestRenderer.create( + + + }> + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + + let call = callback.mock.calls[0]; + + // Initial render (with error) + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(100100); // durations + expect(call[3]).toBe(10011); // commit start time (before mutations or effects) + + call = callback.mock.calls[1]; + + // Cleanup render from error boundary + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('nested-update'); + expect(call[2]).toBe(100000000); // durations + expect(call[3]).toBe(10110111); // commit start time (before mutations or effects) + }); + + it('should properly report time in layout effect cleanup functions even when there are errors', () => { + const callback = jest.fn(); + + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + return this.state.error === null + ? this.props.children + : this.props.fallback; + } + } + + const ComponentWithEffects = ({ + cleanupDuration, + duration, + effectDuration, + shouldThrow = false, + }) => { + React.useLayoutEffect(() => { + Scheduler.unstable_advanceTime(effectDuration); + return () => { + Scheduler.unstable_advanceTime(cleanupDuration); + if (shouldThrow) { + throw Error('expected'); + } + }; + }); + Scheduler.unstable_advanceTime(duration); + return null; + }; + + Scheduler.unstable_advanceTime(1); + + let renderer = null; + + ReactTestRendererAct(() => { + renderer = ReactTestRenderer.create( + + + }> + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + // Initial render + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(100100); // durations + expect(call[3]).toBe(10011); // commit start time (before mutations or effects) + + callback.mockClear(); + + // Test an error that happens during an cleanup function + + ReactTestRendererAct(() => { + renderer.update( + + + }> + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + + call = callback.mock.calls[0]; + + // Update (that throws) + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(1101100); // durations + expect(call[3]).toBe(120121); // commit start time (before mutations or effects) + + call = callback.mock.calls[1]; + + // Cleanup render from error boundary + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('nested-update'); + expect(call[2]).toBe(100001000); // durations + expect(call[3]).toBe(11221221); // commit start time (before mutations or effects) + }); +}); + +describe(`onPostCommit`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules(); + }); + + it('should report time spent in passive effects', () => { + const callback = jest.fn(); + + const ComponentWithEffects = () => { + React.useLayoutEffect(() => { + // This layout effect is here to verify that its time isn't reported. + Scheduler.unstable_advanceTime(5); + return () => { + Scheduler.unstable_advanceTime(7); + }; + }); + React.useEffect(() => { + Scheduler.unstable_advanceTime(10); + return () => { + Scheduler.unstable_advanceTime(100); + }; + }, []); + React.useEffect(() => { + Scheduler.unstable_advanceTime(1000); + return () => { + Scheduler.unstable_advanceTime(10000); + }; + }); + return null; + }; + + Scheduler.unstable_advanceTime(1); + + let renderer; + ReactTestRendererAct(() => { + renderer = ReactTestRenderer.create( + + + , + ); + }); + Scheduler.unstable_flushAll(); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('mount-test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(1010); // durations + expect(call[3]).toBe(1); // commit start time (before mutations or effects) + + Scheduler.unstable_advanceTime(1); + + ReactTestRendererAct(() => { + renderer.update( + + + , + ); + }); + Scheduler.unstable_flushAll(); + + expect(callback).toHaveBeenCalledTimes(2); + + call = callback.mock.calls[1]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('update-test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(11000); // durations + expect(call[3]).toBe(1017); // commit start time (before mutations or effects) + + Scheduler.unstable_advanceTime(1); + + ReactTestRendererAct(() => { + renderer.update( + , + ); + }); + Scheduler.unstable_flushAll(); + + expect(callback).toHaveBeenCalledTimes(3); + + call = callback.mock.calls[2]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('unmount-test'); + expect(call[1]).toBe('update'); + // TODO (bvaughn) The duration reported below should be 10100, but is 0 + // by the time the passive effect is flushed its parent Fiber pointer is gone. + // If we refactor to preserve the unmounted Fiber tree we could fix this. + // The current implementation would require too much extra overhead to track this. + expect(call[2]).toBe(0); // durations + expect(call[3]).toBe(12030); // commit start time (before mutations or effects) + }); + + it('should report time spent in passive effects with cascading renders', () => { + const callback = jest.fn(); + + const ComponentWithEffects = () => { + const [didMount, setDidMount] = React.useState(false); + Scheduler.unstable_advanceTime(1000); + React.useEffect(() => { + if (!didMount) { + setDidMount(true); + } + Scheduler.unstable_advanceTime(didMount ? 30 : 10); + return () => { + Scheduler.unstable_advanceTime(100); + }; + }, [didMount]); + return null; + }; + + Scheduler.unstable_advanceTime(1); + + ReactTestRendererAct(() => { + ReactTestRenderer.create( + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('mount-test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(10); // durations + expect(call[3]).toBe(1001); // commit start time (before mutations or effects) + + call = callback.mock.calls[1]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('mount-test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(130); // durations + expect(call[3]).toBe(2011); // commit start time (before mutations or effects) + }); + + it('should bubble time spent in effects to higher profilers', () => { + const callback = jest.fn(); + + const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => { + const setCount = React.useState(0)[1]; + if (setCountRef != null) { + setCountRef.current = setCount; + } + React.useEffect(() => { + Scheduler.unstable_advanceTime(duration); + return () => { + Scheduler.unstable_advanceTime(cleanupDuration); + }; + }); + Scheduler.unstable_advanceTime(1); + return null; + }; + + const setCountRef = React.createRef(null); + + let renderer = null; + ReactTestRendererAct(() => { + renderer = ReactTestRenderer.create( + + + + + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root-mount'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(1010); // durations + expect(call[3]).toBe(2); // commit start time (before mutations or effects) + + ReactTestRendererAct(() => setCountRef.current(count => count + 1)); + + expect(callback).toHaveBeenCalledTimes(2); + + call = callback.mock.calls[1]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root-mount'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(110); // durations + expect(call[3]).toBe(1013); // commit start time (before mutations or effects) + + ReactTestRendererAct(() => { + renderer.update( + + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(3); + + call = callback.mock.calls[2]; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('root-update'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(1100); // durations + expect(call[3]).toBe(1124); // commit start time (before mutations or effects) + }); + + it('should properly report time in passive effects even when there are errors', () => { + const callback = jest.fn(); + + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + return this.state.error === null + ? this.props.children + : this.props.fallback; + } + } + + const ComponentWithEffects = ({ + cleanupDuration, + duration, + effectDuration, + shouldThrow, + }) => { + React.useEffect(() => { + Scheduler.unstable_advanceTime(effectDuration); + if (shouldThrow) { + throw Error('expected'); + } + return () => { + Scheduler.unstable_advanceTime(cleanupDuration); + }; + }); + Scheduler.unstable_advanceTime(duration); + return null; + }; + + Scheduler.unstable_advanceTime(1); + + // Test an error that happens during an effect + + ReactTestRendererAct(() => { + ReactTestRenderer.create( + + + }> + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + + let call = callback.mock.calls[0]; + + // Initial render (with error) + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(100100); // durations + expect(call[3]).toBe(10011); // commit start time (before mutations or effects) + + call = callback.mock.calls[1]; + + // Cleanup render from error boundary + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(100000000); // durations + expect(call[3]).toBe(10110111); // commit start time (before mutations or effects) + }); + + it('should properly report time in passive effect cleanup functions even when there are errors', () => { + const callback = jest.fn(); + + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + return this.state.error === null + ? this.props.children + : this.props.fallback; + } + } + + const ComponentWithEffects = ({ + cleanupDuration, + duration, + effectDuration, + shouldThrow = false, + id, + }) => { + React.useEffect(() => { + Scheduler.unstable_advanceTime(effectDuration); + return () => { + Scheduler.unstable_advanceTime(cleanupDuration); + if (shouldThrow) { + throw Error('expected'); + } + }; + }); + Scheduler.unstable_advanceTime(duration); + return null; + }; + + Scheduler.unstable_advanceTime(1); + + let renderer = null; + + ReactTestRendererAct(() => { + renderer = ReactTestRenderer.create( + + + }> + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(1); + + let call = callback.mock.calls[0]; + + // Initial render + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(100100); // durations + expect(call[3]).toBe(10011); // commit start time (before mutations or effects) + + callback.mockClear(); + + // Test an error that happens during an cleanup function + + ReactTestRendererAct(() => { + renderer.update( + + + }> + + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + + call = callback.mock.calls[0]; + + // Update (that throws) + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('update'); + // We continue flushing pending effects even if one throws. + expect(call[2]).toBe(1101100); // durations + expect(call[3]).toBe(120121); // commit start time (before mutations or effects) + + call = callback.mock.calls[1]; + + // Cleanup render from error boundary + expect(call).toHaveLength(4); + expect(call[0]).toBe('root'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(100000000); // durations + // The commit time varies because the above duration time varies + expect(call[3]).toBe(11221221); // commit start time (before mutations or effects) + }); +}); + +describe(`onNestedUpdateScheduled`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules({ + enableProfilerNestedUpdateScheduledHook: true, + useNoopRenderer: true, + }); + }); + + it('is not called when the legacy render API is used to schedule an update', () => { + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.renderLegacySyncRoot( + +
    initial
    +
    , + ); + + ReactNoop.renderLegacySyncRoot( + +
    update
    +
    , + ); + + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + }); + + it('is not called when the root API is used to schedule an update', () => { + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.render( + +
    initial
    +
    , + ); + + ReactNoop.render( + +
    update
    +
    , + ); + + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + }); + + it('is called when a function component schedules an update during a layout effect', () => { + function Component() { + const [didMount, setDidMount] = React.useState(false); + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + Scheduler.unstable_yieldValue(`Component:${didMount}`); + return didMount; + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); + expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); + }); + + it('is called when a function component schedules a batched update during a layout effect', () => { + function Component() { + const [didMount, setDidMount] = React.useState(false); + React.useLayoutEffect(() => { + ReactNoop.batchedUpdates(() => { + setDidMount(true); + }); + }, []); + Scheduler.unstable_yieldValue(`Component:${didMount}`); + return didMount; + } + + const onNestedUpdateScheduled = jest.fn(); + const onRender = jest.fn(); + + ReactNoop.render( + + + , + ); + expect(Scheduler).toFlushAndYield(['Component:false', 'Component:true']); + + expect(onRender).toHaveBeenCalledTimes(2); + expect(onRender.mock.calls[0][1]).toBe('mount'); + expect(onRender.mock.calls[1][1]).toBe('nested-update'); + + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); + expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('root'); + }); + + it('bubbles up and calls all ancestor Profilers', () => { + function Component() { + const [didMount, setDidMount] = React.useState(false); + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + Scheduler.unstable_yieldValue(`Component:${didMount}`); + return didMount; + } + const onNestedUpdateScheduledOne = jest.fn(); + const onNestedUpdateScheduledTwo = jest.fn(); + const onNestedUpdateScheduledThree = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + <> + + + + + , + ); + }); + + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + expect(onNestedUpdateScheduledOne).toHaveBeenCalledTimes(1); + expect(onNestedUpdateScheduledOne.mock.calls[0][0]).toBe('one'); + expect(onNestedUpdateScheduledTwo).toHaveBeenCalledTimes(1); + expect(onNestedUpdateScheduledTwo.mock.calls[0][0]).toBe('two'); + expect(onNestedUpdateScheduledThree).not.toHaveBeenCalled(); + }); + + it('is not called when an update is scheduled for another doort during a layout effect', () => { + const setStateRef = React.createRef(null); + + function ComponentRootOne() { + const [state, setState] = React.useState(false); + setStateRef.current = setState; + Scheduler.unstable_yieldValue(`ComponentRootOne:${state}`); + return state; + } + + function ComponentRootTwo() { + React.useLayoutEffect(() => { + setStateRef.current(true); + }, []); + Scheduler.unstable_yieldValue('ComponentRootTwo'); + return null; + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.renderToRootWithID( + + + , + 1, + ); + + ReactNoop.renderToRootWithID( + + + , + 2, + ); + }); + + expect(Scheduler).toHaveYielded([ + 'ComponentRootOne:false', + 'ComponentRootTwo', + 'ComponentRootOne:true', + ]); + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + }); + + it('is not called when a function component schedules an update during a passive effect', () => { + function Component() { + const [didMount, setDidMount] = React.useState(false); + React.useEffect(() => { + setDidMount(true); + }, []); + Scheduler.unstable_yieldValue(`Component:${didMount}`); + return didMount; + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + }); + + it('is not called when a function component schedules an update outside of render', () => { + const updateFnRef = React.createRef(null); + + function Component() { + const [state, setState] = React.useState(false); + updateFnRef.current = () => setState(true); + Scheduler.unstable_yieldValue(`Component:${state}`); + return state; + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Component:false']); + + ReactNoop.act(() => { + updateFnRef.current(); + }); + expect(Scheduler).toHaveYielded(['Component:true']); + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + }); + + it('it is not called when a component schedules an update during render', () => { + function Component() { + const [state, setState] = React.useState(false); + if (state === false) { + setState(true); + } + Scheduler.unstable_yieldValue(`Component:${state}`); + return state; + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + }); + + it('it is called when a component schedules an update from a ref callback', () => { + function Component({mountChild}) { + const [refAttached, setRefAttached] = React.useState(false); + const [refDetached, setRefDetached] = React.useState(false); + const refSetter = React.useCallback(ref => { + if (ref !== null) { + setRefAttached(true); + } else { + setRefDetached(true); + } + }, []); + Scheduler.unstable_yieldValue(`Component:${refAttached}:${refDetached}`); + return mountChild ?
    : null; + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + + expect(Scheduler).toHaveYielded([ + 'Component:false:false', + 'Component:true:false', + ]); + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); + expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + + expect(Scheduler).toHaveYielded([ + 'Component:true:false', + 'Component:true:true', + ]); + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(2); + expect(onNestedUpdateScheduled.mock.calls[1][0]).toBe('test'); + }); + + it('is called when a class component schedules an update from the componentDidMount lifecycles', () => { + class Component extends React.Component { + state = { + value: false, + }; + componentDidMount() { + this.setState({value: true}); + } + render() { + const {value} = this.state; + Scheduler.unstable_yieldValue(`Component:${value}`); + return value; + } + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); + expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); + }); + + it('is called when a class component schedules an update from the componentDidUpdate lifecycles', () => { + class Component extends React.Component { + state = { + nestedUpdateSheduled: false, + }; + componentDidUpdate(prevProps, prevState) { + if ( + this.props.scheduleNestedUpdate && + !this.state.nestedUpdateSheduled + ) { + this.setState({nestedUpdateSheduled: true}); + } + } + render() { + const {scheduleNestedUpdate} = this.props; + const {nestedUpdateSheduled} = this.state; + Scheduler.unstable_yieldValue( + `Component:${scheduleNestedUpdate}:${nestedUpdateSheduled}`, + ); + return nestedUpdateSheduled; + } + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Component:false:false']); + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + + expect(Scheduler).toHaveYielded([ + 'Component:true:false', + 'Component:true:true', + ]); + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); + expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); + }); + + it('is not called when a class component schedules an update outside of render', () => { + const updateFnRef = React.createRef(null); + + class Component extends React.Component { + state = { + value: false, + }; + render() { + const {value} = this.state; + updateFnRef.current = () => this.setState({value: true}); + Scheduler.unstable_yieldValue(`Component:${value}`); + return value; + } + } + + const onNestedUpdateScheduled = jest.fn(); + + ReactNoop.act(() => { + ReactNoop.render( + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Component:false']); + + ReactNoop.act(() => { + updateFnRef.current(); + }); + expect(Scheduler).toHaveYielded(['Component:true']); + expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); + }); + + // TODO Add hydration tests to ensure we don't have false positives called. }); diff --git a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js deleted file mode 100644 index 6c1df098d6..0000000000 --- a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * 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. - * - * @emails react-core - */ - -'use strict'; - -let React; -let ReactFeatureFlags; -let ReactDOM; -let SchedulerTracing; -let Scheduler; - -function loadModules() { - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - - ReactFeatureFlags.enableProfilerTimer = true; - ReactFeatureFlags.enableSchedulerTracing = true; - - React = require('react'); - SchedulerTracing = require('scheduler/tracing'); - ReactDOM = require('react-dom'); - Scheduler = require('scheduler'); -} - -describe('ProfilerDOM', () => { - let onInteractionScheduledWorkCompleted; - let onInteractionTraced; - - beforeEach(() => { - loadModules(); - - onInteractionScheduledWorkCompleted = jest.fn(); - onInteractionTraced = jest.fn(); - - // Verify interaction subscriber methods are called as expected. - SchedulerTracing.unstable_subscribe({ - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled: () => {}, - onWorkScheduled: () => {}, - onWorkStarted: () => {}, - onWorkStopped: () => {}, - }); - }); - - function Text(props) { - Scheduler.unstable_yieldValue(props.text); - return props.text; - } - - // @gate experimental - it('should correctly trace interactions for async roots', async () => { - let resolve; - let thenable = { - then(res) { - resolve = () => { - thenable = null; - res(); - }; - }, - }; - - function Async() { - if (thenable !== null) { - Scheduler.unstable_yieldValue('Suspend! [Async]'); - throw thenable; - } - Scheduler.unstable_yieldValue('Async'); - return 'Async'; - } - - const element = document.createElement('div'); - const root = ReactDOM.createRoot(element); - - let interaction; - let wrappedResolve; - SchedulerTracing.unstable_trace('initial_event', performance.now(), () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions.size).toBe(1); - interaction = Array.from(interactions)[0]; - - root.render( - }> - - , - ); - - wrappedResolve = SchedulerTracing.unstable_wrap(() => resolve()); - }); - - // Render, suspend, and commit fallback - expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']); - expect(element.textContent).toEqual('Loading...'); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - // Ping React to try rendering again - wrappedResolve(); - - // Complete the tree without committing it - expect(Scheduler).toFlushAndYieldThrough(['Async']); - // Still showing the fallback - expect(element.textContent).toEqual('Loading...'); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - expect(Scheduler).toFlushAndYield([]); - expect(element.textContent).toEqual('Async'); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js index bd3f168af4..d1b5bd9b44 100644 --- a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js @@ -15,7 +15,6 @@ describe('ReactProfiler DevTools integration', () => { let ReactFeatureFlags; let ReactTestRenderer; let Scheduler; - let SchedulerTracing; let AdvanceTime; let hook; @@ -31,9 +30,7 @@ describe('ReactProfiler DevTools integration', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableProfilerTimer = true; - ReactFeatureFlags.enableSchedulerTracing = true; Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); React = require('react'); ReactTestRenderer = require('react-test-renderer'); @@ -76,15 +73,7 @@ describe('ReactProfiler DevTools integration', () => { // The time spent in App (above the Profiler) won't be included in the durations, // But needs to be accounted for in the offset times. expect(onRender).toHaveBeenCalledTimes(1); - expect(onRender).toHaveBeenCalledWith( - 'Profiler', - 'mount', - 10, - 10, - 2, - 12, - new Set(), - ); + expect(onRender).toHaveBeenCalledWith('Profiler', 'mount', 10, 10, 2, 12); onRender.mockClear(); // Measure unobservable timing required by the DevTools profiler. @@ -101,15 +90,7 @@ describe('ReactProfiler DevTools integration', () => { // The time spent in App (above the Profiler) won't be included in the durations, // But needs to be accounted for in the offset times. expect(onRender).toHaveBeenCalledTimes(1); - expect(onRender).toHaveBeenCalledWith( - 'Profiler', - 'update', - 6, - 13, - 14, - 20, - new Set(), - ); + expect(onRender).toHaveBeenCalledWith('Profiler', 'update', 6, 13, 14, 20); // Measure unobservable timing required by the DevTools profiler. // At this point, the base time should include both: @@ -157,27 +138,6 @@ describe('ReactProfiler DevTools integration', () => { ).toBe(7); }); - it('should store traced interactions on the HostNode so DevTools can access them', () => { - // Render without an interaction - const rendered = ReactTestRenderer.create(
    ); - - const root = rendered.root._currentFiber().return; - expect(root.stateNode.memoizedInteractions).toContainNoInteractions(); - - Scheduler.unstable_advanceTime(10); - - const eventTime = Scheduler.unstable_now(); - - // Render with an interaction - SchedulerTracing.unstable_trace('some event', eventTime, () => { - rendered.update(
    ); - }); - - expect(root.stateNode.memoizedInteractions).toMatchInteractions([ - {name: 'some event', timestamp: eventTime}, - ]); - }); - // @gate experimental || !enableSyncDefaultUpdates it('regression test: #17159', () => { function Text({text}) { diff --git a/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap index 91d0b594dc..35da4c2961 100644 --- a/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap +++ b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should render children 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should render children 1`] = `
    outside span @@ -14,11 +14,11 @@ exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTr
    `; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `
    `; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `
    `; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:disabled should support nested Profilers 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support nested Profilers 1`] = ` Array [
    outer function component @@ -32,7 +32,7 @@ Array [ ] `; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should render children 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should render children 1`] = `
    outside span @@ -46,75 +46,11 @@ exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTr
    `; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `
    `; +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `
    `; -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:disabled enableProfilerTimer:enabled should support nested Profilers 1`] = ` -Array [ -
    - outer function component -
    , - - inner class component - , - - inner span - , -] -`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should render children 1`] = ` -
    - - outside span - - - inside span - - - function component - -
    -`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 1`] = `null`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `
    `; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:disabled should support nested Profilers 1`] = ` -Array [ -
    - outer function component -
    , - - inner class component - , - - inner span - , -] -`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should render children 1`] = ` -
    - - outside span - - - inside span - - - function component - -
    -`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 1`] = `null`; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `
    `; - -exports[`Profiler works in profiling and non-profiling bundles enableSchedulerTracing:enabled enableProfilerTimer:enabled should support nested Profilers 1`] = ` +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support nested Profilers 1`] = ` Array [
    outer function component diff --git a/packages/react/src/forks/ReactSharedInternals.umd.js b/packages/react/src/forks/ReactSharedInternals.umd.js index 268b792da1..8082fd0eef 100644 --- a/packages/react/src/forks/ReactSharedInternals.umd.js +++ b/packages/react/src/forks/ReactSharedInternals.umd.js @@ -7,7 +7,6 @@ import assign from 'object-assign'; import * as Scheduler from 'scheduler'; -import * as SchedulerTracing from 'scheduler/tracing'; import ReactCurrentDispatcher from '../ReactCurrentDispatcher'; import ReactCurrentOwner from '../ReactCurrentOwner'; import ReactDebugCurrentFrame from '../ReactDebugCurrentFrame'; @@ -28,7 +27,6 @@ const ReactSharedInternals = { // This re-export is only required for UMD bundles; // CJS bundles use the shared NPM package. Scheduler, - SchedulerTracing, }; if (__DEV__) { diff --git a/packages/scheduler/npm/tracing-profiling.js b/packages/scheduler/npm/tracing-profiling.js deleted file mode 100644 index cb2d20c025..0000000000 --- a/packages/scheduler/npm/tracing-profiling.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/scheduler-tracing.profiling.min.js'); -} else { - module.exports = require('./cjs/scheduler-tracing.development.js'); -} diff --git a/packages/scheduler/npm/tracing.js b/packages/scheduler/npm/tracing.js deleted file mode 100644 index 1e318bd901..0000000000 --- a/packages/scheduler/npm/tracing.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/scheduler-tracing.production.min.js'); -} else { - module.exports = require('./cjs/scheduler-tracing.development.js'); -} diff --git a/packages/scheduler/npm/umd/scheduler-tracing.development.js b/packages/scheduler/npm/umd/scheduler-tracing.development.js deleted file mode 100644 index a81bf8fe2f..0000000000 --- a/packages/scheduler/npm/umd/scheduler-tracing.development.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license React - * - * 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. - */ - -'use strict'; - -(function(global, factory) { - // eslint-disable-next-line no-unused-expressions - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('react'))) - : typeof define === 'function' && define.amd // eslint-disable-line no-undef - ? define(['react'], factory) // eslint-disable-line no-undef - : (global.SchedulerTracing = factory(global)); -})(this, function(global) { - function unstable_clear() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_clear.apply( - this, - arguments - ); - } - - function unstable_getCurrent() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_getCurrent.apply( - this, - arguments - ); - } - - function unstable_getThreadID() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_getThreadID.apply( - this, - arguments - ); - } - - function unstable_subscribe() { - // eslint-disable-next-line max-len - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_subscribe.apply( - this, - arguments - ); - } - - function unstable_trace() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_trace.apply( - this, - arguments - ); - } - - function unstable_unsubscribe() { - // eslint-disable-next-line max-len - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_unsubscribe.apply( - this, - arguments - ); - } - - function unstable_wrap() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_wrap.apply( - this, - arguments - ); - } - - return Object.freeze({ - unstable_clear: unstable_clear, - unstable_getCurrent: unstable_getCurrent, - unstable_getThreadID: unstable_getThreadID, - unstable_subscribe: unstable_subscribe, - unstable_trace: unstable_trace, - unstable_unsubscribe: unstable_unsubscribe, - unstable_wrap: unstable_wrap, - }); -}); diff --git a/packages/scheduler/npm/umd/scheduler-tracing.production.min.js b/packages/scheduler/npm/umd/scheduler-tracing.production.min.js deleted file mode 100644 index a81bf8fe2f..0000000000 --- a/packages/scheduler/npm/umd/scheduler-tracing.production.min.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license React - * - * 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. - */ - -'use strict'; - -(function(global, factory) { - // eslint-disable-next-line no-unused-expressions - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('react'))) - : typeof define === 'function' && define.amd // eslint-disable-line no-undef - ? define(['react'], factory) // eslint-disable-line no-undef - : (global.SchedulerTracing = factory(global)); -})(this, function(global) { - function unstable_clear() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_clear.apply( - this, - arguments - ); - } - - function unstable_getCurrent() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_getCurrent.apply( - this, - arguments - ); - } - - function unstable_getThreadID() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_getThreadID.apply( - this, - arguments - ); - } - - function unstable_subscribe() { - // eslint-disable-next-line max-len - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_subscribe.apply( - this, - arguments - ); - } - - function unstable_trace() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_trace.apply( - this, - arguments - ); - } - - function unstable_unsubscribe() { - // eslint-disable-next-line max-len - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_unsubscribe.apply( - this, - arguments - ); - } - - function unstable_wrap() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_wrap.apply( - this, - arguments - ); - } - - return Object.freeze({ - unstable_clear: unstable_clear, - unstable_getCurrent: unstable_getCurrent, - unstable_getThreadID: unstable_getThreadID, - unstable_subscribe: unstable_subscribe, - unstable_trace: unstable_trace, - unstable_unsubscribe: unstable_unsubscribe, - unstable_wrap: unstable_wrap, - }); -}); diff --git a/packages/scheduler/npm/umd/scheduler-tracing.profiling.min.js b/packages/scheduler/npm/umd/scheduler-tracing.profiling.min.js deleted file mode 100644 index a81bf8fe2f..0000000000 --- a/packages/scheduler/npm/umd/scheduler-tracing.profiling.min.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license React - * - * 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. - */ - -'use strict'; - -(function(global, factory) { - // eslint-disable-next-line no-unused-expressions - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('react'))) - : typeof define === 'function' && define.amd // eslint-disable-line no-undef - ? define(['react'], factory) // eslint-disable-line no-undef - : (global.SchedulerTracing = factory(global)); -})(this, function(global) { - function unstable_clear() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_clear.apply( - this, - arguments - ); - } - - function unstable_getCurrent() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_getCurrent.apply( - this, - arguments - ); - } - - function unstable_getThreadID() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_getThreadID.apply( - this, - arguments - ); - } - - function unstable_subscribe() { - // eslint-disable-next-line max-len - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_subscribe.apply( - this, - arguments - ); - } - - function unstable_trace() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_trace.apply( - this, - arguments - ); - } - - function unstable_unsubscribe() { - // eslint-disable-next-line max-len - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_unsubscribe.apply( - this, - arguments - ); - } - - function unstable_wrap() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.unstable_wrap.apply( - this, - arguments - ); - } - - return Object.freeze({ - unstable_clear: unstable_clear, - unstable_getCurrent: unstable_getCurrent, - unstable_getThreadID: unstable_getThreadID, - unstable_subscribe: unstable_subscribe, - unstable_trace: unstable_trace, - unstable_unsubscribe: unstable_unsubscribe, - unstable_wrap: unstable_wrap, - }); -}); diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json index 04daf3e6a3..e8ac46571b 100644 --- a/packages/scheduler/package.json +++ b/packages/scheduler/package.json @@ -25,8 +25,6 @@ "README.md", "build-info.json", "index.js", - "tracing.js", - "tracing-profiling.js", "unstable_mock.js", "unstable_no_dom.js", "unstable_post_task.js", diff --git a/packages/scheduler/src/Tracing.js b/packages/scheduler/src/Tracing.js deleted file mode 100644 index cc71b03f90..0000000000 --- a/packages/scheduler/src/Tracing.js +++ /dev/null @@ -1,264 +0,0 @@ -/** - * 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 {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; - -export type Interaction = {| - __count: number, - id: number, - name: string, - timestamp: number, -|}; - -export type Subscriber = { - // A new interaction has been created via the trace() method. - onInteractionTraced: (interaction: Interaction) => void, - - // All scheduled async work for an interaction has finished. - onInteractionScheduledWorkCompleted: (interaction: Interaction) => void, - - // New async work has been scheduled for a set of interactions. - // When this work is later run, onWorkStarted/onWorkStopped will be called. - // A batch of async/yieldy work may be scheduled multiple times before completing. - // In that case, onWorkScheduled may be called more than once before onWorkStopped. - // Work is scheduled by a "thread" which is identified by a unique ID. - onWorkScheduled: (interactions: Set, threadID: number) => void, - - // A batch of scheduled work has been canceled. - // Work is done by a "thread" which is identified by a unique ID. - onWorkCanceled: (interactions: Set, threadID: number) => void, - - // A batch of work has started for a set of interactions. - // When this work is complete, onWorkStopped will be called. - // Work is not always completed synchronously; yielding may occur in between. - // A batch of async/yieldy work may also be re-started before completing. - // In that case, onWorkStarted may be called more than once before onWorkStopped. - // Work is done by a "thread" which is identified by a unique ID. - onWorkStarted: (interactions: Set, threadID: number) => void, - - // A batch of work has completed for a set of interactions. - // Work is done by a "thread" which is identified by a unique ID. - onWorkStopped: (interactions: Set, threadID: number) => void, - ... -}; - -export type InteractionsRef = {|current: Set|}; - -export type SubscriberRef = {|current: Subscriber | null|}; - -const DEFAULT_THREAD_ID = 0; - -// Counters used to generate unique IDs. -let interactionIDCounter: number = 0; -let threadIDCounter: number = 0; - -// Set of currently traced interactions. -// Interactions "stack"– -// Meaning that newly traced interactions are appended to the previously active set. -// When an interaction goes out of scope, the previous set (if any) is restored. -let interactionsRef: InteractionsRef = (null: any); - -// Listener(s) to notify when interactions begin and end. -let subscriberRef: SubscriberRef = (null: any); - -if (enableSchedulerTracing) { - interactionsRef = { - current: new Set(), - }; - subscriberRef = { - current: null, - }; -} - -export {interactionsRef as __interactionsRef, subscriberRef as __subscriberRef}; - -export function unstable_clear(callback: Function): any { - if (!enableSchedulerTracing) { - return callback(); - } - - const prevInteractions = interactionsRef.current; - interactionsRef.current = new Set(); - - try { - return callback(); - } finally { - interactionsRef.current = prevInteractions; - } -} - -export function unstable_getCurrent(): Set | null { - if (!enableSchedulerTracing) { - return null; - } else { - return interactionsRef.current; - } -} - -export function unstable_getThreadID(): number { - return ++threadIDCounter; -} - -export function unstable_trace( - name: string, - timestamp: number, - callback: Function, - threadID: number = DEFAULT_THREAD_ID, -): any { - if (!enableSchedulerTracing) { - return callback(); - } - - const interaction: Interaction = { - __count: 1, - id: interactionIDCounter++, - name, - timestamp, - }; - - const prevInteractions = interactionsRef.current; - - // Traced interactions should stack/accumulate. - // To do that, clone the current interactions. - // The previous set will be restored upon completion. - const interactions = new Set(prevInteractions); - interactions.add(interaction); - interactionsRef.current = interactions; - - const subscriber = subscriberRef.current; - let returnValue; - - try { - if (subscriber !== null) { - subscriber.onInteractionTraced(interaction); - } - } finally { - try { - if (subscriber !== null) { - subscriber.onWorkStarted(interactions, threadID); - } - } finally { - try { - returnValue = callback(); - } finally { - interactionsRef.current = prevInteractions; - - try { - if (subscriber !== null) { - subscriber.onWorkStopped(interactions, threadID); - } - } finally { - interaction.__count--; - - // If no async work was scheduled for this interaction, - // Notify subscribers that it's completed. - if (subscriber !== null && interaction.__count === 0) { - subscriber.onInteractionScheduledWorkCompleted(interaction); - } - } - } - } - } - - return returnValue; -} - -export function unstable_wrap( - callback: Function, - threadID: number = DEFAULT_THREAD_ID, -): Function { - if (!enableSchedulerTracing) { - return callback; - } - - const wrappedInteractions = interactionsRef.current; - - let subscriber = subscriberRef.current; - if (subscriber !== null) { - subscriber.onWorkScheduled(wrappedInteractions, threadID); - } - - // Update the pending async work count for the current interactions. - // Update after calling subscribers in case of error. - wrappedInteractions.forEach(interaction => { - interaction.__count++; - }); - - let hasRun = false; - - function wrapped() { - const prevInteractions = interactionsRef.current; - interactionsRef.current = wrappedInteractions; - - subscriber = subscriberRef.current; - - try { - let returnValue; - - try { - if (subscriber !== null) { - subscriber.onWorkStarted(wrappedInteractions, threadID); - } - } finally { - try { - returnValue = callback.apply(undefined, arguments); - } finally { - interactionsRef.current = prevInteractions; - - if (subscriber !== null) { - subscriber.onWorkStopped(wrappedInteractions, threadID); - } - } - } - - return returnValue; - } finally { - if (!hasRun) { - // We only expect a wrapped function to be executed once, - // But in the event that it's executed more than once– - // Only decrement the outstanding interaction counts once. - hasRun = true; - - // Update pending async counts for all wrapped interactions. - // If this was the last scheduled async work for any of them, - // Mark them as completed. - wrappedInteractions.forEach(interaction => { - interaction.__count--; - - if (subscriber !== null && interaction.__count === 0) { - subscriber.onInteractionScheduledWorkCompleted(interaction); - } - }); - } - } - } - - wrapped.cancel = function cancel() { - subscriber = subscriberRef.current; - - try { - if (subscriber !== null) { - subscriber.onWorkCanceled(wrappedInteractions, threadID); - } - } finally { - // Update pending async counts for all wrapped interactions. - // If this was the last scheduled async work for any of them, - // Mark them as completed. - wrappedInteractions.forEach(interaction => { - interaction.__count--; - - if (subscriber && interaction.__count === 0) { - subscriber.onInteractionScheduledWorkCompleted(interaction); - } - }); - } - }; - - return wrapped; -} diff --git a/packages/scheduler/src/TracingSubscriptions.js b/packages/scheduler/src/TracingSubscriptions.js deleted file mode 100644 index 9fd687adf4..0000000000 --- a/packages/scheduler/src/TracingSubscriptions.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * 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 {Interaction, Subscriber} from './Tracing'; - -import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; -import {__subscriberRef} from './Tracing'; - -let subscribers: Set = (null: any); -if (enableSchedulerTracing) { - subscribers = new Set(); -} - -export function unstable_subscribe(subscriber: Subscriber): void { - if (enableSchedulerTracing) { - subscribers.add(subscriber); - - if (subscribers.size === 1) { - __subscriberRef.current = { - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled, - onWorkScheduled, - onWorkStarted, - onWorkStopped, - }; - } - } -} - -export function unstable_unsubscribe(subscriber: Subscriber): void { - if (enableSchedulerTracing) { - subscribers.delete(subscriber); - - if (subscribers.size === 0) { - __subscriberRef.current = null; - } - } -} - -function onInteractionTraced(interaction: Interaction): void { - let didCatchError = false; - let caughtError = null; - - subscribers.forEach(subscriber => { - try { - subscriber.onInteractionTraced(interaction); - } catch (error) { - if (!didCatchError) { - didCatchError = true; - caughtError = error; - } - } - }); - - if (didCatchError) { - throw caughtError; - } -} - -function onInteractionScheduledWorkCompleted(interaction: Interaction): void { - let didCatchError = false; - let caughtError = null; - - subscribers.forEach(subscriber => { - try { - subscriber.onInteractionScheduledWorkCompleted(interaction); - } catch (error) { - if (!didCatchError) { - didCatchError = true; - caughtError = error; - } - } - }); - - if (didCatchError) { - throw caughtError; - } -} - -function onWorkScheduled( - interactions: Set, - threadID: number, -): void { - let didCatchError = false; - let caughtError = null; - - subscribers.forEach(subscriber => { - try { - subscriber.onWorkScheduled(interactions, threadID); - } catch (error) { - if (!didCatchError) { - didCatchError = true; - caughtError = error; - } - } - }); - - if (didCatchError) { - throw caughtError; - } -} - -function onWorkStarted(interactions: Set, threadID: number): void { - let didCatchError = false; - let caughtError = null; - - subscribers.forEach(subscriber => { - try { - subscriber.onWorkStarted(interactions, threadID); - } catch (error) { - if (!didCatchError) { - didCatchError = true; - caughtError = error; - } - } - }); - - if (didCatchError) { - throw caughtError; - } -} - -function onWorkStopped(interactions: Set, threadID: number): void { - let didCatchError = false; - let caughtError = null; - - subscribers.forEach(subscriber => { - try { - subscriber.onWorkStopped(interactions, threadID); - } catch (error) { - if (!didCatchError) { - didCatchError = true; - caughtError = error; - } - } - }); - - if (didCatchError) { - throw caughtError; - } -} - -function onWorkCanceled( - interactions: Set, - threadID: number, -): void { - let didCatchError = false; - let caughtError = null; - - subscribers.forEach(subscriber => { - try { - subscriber.onWorkCanceled(interactions, threadID); - } catch (error) { - if (!didCatchError) { - didCatchError = true; - caughtError = error; - } - } - }); - - if (didCatchError) { - throw caughtError; - } -} diff --git a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js index d6dfeb2081..fde4ee8af2 100644 --- a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js +++ b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js @@ -37,30 +37,10 @@ describe('Scheduling UMD bundle', () => { global.MessageChannel = undefined; }); - function filterPrivateKeys(name) { - // Be very careful adding things to this filter! - // It's easy to introduce bugs by doing it: - // https://github.com/facebook/react/issues/14904 - switch (name) { - case '__interactionsRef': - case '__subscriberRef': - // Don't forward these. (TODO: why?) - return false; - default: - return true; - } - } - function validateForwardedAPIs(api, forwardedAPIs) { - const apiKeys = Object.keys(api) - .filter(filterPrivateKeys) - .sort(); + const apiKeys = Object.keys(api).sort(); forwardedAPIs.forEach(forwardedAPI => { - expect( - Object.keys(forwardedAPI) - .filter(filterPrivateKeys) - .sort(), - ).toEqual(apiKeys); + expect(Object.keys(forwardedAPI).sort()).toEqual(apiKeys); }); } @@ -78,19 +58,4 @@ describe('Scheduling UMD bundle', () => { secretAPI.Scheduler, ]); }); - - it('should define the same tracing API', () => { - const api = require('../../tracing'); - const umdAPIDev = require('../../npm/umd/scheduler-tracing.development'); - const umdAPIProd = require('../../npm/umd/scheduler-tracing.production.min'); - const umdAPIProfiling = require('../../npm/umd/scheduler-tracing.profiling.min'); - const secretAPI = require('react/src/forks/ReactSharedInternals.umd') - .default; - validateForwardedAPIs(api, [ - umdAPIDev, - umdAPIProd, - umdAPIProfiling, - secretAPI.SchedulerTracing, - ]); - }); }); diff --git a/packages/scheduler/src/__tests__/Tracing-test.internal.js b/packages/scheduler/src/__tests__/Tracing-test.internal.js deleted file mode 100644 index 658b69313c..0000000000 --- a/packages/scheduler/src/__tests__/Tracing-test.internal.js +++ /dev/null @@ -1,375 +0,0 @@ -/** - * 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. - * - * @jest-environment node - */ -'use strict'; - -describe('Tracing', () => { - let SchedulerTracing; - let ReactFeatureFlags; - - let advanceTimeBy; - let currentTime; - - function loadModules({enableSchedulerTracing}) { - jest.resetModules(); - jest.useFakeTimers(); - - currentTime = 0; - Date.now = jest.fn().mockImplementation(() => currentTime); - - advanceTimeBy = amount => { - currentTime += amount; - }; - - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; - - SchedulerTracing = require('scheduler/tracing'); - } - - describe('enableSchedulerTracing enabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: true})); - - it('should return the value of a traced function', () => { - expect( - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => 123), - ).toBe(123); - }); - - it('should return the value of a clear function', () => { - expect(SchedulerTracing.unstable_clear(() => 123)).toBe(123); - }); - - it('should return the value of a wrapped function', () => { - let wrapped; - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { - wrapped = SchedulerTracing.unstable_wrap(() => 123); - }); - expect(wrapped()).toBe(123); - }); - - it('should pass arguments through to a wrapped function', done => { - let wrapped; - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { - wrapped = SchedulerTracing.unstable_wrap((param1, param2) => { - expect(param1).toBe('foo'); - expect(param2).toBe('bar'); - done(); - }); - }); - wrapped('foo', 'bar'); - }); - - it('should return an empty set when outside of a traced event', () => { - expect(SchedulerTracing.unstable_getCurrent()).toContainNoInteractions(); - }); - - it('should report the traced interaction from within the trace callback', done => { - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('some event', currentTime, () => { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'some event', timestamp: 100}, - ]); - - done(); - }); - }); - - it('should report the traced interaction from within wrapped callbacks', done => { - let wrappedIndirection; - - function indirection() { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'some event', timestamp: 100}, - ]); - - done(); - } - - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('some event', currentTime, () => { - wrappedIndirection = SchedulerTracing.unstable_wrap(indirection); - }); - - advanceTimeBy(50); - - wrappedIndirection(); - }); - - it('should clear the interaction stack for traced callbacks', () => { - let innerTestReached = false; - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - - SchedulerTracing.unstable_clear(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions( - [], - ); - - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'inner event'}, - ]); - - innerTestReached = true; - }); - }); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - }); - - expect(innerTestReached).toBe(true); - }); - - it('should clear the interaction stack for wrapped callbacks', () => { - let innerTestReached = false; - let wrappedIndirection; - - const indirection = jest.fn(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - - SchedulerTracing.unstable_clear(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions( - [], - ); - - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'inner event'}, - ]); - - innerTestReached = true; - }); - }); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event'}, - ]); - }); - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - wrappedIndirection = SchedulerTracing.unstable_wrap(indirection); - }); - - wrappedIndirection(); - - expect(innerTestReached).toBe(true); - }); - - it('should support nested traced events', done => { - advanceTimeBy(100); - - let innerIndirectionTraced = false; - let outerIndirectionTraced = false; - - function innerIndirection() { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - {name: 'inner event', timestamp: 150}, - ]); - - innerIndirectionTraced = true; - } - - function outerIndirection() { - const interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - outerIndirectionTraced = true; - } - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - // Verify the current traced event - let interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - advanceTimeBy(50); - - const wrapperOuterIndirection = SchedulerTracing.unstable_wrap( - outerIndirection, - ); - - let wrapperInnerIndirection; - let innerEventTraced = false; - - // Verify that a nested event is properly traced - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - {name: 'inner event', timestamp: 150}, - ]); - - // Verify that a wrapped outer callback is properly traced - wrapperOuterIndirection(); - expect(outerIndirectionTraced).toBe(true); - - wrapperInnerIndirection = SchedulerTracing.unstable_wrap( - innerIndirection, - ); - - innerEventTraced = true; - }); - - expect(innerEventTraced).toBe(true); - - // Verify that the original event is restored - interactions = SchedulerTracing.unstable_getCurrent(); - expect(interactions).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - // Verify that a wrapped nested callback is properly traced - wrapperInnerIndirection(); - expect(innerIndirectionTraced).toBe(true); - - done(); - }); - }); - - describe('error handling', () => { - it('should reset state appropriately when an error occurs in a trace callback', done => { - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(() => { - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - throw Error('intentional'); - }); - }).toThrow(); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - done(); - }); - }); - - it('should reset state appropriately when an error occurs in a wrapped callback', done => { - advanceTimeBy(100); - - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - let wrappedCallback; - - SchedulerTracing.unstable_trace('inner event', currentTime, () => { - wrappedCallback = SchedulerTracing.unstable_wrap(() => { - throw Error('intentional'); - }); - }); - - expect(wrappedCallback).toThrow(); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'outer event', timestamp: 100}, - ]); - - done(); - }); - }); - }); - - describe('advanced integration', () => { - it('should return a unique threadID per request', () => { - expect(SchedulerTracing.unstable_getThreadID()).not.toBe( - SchedulerTracing.unstable_getThreadID(), - ); - }); - - it('should expose the current set of interactions to be externally manipulated', () => { - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(SchedulerTracing.__interactionsRef.current).toBe( - SchedulerTracing.unstable_getCurrent(), - ); - - SchedulerTracing.__interactionsRef.current = new Set([ - {name: 'override event'}, - ]); - - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - {name: 'override event'}, - ]); - }); - }); - - it('should expose a subscriber ref to be externally manipulated', () => { - SchedulerTracing.unstable_trace('outer event', currentTime, () => { - expect(SchedulerTracing.__subscriberRef).toEqual({ - current: null, - }); - }); - }); - }); - }); - - describe('enableSchedulerTracing disabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: false})); - - it('should return the value of a traced function', () => { - expect( - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => 123), - ).toBe(123); - }); - - it('should return the value of a wrapped function', () => { - let wrapped; - SchedulerTracing.unstable_trace('arbitrary', currentTime, () => { - wrapped = SchedulerTracing.unstable_wrap(() => 123); - }); - expect(wrapped()).toBe(123); - }); - - it('should return null for traced interactions', () => { - expect(SchedulerTracing.unstable_getCurrent()).toBe(null); - }); - - it('should execute traced callbacks', done => { - SchedulerTracing.unstable_trace('some event', currentTime, () => { - expect(SchedulerTracing.unstable_getCurrent()).toBe(null); - - done(); - }); - }); - - it('should return the value of a clear function', () => { - expect(SchedulerTracing.unstable_clear(() => 123)).toBe(123); - }); - - it('should execute wrapped callbacks', done => { - const wrappedCallback = SchedulerTracing.unstable_wrap(() => { - expect(SchedulerTracing.unstable_getCurrent()).toBe(null); - - done(); - }); - - wrappedCallback(); - }); - - describe('advanced integration', () => { - it('should not create unnecessary objects', () => { - expect(SchedulerTracing.__interactionsRef).toBe(null); - }); - }); - }); -}); diff --git a/packages/scheduler/src/__tests__/Tracing-test.js b/packages/scheduler/src/__tests__/Tracing-test.js deleted file mode 100644 index 5259be4792..0000000000 --- a/packages/scheduler/src/__tests__/Tracing-test.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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. - * - * @jest-environment node - */ -'use strict'; - -describe('Tracing', () => { - let SchedulerTracing; - - beforeEach(() => { - jest.resetModules(); - - SchedulerTracing = require('scheduler/tracing'); - }); - - it('should return the value of a traced function', () => { - expect(SchedulerTracing.unstable_trace('arbitrary', 0, () => 123)).toBe( - 123, - ); - }); - - it('should return the value of a wrapped function', () => { - let wrapped; - SchedulerTracing.unstable_trace('arbitrary', 0, () => { - wrapped = SchedulerTracing.unstable_wrap(() => 123); - }); - expect(wrapped()).toBe(123); - }); - - it('should execute traced callbacks', done => { - SchedulerTracing.unstable_trace('some event', 0, () => { - done(); - }); - }); - - it('should return the value of a clear function', () => { - expect(SchedulerTracing.unstable_clear(() => 123)).toBe(123); - }); - - it('should execute wrapped callbacks', done => { - const wrappedCallback = SchedulerTracing.unstable_wrap(() => { - done(); - }); - - wrappedCallback(); - }); -}); diff --git a/packages/scheduler/src/__tests__/TracingSubscriptions-test.internal.js b/packages/scheduler/src/__tests__/TracingSubscriptions-test.internal.js deleted file mode 100644 index 6eff59a6cf..0000000000 --- a/packages/scheduler/src/__tests__/TracingSubscriptions-test.internal.js +++ /dev/null @@ -1,621 +0,0 @@ -/** - * 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. - * - * @jest-environment node - */ -'use strict'; - -describe('TracingSubscriptions', () => { - let SchedulerTracing; - let ReactFeatureFlags; - - let currentTime; - - let onInteractionScheduledWorkCompleted; - let onInteractionTraced; - let onWorkCanceled; - let onWorkScheduled; - let onWorkStarted; - let onWorkStopped; - let throwInOnInteractionScheduledWorkCompleted; - let throwInOnInteractionTraced; - let throwInOnWorkCanceled; - let throwInOnWorkScheduled; - let throwInOnWorkStarted; - let throwInOnWorkStopped; - let firstSubscriber; - let secondSubscriber; - - const firstEvent = {id: 0, name: 'first', timestamp: 0}; - const secondEvent = {id: 1, name: 'second', timestamp: 0}; - const threadID = 123; - - function loadModules({enableSchedulerTracing, autoSubscribe = true}) { - jest.resetModules(); - jest.useFakeTimers(); - - currentTime = 0; - - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; - - SchedulerTracing = require('scheduler/tracing'); - - throwInOnInteractionScheduledWorkCompleted = false; - throwInOnInteractionTraced = false; - throwInOnWorkCanceled = false; - throwInOnWorkScheduled = false; - throwInOnWorkStarted = false; - throwInOnWorkStopped = false; - - onInteractionScheduledWorkCompleted = jest.fn(() => { - if (throwInOnInteractionScheduledWorkCompleted) { - throw Error('Expected error onInteractionScheduledWorkCompleted'); - } - }); - onInteractionTraced = jest.fn(() => { - if (throwInOnInteractionTraced) { - throw Error('Expected error onInteractionTraced'); - } - }); - onWorkCanceled = jest.fn(() => { - if (throwInOnWorkCanceled) { - throw Error('Expected error onWorkCanceled'); - } - }); - onWorkScheduled = jest.fn(() => { - if (throwInOnWorkScheduled) { - throw Error('Expected error onWorkScheduled'); - } - }); - onWorkStarted = jest.fn(() => { - if (throwInOnWorkStarted) { - throw Error('Expected error onWorkStarted'); - } - }); - onWorkStopped = jest.fn(() => { - if (throwInOnWorkStopped) { - throw Error('Expected error onWorkStopped'); - } - }); - - firstSubscriber = { - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled, - onWorkScheduled, - onWorkStarted, - onWorkStopped, - }; - - secondSubscriber = { - onInteractionScheduledWorkCompleted: jest.fn(), - onInteractionTraced: jest.fn(), - onWorkCanceled: jest.fn(), - onWorkScheduled: jest.fn(), - onWorkStarted: jest.fn(), - onWorkStopped: jest.fn(), - }; - - if (autoSubscribe) { - SchedulerTracing.unstable_subscribe(firstSubscriber); - SchedulerTracing.unstable_subscribe(secondSubscriber); - } - } - - describe('enabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: true})); - - it('should lazily subscribe to tracing and unsubscribe again if there are no external subscribers', () => { - loadModules({enableSchedulerTracing: true, autoSubscribe: false}); - - expect(SchedulerTracing.__subscriberRef.current).toBe(null); - SchedulerTracing.unstable_subscribe(firstSubscriber); - expect(SchedulerTracing.__subscriberRef.current).toBeDefined(); - SchedulerTracing.unstable_subscribe(secondSubscriber); - expect(SchedulerTracing.__subscriberRef.current).toBeDefined(); - SchedulerTracing.unstable_unsubscribe(secondSubscriber); - expect(SchedulerTracing.__subscriberRef.current).toBeDefined(); - SchedulerTracing.unstable_unsubscribe(firstSubscriber); - expect(SchedulerTracing.__subscriberRef.current).toBe(null); - }); - - describe('error handling', () => { - it('should cover onInteractionTraced/onWorkStarted within', done => { - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - const mock = jest.fn(); - - // It should call the callback before re-throwing - throwInOnInteractionTraced = true; - expect(() => - SchedulerTracing.unstable_trace( - secondEvent.name, - currentTime, - mock, - threadID, - ), - ).toThrow('Expected error onInteractionTraced'); - throwInOnInteractionTraced = false; - expect(mock).toHaveBeenCalledTimes(1); - - throwInOnWorkStarted = true; - expect(() => - SchedulerTracing.unstable_trace( - secondEvent.name, - currentTime, - mock, - threadID, - ), - ).toThrow('Expected error onWorkStarted'); - expect(mock).toHaveBeenCalledTimes(2); - - // It should restore the previous/outer interactions - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - firstEvent, - ]); - - // It should call other subscribers despite the earlier error - expect(secondSubscriber.onInteractionTraced).toHaveBeenCalledTimes(3); - expect(secondSubscriber.onWorkStarted).toHaveBeenCalledTimes(3); - - done(); - }); - }); - - it('should cover onWorkStopped within trace', done => { - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - let innerInteraction; - const mock = jest.fn(() => { - innerInteraction = Array.from( - SchedulerTracing.unstable_getCurrent(), - )[1]; - }); - - throwInOnWorkStopped = true; - expect(() => - SchedulerTracing.unstable_trace( - secondEvent.name, - currentTime, - mock, - ), - ).toThrow('Expected error onWorkStopped'); - throwInOnWorkStopped = false; - - // It should restore the previous/outer interactions - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - firstEvent, - ]); - - // It should update the interaction count so as not to interfere with subsequent calls - expect(innerInteraction.__count).toBe(0); - - // It should call other subscribers despite the earlier error - expect(secondSubscriber.onWorkStopped).toHaveBeenCalledTimes(1); - - done(); - }); - }); - - it('should cover onInteractionScheduledWorkCompleted within trace', done => { - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - const mock = jest.fn(); - - throwInOnInteractionScheduledWorkCompleted = true; - expect(() => - SchedulerTracing.unstable_trace( - secondEvent.name, - currentTime, - mock, - ), - ).toThrow('Expected error onInteractionScheduledWorkCompleted'); - throwInOnInteractionScheduledWorkCompleted = false; - - // It should restore the previous/outer interactions - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - firstEvent, - ]); - - // It should call other subscribers despite the earlier error - expect( - secondSubscriber.onInteractionScheduledWorkCompleted, - ).toHaveBeenCalledTimes(1); - - done(); - }); - }); - - it('should cover the callback within trace', done => { - expect(onWorkStarted).not.toHaveBeenCalled(); - expect(onWorkStopped).not.toHaveBeenCalled(); - - expect(() => { - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - throw Error('Expected error callback'); - }); - }).toThrow('Expected error callback'); - - expect(onWorkStarted).toHaveBeenCalledTimes(1); - expect(onWorkStopped).toHaveBeenCalledTimes(1); - - done(); - }); - - it('should cover onWorkScheduled within wrap', done => { - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - const interaction = Array.from( - SchedulerTracing.unstable_getCurrent(), - )[0]; - const beforeCount = interaction.__count; - - throwInOnWorkScheduled = true; - expect(() => SchedulerTracing.unstable_wrap(() => {})).toThrow( - 'Expected error onWorkScheduled', - ); - - // It should not update the interaction count so as not to interfere with subsequent calls - expect(interaction.__count).toBe(beforeCount); - - // It should call other subscribers despite the earlier error - expect(secondSubscriber.onWorkScheduled).toHaveBeenCalledTimes(1); - - done(); - }); - }); - - it('should cover onWorkStarted within wrap', () => { - const mock = jest.fn(); - let interaction, wrapped; - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - wrapped = SchedulerTracing.unstable_wrap(mock); - }); - expect(interaction.__count).toBe(1); - - throwInOnWorkStarted = true; - expect(wrapped).toThrow('Expected error onWorkStarted'); - - // It should call the callback before re-throwing - expect(mock).toHaveBeenCalledTimes(1); - - // It should update the interaction count so as not to interfere with subsequent calls - expect(interaction.__count).toBe(0); - - // It should call other subscribers despite the earlier error - expect(secondSubscriber.onWorkStarted).toHaveBeenCalledTimes(2); - }); - - it('should cover onWorkStopped within wrap', done => { - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - const outerInteraction = Array.from( - SchedulerTracing.unstable_getCurrent(), - )[0]; - expect(outerInteraction.__count).toBe(1); - - let wrapped; - let innerInteraction; - - SchedulerTracing.unstable_trace(secondEvent.name, currentTime, () => { - innerInteraction = Array.from( - SchedulerTracing.unstable_getCurrent(), - )[1]; - expect(outerInteraction.__count).toBe(1); - expect(innerInteraction.__count).toBe(1); - - wrapped = SchedulerTracing.unstable_wrap(jest.fn()); - expect(outerInteraction.__count).toBe(2); - expect(innerInteraction.__count).toBe(2); - }); - - expect(outerInteraction.__count).toBe(2); - expect(innerInteraction.__count).toBe(1); - - throwInOnWorkStopped = true; - expect(wrapped).toThrow('Expected error onWorkStopped'); - throwInOnWorkStopped = false; - - // It should restore the previous interactions - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - outerInteraction, - ]); - - // It should update the interaction count so as not to interfere with subsequent calls - expect(outerInteraction.__count).toBe(1); - expect(innerInteraction.__count).toBe(0); - - expect(secondSubscriber.onWorkStopped).toHaveBeenCalledTimes(2); - - done(); - }); - }); - - it('should cover the callback within wrap', done => { - expect(onWorkStarted).not.toHaveBeenCalled(); - expect(onWorkStopped).not.toHaveBeenCalled(); - - let wrapped; - let interaction; - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - wrapped = SchedulerTracing.unstable_wrap(() => { - throw Error('Expected error wrap'); - }); - }); - - expect(onWorkStarted).toHaveBeenCalledTimes(1); - expect(onWorkStopped).toHaveBeenCalledTimes(1); - - expect(wrapped).toThrow('Expected error wrap'); - - expect(onWorkStarted).toHaveBeenCalledTimes(2); - expect(onWorkStopped).toHaveBeenCalledTimes(2); - expect(onWorkStopped).toHaveBeenLastNotifiedOfWork([interaction]); - - done(); - }); - - it('should cover onWorkCanceled within wrap', () => { - let interaction, wrapped; - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; - wrapped = SchedulerTracing.unstable_wrap(jest.fn()); - }); - expect(interaction.__count).toBe(1); - - throwInOnWorkCanceled = true; - expect(wrapped.cancel).toThrow('Expected error onWorkCanceled'); - - expect(onWorkCanceled).toHaveBeenCalledTimes(1); - - // It should update the interaction count so as not to interfere with subsequent calls - expect(interaction.__count).toBe(0); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(firstEvent); - - // It should call other subscribers despite the earlier error - expect(secondSubscriber.onWorkCanceled).toHaveBeenCalledTimes(1); - }); - }); - - it('calls lifecycle methods for trace', () => { - expect(onInteractionTraced).not.toHaveBeenCalled(); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - SchedulerTracing.unstable_trace( - firstEvent.name, - currentTime, - () => { - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - firstEvent, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onWorkStarted).toHaveBeenCalledTimes(1); - expect(onWorkStarted).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent]), - threadID, - ); - expect(onWorkStopped).not.toHaveBeenCalled(); - - SchedulerTracing.unstable_trace( - secondEvent.name, - currentTime, - () => { - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - secondEvent, - ); - expect( - onInteractionScheduledWorkCompleted, - ).not.toHaveBeenCalled(); - expect(onWorkStarted).toHaveBeenCalledTimes(2); - expect(onWorkStarted).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent, secondEvent]), - threadID, - ); - expect(onWorkStopped).not.toHaveBeenCalled(); - }, - threadID, - ); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(secondEvent); - expect(onWorkStopped).toHaveBeenCalledTimes(1); - expect(onWorkStopped).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent, secondEvent]), - threadID, - ); - }, - threadID, - ); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(firstEvent); - expect(onWorkScheduled).not.toHaveBeenCalled(); - expect(onWorkCanceled).not.toHaveBeenCalled(); - expect(onWorkStarted).toHaveBeenCalledTimes(2); - expect(onWorkStopped).toHaveBeenCalledTimes(2); - expect(onWorkStopped).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent]), - threadID, - ); - }); - - it('calls lifecycle methods for wrap', () => { - const unwrapped = jest.fn(); - let wrapped; - - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - firstEvent, - ); - - SchedulerTracing.unstable_trace(secondEvent.name, currentTime, () => { - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - secondEvent, - ); - - wrapped = SchedulerTracing.unstable_wrap(unwrapped, threadID); - expect(onWorkScheduled).toHaveBeenCalledTimes(1); - expect(onWorkScheduled).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent, secondEvent]), - threadID, - ); - }); - }); - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - wrapped(); - expect(unwrapped).toHaveBeenCalled(); - - expect(onWorkScheduled).toHaveBeenCalledTimes(1); - expect(onWorkCanceled).not.toHaveBeenCalled(); - expect(onWorkStarted).toHaveBeenCalledTimes(3); - expect(onWorkStarted).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent, secondEvent]), - threadID, - ); - expect(onWorkStopped).toHaveBeenCalledTimes(3); - expect(onWorkStopped).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent, secondEvent]), - threadID, - ); - - expect( - onInteractionScheduledWorkCompleted.mock.calls[0][0], - ).toMatchInteraction(firstEvent); - expect( - onInteractionScheduledWorkCompleted.mock.calls[1][0], - ).toMatchInteraction(secondEvent); - }); - - it('should call the correct interaction subscriber methods when a wrapped callback is canceled', () => { - const fnOne = jest.fn(); - const fnTwo = jest.fn(); - let wrappedOne, wrappedTwo; - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - wrappedOne = SchedulerTracing.unstable_wrap(fnOne, threadID); - SchedulerTracing.unstable_trace(secondEvent.name, currentTime, () => { - wrappedTwo = SchedulerTracing.unstable_wrap(fnTwo, threadID); - }); - }); - - expect(onInteractionTraced).toHaveBeenCalledTimes(2); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onWorkCanceled).not.toHaveBeenCalled(); - expect(onWorkStarted).toHaveBeenCalledTimes(2); - expect(onWorkStopped).toHaveBeenCalledTimes(2); - - wrappedTwo.cancel(); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(secondEvent); - expect(onWorkCanceled).toHaveBeenCalledTimes(1); - expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent, secondEvent]), - threadID, - ); - - wrappedOne.cancel(); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(firstEvent); - expect(onWorkCanceled).toHaveBeenCalledTimes(2); - expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork( - new Set([firstEvent]), - threadID, - ); - - expect(fnOne).not.toHaveBeenCalled(); - expect(fnTwo).not.toHaveBeenCalled(); - }); - - it('should not end an interaction twice if wrap is used to schedule follow up work within another wrap', () => { - const fnOne = jest.fn(() => { - wrappedTwo = SchedulerTracing.unstable_wrap(fnTwo, threadID); - }); - const fnTwo = jest.fn(); - let wrappedOne, wrappedTwo; - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - wrappedOne = SchedulerTracing.unstable_wrap(fnOne, threadID); - }); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - wrappedOne(); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - wrappedTwo(); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(firstEvent); - }); - - it('should not decrement the interaction count twice if a wrapped function is run twice', () => { - const unwrappedOne = jest.fn(); - const unwrappedTwo = jest.fn(); - let wrappedOne, wrappedTwo; - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => { - wrappedOne = SchedulerTracing.unstable_wrap(unwrappedOne, threadID); - wrappedTwo = SchedulerTracing.unstable_wrap(unwrappedTwo, threadID); - }); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - wrappedOne(); - - expect(unwrappedOne).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - wrappedOne(); - - expect(unwrappedOne).toHaveBeenCalledTimes(2); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - wrappedTwo(); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(firstEvent); - }); - - it('should unsubscribe', () => { - SchedulerTracing.unstable_unsubscribe(firstSubscriber); - SchedulerTracing.unstable_trace(firstEvent.name, currentTime, () => {}); - - expect(onInteractionTraced).not.toHaveBeenCalled(); - }); - }); - - describe('disabled', () => { - beforeEach(() => loadModules({enableSchedulerTracing: false})); - - // TODO - }); -}); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index c8c2375bab..757d9ab28b 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -51,9 +51,6 @@ export const enableProfilerNestedUpdatePhase = false; // This callback accepts the component type (class instance or function) the update is scheduled for. export const enableProfilerNestedUpdateScheduledHook = false; -// Trace which interactions trigger each commit. -export const enableSchedulerTracing = __PROFILE__; - // Track which Fiber(s) schedule render work. export const enableUpdaterTracking = __PROFILE__; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 19c89f77f9..03b70aae07 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -155,8 +155,6 @@ export type MutableSource> = {| // This doesn't require a value to be passed to either handler. export interface Wakeable { then(onFulfill: () => mixed, onReject: () => mixed): void | Wakeable; - // Special flag to opt out of tracing interactions across a Suspense boundary. - __reactDoNotTraceInteractions?: boolean; } // The subset of a Promise that React APIs rely on. This resolves a value. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index e712288cf1..ea9224d5a9 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -17,7 +17,6 @@ export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = false; export const enableProfilerNestedUpdatePhase = false; export const enableProfilerNestedUpdateScheduledHook = false; -export const enableSchedulerTracing = __PROFILE__; export const enableUpdaterTracking = false; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 18375388be..251bb310c1 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -19,7 +19,6 @@ export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = false; export const enableProfilerNestedUpdatePhase = false; export const enableProfilerNestedUpdateScheduledHook = false; -export const enableSchedulerTracing = __PROFILE__; export const enableUpdaterTracking = false; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index d7605e5ebb..aeee968922 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -19,7 +19,6 @@ export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = false; export const enableProfilerNestedUpdatePhase = false; export const enableProfilerNestedUpdateScheduledHook = false; -export const enableSchedulerTracing = __PROFILE__; export const enableUpdaterTracking = false; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index e3e4ea0f58..88f2287cd8 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -19,7 +19,6 @@ export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = false; export const enableProfilerNestedUpdatePhase = false; export const enableProfilerNestedUpdateScheduledHook = false; -export const enableSchedulerTracing = __PROFILE__; export const enableUpdaterTracking = false; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 4d762c0827..5d43505fa1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -19,7 +19,6 @@ export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = false; export const enableProfilerNestedUpdatePhase = false; export const enableProfilerNestedUpdateScheduledHook = false; -export const enableSchedulerTracing = __PROFILE__; export const enableUpdaterTracking = false; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index bd4137d3e7..0edbfad4ab 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -19,7 +19,6 @@ export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = false; export const enableProfilerNestedUpdatePhase = false; export const enableProfilerNestedUpdateScheduledHook = false; -export const enableSchedulerTracing = __PROFILE__; export const enableUpdaterTracking = false; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index dacb018e3b..98dbae4505 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -19,7 +19,6 @@ export const enableProfilerTimer = false; export const enableProfilerCommitHooks = false; export const enableProfilerNestedUpdatePhase = false; export const enableProfilerNestedUpdateScheduledHook = false; -export const enableSchedulerTracing = false; export const enableUpdaterTracking = false; export const enableSuspenseServerRenderer = true; export const enableSelectiveHydration = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 871d4cc48f..afae32b408 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -55,7 +55,6 @@ export const enableSchedulingProfiler = // Note: we'll want to remove this when we to userland implementation. // For now, we'll turn it on for everyone because it's *already* on for everyone in practice. // At least this will let us stop shipping implementation to all users. -export const enableSchedulerTracing = true; export const enableSchedulerDebugging = true; export const warnAboutDeprecatedLifecycles = true; diff --git a/packages/shared/forks/SchedulerTracing.umd.js b/packages/shared/forks/SchedulerTracing.umd.js deleted file mode 100644 index 2fb835e95b..0000000000 --- a/packages/shared/forks/SchedulerTracing.umd.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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 * as React from 'react'; - -const ReactInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; - -const { - __interactionsRef, - __subscriberRef, - unstable_clear, - unstable_getCurrent, - unstable_getThreadID, - unstable_subscribe, - unstable_trace, - unstable_unsubscribe, - unstable_wrap, -} = ReactInternals.SchedulerTracing; - -export { - __interactionsRef, - __subscriberRef, - unstable_clear, - unstable_getCurrent, - unstable_getThreadID, - unstable_subscribe, - unstable_trace, - unstable_unsubscribe, - unstable_wrap, -}; diff --git a/scripts/jest/matchers/interactionTracingMatchers.js b/scripts/jest/matchers/interactionTracingMatchers.js deleted file mode 100644 index 8281d7d862..0000000000 --- a/scripts/jest/matchers/interactionTracingMatchers.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict'; - -const jestDiff = require('jest-diff').default; - -function toContainNoInteractions(actualSet) { - return { - message: () => - this.isNot - ? `Expected interactions but there were none.` - : `Expected no interactions but there were ${actualSet.size}.`, - pass: actualSet.size === 0, - }; -} - -function toHaveBeenLastNotifiedOfInteraction( - mockFunction, - expectedInteraction -) { - const calls = mockFunction.mock.calls; - if (calls.length === 0) { - return { - message: () => 'Mock function was not called', - pass: false, - }; - } - - const [actualInteraction] = calls[calls.length - 1]; - - return toMatchInteraction(actualInteraction, expectedInteraction); -} - -function toHaveBeenLastNotifiedOfWork( - mockFunction, - expectedInteractions, - expectedThreadID = undefined -) { - const calls = mockFunction.mock.calls; - if (calls.length === 0) { - return { - message: () => 'Mock function was not called', - pass: false, - }; - } - - const [actualInteractions, actualThreadID] = calls[calls.length - 1]; - - if (expectedThreadID !== undefined) { - if (expectedThreadID !== actualThreadID) { - return { - message: () => jestDiff(expectedThreadID + '', actualThreadID + ''), - pass: false, - }; - } - } - - return toMatchInteractions(actualInteractions, expectedInteractions); -} - -function toMatchInteraction(actual, expected) { - let attribute; - for (attribute in expected) { - if (actual[attribute] !== expected[attribute]) { - return { - message: () => jestDiff(expected, actual), - pass: false, - }; - } - } - - return {pass: true}; -} - -function toMatchInteractions(actualSetOrArray, expectedSetOrArray) { - const actualArray = Array.from(actualSetOrArray); - const expectedArray = Array.from(expectedSetOrArray); - - if (actualArray.length !== expectedArray.length) { - return { - message: () => - `Expected ${expectedArray.length} interactions but there were ${actualArray.length}`, - pass: false, - }; - } - - for (let i = 0; i < actualArray.length; i++) { - const result = toMatchInteraction(actualArray[i], expectedArray[i]); - if (result.pass === false) { - return result; - } - } - - return {pass: true}; -} - -module.exports = { - toContainNoInteractions, - toHaveBeenLastNotifiedOfInteraction, - toHaveBeenLastNotifiedOfWork, - toMatchInteraction, - toMatchInteractions, -}; diff --git a/scripts/jest/matchers/profilerMatchers.js b/scripts/jest/matchers/profilerMatchers.js index bc851ffd9e..e69de29bb2 100644 --- a/scripts/jest/matchers/profilerMatchers.js +++ b/scripts/jest/matchers/profilerMatchers.js @@ -1,72 +0,0 @@ -'use strict'; - -const jestDiff = require('jest-diff').default; - -function toHaveLastRenderedWithNoInteractions(onRenderMockFn) { - const calls = onRenderMockFn.mock.calls; - if (calls.length === 0) { - return { - message: () => 'Mock onRender function was not called', - pass: false, - }; - } -} - -function toHaveLastRenderedWithInteractions( - onRenderMockFn, - expectedInteractions -) { - const calls = onRenderMockFn.mock.calls; - if (calls.length === 0) { - return { - message: () => 'Mock onRender function was not called', - pass: false, - }; - } - - const lastCall = calls[calls.length - 1]; - const actualInteractions = lastCall[6]; - - return toMatchInteractions(actualInteractions, expectedInteractions); -} - -function toMatchInteraction(actual, expected) { - let attribute; - for (attribute in expected) { - if (actual[attribute] !== expected[attribute]) { - return { - message: () => jestDiff(expected, actual), - pass: false, - }; - } - } - - return {pass: true}; -} - -function toMatchInteractions(actualSetOrArray, expectedSetOrArray) { - const actualArray = Array.from(actualSetOrArray); - const expectedArray = Array.from(expectedSetOrArray); - - if (actualArray.length !== expectedArray.length) { - return { - message: () => - `Expected ${expectedArray.length} interactions but there were ${actualArray.length}`, - pass: false, - }; - } - - for (let i = 0; i < actualArray.length; i++) { - const result = toMatchInteraction(actualArray[i], expectedArray[i]); - if (result.pass === false) { - return result; - } - } - - return {pass: true}; -} - -module.exports = { - toHaveLastRenderedWithInteractions, - toHaveLastRenderedWithNoInteractions, -}; diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index 34f5633ea3..abb358f817 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -45,8 +45,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { } expect.extend({ - ...require('./matchers/interactionTracingMatchers'), - ...require('./matchers/profilerMatchers'), ...require('./matchers/toWarnDev'), ...require('./matchers/reactTestMatchers'), }); diff --git a/scripts/jest/spec-equivalence-reporter/setupTests.js b/scripts/jest/spec-equivalence-reporter/setupTests.js index 997abd76fb..31814bb754 100644 --- a/scripts/jest/spec-equivalence-reporter/setupTests.js +++ b/scripts/jest/spec-equivalence-reporter/setupTests.js @@ -46,8 +46,6 @@ global.spyOnProd = function(...args) { }; expect.extend({ - ...require('../matchers/interactionTracingMatchers'), - ...require('../matchers/profilerMatchers'), ...require('../matchers/toWarnDev'), ...require('../matchers/reactTestMatchers'), }); diff --git a/scripts/release/shared-commands/test-tracing-fixture.js b/scripts/release/shared-commands/test-tracing-fixture.js deleted file mode 100644 index 781e400147..0000000000 --- a/scripts/release/shared-commands/test-tracing-fixture.js +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const {join} = require('path'); -const puppeteer = require('puppeteer'); -const theme = require('../theme'); -const {logPromise} = require('../utils'); - -const validate = async ({cwd}) => { - const browser = await puppeteer.launch(); - const page = await browser.newPage(); - - await page.goto( - 'file://' + join(cwd, 'fixtures/tracing/index.html?puppeteer=true') - ); - - try { - return await page.evaluate(() => { - const button = document.getElementById('run-test-button'); - button.click(); - - const items = document.querySelectorAll('[data-value]'); - - if (items.length === 0) { - return 'No results were found.'; - } - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.getAttribute('data-value') !== 'All checks pass') { - return `Unexpected result, "${item.getAttribute('data-value')}"`; - } - } - - return null; - }); - } finally { - await browser.close(); - } -}; - -const run = async ({cwd}) => { - const errorMessage = await logPromise( - validate({cwd}), - 'Verifying "scheduler/tracing" fixture' - ); - if (errorMessage) { - console.error( - theme.error('✗'), - 'Verifying "scheduler/tracing" fixture\n ', - theme.error(errorMessage) - ); - process.exit(1); - } -}; - -module.exports = run; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 2890c55181..b34c007787 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -805,24 +805,6 @@ const bundles = [ global: 'ReactFreshRuntime', externals: [], }, - - { - bundleTypes: [ - FB_WWW_DEV, - FB_WWW_PROD, - FB_WWW_PROFILING, - NODE_DEV, - NODE_PROD, - NODE_PROFILING, - RN_FB_DEV, - RN_FB_PROD, - RN_FB_PROFILING, - ], - moduleType: ISOMORPHIC, - entry: 'scheduler/tracing', - global: 'SchedulerTracing', - externals: [], - }, ]; // Based on deep-freeze by substack (public domain) diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 2c64c6219e..84211da5c5 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -171,25 +171,6 @@ const forks = Object.freeze({ } }, - 'scheduler/tracing': (bundleType, entry, dependencies) => { - switch (bundleType) { - case UMD_DEV: - case UMD_PROD: - case UMD_PROFILING: - if (dependencies.indexOf('react') === -1) { - // It's only safe to use this fork for modules that depend on React, - // because they read the re-exported API from the SECRET_INTERNALS object. - return null; - } - // Optimization: for UMDs, use the API that is already a part of the React - // package instead of requiring it to be loaded via a separate