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

Ensure React object shape is monomorphic #14267

Closed
wants to merge 2 commits into from

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Nov 18, 2018

The main React object's property call-sites suffer from deoptimizing because we assign properties to the object after its creation, thus changing the object's shape (hidden class). This moves all properties back into the object literal.

this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought this conditional was stripped from production and production+profiling builds (as part of uglification).

Removing it will cause non-profiling production builds to increase memory size, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the CJS bundles in NPM, it looks to me like the condition is being removed from all but the dev builds.

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, my above comment only applies to builds where this flag is static ^

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was testing this on the FB bundle, sorry, I should have stated. We need to somehow make this static for FB too otherwise it seems most, if not all of our fibers on FB code end up getting deoptimized on V8 :(

Copy link
Contributor

@bvaughn bvaughn Nov 18, 2018

Choose a reason for hiding this comment

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

It is static for production bundles– just not the dev bundle because we don't dead code eliminate it.

We choose which bundle to serve based on a gatekeeper. (We switch at the level of the react-dom bundle.) Within the bundles– profiling or production+profiling– we've already DCE'd so the condition has been removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I remove the change, as it's not the fix. Something else must be touching those properties. Thanks for the help, I'll keep digging.

@sizebot
Copy link

sizebot commented Nov 18, 2018

React: size: 🔺+0.4%, gzip: 🔺+0.3%

Details of bundled changes.

Comparing: d204747...ddf954e

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js +1.5% +0.5% 95.93 KB 97.37 KB 25.21 KB 25.33 KB UMD_DEV
react.production.min.js 🔺+0.4% 🔺+0.3% 11.54 KB 11.58 KB 4.58 KB 4.59 KB UMD_PROD
react.profiling.min.js +0.3% +0.2% 13.69 KB 13.73 KB 5.11 KB 5.11 KB UMD_PROFILING
react.development.js +2.4% +0.8% 59.89 KB 61.33 KB 16.21 KB 16.33 KB NODE_DEV
react.production.min.js 🔺+0.6% 🔺+0.3% 6.09 KB 6.13 KB 2.6 KB 2.61 KB NODE_PROD
React-dev.js +2.6% +0.9% 57.34 KB 58.85 KB 15.43 KB 15.56 KB FB_WWW_DEV
React-prod.js 🔺+2.8% 🔺+1.2% 15.01 KB 15.42 KB 4.04 KB 4.09 KB FB_WWW_PROD
React-profiling.js +2.8% +1.2% 15.01 KB 15.42 KB 4.04 KB 4.09 KB FB_WWW_PROFILING

Generated by 🚫 dangerJS

@trueadm trueadm changed the title Ensure hot object shapes are monomorphic Ensure React object shape is monomorphic Nov 18, 2018
this.useState = null;
}

const React = new ReactObject();
Copy link
Contributor

Choose a reason for hiding this comment

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

The same could be accomplished, without requiring a constructor or making this more difficult to convert to ES modules, by just using conditionals for each assignment.

React.ConcurrentMode = enableStableConcurrentModeAPIs ? REACT_CONCURRENT_MODE_TYPE : undefined;
// et al

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried that and the object still depots, likely because it has quite a few properties.

Copy link
Contributor

Choose a reason for hiding this comment

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

😢

(tangentially, it'd be great if an ES modules export was made available in time for 16.7, especially with all the additional commonly used exports exposed by hooks)

this.useState = null;
}

const React = new ReactObject();

