From 9f06bc345f78922bbe6f428280311b39a305e324 Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Fri, 26 Apr 2019 05:34:16 -0700 Subject: [PATCH 1/2] Add Flow types for Fiber Fixes https://github.com/bvaughn/react-devtools-experimental/issues/165 --- .flowconfig | 1 + src/backend/renderer.js | 110 ++++++++++++++++++++++++++++------------ src/backend/types.js | 84 +++++++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 35 deletions(-) diff --git a/.flowconfig b/.flowconfig index c2f42c17c7..3280cac0e8 100644 --- a/.flowconfig +++ b/.flowconfig @@ -26,6 +26,7 @@ shells/dev/build/* [options] esproposal.class_instance_fields=enable +esproposal.optional_chaining=enable suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe suppress_comment=\\(.\\|\n\\)*\\$FlowIssue suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore diff --git a/src/backend/renderer.js b/src/backend/renderer.js index c4e515829b..052ec1ceff 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -573,7 +573,6 @@ export function attach( } } - // eslint-disable-next-line no-unused-vars function haveProfilerTimesChanged( prevFiber: Fiber, nextFiber: Fiber @@ -715,7 +714,7 @@ export function attach( const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration'); if (isProfilingSupported) { idToRootMap.set(id, currentRootID); - idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration); + idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration || 0); } const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner'); @@ -732,7 +731,9 @@ export function attach( const ownerID = _debugOwner != null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0; - const parentID = getFiberID(getPrimaryFiber(parentFiber)); + const parentID = parentFiber + ? getFiberID(getPrimaryFiber(parentFiber)) + : 0; let displayNameStringID = getStringID(displayName); let keyStringID = getStringID(key); @@ -748,14 +749,14 @@ export function attach( if (isProfiling) { // Tree base duration updates are included in the operations typed array. // So we have to convert them from milliseconds to microseconds so we can send them as ints. - const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000); + const treeBaseDuration = Math.floor((fiber.treeBaseDuration || 0) * 1000); pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); pushOperation(id); pushOperation(treeBaseDuration); const { actualDuration } = fiber; - if (actualDuration > 0) { + if (actualDuration != null && actualDuration > 0) { // If profiling is active, store durations for elements that were rendered during the commit. const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); metadata.actualDurations.push(id, actualDuration); @@ -849,8 +850,12 @@ export function attach( // get the fallback child from the inner fragment and mount // it as if it was our own child. Updates handle this too. const primaryChildFragment = fiber.child; - const fallbackChildFragment = primaryChildFragment.sibling; - const fallbackChild = fallbackChildFragment.child; + const fallbackChildFragment = primaryChildFragment + ? primaryChildFragment.sibling + : null; + const fallbackChild = fallbackChildFragment + ? fallbackChildFragment.child + : null; if (fallbackChild !== null) { mountFiberRecursively( fallbackChild, @@ -893,9 +898,11 @@ export function attach( if (isTimedOutSuspense) { // If it's showing fallback tree, let's traverse it instead. const primaryChildFragment = fiber.child; - const fallbackChildFragment = primaryChildFragment.sibling; + const fallbackChildFragment = primaryChildFragment + ? primaryChildFragment.sibling + : null; // Skip over to the real Fiber child. - child = fallbackChildFragment.child; + child = fallbackChildFragment ? fallbackChildFragment.child : null; } while (child !== null) { @@ -913,20 +920,27 @@ export function attach( const id = getFiberID(getPrimaryFiber(fiber)); const { actualDuration, treeBaseDuration } = fiber; - idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration); + idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration || 0); if (isProfiling) { - if (treeBaseDuration !== fiber.alternate.treeBaseDuration) { + const { alternate } = fiber; + + if ( + treeBaseDuration !== + (alternate ? alternate.treeBaseDuration : undefined) + ) { // Tree base duration updates are included in the operations typed array. // So we have to convert them from milliseconds to microseconds so we can send them as ints. - const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000); + const treeBaseDuration = Math.floor( + (fiber.treeBaseDuration || 0) * 1000 + ); pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); pushOperation(getFiberID(getPrimaryFiber(fiber))); pushOperation(treeBaseDuration); } - if (haveProfilerTimesChanged(fiber.alternate, fiber)) { - if (actualDuration > 0) { + if (alternate ? haveProfilerTimesChanged(alternate, fiber) : true) { + if (actualDuration != null && actualDuration > 0) { // If profiling is active, store durations for elements that were rendered during the commit. const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); metadata.actualDurations.push(id, actualDuration); @@ -1008,11 +1022,19 @@ export function attach( if (prevDidTimeout && nextDidTimeOut) { // Fallback -> Fallback: // 1. Reconcile fallback set. - const nextFallbackChildSet = nextFiber.child.sibling; + const nextFiberChild = nextFiber.child; + const nextFallbackChildSet = nextFiberChild + ? nextFiberChild.sibling + : null; // Note: We can't use nextFiber.child.sibling.alternate // because the set is special and alternate may not exist. - const prevFallbackChildSet = prevFiber.child.sibling; + const prevFiberChild = prevFiber.child; + const prevFallbackChildSet = prevFiberChild + ? prevFiberChild.sibling + : null; if ( + nextFallbackChildSet != null && + prevFallbackChildSet != null && updateFiberRecursively( nextFallbackChildSet, prevFallbackChildSet, @@ -1038,9 +1060,14 @@ export function attach( // We need to manually walk the previous tree and record unmounts. unmountFiberChildrenRecursively(prevFiber); // 2. Mount fallback set - const nextFallbackChildSet = nextFiber.child.sibling; - mountFiberRecursively(nextFallbackChildSet, nextFiber, true); - shouldResetChildren = true; + const nextFiberChild = nextFiber.child; + const nextFallbackChildSet = nextFiberChild + ? nextFiberChild.sibling + : null; + if (nextFallbackChildSet != null) { + mountFiberRecursively(nextFallbackChildSet, nextFiber, true); + shouldResetChildren = true; + } } else { // Common case: Primary -> Primary. // This is the same codepath as for non-Suspense fibers. @@ -1109,9 +1136,12 @@ export function attach( let nextChildSet = nextFiber.child; if (nextDidTimeOut) { // Special case: timed-out Suspense renders the fallback set. - nextChildSet = nextFiber.child.sibling; + const nextFiberChild = nextFiber.child; + nextChildSet = nextFiberChild ? nextFiberChild.sibling : null; + } + if (nextChildSet != null) { + recordResetChildren(nextFiber, nextChildSet); } - recordResetChildren(nextFiber, nextChildSet); // We've handled the child order change for this Fiber. // Since it's included, there's no need to invalidate parent child order. return false; @@ -1260,7 +1290,7 @@ export function attach( function findNativeByFiberID(id: number) { try { - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber === null) { return null; } @@ -1550,6 +1580,11 @@ export function attach( // Find the currently mounted version of this fiber (so we don't show the wrong props and state). fiber = findCurrentFiberUsingSlowPath(fiber); + if (fiber == null) { + console.warn(`Could not find Fiber with id "${id}"`); + return null; + } + const { _debugOwner, _debugSource, @@ -1558,7 +1593,7 @@ export function attach( memoizedState, tag, type, - } = ((fiber: any): Fiber); + } = fiber; const usesHooks = (tag === FunctionComponent || @@ -1632,7 +1667,7 @@ export function attach( displayName: getDataForFiber(owner).displayName || 'Unknown', id: getFiberID(getPrimaryFiber(owner)), }); - owner = owner._debugOwner; + owner = owner._debugOwner || null; } } @@ -1729,13 +1764,21 @@ export function attach( } } + function findCurrentFiberUsingSlowPathById(id: number): Fiber | null { + const fiber = idToFiberMap.get(id); + if (fiber) { + return findCurrentFiberUsingSlowPath(fiber); + } + return null; + } + function setInHook( id: number, index: number, path: Array, value: any ) { - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { if (typeof overrideHookState === 'function') { overrideHookState(fiber, index, path, value); @@ -1744,7 +1787,7 @@ export function attach( } function setInProps(id: number, path: Array, value: any) { - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { const instance = fiber.stateNode; if (instance === null) { @@ -1759,7 +1802,7 @@ export function attach( } function setInState(id: number, path: Array, value: any) { - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { const instance = fiber.stateNode; setInObject(instance.state, path, value); @@ -1773,7 +1816,7 @@ export function attach( // We need to remove the first part of the path (the "value") before continuing. path = path.slice(1); - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { const instance = fiber.stateNode; if (path.length === 0) { @@ -1940,17 +1983,18 @@ export function attach( } const initialTreeBaseDurations = []; - ((initialTreeBaseDurationsMap: any): Map).forEach( - (treeBaseDuration, id) => { + if (initialTreeBaseDurationsMap != null) { + initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => { if ( - ((initialIDToRootMap: any): Map).get(id) === rootID + initialIDToRootMap != null && + initialIDToRootMap.get(id) === rootID ) { // We don't need to convert milliseconds to microseconds in this case, // because the profiling summary is JSON serialized. initialTreeBaseDurations.push(id, treeBaseDuration); } - } - ); + }); + } return { commitDurations, diff --git a/src/backend/types.js b/src/backend/types.js index 0bd1648233..705d2bc7ca 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -7,8 +7,88 @@ type BundleType = | 0 // PROD | 1; // DEV -// TODO: Better type for Fiber -export type Fiber = Object; +export type WorkTag = number; +export type TypeOfMode = number; +export type SideEffectTag = number; +export type ExpirationTime = number; +export type RefObject = {| + current: any, +|}; +export type Source = { + fileName: string, + lineNumber: number, +}; +export type HookType = + | 'useState' + | 'useReducer' + | 'useContext' + | 'useRef' + | 'useEffect' + | 'useLayoutEffect' + | 'useCallback' + | 'useMemo' + | 'useImperativeHandle' + | 'useDebugValue'; + +// Copied modified from https://github.com/facebook/react/blob/v16.9.0-alpha.0/packages/react-reconciler/src/ReactFiber.js +export type Fiber = {| + tag: WorkTag, + + key: null | string, + + elementType: any, + + type: any, + + stateNode: any, + + return: Fiber | null, + + child: Fiber | null, + sibling: Fiber | null, + index: number, + + ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject, + + pendingProps: any, // This type will be more specific once we overload the tag. + memoizedProps: any, // The props used to create the output. + + // updateQueue: UpdateQueue | null, + + memoizedState: any, + + // contextDependencies: ContextDependencyList | null, + + mode: TypeOfMode, + + effectTag: SideEffectTag, + + nextEffect: Fiber | null, + + firstEffect: Fiber | null, + lastEffect: Fiber | null, + + expirationTime: ExpirationTime, + + childExpirationTime: ExpirationTime, + + alternate: Fiber | null, + + actualDuration?: number, + + actualStartTime?: number, + + selfBaseDuration?: number, + + treeBaseDuration?: number, + + _debugID?: number, + _debugSource?: Source | null, + _debugOwner?: Fiber | null, + _debugIsCurrentlyTiming?: boolean, + + _debugHookTypes?: Array | null, +|}; // TODO: If it's useful for the frontend to know which types of data an Element has // (e.g. props, state, context, hooks) then we could add a bitmask field for this From d35cf80921245ff7da4bcc7c17f68082f21088fb Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Sun, 28 Apr 2019 12:57:28 -0700 Subject: [PATCH 2/2] Remove Fiber type properties we don't use in DevTools --- src/backend/types.js | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/backend/types.js b/src/backend/types.js index 705d2bc7ca..4a2c5372c9 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -8,7 +8,6 @@ type BundleType = | 1; // DEV export type WorkTag = number; -export type TypeOfMode = number; export type SideEffectTag = number; export type ExpirationTime = number; export type RefObject = {| @@ -30,7 +29,9 @@ export type HookType = | 'useImperativeHandle' | 'useDebugValue'; -// Copied modified from https://github.com/facebook/react/blob/v16.9.0-alpha.0/packages/react-reconciler/src/ReactFiber.js +// The Fiber type is copied from React and should be kept in sync: +// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiber.js +// The properties we don't use in DevTools are omitted. export type Fiber = {| tag: WorkTag, @@ -53,41 +54,20 @@ export type Fiber = {| pendingProps: any, // This type will be more specific once we overload the tag. memoizedProps: any, // The props used to create the output. - // updateQueue: UpdateQueue | null, - memoizedState: any, - // contextDependencies: ContextDependencyList | null, - - mode: TypeOfMode, - effectTag: SideEffectTag, - nextEffect: Fiber | null, - - firstEffect: Fiber | null, - lastEffect: Fiber | null, - - expirationTime: ExpirationTime, - - childExpirationTime: ExpirationTime, - alternate: Fiber | null, actualDuration?: number, actualStartTime?: number, - selfBaseDuration?: number, - treeBaseDuration?: number, - _debugID?: number, _debugSource?: Source | null, _debugOwner?: Fiber | null, - _debugIsCurrentlyTiming?: boolean, - - _debugHookTypes?: Array | null, |}; // TODO: If it's useful for the frontend to know which types of data an Element has