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

Change ReactChildrenMutationWarningHook to Object.freeze children #7455

Merged
merged 1 commit into from
Sep 13, 2016

Conversation

keyz
Copy link
Contributor

@keyz keyz commented Aug 10, 2016

Alternative of #7410 that fixes #7406. This should also fix #7424 and #7426 (the false-positive warning).
Saving and comparing the shadowChildren is pretty expensive -- we can Object.freeze the JSX children instead.

Benchmark:

Comparing master.txt (control) vs freeze.txt (test)
Significant differences marked by ***
% change from control to test, with 99% CIs:

* factory_ms_jsc_jit
    % change:  +0.38% [ -6.59%,  +7.36%]
    means: 27.6427 (control), 27.7558 (test)
* factory_ms_jsc_nojit
    % change: -10.22% [-15.03%,  -5.41%]  ***
    means: 26.3667 (control), 23.6813 (test)
* factory_ms_node
    % change:  -9.60% [-13.73%,  -5.46%]  ***
    means: 98.522 (control), 89.092 (test)
* ssr_pe_cold_ms_jsc_jit
    % change:  -8.47% [-11.57%,  -5.37%]  ***
    means: 173.219 (control), 158.576 (test)
* ssr_pe_cold_ms_jsc_nojit
    % change: -12.49% [-16.45%,  -8.53%]  ***
    means: 210.597 (control), 184.345 (test)
* ssr_pe_cold_ms_node
    % change:  -7.61% [-10.66%,  -4.56%]  ***
    means: 219.05 (control), 202.417 (test)

Reviewers:
@jimfb @vjeux

@@ -194,12 +186,28 @@ ReactElement.createElement = function(type, config, children) {
// the newly allocated props object.
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
if (Array.isArray(children)) {
let childArray = children.slice(0);
Copy link
Contributor Author

@keyz keyz Aug 10, 2016

Choose a reason for hiding this comment

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

I think it's important that the behavior stays the same across different envs so I call slice here regardless of the environment (prod vs. dev). Without this we won't be able to catch cases like var children = [...]; <Comp children={children}> since we don't want to freeze the original array reference from userland.

Copy link
Contributor

Choose a reason for hiding this comment

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

cc @sebmarkbage performance?

@gaearon
Copy link
Collaborator

gaearon commented Aug 10, 2016

Is this incremental improvement? If so, can I merge #7410 until this gets reviewed?

} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
let childArray = Array(childrenLength);
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be const, right? The reference identity remains the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah, good catch!

@jimfb
Copy link
Contributor

jimfb commented Aug 10, 2016

At a high level, this looks fine to me. I flagged the slice as a performance note for Sebastian. It would suck if we regress the prod build performance because we were trying to improve dev build performance. If Sebastian signs off on the extra allocation/copy, I have no objections to this diff ( 👍 ).

@ghost ghost added the CLA Signed label Aug 10, 2016
@Montana
Copy link

Montana commented Aug 11, 2016

So we can't merge #7410?

@gaearon
Copy link
Collaborator

gaearon commented Aug 11, 2016

I already merged #7410 in the meantime but this might be a better solution.


class Wrapper extends React.Component {
componentDidMount() {
setTimeout(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is setTimeout here necessary?

@keyz
Copy link
Contributor Author

keyz commented Aug 14, 2016

Rebased and updated.
Here's a new benchmark that compares with the current master:

Comparing master.prod.txt (control) vs freeze.prod.txt (test)
Significant differences marked by ***
% change from control to test, with 99% CIs:

* factory_ms_jsc_jit
    % change:  -0.43% [ -5.25%,  +4.39%]
    means: 20.1321 (control), 20.0488 (test)
* factory_ms_jsc_nojit
    % change:  +0.72% [ -4.64%,  +6.08%]
    means: 18.2618 (control), 18.3966 (test)
* factory_ms_node
    % change:  +0.14% [ -2.57%,  +2.85%]
    means: 63.5866 (control), 63.6804 (test)
* ssr_pe_cold_ms_jsc_jit
    % change:  +1.98% [ -1.90%,  +5.87%]
    means: 29.2908 (control), 29.8738 (test)
* ssr_pe_cold_ms_jsc_nojit
    % change:  -1.68% [ -6.00%,  +2.64%]
    means: 31.9364 (control), 31.4052 (test)
* ssr_pe_cold_ms_node
    % change:  -0.21% [ -2.61%,  +2.20%]
    means: 58.2109 (control), 58.0934 (test)
Comparing master.dev.txt (control) vs freeze.dev.txt (test)
Significant differences marked by ***
% change from control to test, with 99% CIs:

* factory_ms_jsc_jit
    % change:  -2.74% [ -7.44%,  +1.97%]
    means: 27.3811 (control), 26.6391 (test)
* factory_ms_jsc_nojit
    % change:  +0.33% [ -4.34%,  +5.00%]
    means: 24.6727 (control), 24.7591 (test)
* factory_ms_node
    % change:  -0.37% [ -2.90%,  +2.17%]
    means: 90.5355 (control), 90.2092 (test)
* ssr_pe_cold_ms_jsc_jit
    % change:  -4.46% [ -7.64%,  -1.27%]  ***
    means: 87.2714 (control), 83.392 (test)
* ssr_pe_cold_ms_jsc_nojit
    % change:  -5.38% [ -9.78%,  -0.98%]  ***
    means: 93.9902 (control), 88.9515 (test)
* ssr_pe_cold_ms_node
    % change:  -4.82% [ -6.69%,  -2.95%]  ***
    means: 132.504 (control), 126.122 (test)

@keyz
Copy link
Contributor Author

keyz commented Aug 30, 2016

Friendly ping 😀: I just rebased and updated this PR. Do we want to take this approach to fix #7424?

To sum up, it calls .slice() when people explicitly pass an array as children (<Comp children={[...]}/> or <Comp>{[...]}<Comp/>) and freezes the sliced copy, so that we won't accidentally freeze userland data. We call slice in both prod and dev to align their behaviors, which regresses the prod build perf a little bit. A benchmark can be found above.

@gaearon
Copy link
Collaborator

gaearon commented Aug 30, 2016

Can you please help me understand how to read this profiler script output?
I don’t know if + is good or - is good.

if (childrenLength === 1) {
props.children = children;
if (Array.isArray(children)) {
childArray = children.slice(0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this mean that if we pass them through multiple levels of components, the same array could get slice(0)d many times?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's unfortunate. :( Do you think there's a better way to do this?

However, it should be more performant than the current approach, which slices in the ReactElement() function (https://github.com/facebook/react/blob/master/src/isomorphic/classic/element/ReactElement.js#L138) for every component that has more than one child. The new one slices in createElement and only when an user explicitly passes an array as children. If this makes sense then I guess the question is whether we want to slice in prod.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I’m mostly worried about prod here.
I don’t think we can afford to do more allocations in prod.

@keyz
Copy link
Contributor Author

keyz commented Aug 30, 2016

Yeah, sorry, - is good and + is bad. cc @spicyj

@ghost ghost added the CLA Signed label Aug 30, 2016
@gaearon
Copy link
Collaborator

gaearon commented Aug 30, 2016

What does the benchmark measure?
Does it include memory usage?

@keyz
Copy link
Contributor Author

keyz commented Aug 30, 2016

No, it only measures time. The percentages (-2.74% [ -7.44%, +1.97%]) are mean, low, and high (https://github.com/facebook/react/blob/master/scripts/bench/analyze.py#L102-L107).

@gaearon
Copy link
Collaborator

gaearon commented Aug 30, 2016

I don’t think allocating more in production is the way to go here.
I think we should either keep this warning as is, or write a lint rule that warns on

this.props.children.<mutatingMethod>()

or

(var|const|let) <name> = (this.)?props.children;
<name>.{mutatingMethod}()

If this covers most cases and gets included into the default set of eslint-plugin-react, Create React App, and www codebase, we can remove the warning altogether IMO.

@sophiebits
Copy link
Collaborator

sophiebits commented Aug 30, 2016

It probably suffices to freeze the array if ReactElement creates it and do nothing otherwise, don't you think?

@gaearon
Copy link
Collaborator

gaearon commented Aug 30, 2016

I’m not sure when people would try to push things into children so hard to say.
Maybe @jimfb can show a few more examples of how people violated this warning in practice.
I thought that pushing things into an array created by map() is one of those use cases.

@ghost ghost added the CLA Signed label Aug 30, 2016
@gaearon
Copy link
Collaborator

gaearon commented Sep 12, 2016

Could you rebase pls?

@gaearon gaearon self-assigned this Sep 12, 2016
@ghost ghost added the CLA Signed label Sep 12, 2016
@keyz keyz force-pushed the freeze-children branch 2 times, most recently from b310f7f to 51752fb Compare September 12, 2016 19:17
- only freeze children array created by createElement
@keyz
Copy link
Contributor Author

keyz commented Sep 12, 2016

Sure, just rebased.

@ghost ghost added the CLA Signed label Sep 12, 2016
@gaearon gaearon added this to the 15-next milestone Sep 13, 2016
@gaearon gaearon merged commit 38c4ade into facebook:master Sep 13, 2016
@gaearon
Copy link
Collaborator

gaearon commented Sep 13, 2016

Thanks!

@zpao
Copy link
Member

zpao commented Sep 15, 2016

FYI: There's no way this is a patch level change - it changes what was a warning to start throwing. Sure, it's dev-only but that's not ok to take in a patch release (we avoid introducing new warnings in patch releases, we definitely can't start throwing).

Let's be a bit more conservative with the semver assignment so we don't sneak breakages in at the wrong time.

@just-boris
Copy link
Contributor

@zpao does that mean that there will be 15.4 release soon?

@gaearon
Copy link
Collaborator

gaearon commented Sep 16, 2016

I'll be more conservative, thanks for correcting!

@zpao zpao modified the milestones: 15-next, 15.4.0 Oct 4, 2016
zpao pushed a commit that referenced this pull request Oct 4, 2016
- only freeze children array created by createElement
(cherry picked from commit 38c4ade)
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.

False-positive mutation warning Memory leak in React 15.3.0 non-production server side rendering
8 participants