diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap index d5e244adf062f..a92d85a6e624b 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/ownersListContext-test.js.snap @@ -13,18 +13,21 @@ Array [ "displayName": "Grandparent", "hocDisplayNames": null, "id": 2, + "key": null, "type": 5, }, Object { "displayName": "Parent", "hocDisplayNames": null, "id": 3, + "key": null, "type": 5, }, Object { "displayName": "Child", "hocDisplayNames": null, "id": 4, + "key": null, "type": 5, }, ] @@ -44,18 +47,21 @@ Array [ "displayName": "Grandparent", "hocDisplayNames": null, "id": 2, + "key": null, "type": 5, }, Object { "displayName": "Parent", "hocDisplayNames": null, "id": 3, + "key": null, "type": 5, }, Object { "displayName": "Child", "hocDisplayNames": null, "id": 4, + "key": null, "type": 5, }, ] @@ -67,12 +73,14 @@ Array [ "displayName": "Grandparent", "hocDisplayNames": null, "id": 2, + "key": null, "type": 5, }, Object { "displayName": "Parent", "hocDisplayNames": null, "id": 3, + "key": null, "type": 5, }, ] @@ -84,6 +92,7 @@ Array [ "displayName": "Grandparent", "hocDisplayNames": null, "id": 2, + "key": null, "type": 5, }, Object { @@ -92,6 +101,7 @@ Array [ "Memo", ], "id": 3, + "key": null, "type": 8, }, Object { @@ -100,6 +110,7 @@ Array [ "ForwardRef", ], "id": 4, + "key": null, "type": 6, }, ] @@ -116,6 +127,7 @@ Array [ "displayName": "Grandparent", "hocDisplayNames": null, "id": 2, + "key": null, "type": 5, }, ] 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 c9e7f9770159f..9b94c71e6ed12 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 @@ -43,6 +43,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 16, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -82,6 +91,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 15, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -110,6 +128,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 18, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -165,6 +192,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 12, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -222,6 +258,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 25, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -261,6 +306,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 35, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -291,6 +345,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 45, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -392,6 +455,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 12, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -488,6 +560,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 25, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -548,6 +629,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 35, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -590,6 +680,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 45, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -736,7 +835,7 @@ Object { "snapshots": Array [], }, ], - "version": 4, + "version": 5, } `; @@ -855,6 +954,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 11, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -933,6 +1041,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 22, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -1029,6 +1146,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 35, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -1170,7 +1296,7 @@ Object { "snapshots": Array [], }, ], - "version": 4, + "version": 5, } `; @@ -1230,6 +1356,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 13, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Map { @@ -1266,6 +1401,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 34, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Map { @@ -1293,6 +1437,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 44, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -1470,6 +1623,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 24, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 11, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -1554,6 +1716,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 34, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 6, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -1723,6 +1894,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 13, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -1783,6 +1963,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 34, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -1825,6 +2014,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 44, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -2062,6 +2260,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 24, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 11, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -2143,6 +2350,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 34, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 6, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -2235,7 +2451,7 @@ Object { ], }, ], - "version": 4, + "version": 5, } `; @@ -2258,6 +2474,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Suspense", @@ -2327,6 +2552,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -2359,6 +2593,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -2385,6 +2628,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Component", + "hocDisplayNames": null, + "id": 3, + "key": null, + "type": 5, + }, + ], } `; @@ -2411,6 +2663,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Component", + "hocDisplayNames": null, + "id": 3, + "key": null, + "type": 5, + }, + ], } `; @@ -2441,6 +2702,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -2496,6 +2766,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -2546,6 +2825,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -2578,6 +2866,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Component", + "hocDisplayNames": null, + "id": 3, + "key": null, + "type": 5, + }, + ], }, Object { "changeDescriptions": Array [ @@ -2610,6 +2907,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Component", + "hocDisplayNames": null, + "id": 3, + "key": null, + "type": 5, + }, + ], }, Object { "changeDescriptions": Array [ @@ -2658,6 +2964,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Component", @@ -2747,7 +3062,7 @@ Object { "snapshots": Array [], }, ], - "version": 4, + "version": 5, } `; @@ -2814,6 +3129,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -2886,6 +3210,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "LegacyContextProvider", + "hocDisplayNames": null, + "id": 2, + "key": null, + "type": 1, + }, + ], } `; @@ -2954,6 +3287,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -3023,6 +3365,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -3091,6 +3442,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], } `; @@ -3218,6 +3578,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -3338,6 +3707,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "LegacyContextProvider", + "hocDisplayNames": null, + "id": 2, + "key": null, + "type": 1, + }, + ], }, Object { "changeDescriptions": Array [ @@ -3460,6 +3838,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -3583,6 +3970,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -3705,6 +4101,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 0, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "LegacyContextProvider", @@ -3917,7 +4322,7 @@ Object { "snapshots": Array [], }, ], - "version": 4, + "version": 5, } `; @@ -4020,6 +4425,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Normal", "timestamp": 11, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, Object { "changeDescriptions": Array [ @@ -4100,6 +4514,15 @@ Object { "passiveEffectDuration": null, "priorityLevel": "Immediate", "timestamp": 22, + "updaters": Array [ + Object { + "displayName": "Anonymous", + "hocDisplayNames": null, + "id": 1, + "key": null, + "type": 11, + }, + ], }, ], "displayName": "Parent", @@ -4237,6 +4660,6 @@ Object { "snapshots": Array [], }, ], - "version": 4, + "version": 5, } `; diff --git a/packages/react-devtools-shared/src/__tests__/profilingUtils-test.js b/packages/react-devtools-shared/src/__tests__/profilingUtils-test.js index 5cf07cf472eeb..2c9268da2adbf 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingUtils-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingUtils-test.js @@ -22,6 +22,6 @@ describe('profiling utils', () => { dataForRoots: [], }: any), ), - ).toThrow('Unsupported profiler export version "0"'); + ).toThrow('Unsupported profile export version'); }); }); diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index fcc7895356aae..091d755009b42 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -51,7 +51,7 @@ import type { ComponentFilter, ElementType, } from 'react-devtools-shared/src/types'; -import type {Owner, InspectedElement} from '../types'; +import type {InspectedElement, SerializedElement} from '../types'; export type InternalInstance = Object; type LegacyRenderer = Object; @@ -767,6 +767,7 @@ export function attach( owners.push({ displayName: getData(owner).displayName || 'Unknown', id: getID(owner), + key: element.key, type: getElementType(owner), }); if (owner._currentElement) { @@ -1047,7 +1048,7 @@ export function attach( // Not implemented. } - function getOwnersList(id: number): Array | null { + function getOwnersList(id: number): Array | null { // Not implemented. return null; } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 09042d0446b29..b006c9166f53c 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -81,6 +81,8 @@ import { } from './ReactSymbols'; import {format} from './utils'; import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags'; +import is from 'shared/objectIs'; +import isArray from 'shared/isArray'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type { @@ -91,13 +93,13 @@ import type { InspectedElementPayload, InstanceAndStyle, NativeType, - Owner, PathFrame, PathMatch, ProfilingDataBackend, ProfilingDataForRootBackend, ReactRenderer, RendererInterface, + SerializedElement, WorkTagMap, } from './types'; import type {Interaction} from 'react-devtools-shared/src/devtools/views/Profiler/types'; @@ -105,8 +107,6 @@ import type { ComponentFilter, ElementType, } from 'react-devtools-shared/src/types'; -import is from 'shared/objectIs'; -import isArray from 'shared/isArray'; type getDisplayNameForFiberType = (fiber: Fiber) => string | null; type getTypeSymbolType = (type: any) => Symbol | number; @@ -2172,6 +2172,7 @@ export function attach( ), maxActualDuration: 0, priorityLevel: null, + updaters: getUpdatersList(root), effectDuration: null, passiveEffectDuration: null, }; @@ -2184,6 +2185,12 @@ export function attach( } } + function getUpdatersList(root): Array | null { + return root.memoizedUpdaters != null + ? Array.from(root.memoizedUpdaters).map(fiberToSerializedElement) + : null; + } + function handleCommitFiberUnmount(fiber) { // This is not recursive. // We can't traverse fibers after unmounting so instead @@ -2241,6 +2248,8 @@ export function attach( priorityLevel: priorityLevel == null ? null : formatPriorityLevel(priorityLevel), + updaters: getUpdatersList(root), + // Initialize to null; if new enough React version is running, // these values will be read during separate handlePostCommitFiberRoot() call. effectDuration: null, @@ -2637,7 +2646,16 @@ export function attach( } } - function getOwnersList(id: number): Array | null { + function fiberToSerializedElement(fiber: Fiber): SerializedElement { + return { + displayName: getDisplayNameForFiber(fiber) || 'Anonymous', + id: getFiberID(getPrimaryFiber(fiber)), + key: fiber.key, + type: getElementTypeForFiber(fiber), + }; + } + + function getOwnersList(id: number): Array | null { const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber == null) { return null; @@ -2645,22 +2663,12 @@ export function attach( const {_debugOwner} = fiber; - const owners = [ - { - displayName: getDisplayNameForFiber(fiber) || 'Anonymous', - id, - type: getElementTypeForFiber(fiber), - }, - ]; + const owners: Array = [fiberToSerializedElement(fiber)]; if (_debugOwner) { let owner = _debugOwner; while (owner !== null) { - owners.unshift({ - displayName: getDisplayNameForFiber(owner) || 'Anonymous', - id: getFiberID(getPrimaryFiber(owner)), - type: getElementTypeForFiber(owner), - }); + owners.unshift(fiberToSerializedElement(owner)); owner = owner._debugOwner || null; } } @@ -2791,11 +2799,7 @@ export function attach( owners = []; let owner = _debugOwner; while (owner !== null) { - owners.push({ - displayName: getDisplayNameForFiber(owner) || 'Anonymous', - id: getFiberID(getPrimaryFiber(owner)), - type: getElementTypeForFiber(owner), - }); + owners.push(fiberToSerializedElement(owner)); owner = owner._debugOwner || null; } } @@ -3380,6 +3384,7 @@ export function attach( maxActualDuration: number, passiveEffectDuration: number | null, priorityLevel: string | null, + updaters: Array | null, |}; type CommitProfilingMetadataMap = Map>; @@ -3438,6 +3443,7 @@ export function attach( passiveEffectDuration, priorityLevel, commitTime, + updaters, } = commitProfilingData; const interactionIDs: Array = []; @@ -3478,6 +3484,7 @@ export function attach( passiveEffectDuration, priorityLevel, timestamp: commitTime, + updaters, }); }); diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index ea83d77666175..f80211168c634 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -168,6 +168,7 @@ export type CommitDataBackend = {| passiveEffectDuration: number | null, priorityLevel: string | null, timestamp: number, + updaters: Array | null, |}; export type ProfilingDataForRootBackend = {| @@ -199,15 +200,16 @@ export type PathMatch = {| isFullMatch: boolean, |}; -export type Owner = {| +export type SerializedElement = {| displayName: string | null, id: number, + key: number | string | null, type: ElementType, |}; export type OwnersList = {| id: number, - owners: Array | null, + owners: Array | null, |}; export type InspectedElement = {| @@ -244,7 +246,7 @@ export type InspectedElement = {| warnings: Array<[string, number]>, // List of owners - owners: Array | null, + owners: Array | null, // Location of component in source code. source: Source | null, @@ -322,7 +324,7 @@ export type RendererInterface = { getDisplayNameForFiberID: GetDisplayNameForFiberID, getInstanceAndStyle(id: number): InstanceAndStyle, getProfilingData(): ProfilingDataBackend, - getOwnersList: (id: number) => Array | null, + getOwnersList: (id: number) => Array | null, getPathForElement: (id: number) => Array | null, handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void, handleCommitFiberUnmount: (fiber: Object) => void, diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index f8868111e095d..bae23abc01223 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -41,7 +41,7 @@ export const LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY = export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY = 'React::DevTools::traceUpdatesEnabled'; -export const PROFILER_EXPORT_VERSION = 4; +export const PROFILER_EXPORT_VERSION = 5; export const CHANGE_LOG_URL = 'https://github.com/facebook/react/blob/master/packages/react-devtools/CHANGELOG.md'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index 4d8662d80dae5..e7b4165910926 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -35,7 +35,7 @@ import { import styles from './InspectedElementView.css'; import type {ContextMenuContextType} from '../context'; -import type {Element, InspectedElement, Owner} from './types'; +import type {Element, InspectedElement, SerializedElement} from './types'; import type {ElementType} from 'react-devtools-shared/src/types'; export type CopyPath = (path: Array) => void; @@ -127,7 +127,7 @@ export default function InspectedElementView({
rendered by
{showOwnersList && - ((owners: any): Array).map(owner => ( + ((owners: any): Array).map(owner => ( Array | null; +type Context = (id: number) => Array | null; const OwnersListContext = createContext(((null: any): Context)); OwnersListContext.displayName = 'OwnersListContext'; -type ResolveFn = (ownersList: Array | null) => void; +type ResolveFn = (ownersList: Array | null) => void; type InProgressRequest = {| - promise: Thenable>, + promise: Thenable>, resolveFn: ResolveFn, |}; const inProgressRequests: WeakMap = new WeakMap(); -const resource: Resource> = createResource( +const resource: Resource< + Element, + Element, + Array, +> = createResource( (element: Element) => { const request = inProgressRequests.get(element); if (request != null) { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js index 8e106e8f044fa..d7efafee6c522 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -27,16 +27,16 @@ import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import {useIsOverflowing} from '../hooks'; import {StoreContext} from '../context'; -import type {Owner} from './types'; +import type {SerializedElement} from './types'; import styles from './OwnersStack.css'; -type SelectOwner = (owner: Owner | null) => void; +type SelectOwner = (owner: SerializedElement | null) => void; type ACTION_UPDATE_OWNER_ID = {| type: 'UPDATE_OWNER_ID', ownerID: number | null, - owners: Array, + owners: Array, |}; type ACTION_UPDATE_SELECTED_INDEX = {| type: 'UPDATE_SELECTED_INDEX', @@ -47,7 +47,7 @@ type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX; type State = {| ownerID: number | null, - owners: Array, + owners: Array, selectedIndex: number, |}; @@ -104,7 +104,7 @@ export default function OwnerStack() { const {owners, selectedIndex} = state; const selectOwner = useCallback( - (owner: Owner | null) => { + (owner: SerializedElement | null) => { if (owner !== null) { const index = owners.indexOf(owner); dispatch({ @@ -197,7 +197,7 @@ export default function OwnerStack() { } type ElementsDropdownProps = { - owners: Array, + owners: Array, selectedIndex: number, selectOwner: SelectOwner, ... @@ -245,7 +245,7 @@ function ElementsDropdown({ type ElementViewProps = { isSelected: boolean, - owner: Owner, + owner: SerializedElement, selectOwner: SelectOwner, ... }; @@ -278,7 +278,7 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) { } type BackToOwnerButtonProps = {| - owners: Array, + owners: Array, selectedIndex: number, selectOwner: SelectOwner, |}; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/types.js b/packages/react-devtools-shared/src/devtools/views/Components/types.js index 11eb544c3f789..e09a251392bfe 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/types.js @@ -44,16 +44,17 @@ export type Element = {| weight: number, |}; -export type Owner = {| +export type SerializedElement = {| displayName: string | null, id: number, + key: number | string | null, hocDisplayNames: Array | null, type: ElementType, |}; export type OwnersList = {| id: number, - owners: Array | null, + owners: Array | null, |}; export type InspectedElementResponseType = @@ -94,7 +95,7 @@ export type InspectedElement = {| warnings: Array<[string, number]>, // List of owners - owners: Array | null, + owners: Array | null, // Location of component in source code. source: Source | null, 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 34917846eb329..d82f10b3d73a5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.css @@ -30,6 +30,7 @@ .Interactions { margin: 0 0 0.5rem; } +.NoInteractions, .Interaction { display: block; width: 100%; @@ -45,6 +46,10 @@ 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 ed913984cc7a0..dcfaf7d68b3be 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js @@ -10,8 +10,10 @@ import * as React from 'react'; import {Fragment, useContext} from 'react'; import {ProfilerContext} from './ProfilerContext'; +import Updaters from './Updaters'; import {formatDuration, formatTime} from './utils'; import {StoreContext} from '../context'; +import {getCommitTree} from './CommitTreeBuilder'; import styles from './SidebarCommitInfo.css'; @@ -39,6 +41,7 @@ export default function SidebarCommitInfo(_: Props) { passiveEffectDuration, priorityLevel, timestamp, + updaters, } = profilerStore.getCommitData(rootID, selectedCommitIndex); const viewInteraction = interactionID => { @@ -49,6 +52,15 @@ export default function SidebarCommitInfo(_: Props) { const hasCommitPhaseDurations = effectDuration !== null || passiveEffectDuration !== null; + const commitTree = + updaters !== null + ? getCommitTree({ + commitIndex: selectedCommitIndex, + profilerStore, + rootID, + }) + : null; + return (
Commit information
@@ -102,11 +114,18 @@ export default function SidebarCommitInfo(_: Props) { )} + {updaters !== null && commitTree !== null && ( +
  • + ? + +
  • + )} +
  • :
    {interactionIDs.length === 0 ? ( -
    None
    +
    (none)
    ) : null} {interactionIDs.map(interactionID => { const interaction = interactions.get(interactionID); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.css b/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.css new file mode 100644 index 0000000000000..18d7f7f8fad09 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.css @@ -0,0 +1,25 @@ +.Updaters { + margin: 0 0 0.5rem; +} + +.NoUpdaters, +.Updater, +.UnmountedUpdater { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + padding: 0.25rem 0.5rem; + color: var(--color-text); +} +.Updater:focus, +.Updater:hover { + outline: none; + background-color: var(--color-background-hover); +} + +.NoUpdaters, +.UnmountedUpdater { + color: var(--color-dim); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.js new file mode 100644 index 0000000000000..9e2518612e455 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.js @@ -0,0 +1,55 @@ +/** + * 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 {CommitTree} from './types'; +import type {SerializedElement} from '../Components/types'; + +import * as React from 'react'; +import {useContext} from 'react'; +import {ProfilerContext} from './ProfilerContext'; +import styles from './Updaters.css'; + +export type Props = {| + commitTree: CommitTree, + updaters: Array, +|}; + +export default function Updaters({commitTree, updaters}: Props) { + const {selectFiber} = useContext(ProfilerContext); + + const children = + updaters.length > 0 ? ( + updaters.map((serializedElement: SerializedElement) => { + const {displayName, id, key} = serializedElement; + const isVisibleInTree = commitTree.nodes.has(id); + if (isVisibleInTree) { + return ( + + ); + } else { + return ( +
    + {displayName} {key ? `key="${key}"` : ''} +
    + ); + } + }) + ) : ( +
    + (unknown) +
    + ); + + return
    {children}
    ; +} 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 11e99c05b1bec..90c56b3529be4 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/types.js @@ -8,6 +8,7 @@ */ import type {ElementType} from 'react-devtools-shared/src/types'; +import type {SerializedElement} from '../Components/types'; export type CommitTreeNode = {| id: number, @@ -80,6 +81,9 @@ export type CommitDataFrontend = {| // When did this commit occur (relative to the start of profiling) timestamp: number, + + // Fiber(s) responsible for scheduling this update. + updaters: Array | null, |}; export type ProfilingDataForRootFrontend = {| @@ -131,6 +135,7 @@ export type CommitDataExport = {| passiveEffectDuration: number | null, priorityLevel: string | null, timestamp: number, + updaters: Array | null, |}; export type ProfilingDataForRootExport = {| @@ -148,6 +153,6 @@ export type ProfilingDataForRootExport = {| // Serializable version of ProfilingDataFrontend data. export type ProfilingDataExport = {| - version: 4, + version: 5, dataForRoots: Array, |}; 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 c667276cfafac..55f7ecb5c1452 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js @@ -8,6 +8,7 @@ */ import {PROFILER_EXPORT_VERSION} from 'react-devtools-shared/src/constants'; +import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils'; import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types'; import type { @@ -84,6 +85,23 @@ export function prepareProfilingDataFrontendFromBackendAndStore( passiveEffectDuration: commitDataBackend.passiveEffectDuration, priorityLevel: commitDataBackend.priorityLevel, timestamp: commitDataBackend.timestamp, + updaters: + commitDataBackend.updaters !== null + ? commitDataBackend.updaters.map(serializedElement => { + const [ + serializedElementDisplayName, + serializedElementHocDisplayNames, + ] = separateDisplayNameAndHOCs( + serializedElement.displayName, + serializedElement.type, + ); + return { + ...serializedElement, + displayName: serializedElementDisplayName, + hocDisplayNames: serializedElementHocDisplayNames, + }; + }) + : null, }), ); @@ -111,7 +129,9 @@ export function prepareProfilingDataFrontendFromExport( const {version} = profilingDataExport; if (version !== PROFILER_EXPORT_VERSION) { - throw Error(`Unsupported profiler export version "${version}"`); + throw Error( + `Unsupported profile export version "${version}". Supported version is "${PROFILER_EXPORT_VERSION}".`, + ); } const dataForRoots: Map = new Map(); @@ -138,6 +158,7 @@ export function prepareProfilingDataFrontendFromExport( passiveEffectDuration, priorityLevel, timestamp, + updaters, }) => ({ changeDescriptions: changeDescriptions != null ? new Map(changeDescriptions) : null, @@ -149,6 +170,7 @@ export function prepareProfilingDataFrontendFromExport( passiveEffectDuration, priorityLevel, timestamp, + updaters, }), ), displayName, @@ -193,6 +215,7 @@ export function prepareProfilingDataExport( passiveEffectDuration, priorityLevel, timestamp, + updaters, }) => ({ changeDescriptions: changeDescriptions != null @@ -206,6 +229,7 @@ export function prepareProfilingDataExport( passiveEffectDuration, priorityLevel, timestamp, + updaters, }), ), displayName, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 8f890f4dfd9cb..1006a867f39df 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -37,6 +37,7 @@ import { enableStrictEffects, deletedTreeCleanUpLevel, enableSuspenseLayoutEffectSemantics, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -89,7 +90,7 @@ import { resetCurrentFiber as resetCurrentDebugFiberInDEV, setCurrentFiber as setCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; - +import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import {onCommitUnmount} from './ReactFiberDevToolsHook.new'; import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; import { @@ -137,6 +138,7 @@ import { resolveRetryWakeable, markCommitTimeOfFallback, enqueuePendingPassiveProfilerEffect, + restorePendingUpdaters, } from './ReactFiberWorkLoop.new'; import { NoFlags as NoHookEffect, @@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; let nextEffect: Fiber | null = null; +// Used for Profiling builds to track updaters. +let inProgressLanes: Lanes | null = null; +let inProgressRoot: FiberRoot | null = null; + const callComponentWillUnmountWithTimer = function(current, instance) { instance.props = current.memoizedProps; instance.state = current.memoizedState; @@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) { } } retryCache.add(wakeable); + + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + if (inProgressLanes !== null && inProgressRoot !== null) { + // If we have pending work still, associate the original updaters with it. + restorePendingUpdaters(inProgressRoot, inProgressLanes); + } else { + throw Error( + 'Expected finished root and lanes to be set. This is a bug in React.', + ); + } + } + } + wakeable.then(retry, retry); } }); @@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) { resetTextContent(current.stateNode); } -export function commitMutationEffects(root: FiberRoot, firstChild: Fiber) { +export function commitMutationEffects( + root: FiberRoot, + firstChild: Fiber, + committedLanes: Lanes, +) { + inProgressLanes = committedLanes; + inProgressRoot = root; nextEffect = firstChild; + commitMutationEffects_begin(root); + + inProgressLanes = null; + inProgressRoot = null; } function commitMutationEffects_begin(root: FiberRoot) { @@ -2280,8 +2310,14 @@ export function commitLayoutEffects( root: FiberRoot, committedLanes: Lanes, ): void { + inProgressLanes = committedLanes; + inProgressRoot = root; nextEffect = finishedWork; + commitLayoutEffects_begin(finishedWork, root, committedLanes); + + inProgressLanes = null; + inProgressRoot = null; } function commitLayoutEffects_begin( diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 0c68fdaf0b3da..05a373c868683 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -37,6 +37,7 @@ import { enableStrictEffects, deletedTreeCleanUpLevel, enableSuspenseLayoutEffectSemantics, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -89,7 +90,7 @@ import { resetCurrentFiber as resetCurrentDebugFiberInDEV, setCurrentFiber as setCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; - +import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import {onCommitUnmount} from './ReactFiberDevToolsHook.old'; import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; import { @@ -137,6 +138,7 @@ import { resolveRetryWakeable, markCommitTimeOfFallback, enqueuePendingPassiveProfilerEffect, + restorePendingUpdaters, } from './ReactFiberWorkLoop.old'; import { NoFlags as NoHookEffect, @@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; let nextEffect: Fiber | null = null; +// Used for Profiling builds to track updaters. +let inProgressLanes: Lanes | null = null; +let inProgressRoot: FiberRoot | null = null; + const callComponentWillUnmountWithTimer = function(current, instance) { instance.props = current.memoizedProps; instance.state = current.memoizedState; @@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) { } } retryCache.add(wakeable); + + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + if (inProgressLanes !== null && inProgressRoot !== null) { + // If we have pending work still, associate the original updaters with it. + restorePendingUpdaters(inProgressRoot, inProgressLanes); + } else { + throw Error( + 'Expected finished root and lanes to be set. This is a bug in React.', + ); + } + } + } + wakeable.then(retry, retry); } }); @@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) { resetTextContent(current.stateNode); } -export function commitMutationEffects(root: FiberRoot, firstChild: Fiber) { +export function commitMutationEffects( + root: FiberRoot, + firstChild: Fiber, + committedLanes: Lanes, +) { + inProgressLanes = committedLanes; + inProgressRoot = root; nextEffect = firstChild; + commitMutationEffects_begin(root); + + inProgressLanes = null; + inProgressRoot = null; } function commitMutationEffects_begin(root: FiberRoot) { @@ -2280,8 +2310,14 @@ export function commitLayoutEffects( root: FiberRoot, committedLanes: Lanes, ): void { + inProgressLanes = committedLanes; + inProgressRoot = root; nextEffect = finishedWork; + commitLayoutEffects_begin(finishedWork, root, committedLanes); + + inProgressLanes = null; + inProgressRoot = null; } function commitLayoutEffects_begin( diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 2a9571ece689c..32bb8139c7657 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -35,7 +35,12 @@ export type Lanes = number; export type Lane = number; export type LaneMap = Array; -import {enableCache, enableSchedulingProfiler} from 'shared/ReactFeatureFlags'; +import { + enableCache, + enableSchedulingProfiler, + enableUpdaterTracking, +} from 'shared/ReactFeatureFlags'; +import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; // Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler. // If those values are changed that package should be rebuilt and redeployed. @@ -742,6 +747,57 @@ export function getBumpedLaneForHydration( return lane; } +export function addFiberToLanesMap( + root: FiberRoot, + fiber: Fiber, + lanes: Lanes | Lane, +) { + if (!enableUpdaterTracking) { + return; + } + if (!isDevToolsPresent) { + return; + } + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap; + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + + const updaters = pendingUpdatersLaneMap[index]; + updaters.add(fiber); + + lanes &= ~lane; + } +} + +export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) { + if (!enableUpdaterTracking) { + return; + } + if (!isDevToolsPresent) { + return; + } + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap; + const memoizedUpdaters = root.memoizedUpdaters; + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + + const updaters = pendingUpdatersLaneMap[index]; + if (updaters.size > 0) { + updaters.forEach(fiber => { + const alternate = fiber.alternate; + if (alternate === null || !memoizedUpdaters.has(alternate)) { + memoizedUpdaters.add(fiber); + } + }); + updaters.clear(); + } + + lanes &= ~lane; + } +} + const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; // Count leading zeros. Only used on lanes, so assume input is an integer. diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 2a9571ece689c..b577a67aa213f 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -35,7 +35,12 @@ export type Lanes = number; export type Lane = number; export type LaneMap = Array; -import {enableCache, enableSchedulingProfiler} from 'shared/ReactFeatureFlags'; +import { + enableCache, + enableSchedulingProfiler, + enableUpdaterTracking, +} from 'shared/ReactFeatureFlags'; +import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; // Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler. // If those values are changed that package should be rebuilt and redeployed. @@ -742,6 +747,57 @@ export function getBumpedLaneForHydration( return lane; } +export function addFiberToLanesMap( + root: FiberRoot, + fiber: Fiber, + lanes: Lanes | Lane, +) { + if (!enableUpdaterTracking) { + return; + } + if (!isDevToolsPresent) { + return; + } + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap; + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + + const updaters = pendingUpdatersLaneMap[index]; + updaters.add(fiber); + + lanes &= ~lane; + } +} + +export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) { + if (!enableUpdaterTracking) { + return; + } + if (!isDevToolsPresent) { + return; + } + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap; + const memoizedUpdaters = root.memoizedUpdaters; + while (lanes > 0) { + const index = laneToIndex(lanes); + const lane = 1 << index; + + const updaters = pendingUpdatersLaneMap[index]; + if (updaters.size > 0) { + updaters.forEach(fiber => { + const alternate = fiber.alternate; + if (alternate === null || !memoizedUpdaters.has(alternate)) { + memoizedUpdaters.add(fiber); + } + }); + updaters.clear(); + } + + lanes &= ~lane; + } +} + const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; // Count leading zeros. Only used on lanes, so assume input is an integer. diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 1eb637891ebe6..9201a35980753 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -16,6 +16,7 @@ import { NoLane, NoLanes, NoTimestamp, + TotalLanes, createLaneMap, } from './ReactFiberLane.new'; import { @@ -24,6 +25,7 @@ import { enableCache, enableProfilerCommitHooks, enableProfilerTimer, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import {unstable_getThreadID} from 'scheduler/tracing'; import {initializeUpdateQueue} from './ReactUpdateQueue.new'; @@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.passiveEffectDuration = 0; } + if (enableUpdaterTracking) { + this.memoizedUpdaters = new Set(); + const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []); + for (let i = 0; i < TotalLanes; i++) { + pendingUpdatersLaneMap.push(new Set()); + } + } + if (__DEV__) { switch (tag) { case ConcurrentRoot: diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index e44757248b427..f2968b314ecae 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -16,6 +16,7 @@ import { NoLane, NoLanes, NoTimestamp, + TotalLanes, createLaneMap, } from './ReactFiberLane.old'; import { @@ -24,6 +25,7 @@ import { enableCache, enableProfilerCommitHooks, enableProfilerTimer, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import {unstable_getThreadID} from 'scheduler/tracing'; import {initializeUpdateQueue} from './ReactUpdateQueue.old'; @@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.passiveEffectDuration = 0; } + if (enableUpdaterTracking) { + this.memoizedUpdaters = new Set(); + const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []); + for (let i = 0; i < TotalLanes; i++) { + pendingUpdatersLaneMap.push(new Set()); + } + } + if (__DEV__) { switch (tag) { case ConcurrentRoot: diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index 2641bdd3b345e..e100561ed86e5 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -39,6 +39,7 @@ import { enableDebugTracing, enableSchedulingProfiler, enableLazyContextPropagation, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import {createCapturedValue} from './ReactCapturedValue'; import { @@ -60,12 +61,13 @@ import { markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, pingSuspendedRoot, + restorePendingUpdaters, } from './ReactFiberWorkLoop.new'; import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new'; import {logCapturedError} from './ReactFiberErrorLogger'; import {logComponentSuspended} from './DebugTracing'; import {markComponentSuspended} from './SchedulingProfiler'; - +import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import { SyncLane, NoTimestamp, @@ -177,6 +179,12 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) { // Memoize using the thread ID to prevent redundant listeners. threadIDs.add(lanes); const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes); + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + // If we have pending work still, restore the original updaters + restorePendingUpdaters(root, lanes); + } + } wakeable.then(ping, ping); } } @@ -191,6 +199,13 @@ function throwException( // The source fiber did not complete. sourceFiber.flags |= Incomplete; + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + // If we have pending work still, restore the original updaters + restorePendingUpdaters(root, rootRenderLanes); + } + } + if ( value !== null && typeof value === 'object' && diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 294b807ebcb1b..cdb02cb8f1886 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -39,6 +39,7 @@ import { enableDebugTracing, enableSchedulingProfiler, enableLazyContextPropagation, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import {createCapturedValue} from './ReactCapturedValue'; import { @@ -60,12 +61,13 @@ import { markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, pingSuspendedRoot, + restorePendingUpdaters, } from './ReactFiberWorkLoop.old'; import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old'; import {logCapturedError} from './ReactFiberErrorLogger'; import {logComponentSuspended} from './DebugTracing'; import {markComponentSuspended} from './SchedulingProfiler'; - +import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import { SyncLane, NoTimestamp, @@ -177,6 +179,12 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) { // Memoize using the thread ID to prevent redundant listeners. threadIDs.add(lanes); const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes); + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + // If we have pending work still, restore the original updaters + restorePendingUpdaters(root, lanes); + } + } wakeable.then(ping, ping); } } @@ -191,6 +199,13 @@ function throwException( // The source fiber did not complete. sourceFiber.flags |= Incomplete; + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + // If we have pending work still, restore the original updaters + restorePendingUpdaters(root, rootRenderLanes); + } + } + if ( value !== null && typeof value === 'object' && diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 3344361075e66..b52680c91119b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -32,6 +32,7 @@ import { disableSchedulerTimeoutInWorkLoop, enableStrictEffects, skipUnmountedBoundaries, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -159,6 +160,8 @@ import { markRootFinished, areLanesExpired, getHighestPriorityLane, + addFiberToLanesMap, + movePendingFibersToMemoized, } from './ReactFiberLane.new'; import { DiscreteEventPriority, @@ -232,6 +235,7 @@ import { import { onCommitRoot as onCommitRootDevTools, onPostCommitRoot as onPostCommitRootDevTools, + isDevToolsPresent, } from './ReactFiberDevToolsHook.new'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; @@ -470,6 +474,12 @@ export function scheduleUpdateOnFiber( return null; } + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + addFiberToLanesMap(root, fiber, lane); + } + } + // Mark that the root has a pending update. markRootUpdated(root, lane, eventTime); @@ -1427,6 +1437,22 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + const memoizedUpdaters = root.memoizedUpdaters; + if (memoizedUpdaters.size > 0) { + restorePendingUpdaters(root, workInProgressRootRenderLanes); + memoizedUpdaters.clear(); + } + + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set. + // If we bailout on this work, we'll move them back (like above). + // It's important to move them now in case the work spawns more work at the same priority with different updaters. + // That way we can keep the current update and future updates separate. + movePendingFibersToMemoized(root, lanes); + } + } + prepareFreshStack(root, lanes); startWorkOnPendingInteractions(root, lanes); } @@ -1502,6 +1528,22 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + const memoizedUpdaters = root.memoizedUpdaters; + if (memoizedUpdaters.size > 0) { + restorePendingUpdaters(root, workInProgressRootRenderLanes); + memoizedUpdaters.clear(); + } + + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set. + // If we bailout on this work, we'll move them back (like above). + // It's important to move them now in case the work spawns more work at the same priority with different updaters. + // That way we can keep the current update and future updates separate. + movePendingFibersToMemoized(root, lanes); + } + } + resetRenderTimer(); prepareFreshStack(root, lanes); startWorkOnPendingInteractions(root, lanes); @@ -1853,7 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) { } // The next phase is the mutation phase, where we mutate the host tree. - commitMutationEffects(root, finishedWork); + commitMutationEffects(root, finishedWork, lanes); if (shouldFireAfterActiveInstanceBlur) { afterActiveInstanceBlur(); @@ -1985,6 +2027,12 @@ function commitRootImpl(root, renderPriorityLevel) { onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel); + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + root.memoizedUpdaters.clear(); + } + } + if (__DEV__) { onCommitRootTestSelector(); } @@ -2788,6 +2836,21 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) { // a 'shared' variable that changes when act() opens/closes in tests. export const IsThisRendererActing = {current: (false: boolean)}; +export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void { + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + const memoizedUpdaters = root.memoizedUpdaters; + memoizedUpdaters.forEach(schedulingFiber => { + addFiberToLanesMap(root, schedulingFiber, lanes); + }); + + // This function intentionally does not clear memoized updaters. + // Those may still be relevant to the current commit + // and a future one (e.g. Suspense). + } + } +} + export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void { if (__DEV__) { if ( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 73230f5787772..08553e392818e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -32,6 +32,7 @@ import { disableSchedulerTimeoutInWorkLoop, enableStrictEffects, skipUnmountedBoundaries, + enableUpdaterTracking, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -159,6 +160,8 @@ import { markRootFinished, areLanesExpired, getHighestPriorityLane, + addFiberToLanesMap, + movePendingFibersToMemoized, } from './ReactFiberLane.old'; import { DiscreteEventPriority, @@ -232,6 +235,7 @@ import { import { onCommitRoot as onCommitRootDevTools, onPostCommitRoot as onPostCommitRootDevTools, + isDevToolsPresent, } from './ReactFiberDevToolsHook.old'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; @@ -470,6 +474,12 @@ export function scheduleUpdateOnFiber( return null; } + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + addFiberToLanesMap(root, fiber, lane); + } + } + // Mark that the root has a pending update. markRootUpdated(root, lane, eventTime); @@ -1427,6 +1437,22 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + const memoizedUpdaters = root.memoizedUpdaters; + if (memoizedUpdaters.size > 0) { + restorePendingUpdaters(root, workInProgressRootRenderLanes); + memoizedUpdaters.clear(); + } + + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set. + // If we bailout on this work, we'll move them back (like above). + // It's important to move them now in case the work spawns more work at the same priority with different updaters. + // That way we can keep the current update and future updates separate. + movePendingFibersToMemoized(root, lanes); + } + } + prepareFreshStack(root, lanes); startWorkOnPendingInteractions(root, lanes); } @@ -1502,6 +1528,22 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + const memoizedUpdaters = root.memoizedUpdaters; + if (memoizedUpdaters.size > 0) { + restorePendingUpdaters(root, workInProgressRootRenderLanes); + memoizedUpdaters.clear(); + } + + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set. + // If we bailout on this work, we'll move them back (like above). + // It's important to move them now in case the work spawns more work at the same priority with different updaters. + // That way we can keep the current update and future updates separate. + movePendingFibersToMemoized(root, lanes); + } + } + resetRenderTimer(); prepareFreshStack(root, lanes); startWorkOnPendingInteractions(root, lanes); @@ -1853,7 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) { } // The next phase is the mutation phase, where we mutate the host tree. - commitMutationEffects(root, finishedWork); + commitMutationEffects(root, finishedWork, lanes); if (shouldFireAfterActiveInstanceBlur) { afterActiveInstanceBlur(); @@ -1985,6 +2027,12 @@ function commitRootImpl(root, renderPriorityLevel) { onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel); + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + root.memoizedUpdaters.clear(); + } + } + if (__DEV__) { onCommitRootTestSelector(); } @@ -2788,6 +2836,21 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) { // a 'shared' variable that changes when act() opens/closes in tests. export const IsThisRendererActing = {current: (false: boolean)}; +export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void { + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + const memoizedUpdaters = root.memoizedUpdaters; + memoizedUpdaters.forEach(schedulingFiber => { + addFiberToLanesMap(root, schedulingFiber, lanes); + }); + + // This function intentionally does not clear memoized updaters. + // Those may still be relevant to the current commit + // and a future one (e.g. Suspense). + } + } +} + export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void { if (__DEV__) { if ( diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 4ad4cb122b5c9..0c34eeda8ed63 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -249,6 +249,13 @@ type ProfilingOnlyFiberRootProperties = {| 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 = {| + memoizedUpdaters: Set, + pendingUpdatersLaneMap: LaneMap>, +|}; + export type SuspenseHydrationCallbacks = { onHydrated?: (suspenseInstance: SuspenseInstance) => void, onDeleted?: (suspenseInstance: SuspenseInstance) => void, @@ -269,6 +276,7 @@ export type FiberRoot = { ...BaseFiberRootProperties, ...ProfilingOnlyFiberRootProperties, ...SuspenseCallbackOnlyFiberRootProperties, + ...UpdaterTrackingOnlyFiberRootProperties, ... }; diff --git a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js new file mode 100644 index 0000000000000..a2f6ed4bd04cb --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js @@ -0,0 +1,521 @@ +/** + * 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 ReactTestUtils; +let Scheduler; +let mockDevToolsHook; +let allSchedulerTags; +let allSchedulerTypes; +let onCommitRootShouldYield; + +describe('updaters', () => { + beforeEach(() => { + jest.resetModules(); + + allSchedulerTags = []; + allSchedulerTypes = []; + + onCommitRootShouldYield = true; + + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableUpdaterTracking = true; + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + + mockDevToolsHook = { + injectInternals: jest.fn(() => {}), + isDevToolsPresent: true, + onCommitRoot: jest.fn(fiberRoot => { + if (onCommitRootShouldYield) { + Scheduler.unstable_yieldValue('onCommitRoot'); + } + const schedulerTags = []; + const schedulerTypes = []; + fiberRoot.memoizedUpdaters.forEach(fiber => { + schedulerTags.push(fiber.tag); + schedulerTypes.push(fiber.elementType); + }); + allSchedulerTags.push(schedulerTags); + allSchedulerTypes.push(schedulerTypes); + }), + onCommitUnmount: jest.fn(() => {}), + onPostCommitRoot: jest.fn(() => {}), + onScheduleRoot: jest.fn(() => {}), + }; + + jest.mock( + 'react-reconciler/src/ReactFiberDevToolsHook.old', + () => mockDevToolsHook, + ); + jest.mock( + 'react-reconciler/src/ReactFiberDevToolsHook.new', + () => mockDevToolsHook, + ); + + React = require('react'); + ReactDOM = require('react-dom'); + ReactTestUtils = require('react-dom/test-utils'); + Scheduler = require('scheduler'); + }); + + it('should report the (host) root as the scheduler for root-level render', async () => { + const {HostRoot} = require('react-reconciler/src/ReactWorkTags'); + + const Parent = () => ; + const Child = () => null; + const container = document.createElement('div'); + + await ReactTestUtils.act(async () => { + ReactDOM.render(, container); + }); + expect(allSchedulerTags).toEqual([[HostRoot]]); + + await ReactTestUtils.act(async () => { + ReactDOM.render(, container); + }); + expect(allSchedulerTags).toEqual([[HostRoot], [HostRoot]]); + }); + + it('should report a function component as the scheduler for a hooks update', async () => { + let scheduleForA = null; + let scheduleForB = null; + + const Parent = () => ( + + + + + ); + const SchedulingComponentA = () => { + const [count, setCount] = React.useState(0); + scheduleForA = () => setCount(prevCount => prevCount + 1); + return ; + }; + const SchedulingComponentB = () => { + const [count, setCount] = React.useState(0); + scheduleForB = () => setCount(prevCount => prevCount + 1); + return ; + }; + const Child = () => null; + + await ReactTestUtils.act(async () => { + ReactDOM.render(, document.createElement('div')); + }); + expect(scheduleForA).not.toBeNull(); + expect(scheduleForB).not.toBeNull(); + expect(allSchedulerTypes).toEqual([[null]]); + + await ReactTestUtils.act(async () => { + scheduleForA(); + }); + expect(allSchedulerTypes).toEqual([[null], [SchedulingComponentA]]); + + await ReactTestUtils.act(async () => { + scheduleForB(); + }); + expect(allSchedulerTypes).toEqual([ + [null], + [SchedulingComponentA], + [SchedulingComponentB], + ]); + }); + + it('should report a class component as the scheduler for a setState update', async () => { + const Parent = () => ; + class SchedulingComponent extends React.Component { + state = {}; + render() { + instance = this; + return ; + } + } + const Child = () => null; + let instance; + await ReactTestUtils.act(async () => { + ReactDOM.render(, document.createElement('div')); + }); + expect(allSchedulerTypes).toEqual([[null]]); + + expect(instance).not.toBeNull(); + await ReactTestUtils.act(async () => { + instance.setState({}); + }); + expect(allSchedulerTypes).toEqual([[null], [SchedulingComponent]]); + }); + + // @gate experimental + it('should cover cascading updates', async () => { + let triggerActiveCascade = null; + let triggerPassiveCascade = null; + + const Parent = () => ; + const SchedulingComponent = () => { + const [cascade, setCascade] = React.useState(null); + triggerActiveCascade = () => setCascade('active'); + triggerPassiveCascade = () => setCascade('passive'); + return ; + }; + const CascadingChild = ({cascade}) => { + const [count, setCount] = React.useState(0); + Scheduler.unstable_yieldValue(`CascadingChild ${count}`); + React.useLayoutEffect(() => { + if (cascade === 'active') { + setCount(prevCount => prevCount + 1); + } + return () => {}; + }, [cascade]); + React.useEffect(() => { + if (cascade === 'passive') { + setCount(prevCount => prevCount + 1); + } + return () => {}; + }, [cascade]); + return count; + }; + + const root = ReactDOM.unstable_createRoot(document.createElement('div')); + await ReactTestUtils.act(async () => { + root.render(); + expect(Scheduler).toFlushAndYieldThrough([ + 'CascadingChild 0', + 'onCommitRoot', + ]); + }); + expect(triggerActiveCascade).not.toBeNull(); + expect(triggerPassiveCascade).not.toBeNull(); + expect(allSchedulerTypes).toEqual([[null]]); + + await ReactTestUtils.act(async () => { + triggerActiveCascade(); + expect(Scheduler).toFlushAndYieldThrough([ + 'CascadingChild 0', + 'onCommitRoot', + 'CascadingChild 1', + 'onCommitRoot', + ]); + }); + expect(allSchedulerTypes).toEqual([ + [null], + [SchedulingComponent], + [CascadingChild], + ]); + + await ReactTestUtils.act(async () => { + triggerPassiveCascade(); + expect(Scheduler).toFlushAndYieldThrough([ + 'CascadingChild 1', + 'onCommitRoot', + 'CascadingChild 2', + 'onCommitRoot', + ]); + }); + expect(allSchedulerTypes).toEqual([ + [null], + [SchedulingComponent], + [CascadingChild], + [SchedulingComponent], + [CascadingChild], + ]); + + // Verify no outstanding flushes + Scheduler.unstable_flushAll(); + }); + + it('should cover suspense pings', async done => { + let data = null; + let resolver = null; + let promise = null; + const fakeCacheRead = () => { + if (data === null) { + promise = new Promise(resolve => { + resolver = resolvedData => { + data = resolvedData; + resolve(resolvedData); + }; + }); + throw promise; + } else { + return data; + } + }; + const Parent = () => ( + }> + + + ); + const Fallback = () => null; + let setShouldSuspend = null; + const Suspender = ({suspend}) => { + const tuple = React.useState(false); + setShouldSuspend = tuple[1]; + if (tuple[0] === true) { + return fakeCacheRead(); + } else { + return null; + } + }; + + await ReactTestUtils.act(async () => { + ReactDOM.render(, document.createElement('div')); + expect(Scheduler).toHaveYielded(['onCommitRoot']); + }); + expect(setShouldSuspend).not.toBeNull(); + expect(allSchedulerTypes).toEqual([[null]]); + + await ReactTestUtils.act(async () => { + setShouldSuspend(true); + }); + expect(Scheduler).toHaveYielded(['onCommitRoot']); + expect(allSchedulerTypes).toEqual([[null], [Suspender]]); + + expect(resolver).not.toBeNull(); + await ReactTestUtils.act(() => { + resolver('abc'); + return promise; + }); + expect(Scheduler).toHaveYielded(['onCommitRoot']); + expect(allSchedulerTypes).toEqual([[null], [Suspender], [Suspender]]); + + // Verify no outstanding flushes + Scheduler.unstable_flushAll(); + + 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; + + const Parent = () => { + const [shouldError, setShouldError] = React.useState(false); + triggerError = () => setShouldError(true); + return shouldError ? ( + + + + ) : ( + + + + ); + }; + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } + } + const Yield = ({value}) => { + Scheduler.unstable_yieldValue(value); + return null; + }; + const BrokenRender = () => { + throw new Error('Hello'); + }; + + const root = ReactDOM.unstable_createRoot(document.createElement('div')); + await ReactTestUtils.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['initial', 'onCommitRoot']); + expect(triggerError).not.toBeNull(); + + allSchedulerTypes.splice(0); + onCommitRootShouldYield = true; + + await ReactTestUtils.act(async () => { + triggerError(); + }); + expect(Scheduler).toHaveYielded(['onCommitRoot', 'error', 'onCommitRoot']); + expect(allSchedulerTypes).toEqual([[Parent], [ErrorBoundary]]); + + // Verify no outstanding flushes + Scheduler.unstable_flushAll(); + }); + + // @gate experimental + it('should distinguish between updaters in the case of interleaved work', async () => { + const { + FunctionComponent, + HostRoot, + } = require('react-reconciler/src/ReactWorkTags'); + + let triggerLowPriorityUpdate = null; + let triggerSyncPriorityUpdate = null; + + const SyncPriorityUpdater = () => { + const [count, setCount] = React.useState(0); + triggerSyncPriorityUpdate = () => setCount(prevCount => prevCount + 1); + Scheduler.unstable_yieldValue(`SyncPriorityUpdater ${count}`); + return ; + }; + const LowPriorityUpdater = () => { + const [count, setCount] = React.useState(0); + triggerLowPriorityUpdate = () => setCount(prevCount => prevCount + 1); + Scheduler.unstable_yieldValue(`LowPriorityUpdater ${count}`); + return ; + }; + const Yield = ({value}) => { + Scheduler.unstable_yieldValue(`Yield ${value}`); + return null; + }; + + const root = ReactDOM.unstable_createRoot(document.createElement('div')); + root.render( + + + + , + ); + + // Render everything initially. + expect(Scheduler).toFlushAndYield([ + 'SyncPriorityUpdater 0', + 'Yield HighPriority 0', + 'LowPriorityUpdater 0', + 'Yield LowPriority 0', + 'onCommitRoot', + ]); + expect(triggerLowPriorityUpdate).not.toBeNull(); + expect(triggerSyncPriorityUpdate).not.toBeNull(); + expect(allSchedulerTags).toEqual([[HostRoot]]); + + // Render a partial update, but don't finish. + ReactTestUtils.act(() => { + triggerLowPriorityUpdate(); + expect(Scheduler).toFlushAndYieldThrough(['LowPriorityUpdater 1']); + expect(allSchedulerTags).toEqual([[HostRoot]]); + + // Interrupt with higher priority work. + ReactDOM.flushSync(triggerSyncPriorityUpdate); + expect(Scheduler).toHaveYielded([ + 'SyncPriorityUpdater 1', + 'Yield HighPriority 1', + 'onCommitRoot', + ]); + expect(allSchedulerTypes).toEqual([[null], [SyncPriorityUpdater]]); + + // Finish the initial partial update + triggerLowPriorityUpdate(); + expect(Scheduler).toFlushAndYield([ + 'LowPriorityUpdater 2', + 'Yield LowPriority 2', + 'onCommitRoot', + ]); + }); + expect(allSchedulerTags).toEqual([ + [HostRoot], + [FunctionComponent], + [FunctionComponent], + ]); + expect(allSchedulerTypes).toEqual([ + [null], + [SyncPriorityUpdater], + [LowPriorityUpdater], + ]); + + // Verify no outstanding flushes + Scheduler.unstable_flushAll(); + }); +}); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 538a64e193dc8..0729d53d7f3d0 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -54,6 +54,9 @@ 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__; + // SSR experiments export const enableSuspenseServerRenderer = __EXPERIMENTAL__; export const enableSelectiveHydration = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0b1659d1625b9..826afbc1c9e18 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -18,6 +18,7 @@ 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; export const enableLazyElements = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 80e113c8e5bfd..99fbcd61b5be1 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -20,6 +20,7 @@ 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; export const enableLazyElements = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index f5440f7a25f2e..72a390ca9cd7c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -20,6 +20,7 @@ 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; export const enableLazyElements = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 8f31c4e7f84bb..16243e404611d 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -20,6 +20,7 @@ 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; export const enableLazyElements = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 2e04322a32ced..190d006b27fa5 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -20,6 +20,7 @@ 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; export const enableLazyElements = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index bb1c40537287b..61a48fbbd5bce 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -20,6 +20,7 @@ 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; export const enableLazyElements = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 7a12f6041d934..71e77cf6971ab 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -20,6 +20,7 @@ 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; export const enableLazyElements = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 01937c7b125b7..9b9ec4310c9c3 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -25,6 +25,7 @@ export const enableSuspenseLayoutEffectSemantics = __VARIANT__; // // NOTE: This feature will only work in DEV mode; all callsights are wrapped with __DEV__. export const enableDebugTracing = __EXPERIMENTAL__; +export const enableUpdaterTracking = false; export const enableSchedulingProfiler = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 1e8908ea4cde5..eed614e4ae7ef 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -44,6 +44,8 @@ export const enableProfilerCommitHooks = __PROFILE__; export const enableProfilerNestedUpdatePhase = __PROFILE__; export const enableProfilerNestedUpdateScheduledHook = __PROFILE__ && dynamicFeatureFlags.enableProfilerNestedUpdateScheduledHook; +export const enableUpdaterTracking = + __PROFILE__ && dynamicFeatureFlags.enableUpdaterTracking; // Logs additional User Timing API marks for use with an experimental profiling tool. export const enableSchedulingProfiler =