-
Notifications
You must be signed in to change notification settings - Fork 49.9k
Add unstable context bailout for profiling #30407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,7 @@ import { | |
| enableUseDeferredValueInitialArg, | ||
| disableLegacyMode, | ||
| enableNoCloningMemoCache, | ||
| enableContextProfiling, | ||
| } from 'shared/ReactFeatureFlags'; | ||
| import { | ||
| REACT_CONTEXT_TYPE, | ||
|
|
@@ -81,7 +82,11 @@ import { | |
| ContinuousEventPriority, | ||
| higherEventPriority, | ||
| } from './ReactEventPriorities'; | ||
| import {readContext, checkIfContextChanged} from './ReactFiberNewContext'; | ||
| import { | ||
| readContext, | ||
| readContextAndCompare, | ||
| checkIfContextChanged, | ||
| } from './ReactFiberNewContext'; | ||
| import {HostRoot, CacheComponent, HostComponent} from './ReactWorkTags'; | ||
| import { | ||
| LayoutStatic as LayoutStaticEffect, | ||
|
|
@@ -1053,6 +1058,13 @@ function updateWorkInProgressHook(): Hook { | |
| return workInProgressHook; | ||
| } | ||
|
|
||
| function unstable_useContextWithBailout<T>( | ||
| context: ReactContext<T>, | ||
| compare: (T => mixed) | null, | ||
| ): T { | ||
| return readContextAndCompare(context, compare); | ||
|
||
| } | ||
|
|
||
| // NOTE: defining two versions of this function to avoid size impact when this feature is disabled. | ||
| // Previously this function was inlined, the additional `memoCache` property makes it not inlined. | ||
| let createFunctionComponentUpdateQueue: () => FunctionComponentUpdateQueue; | ||
|
|
@@ -3689,6 +3701,10 @@ if (enableAsyncActions) { | |
| if (enableAsyncActions) { | ||
| (ContextOnlyDispatcher: Dispatcher).useOptimistic = throwInvalidHookError; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (ContextOnlyDispatcher: Dispatcher).unstable_useContextWithBailout = | ||
| throwInvalidHookError; | ||
| } | ||
|
|
||
| const HooksDispatcherOnMount: Dispatcher = { | ||
| readContext, | ||
|
|
@@ -3728,6 +3744,10 @@ if (enableAsyncActions) { | |
| if (enableAsyncActions) { | ||
| (HooksDispatcherOnMount: Dispatcher).useOptimistic = mountOptimistic; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnMount: Dispatcher).unstable_useContextWithBailout = | ||
| unstable_useContextWithBailout; | ||
| } | ||
|
|
||
| const HooksDispatcherOnUpdate: Dispatcher = { | ||
| readContext, | ||
|
|
@@ -3767,6 +3787,10 @@ if (enableAsyncActions) { | |
| if (enableAsyncActions) { | ||
| (HooksDispatcherOnUpdate: Dispatcher).useOptimistic = updateOptimistic; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnUpdate: Dispatcher).unstable_useContextWithBailout = | ||
| unstable_useContextWithBailout; | ||
| } | ||
|
|
||
| const HooksDispatcherOnRerender: Dispatcher = { | ||
| readContext, | ||
|
|
@@ -3806,6 +3830,10 @@ if (enableAsyncActions) { | |
| if (enableAsyncActions) { | ||
| (HooksDispatcherOnRerender: Dispatcher).useOptimistic = rerenderOptimistic; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnRerender: Dispatcher).unstable_useContextWithBailout = | ||
| unstable_useContextWithBailout; | ||
| } | ||
|
|
||
| let HooksDispatcherOnMountInDEV: Dispatcher | null = null; | ||
| let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; | ||
|
|
@@ -4019,6 +4047,14 @@ if (__DEV__) { | |
| return mountOptimistic(passthrough, reducer); | ||
| }; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout = | ||
| function <T>(context: ReactContext<T>, compare: (T => mixed) | null): T { | ||
| currentHookNameInDev = 'useContext'; | ||
| mountHookTypesDev(); | ||
| return unstable_useContextWithBailout(context, compare); | ||
| }; | ||
| } | ||
|
|
||
| HooksDispatcherOnMountWithHookTypesInDEV = { | ||
| readContext<T>(context: ReactContext<T>): T { | ||
|
|
@@ -4200,6 +4236,14 @@ if (__DEV__) { | |
| return mountOptimistic(passthrough, reducer); | ||
| }; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout = | ||
| function <T>(context: ReactContext<T>, compare: (T => mixed) | null): T { | ||
| currentHookNameInDev = 'useContext'; | ||
| updateHookTypesDev(); | ||
| return unstable_useContextWithBailout(context, compare); | ||
| }; | ||
| } | ||
|
|
||
| HooksDispatcherOnUpdateInDEV = { | ||
| readContext<T>(context: ReactContext<T>): T { | ||
|
|
@@ -4380,6 +4424,14 @@ if (__DEV__) { | |
| return updateOptimistic(passthrough, reducer); | ||
| }; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = | ||
| function <T>(context: ReactContext<T>, compare: (T => mixed) | null): T { | ||
| currentHookNameInDev = 'useContext'; | ||
| updateHookTypesDev(); | ||
| return unstable_useContextWithBailout(context, compare); | ||
| }; | ||
| } | ||
|
|
||
| HooksDispatcherOnRerenderInDEV = { | ||
| readContext<T>(context: ReactContext<T>): T { | ||
|
|
@@ -4560,6 +4612,14 @@ if (__DEV__) { | |
| return rerenderOptimistic(passthrough, reducer); | ||
| }; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = | ||
| function <T>(context: ReactContext<T>, compare: (T => mixed) | null): T { | ||
| currentHookNameInDev = 'useContext'; | ||
| updateHookTypesDev(); | ||
| return unstable_useContextWithBailout(context, compare); | ||
| }; | ||
| } | ||
|
|
||
| InvalidNestedHooksDispatcherOnMountInDEV = { | ||
| readContext<T>(context: ReactContext<T>): T { | ||
|
|
@@ -4766,6 +4826,15 @@ if (__DEV__) { | |
| return mountOptimistic(passthrough, reducer); | ||
| }; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = | ||
| function <T>(context: ReactContext<T>, compare: (T => mixed) | null): T { | ||
| currentHookNameInDev = 'useContext'; | ||
| warnInvalidHookAccess(); | ||
| mountHookTypesDev(); | ||
| return unstable_useContextWithBailout(context, compare); | ||
| }; | ||
| } | ||
|
|
||
| InvalidNestedHooksDispatcherOnUpdateInDEV = { | ||
| readContext<T>(context: ReactContext<T>): T { | ||
|
|
@@ -4972,6 +5041,15 @@ if (__DEV__) { | |
| return updateOptimistic(passthrough, reducer); | ||
| }; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = | ||
| function <T>(context: ReactContext<T>, compare: (T => mixed) | null): T { | ||
| currentHookNameInDev = 'useContext'; | ||
| warnInvalidHookAccess(); | ||
| updateHookTypesDev(); | ||
| return unstable_useContextWithBailout(context, compare); | ||
| }; | ||
| } | ||
|
|
||
| InvalidNestedHooksDispatcherOnRerenderInDEV = { | ||
| readContext<T>(context: ReactContext<T>): T { | ||
|
|
@@ -5178,4 +5256,13 @@ if (__DEV__) { | |
| return rerenderOptimistic(passthrough, reducer); | ||
| }; | ||
| } | ||
| if (enableContextProfiling) { | ||
| (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout = | ||
| function <T>(context: ReactContext<T>, compare: (T => mixed) | null): T { | ||
| currentHookNameInDev = 'useContext'; | ||
| warnInvalidHookAccess(); | ||
| updateHookTypesDev(); | ||
| return unstable_useContextWithBailout(context, compare); | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ import type { | |
| Fiber, | ||
| ContextDependency, | ||
| Dependencies, | ||
| ContextDependencyWithCompare, | ||
| } from './ReactInternalTypes'; | ||
| import type {StackCursor} from './ReactFiberStack'; | ||
| import type {Lanes} from './ReactFiberLane'; | ||
|
|
@@ -51,6 +52,8 @@ import { | |
| getHostTransitionProvider, | ||
| HostTransitionContext, | ||
| } from './ReactFiberHostContext'; | ||
| import isArray from '../../shared/isArray'; | ||
| import {enableContextProfiling} from '../../shared/ReactFeatureFlags'; | ||
|
|
||
| const valueCursor: StackCursor<mixed> = createCursor(null); | ||
|
|
||
|
|
@@ -70,7 +73,10 @@ if (__DEV__) { | |
| } | ||
|
|
||
| let currentlyRenderingFiber: Fiber | null = null; | ||
| let lastContextDependency: ContextDependency<mixed> | null = null; | ||
| let lastContextDependency: | ||
| | ContextDependency<mixed> | ||
| | ContextDependencyWithCompare<mixed, mixed> | ||
| | null = null; | ||
| let lastFullyObservedContext: ReactContext<any> | null = null; | ||
|
|
||
| let isDisallowedContextReadInDEV: boolean = false; | ||
|
|
@@ -400,8 +406,24 @@ function propagateContextChanges<T>( | |
| findContext: for (let i = 0; i < contexts.length; i++) { | ||
| const context: ReactContext<T> = contexts[i]; | ||
| // Check if the context matches. | ||
| // TODO: Compare selected values to bail out early. | ||
| if (dependency.context === context) { | ||
| if (enableContextProfiling) { | ||
| const compare = dependency.compare; | ||
| if (compare != null) { | ||
| const newValue = isPrimaryRenderer | ||
| ? dependency.context._currentValue | ||
| : dependency.context._currentValue2; | ||
| if ( | ||
| !checkIfComparedContextValuesChanged( | ||
| dependency.lastComparedValue, | ||
| compare(newValue), | ||
| ) | ||
| ) { | ||
| // Compared value hasn't changed. Bail out early. | ||
| continue findContext; | ||
| } | ||
| } | ||
| } | ||
| // Match! Schedule an update on this fiber. | ||
|
|
||
| // In the lazy implementation, don't mark a dirty flag on the | ||
|
|
@@ -641,6 +663,28 @@ function propagateParentContextChanges( | |
| workInProgress.flags |= DidPropagateContext; | ||
| } | ||
|
|
||
| function checkIfComparedContextValuesChanged( | ||
| oldComparedValue: mixed, | ||
| newComparedValue: mixed, | ||
| ): boolean { | ||
| if (isArray(oldComparedValue) && isArray(newComparedValue)) { | ||
jackpope marked this conversation as resolved.
Show resolved
Hide resolved
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's comment why this array check is safe. You mentioned in the comments but would be good to clarify that returning an array an implicit contract and we don't expect to return other things for now
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||
| for ( | ||
| let i = 0; | ||
| i < oldComparedValue.length && i < newComparedValue.length; | ||
| i++ | ||
| ) { | ||
| if (!is(newComparedValue[i], oldComparedValue[i])) { | ||
| return true; | ||
| } | ||
| } | ||
| } else { | ||
jackpope marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!is(newComparedValue, oldComparedValue)) { | ||
| return true; | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really sure it's worth leaving this in b/c you can't really just change the compiler without also changing this comparison logic since you'd end up with incidental index based comparisons if you ever didn't return a wrapping array. Since its de facto part of the API might as well make anything that violates this API error so you know you messed something up in the compiler
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the |
||
| return false; | ||
| } | ||
|
|
||
| export function checkIfContextChanged( | ||
| currentDependencies: Dependencies, | ||
| ): boolean { | ||
|
|
@@ -659,8 +703,19 @@ export function checkIfContextChanged( | |
| ? context._currentValue | ||
| : context._currentValue2; | ||
| const oldValue = dependency.memoizedValue; | ||
| if (!is(newValue, oldValue)) { | ||
| return true; | ||
| if (enableContextProfiling && dependency.compare != null) { | ||
|
||
| if ( | ||
| checkIfComparedContextValuesChanged( | ||
| dependency.lastComparedValue, | ||
| dependency.compare(newValue), | ||
| ) | ||
| ) { | ||
| return true; | ||
| } | ||
| } else { | ||
| if (!is(newValue, oldValue)) { | ||
| return true; | ||
| } | ||
| } | ||
| dependency = dependency.next; | ||
| } | ||
|
|
@@ -694,6 +749,21 @@ export function prepareToReadContext( | |
| } | ||
| } | ||
|
|
||
| export function readContextAndCompare<C>( | ||
| context: ReactContext<C>, | ||
| compare: (C => mixed) | null, | ||
|
||
| ): C { | ||
| if (!enableLazyContextPropagation) { | ||
| return readContext(context); | ||
| } | ||
|
||
|
|
||
| return readContextForConsumer_withCompare( | ||
| currentlyRenderingFiber, | ||
| context, | ||
| compare, | ||
| ); | ||
| } | ||
|
|
||
| export function readContext<T>(context: ReactContext<T>): T { | ||
| if (__DEV__) { | ||
| // This warning would fire if you read context inside a Hook like useMemo. | ||
|
|
@@ -721,10 +791,59 @@ export function readContextDuringReconciliation<T>( | |
| return readContextForConsumer(consumer, context); | ||
| } | ||
|
|
||
| function readContextForConsumer<T>( | ||
| type ContextCompare<C, V> = C => V | null; | ||
|
|
||
| function readContextForConsumer_withCompare<C, S>( | ||
| consumer: Fiber | null, | ||
| context: ReactContext<T>, | ||
| ): T { | ||
| context: ReactContext<C>, | ||
| compare: (C => S) | null, | ||
| ): C { | ||
| const value = isPrimaryRenderer | ||
| ? context._currentValue | ||
| : context._currentValue2; | ||
|
|
||
| if (lastFullyObservedContext === context) { | ||
| // Nothing to do. We already observe everything in this context. | ||
| } else { | ||
| const contextItem = { | ||
| context: ((context: any): ReactContext<mixed>), | ||
| memoizedValue: value, | ||
| next: null, | ||
| compare: compare ? ((compare: any): ContextCompare<mixed, mixed>) : null, | ||
| lastComparedValue: compare != null ? compare(value) : null, | ||
| }; | ||
|
|
||
| if (lastContextDependency === null) { | ||
| if (consumer === null) { | ||
| throw new Error( | ||
| 'Context can only be read while React is rendering. ' + | ||
| 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + | ||
| 'In function components, you can read it directly in the function body, but not ' + | ||
| 'inside Hooks like useReducer() or useMemo().', | ||
| ); | ||
| } | ||
|
|
||
| // This is the first dependency for this component. Create a new list. | ||
| lastContextDependency = contextItem; | ||
| consumer.dependencies = { | ||
| lanes: NoLanes, | ||
| firstContext: contextItem, | ||
| }; | ||
| if (enableLazyContextPropagation) { | ||
| consumer.flags |= NeedsPropagation; | ||
| } | ||
| } else { | ||
| // Append a new context item. | ||
| lastContextDependency = lastContextDependency.next = contextItem; | ||
| } | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| function readContextForConsumer<C>( | ||
| consumer: Fiber | null, | ||
| context: ReactContext<C>, | ||
| ): C { | ||
| const value = isPrimaryRenderer | ||
| ? context._currentValue | ||
| : context._currentValue2; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name isn't super important b/c it's not going to be observed by anyone but I feel like
compareis a confusing name for this argument because it doesn't do any comparison it just selects a value. why not call itselector something. I think you can land with compare just realized it was just chafing my automatic mental model while reviewing the PRThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated