@samitouri / QOS-React-2 / commits / 522d22f299

[compiler] Recompute values every time

Summary: This PR expands the analysis from the previous in the stack in order to also capture when a value can incorrectly change within a single render, rather than just changing between two renders. In the case where dependencies have changed and so a new value is being computed, we now compute the value twice and compare the results. This would, for example, catch when we call Math.random() in render. The generated code is a little convoluted, because we don't want to have to traverse the generated code and substitute variable names with new ones. Instead, we save the initial value to the cache as normal, then run the computation block again and compare the resulting values to the cached ones. Then, to make sure that the cached values are identical to the computed ones, we reassign the cached values into the output variables. ghstack-source-id: d0f11a4cb2a612cbffdfdcaa9e75efbd6e38019f Pull Request resolved: https://github.com/facebook/react/pull/29657

Mike Vitousek committed May 31, 2024 at 14:06 UTC 522d22f29904f2164210b3ae218b9b69b61e9a4c
8 files changed +215 -123
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+104 -96
@@ -461,6 +461,11 @@ function codegenReactiveScope(
461 ): void {
462 const cacheStoreStatements: Array<t.Statement> = [];
463 const cacheLoadStatements: Array<t.Statement> = [];
464 + const cacheLoads: Array<{
465 + name: t.Identifier;
466 + index: number;
467 + value: t.Expression;
468 + }> = [];
469 const changeExpressions: Array<t.Expression> = [];
470 const changeExpressionComments: Array<string> = [];
471 const outputComments: Array<string> = [];
@@ -488,6 +493,10 @@ function codegenReactiveScope(
493 } else {
494 changeExpressions.push(comparison);
495 }
496 + /*
497 + * Adding directly to cacheStoreStatements rather than cacheLoads, because there
498 + * is no corresponding cacheLoadStatement for dependencies
499 + */
500 cacheStoreStatements.push(
501 t.expressionStatement(
502 t.assignmentExpression(
@@ -523,32 +532,7 @@ function codegenReactiveScope(
532 t.variableDeclaration("let", [t.variableDeclarator(name)])
533 );
534 }
526 - cacheStoreStatements.push(
527 - t.expressionStatement(
528 - t.assignmentExpression(
529 - "=",
530 - t.memberExpression(
531 - t.identifier(cx.synthesizeName("$")),
532 - t.numericLiteral(index),
533 - true
534 - ),
535 - wrapCacheDep(cx, name)
536 - )
537 - )
538 - );
539 - cacheLoadStatements.push(
540 - t.expressionStatement(
541 - t.assignmentExpression(
542 - "=",
543 - name,
544 - t.memberExpression(
545 - t.identifier(cx.synthesizeName("$")),
546 - t.numericLiteral(index),
547 - true
548 - )
549 - )
550 - )
551 - );
535 + cacheLoads.push({ name, index, value: wrapCacheDep(cx, name) });
536 cx.declare(identifier);
537 }
538 for (const reassignment of scope.reassignments) {
@@ -558,34 +542,9 @@ function codegenReactiveScope(
542 }
543 const name = convertIdentifier(reassignment);
544 outputComments.push(name.name);
561 -
562 - cacheStoreStatements.push(
563 - t.expressionStatement(
564 - t.assignmentExpression(
565 - "=",
566 - t.memberExpression(
567 - t.identifier(cx.synthesizeName("$")),
568 - t.numericLiteral(index),
569 - true
570 - ),
571 - wrapCacheDep(cx, name)
572 - )
573 - )
574 - );
575 - cacheLoadStatements.push(
576 - t.expressionStatement(
577 - t.assignmentExpression(
578 - "=",
579 - name,
580 - t.memberExpression(
581 - t.identifier(cx.synthesizeName("$")),
582 - t.numericLiteral(index),
583 - true
584 - )
585 - )
586 - )
587 - );
545 + cacheLoads.push({ name, index, value: wrapCacheDep(cx, name) });
546 }
547 +
548 let testCondition = (changeExpressions as Array<t.Expression>).reduce(
549 (acc: t.Expression | null, ident: t.Expression) => {
550 if (acc == null) {
@@ -632,67 +591,116 @@ function codegenReactiveScope(
591 );
592 }
593 let computationBlock = codegenBlock(cx, block);
594 +
595 let memoStatement;
636 - const memoBlock = t.blockStatement(cacheLoadStatements);
596 if (
597 cx.env.config.enableChangeDetectionForDebugging != null &&
598 changeExpressions.length > 0
599 ) {
600 const detectionFunction =
601 cx.env.config.enableChangeDetectionForDebugging.importSpecifierName;
602 + const cacheLoadOldValueStatements: Array<t.Statement> = [];
603 const changeDetectionStatements: Array<t.Statement> = [];
644 - const oldVarDeclarationStatements: Array<t.Statement> = [];
645 - memoBlock.body.forEach((stmt) => {
646 - if (
647 - stmt.type === "ExpressionStatement" &&
648 - stmt.expression.type === "AssignmentExpression" &&
649 - stmt.expression.left.type === "Identifier"
650 - ) {
651 - const name = stmt.expression.left.name;
652 - const loadName = cx.synthesizeName(`old$${name}`);
653 - oldVarDeclarationStatements.push(
654 - t.variableDeclaration("let", [
655 - t.variableDeclarator(t.identifier(loadName)),
604 + const idempotenceDetectionStatements: Array<t.Statement> = [];
605 +
606 + for (const { name, index, value } of cacheLoads) {
607 + const loadName = cx.synthesizeName(`old$${name.name}`);
608 + const slot = t.memberExpression(
609 + t.identifier(cx.synthesizeName("$")),
610 + t.numericLiteral(index),
611 + true
612 + );
613 + cacheStoreStatements.push(
614 + t.expressionStatement(t.assignmentExpression("=", slot, value))
615 + );
616 + cacheLoadOldValueStatements.push(
617 + t.variableDeclaration("let", [
618 + t.variableDeclarator(t.identifier(loadName), slot),
619 + ])
620 + );
621 + changeDetectionStatements.push(
622 + t.expressionStatement(
623 + t.callExpression(t.identifier(detectionFunction), [
624 + t.identifier(loadName),
625 + name,
626 + t.stringLiteral(name.name),
627 + t.stringLiteral(cx.fnName),
628 + t.stringLiteral("cached"),
629 ])
657 - );
658 - stmt.expression.left = t.identifier(loadName);
659 - changeDetectionStatements.push(
660 - t.expressionStatement(
661 - t.callExpression(t.identifier(detectionFunction), [
662 - t.identifier(loadName),
663 - t.identifier(name),
664 - t.stringLiteral(name),
665 - t.stringLiteral(cx.fnName),
666 - ])
667 - )
668 - );
669 - changeDetectionStatements.push(
670 - t.expressionStatement(
671 - t.assignmentExpression(
672 - "=",
673 - t.identifier(name),
674 - t.identifier(loadName)
675 - )
676 - )
677 - );
678 - }
679 - });
630 + )
631 + );
632 + idempotenceDetectionStatements.push(
633 + t.expressionStatement(
634 + t.callExpression(t.identifier(detectionFunction), [
635 + slot,
636 + name,
637 + t.stringLiteral(name.name),
638 + t.stringLiteral(cx.fnName),
639 + t.stringLiteral("recomputed"),
640 + ])
641 + )
642 + );
643 + idempotenceDetectionStatements.push(
644 + t.expressionStatement(t.assignmentExpression("=", name, slot))
645 + );
646 + }
647 + const condition = cx.synthesizeName("condition");
648 memoStatement = t.blockStatement([
649 ...computationBlock.body,
650 + t.variableDeclaration("let", [
651 + t.variableDeclarator(t.identifier(condition), testCondition),
652 + ]),
653 t.ifStatement(
683 - t.unaryExpression("!", testCondition),
654 + t.unaryExpression("!", t.identifier(condition)),
655 t.blockStatement([
685 - ...oldVarDeclarationStatements,
686 - ...memoBlock.body,
656 + ...cacheLoadOldValueStatements,
657 ...changeDetectionStatements,
658 ])
659 ),
660 ...cacheStoreStatements,
661 + t.ifStatement(
662 + t.identifier(condition),
663 + t.blockStatement([
664 + ...computationBlock.body,
665 + ...idempotenceDetectionStatements,
666 + ])
667 + ),
668 ]);
669 } else {
670 + for (const { name, index, value } of cacheLoads) {
671 + cacheStoreStatements.push(
672 + t.expressionStatement(
673 + t.assignmentExpression(
674 + "=",
675 + t.memberExpression(
676 + t.identifier(cx.synthesizeName("$")),
677 + t.numericLiteral(index),
678 + true
679 + ),
680 + value
681 + )
682 + )
683 + );
684 + cacheLoadStatements.push(
685 + t.expressionStatement(
686 + t.assignmentExpression(
687 + "=",
688 + name,
689 + t.memberExpression(
690 + t.identifier(cx.synthesizeName("$")),
691 + t.numericLiteral(index),
692 + true
693 + )
694 + )
695 + )
696 + );
697 + }
698 computationBlock.body.push(...cacheStoreStatements);
694 -
695 - memoStatement = t.ifStatement(testCondition, computationBlock, memoBlock);
699 + memoStatement = t.ifStatement(
700 + testCondition,
701 + computationBlock,
702 + t.blockStatement(cacheLoadStatements)
703 + );
704 }
705
706 if (cx.env.config.enableMemoizationComments) {
@@ -734,9 +742,9 @@ function codegenReactiveScope(
742 true
743 );
744 }
737 - if (memoBlock.body.length > 0) {
745 + if (cacheLoadStatements.length > 0) {
746 t.addComment(
739 - memoBlock.body[0]!,
747 + cacheLoadStatements[0]!,
748 "leading",
749 ` Inputs did not change, use cached value`,
750 true
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-reassign.expect.md new
+48
@@ -0,0 +1,48 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableChangeDetectionForDebugging
6 +function Component(props) {
7 + let x = null;
8 + if (props.cond) {
9 + x = [];
10 + x.push(props.value);
11 + }
12 + return x;
13 +}
14 +
15 +```
16 +
17 +## Code
18 +
19 +```javascript
20 +import { $structuralCheck } from "react-compiler-runtime";
21 +import { c as _c } from "react/compiler-runtime"; // @enableChangeDetectionForDebugging
22 +function Component(props) {
23 + const $ = _c(2);
24 + let x = null;
25 + if (props.cond) {
26 + {
27 + x = [];
28 + x.push(props.value);
29 + let condition = $[0] !== props.value;
30 + if (!condition) {
31 + let old$x = $[1];
32 + $structuralCheck(old$x, x, "x", "Component", "cached");
33 + }
34 + $[0] = props.value;
35 + $[1] = x;
36 + if (condition) {
37 + x = [];
38 + x.push(props.value);
39 + $structuralCheck($[1], x, "x", "Component", "recomputed");
40 + x = $[1];
41 + }
42 + }
43 + }
44 + return x;
45 +}
46 +
47 +```
48 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-reassign.js new
+9
@@ -0,0 +1,9 @@
1 +// @enableChangeDetectionForDebugging
2 +function Component(props) {
3 + let x = null;
4 + if (props.cond) {
5 + x = [];
6 + x.push(props.value);
7 + }
8 + return x;
9 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-and-other-hook-unpruned-dependency.expect.md
+18 -10
@@ -43,14 +43,18 @@ function Component(props) {
43 let t0;
44 {
45 t0 = f(props.x);
46 - if (!($[0] !== props.x)) {
47 - let old$t0;
48 - old$t0 = $[1];
49 - $structuralCheck(old$t0, t0, "t0", "Component");
50 - t0 = old$t0;
46 + let condition = $[0] !== props.x;
47 + if (!condition) {
48 + let old$t0 = $[1];
49 + $structuralCheck(old$t0, t0, "t0", "Component", "cached");
50 }
51 $[0] = props.x;
52 $[1] = t0;
53 + if (condition) {
54 + t0 = f(props.x);
55 + $structuralCheck($[1], t0, "t0", "Component", "recomputed");
56 + t0 = $[1];
57 + }
58 }
59 const w = t0;
60 const z = useOther(w);
@@ -58,14 +62,18 @@ function Component(props) {
62 let t1;
63 {
64 t1 = <div>{x}</div>;
61 - if (!($[2] !== x)) {
62 - let old$t1;
63 - old$t1 = $[3];
64 - $structuralCheck(old$t1, t1, "t1", "Component");
65 - t1 = old$t1;
65 + let condition = $[2] !== x;
66 + if (!condition) {
67 + let old$t1 = $[3];
68 + $structuralCheck(old$t1, t1, "t1", "Component", "cached");
69 }
70 $[2] = x;
71 $[3] = t1;
72 + if (condition) {
73 + t1 = <div>{x}</div>;
74 + $structuralCheck($[3], t1, "t1", "Component", "recomputed");
75 + t1 = $[3];
76 + }
77 }
78 return t1;
79 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-pruned-dependency-change-detect.expect.md
+9 -5
@@ -32,14 +32,18 @@ function Component(props) {
32 let t1;
33 {
34 t1 = <div>{x}</div>;
35 - if (!($[1] !== x)) {
36 - let old$t1;
37 - old$t1 = $[2];
38 - $structuralCheck(old$t1, t1, "t1", "Component");
39 - t1 = old$t1;
35 + let condition = $[1] !== x;
36 + if (!condition) {
37 + let old$t1 = $[2];
38 + $structuralCheck(old$t1, t1, "t1", "Component", "cached");
39 }
40 $[1] = x;
41 $[2] = t1;
42 + if (condition) {
43 + t1 = <div>{x}</div>;
44 + $structuralCheck($[2], t1, "t1", "Component", "recomputed");
45 + t1 = $[2];
46 + }
47 }
48 return t1;
49 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-unpruned-dependency.expect.md
+23 -10
@@ -39,14 +39,18 @@ function Component(props) {
39 let t0;
40 {
41 t0 = f(props.x);
42 - if (!($[0] !== props.x)) {
43 - let old$t0;
44 - old$t0 = $[1];
45 - $structuralCheck(old$t0, t0, "t0", "Component");
46 - t0 = old$t0;
42 + let condition = $[0] !== props.x;
43 + if (!condition) {
44 + let old$t0 = $[1];
45 + $structuralCheck(old$t0, t0, "t0", "Component", "cached");
46 }
47 $[0] = props.x;
48 $[1] = t0;
49 + if (condition) {
50 + t0 = f(props.x);
51 + $structuralCheck($[1], t0, "t0", "Component", "recomputed");
52 + t0 = $[1];
53 + }
54 }
55 const w = t0;
56 const [x] = useState(w);
@@ -58,15 +62,24 @@ function Component(props) {
62 {w}
63 </div>
64 );
61 - if (!($[2] !== x || $[3] !== w)) {
62 - let old$t1;
63 - old$t1 = $[4];
64 - $structuralCheck(old$t1, t1, "t1", "Component");
65 - t1 = old$t1;
65 + let condition = $[2] !== x || $[3] !== w;
66 + if (!condition) {
67 + let old$t1 = $[4];
68 + $structuralCheck(old$t1, t1, "t1", "Component", "cached");
69 }
70 $[2] = x;
71 $[3] = w;
72 $[4] = t1;
73 + if (condition) {
74 + t1 = (
75 + <div>
76 + {x}
77 + {w}
78 + </div>
79 + );
80 + $structuralCheck($[4], t1, "t1", "Component", "recomputed");
81 + t1 = $[4];
82 + }
83 }
84 return t1;
85 }
compiler/packages/react-compiler-runtime/src/index.ts
+3 -2
@@ -258,10 +258,11 @@ export function $structuralCheck(
258 oldValue: any,
259 newValue: any,
260 variableName: string,
261 - fnName: string
261 + fnName: string,
262 + kind: string
263 ): void {
264 function error(l: string, r: string, path: string, depth: number) {
264 - const str = `${fnName}: ${variableName}${path} changed from ${l} to ${r} at depth ${depth}`;
265 + const str = `${fnName}: [${kind}] ${variableName}${path} changed from ${l} to ${r} at depth ${depth}`;
266 if (seenErrors.has(str)) {
267 return;
268 }
compiler/packages/snap/src/SproutTodoFilter.ts
+1
@@ -498,6 +498,7 @@ const skipFilter = new Set([
498 "useState-pruned-dependency-change-detect",
499 "useState-unpruned-dependency",
500 "useState-and-other-hook-unpruned-dependency",
501 + "change-detect-reassign",
502 ]);
503
504 export default skipFilter;