Skip to content
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

Merged
merged 6 commits into from
Jul 26, 2024

Conversation

jackpope
Copy link
Contributor

This API is not intended to ship. This is a temporary unstable hook for internal performance profiling.

This PR exposes unstable_useContextWithBailout, which takes a compare function in addition to Context. The comparison function is run to determine if Context propagation and render should bail out earlier. unstable_useContextWithBailout returns the full Context value, same as useContext.

We can profile this API against useContext to better measure the cost of Context value updates and gather more data around propagation and render performance.

The bailout logic and test cases are based on #20646

Additionally, this implementation allows multiple values to be compared in one hook by returning a tuple to avoid requiring additional Context consumer hooks in some cases.

Copy link

vercel bot commented Jul 19, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 26, 2024 5:38pm

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jul 19, 2024
@react-sizebot
Copy link

react-sizebot commented Jul 19, 2024

Comparing: f7ee804...ce4479c

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB +0.05% 1.82 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 501.46 kB 501.46 kB = 89.98 kB 89.98 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB +0.05% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 506.28 kB 506.28 kB = 90.68 kB 90.68 kB
facebook-www/ReactDOM-prod.classic.js +0.40% 598.90 kB 601.27 kB +0.48% 105.77 kB 106.28 kB
facebook-www/ReactDOM-prod.modern.js +0.41% 575.01 kB 577.38 kB +0.51% 102.01 kB 102.52 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/React-prod.modern.js +1.40% 22.17 kB 22.48 kB +1.21% 5.79 kB 5.86 kB
facebook-www/React-prod.classic.js +1.40% 22.17 kB 22.48 kB +1.19% 5.79 kB 5.86 kB
facebook-www/React-profiling.modern.js +1.38% 22.60 kB 22.92 kB +1.19% 5.87 kB 5.94 kB
facebook-www/React-profiling.classic.js +1.38% 22.61 kB 22.92 kB +1.21% 5.87 kB 5.94 kB
facebook-www/React-dev.modern.js +0.81% 72.94 kB 73.53 kB +0.47% 16.05 kB 16.13 kB
facebook-www/React-dev.classic.js +0.80% 73.80 kB 74.39 kB +0.50% 16.19 kB 16.27 kB
facebook-www/ReactART-dev.modern.js +0.71% 631.21 kB 635.68 kB +0.67% 101.38 kB 102.06 kB
facebook-www/ReactART-dev.classic.js +0.68% 654.69 kB 659.17 kB +0.65% 105.09 kB 105.76 kB
facebook-www/ReactART-prod.modern.js +0.68% 350.81 kB 353.18 kB +0.86% 59.58 kB 60.09 kB
facebook-www/ReactReconciler-dev.modern.js +0.66% 712.14 kB 716.85 kB +0.62% 113.57 kB 114.27 kB
facebook-www/ReactART-prod.classic.js +0.64% 368.28 kB 370.65 kB +0.77% 62.34 kB 62.82 kB
facebook-www/ReactReconciler-dev.classic.js +0.64% 737.01 kB 741.72 kB +0.60% 117.69 kB 118.39 kB
facebook-www/ReactReconciler-prod.modern.js +0.61% 440.29 kB 442.99 kB +0.75% 71.33 kB 71.87 kB
facebook-www/ReactReconciler-prod.classic.js +0.59% 459.18 kB 461.89 kB +0.72% 74.15 kB 74.68 kB
facebook-react-native/react/cjs/React-prod.js +0.50% 18.56 kB 18.65 kB +0.58% 4.80 kB 4.83 kB
facebook-react-native/react/cjs/React-profiling.js +0.49% 18.99 kB 19.08 kB +0.57% 4.88 kB 4.91 kB
facebook-www/ReactDOM-dev.modern.js +0.43% 1,047.74 kB 1,052.22 kB +0.41% 175.74 kB 176.45 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.42% 1,064.66 kB 1,069.13 kB +0.41% 179.63 kB 180.36 kB
facebook-www/ReactDOM-dev.classic.js +0.41% 1,084.92 kB 1,089.40 kB +0.40% 182.26 kB 183.00 kB
facebook-www/ReactDOM-prod.modern.js +0.41% 575.01 kB 577.38 kB +0.51% 102.01 kB 102.52 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.41% 1,101.84 kB 1,106.31 kB +0.39% 186.23 kB 186.95 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.40% 589.76 kB 592.12 kB +0.47% 105.73 kB 106.23 kB
facebook-www/ReactDOM-prod.classic.js +0.40% 598.90 kB 601.27 kB +0.48% 105.77 kB 106.28 kB
facebook-www/ReactDOM-profiling.modern.js +0.39% 604.34 kB 606.70 kB +0.44% 106.29 kB 106.76 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.39% 613.64 kB 616.01 kB +0.46% 109.47 kB 109.98 kB
facebook-www/ReactDOM-profiling.classic.js +0.38% 628.86 kB 631.23 kB +0.45% 110.24 kB 110.73 kB