if (enableStableConcurrentModeAPIs) {
React.ConcurrentMode = REACT_CONCURRENT_MODE_TYPE;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these lines not affect anything? (Any reason we shouldn't move them and the hooks assignments into the function too?)

// This uses a function constructor to enforce a monomorphic object. Using an object literal
// along with the conditional branches causes this object to deoptimize frequently otherwise.
function ReactObject() {
this.Children = {
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess React.Children.* isn't used often enough to matter?

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

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

I don't know of a reason why we shouldn't do this, but maybe Sebastian should weigh in too.

@sophiebits
Copy link
Collaborator

cc @bmeurer

@gaearon
Copy link
Collaborator

gaearon commented Nov 19, 2018

I don't fully understand if it's meant to be for internal or external usage.

Externally I think this wouldn't solve the problem because most people use React via webpack which already has its own object indirection in the middle. It's like ReactImport.default.createElement. So it probably deopts badly anyway, and this wouldn't fix it.

Copy link
Contributor

@bmeurer bmeurer left a comment

Choose a reason for hiding this comment

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

I sense some confusion here. Objects cannot deoptimize. I think I'm also missing some context / specific insights: What exactly is the observed behavior that you're trying to change here?

@trueadm
Copy link
Contributor Author

trueadm commented Nov 19, 2018

@bmeurer The property callsites on the React object show the V8 deopt message "wrong map". Moving them all into a constructor seems to stop the message from showing up. I found that this is a common message that appears when accessing properties on object literals with large amounts of properties. Why aren't object literals treated the same as function constructors in terms of how they optimize?

@gaearon
Copy link
Collaborator

gaearon commented Nov 19, 2018

In other words we're trying to optimize property access on the React object.

@sebmarkbage
Copy link
Collaborator

If we switch to ES modules exports in our syntax, then Rollup will convert this to an object for CJS anyway. Most likely packagers that consume ES module directly will convert it to an object when it is imported as * which happens often, and we're back to square one. ES module exports also can't be conditional so we'd have to figure out how to deal with that. It would be weird to have an export with the value undefined when you inspect this module in DEV tools.

@sebmarkbage
Copy link
Collaborator

Can we switch to use ES exports in our code and fix this in Rollup?

@Andarist
Copy link
Contributor

@sebmarkbage What kind of change you'd like to see for this in rollup? Their output (module.exports with properties) seems to be sane for common use case

@Jessidhia
Copy link
Contributor

Jessidhia commented Nov 19, 2018

If you really want to avoid having undefined exports you could have something like export * from './ReactConditionalExports' and generate that file with codegen. If you were using webpack it could even just be a custom loader with no associated file ('!!react-custom-exports-loader!').

However, it'd make importing symbols that are not exported by the current build of React a compile error instead of a runtime one. I think this would actually be good for the most part, but it'd require code compatible with both versions (with or without the export) to use import * as React, and then we have the problem of libraries that could, for example, optionally support hooks (they'd only be invoked if you called their custom hook after all) having to either use that or demand a build with hooks available.

PS: even if webpack has to reify a module namespace object, it doesn't actually have to include all exports in it as long as the namespace object doesn't escape. Unfortunately, as long as there are libraries published using babelmodules, the namespace object will escape to the babel helper functions.

@bmeurer
Copy link
Contributor

bmeurer commented Nov 20, 2018

I'm really having a hard time understanding this change and why it makes things better. On a high level this seems like we have a bug in V8 somewhere, since wrt. shape changes there shouldn't be any difference in the two approaches. I guess what I'm missing is the link to bundlers and what this ends up doing in the end.

@trueadm Would it be possible to provide a standalone .js file (ideally runnable in d8 or at least node) that illustrates the problem? I.e. showing good behavior with constructor approach and bad behavior with object literal?

@trueadm
Copy link
Contributor Author

trueadm commented Nov 20, 2018

After speaking to @bmeurer about this, I think I've identified the deopt areas in the case of the React object. We're changing the shape of React object after creating it, both in the above (where we assign the hooks and the ConcurrentMode and profiler properties) and internally at FB where we change the shape yet again.

If I make the conditional properties part of the object literal and change the internal areas where we change the shape of the React object, I no longer see the same "wrong map` deopt message for the object literal approach. So I'll update the PR to reflect my findings and make a diff to change the internal shape change deopt.

undo fiber change

move around conditionals

Change approach to object literal again

Change approach to object literal again
Create two React objects with the condition of hooks enabled

foo
useMutationEffect,
useReducer,
useRef,
useState,
Copy link
Contributor

@bvaughn bvaughn Nov 20, 2018

Choose a reason for hiding this comment

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

Curious why we forked for enableHooks rather than using ternaries like for enableStableConcurrentModeAPIs and __DEV__? e.g.

React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,
  lazy,
  memo,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  Suspense: REACT_SUSPENSE_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,

  ConcurrentMode: enableStableConcurrentModeAPIs
    ? REACT_CONCURRENT_MODE_TYPE
    : null,
  Profiler: enableStableConcurrentModeAPIs ? REACT_PROFILER_TYPE : null,

  unstable_ConcurrentMode: !enableStableConcurrentModeAPIs
    ? REACT_CONCURRENT_MODE_TYPE
    : null,
  unstable_Profiler: !enableStableConcurrentModeAPIs
    ? REACT_PROFILER_TYPE
    : null,

  useCallback: enableHooks ? useCallback : null,
  useContext: enableHooks ? useContext : null,
  useEffect: enableHooks ? useEffect : null,
  useImperativeMethods: enableHooks ? useImperativeMethods : null,
  useLayoutEffect: enableHooks ? useLayoutEffect : null,
  useMemo: enableHooks ? useMemo : null,
  useMutationEffect: enableHooks ? useMutationEffect : null,
  useReducer: enableHooks ? useReducer : null,
  useRef: enableHooks ? useRef : null,
  useState: enableHooks ? useState : null,
};

No big deal I guess, just curious.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did that originally, but @gaearon suggested that it unnecessarily exposed the hooks APIs as properties.

Copy link
Contributor

Choose a reason for hiding this comment

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

True, although...we do the same for the unstable/stable ConcurrentMode and Profiler properties. I"m not sure why that would actually matter. Seems preferable to forking but...I don't feel strongly 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to draw the line somewhere and they seemed less bad to expose. Forking the object 4 times would have been pretty horrible. :D

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I agree completely. I wasn't suggesting that 😅 I don't like even this fork but I think we've already talked about it more than it's worth so I'll 🤐

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm to be clear I don't think it's great to expose ConcurrentMode = null either.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would you feel any differently about undefined?

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

Honestly I think we should compromise and choose one build as "primary". That's the one that should not deopt. It would make sense to me if it's the stable one. So my proposal is to make it like

// Non-alpha React API is one object
const React = {
  Children: ...,
  createRef,
  Component,
  // blabla

  // don't care about undefined in alpha builds
  unstable_ConcurrentMode: enableStableCMAPIs ? undefined : CM,
  unstable_Profiler:  enableStableCMAPIs ? undefined : P,
}

// Different feature flags can tweak it
if (enableHooks) {
  React.useState = ...
}
if (enableCMAPIs) {
  React.ConcurrentMode = React.unstable_ConcurrentMode;
  React.Profiler = React.unstable_Profiler;

  React.unstable_ConcurrentMode = undefined;
  React.unstable_Profiler = undefined;
}

That would make the default open source build optimized, and deopt the alphas. I don't think deopting the alphas is a huge deal since it's already deopted (status quo).

At FB internally, we can either keep it deopted until we lose the feature flags. Or we can re-export all the necessary exports manually in the internal React entry point in www. Which we need to do to solve the PropTypes and createClass deopts there anyway.

@bvaughn
Copy link
Contributor

bvaughn commented Nov 20, 2018

I disagree that that proposal is preferable to having a few undefined properties on the exported object. Why should alpha builds be de-opted?

Sorry to be difficult. I think we should have an objective reason for why Dominic's original approach is actually worse than an intentional de-opt approach for alphas.

@gaearon
Copy link
Collaborator

gaearon commented Nov 20, 2018

Why should alpha builds be de-opted?

They've always been deopted so it's not a new issue. You shouldn't ideally be using them in production anyway.

I disagree that that proposal is preferable to having a few undefined properties on the exported object.

It's not objective but if it appears on the React object, it will likely appear in autocomplete (at least in some editors, and in Chrome). It will appear in tools that display APIs based on Object.keys(). It will look missing and broken so we'll start getting bug reports and complaints about missing documentation. It will also look "confirmed" so at least for Hooks, we'll see people saying "why did it ship in stable". unstable_ sets expectations accordingly. It also feels shady that React.hasOwnProperty('useState') or React.hasOwnProperty('ConcurrentMode') changes value in a minor release not containing it. Even though it's unlikely someone will use this exact feature check.

@Jessidhia
Copy link
Contributor

Jessidhia commented Nov 20, 2018 via email

@bvaughn
Copy link
Contributor

bvaughn commented Nov 20, 2018

They've always been deopted so it's not a new issue. You shouldn't ideally be using them in production anyway.

Yeah I guess that's fair, but since people compare e.g. performance of hooks vs classes, it sucks if we're knowingly continuing to deopt.

@bvaughn
Copy link
Contributor

bvaughn commented Nov 20, 2018

Not sure I agree with hooks "looking confirmed", or at least not that it's more important than deopting. The hasOwnProperty concern is a strong point though.

@gaearon
Copy link
Collaborator

gaearon commented Nov 20, 2018

These look like unreasonable expectations for an ES module, if you're ever
planning to migrate to that.

I don't understand what this comment refers to.

Not sure I agree with hooks "looking confirmed", or at least not that it's more important than deopting.

Deopting affects only alpha. Shipping an API name (even without implementation) affects a stable release.

@bvaughn
Copy link
Contributor

bvaughn commented Nov 20, 2018

Deopting affects only alpha. Shipping an API name (even without implementation) affects a stable release.

Hm, potentially. Maybe. 😄 Anyway, you feel more strongly about this than I do so~ 🙇

@sophiebits
Copy link
Collaborator

I don't understand why adding expando properties would make the existing fast properties slower.

@Andarist
Copy link
Contributor

These look like unreasonable expectations for an ES module, if you're ever
planning to migrate to that.

I don't understand what this comment refers to.

I think what @Kovensky has meant is that with ES modules you won't be able to export things conditionally because of static nature of ES modules.

@gaearon
Copy link
Collaborator

gaearon commented Nov 22, 2018

#14309

@gaearon
Copy link
Collaborator

gaearon commented Nov 23, 2018

I merged #14309 which does this.

@gaearon gaearon closed this Nov 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants