-
Notifications
You must be signed in to change notification settings - Fork 47.5k
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
Add unstable context bailout for profiling #30407
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); | ||
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. probably should make a null compare function equivalent to just returning the context value. There is logic elsewhere that checks if a compare is passed in but this can lead to maybe confusing code paths if you start with a compare function and then remove it on update or vice versa. Basically if you are using this hook the compare should be required internally and we can make the argument optional for convenience by mapping it to some intuitive default compare function. That said since this is a compiler target maybe just make the second argument required? 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. Actually now that I am thinking about the fact that the compiler will always return an array from the compare function there really is no identity mapping. If the context value was a class instance how are you planning on reprsenting that? Just an array with the instance in the first slot? I guess that works and you can opt to use multiple indexes if you detect that there is some kind of object destructuring going on? Still might be worth determining what a null compare argument means semantically or just make it required to avoid the issue 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. This is where the API is designed around testing over direct usage. The goal is to run an A/B performance test. We want some compiler output like
Then we'll define that hook in the app with experiment check like
The experiment check will be stable so we wouldn't go between a compare function and null on any update. We could alternatively pass in some kind of null function
The goal of passing 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. Makes sense |
||
} | ||
|
||
// 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
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 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; | ||
} | ||
} | ||
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 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) { | ||
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. This is where my other comment re: expecting the compare to always be present comes in. Seems potentially if you go from no compare to compare and vice versa since you switch to doing |
||
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, | ||
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. can make this arg required if you make the suggested change |
||
): C { | ||
if (!enableLazyContextPropagation) { | ||
return readContext(context); | ||
} | ||
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. I get why this makes sense logically but seems more appropriate maybe to just make this an error since it's not really valid to build the new flag without the old flag. seems almost certain that this would cause CI to fail if one were to test a build with that particular flag configuration 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 to error if both enableLazyContextPropagation and enableContextProfiling don't pass |
||
|
||
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
compare
is a confusing name for this argument because it doesn't do any comparison it just selects a value. why not call itselect
or 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