Generated by 🚫 dangerJS against 96136ab

context: ReactContext<T>,
compare: (T => mixed) | null,
): T {
return readContextAndCompare(context, compare);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

const {foo, bar} = useContextWithBailoutTest(MyContext, (c) => [c.foo, c.bar])

Then we'll define that hook in the app with experiment check like

function useContextWithBailoutTest(context, compare) {
  const inBailoutExperiment = bailoutExperimentCheck();
  return unstable_useContextWithBailout(context, inBailoutExperiment ? compare : null)
}

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

return unstable_useContextWithBailout(context, inBailoutExperiment ? compare : () => {})

The goal of passing null directly was to prevent setting the extra properties on the dependency and running the compare in propagation for the control side of the test. It's likely negligible in real apps but does show up as additional overhead on benchmarks with very fast updates.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

@@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 is based comparison and ignoring the last compared value

@@ -694,6 +749,21 @@ export function prepareToReadContext(
}
}

export function readContextAndCompare<C>(
context: ReactContext<C>,
compare: (C => mixed) | null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can make this arg required if you make the suggested change

Comment on lines 756 to 758
if (!enableLazyContextPropagation) {
return readContext(context);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to error if both enableLazyContextPropagation and enableContextProfiling don't pass

@@ -1053,6 +1058,13 @@ function updateWorkInProgressHook(): Hook {
return workInProgressHook;
}

function unstable_useContextWithBailout<T>(
context: ReactContext<T>,
compare: (T => mixed) | null,
Copy link
Collaborator

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 it select or something. I think you can land with compare just realized it was just chafing my automatic mental model while reviewing the PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

oldComparedValue: mixed,
newComparedValue: mixed,
): boolean {
if (isArray(oldComparedValue) && isArray(newComparedValue)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Comment on lines 680 to 684
} else {
if (!is(newComparedValue, oldComparedValue)) {
return true;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the is check here in favor of leaning into the array return contract. Also typed the inputs as Arrays and check against lastSelectedValue being available before entering comparison function

@jackpope jackpope merged commit 1350a85 into facebook:main Jul 26, 2024
187 checks passed
@jackpope jackpope deleted the profile-context-access branch July 26, 2024 18:38
gsathya added a commit that referenced this pull request Jul 31, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

ghstack-source-id: 3dde92d74a7ebb4fc688074c628038eb6eff8fb9
Pull Request resolved: #30548
gsathya added a commit that referenced this pull request Jul 31, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Jul 31, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

ghstack-source-id: 0cefe6c7127da23bf2d3f199898ec2030b394527
Pull Request resolved: #30548
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

ghstack-source-id: f01152c777fa2877d4118894557a6b96d572e18f
Pull Request resolved: #30548
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

ghstack-source-id: 6a8d58e9f18a34c00e7555fe1e961d6f2420b01c
Pull Request resolved: #30548
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 2, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

ghstack-source-id: 45b59f0cc825b010ec9c034ceea447fafeef6ed6
Pull Request resolved: #30548
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

ghstack-source-id: 8a1ba69989c13939c6646b7ac6fc5255b4770ac2
Pull Request resolved: #30548
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 6, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 7, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 7, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

[ghstack-poisoned]
gsathya added a commit that referenced this pull request Aug 7, 2024
*This is only for internal profiling, not intended to ship.*

This pass is intended to be used with #30407.

This pass synthesizes selector functions by collecting immediately
destructured context acesses. We bailout for other types of context
access.

This pass lowers context access to use a selector function by passing
the synthesized selector function as the second argument.

ghstack-source-id: 92d0f6ff2fe95cda93f66786f56e97ba9ace95fa
Pull Request resolved: #30548
hoxyq added a commit that referenced this pull request Aug 30, 2024
…ks and useContextWithBailout (#30837)

Related - #30407.

This is experimental-only and FB-only hook. Without these changes,
inspecting an element that is using this hook will throw an error,
because this hook is missing in Dispatcher implementation from React
DevTools, which overrides the original one to build the hook tree.

![Screenshot 2024-08-28 at 18 42
55](https://github.com/user-attachments/assets/e3bccb92-74fb-4e4a-8181-03d13f8512c0)

One nice thing from it is that in case of any potential regressions
related to this experiment, we can quickly triage which implementation
of `useContext` is used by inspecting an element in React DevTools.

Ideally, I should've added some component that is using this hook to
`react-devtools-shell`, so it can be manually tested, but I can't do it
without rewriting the infra for it. This is because this hook is only
available from fb-www builds, and not experimental.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants