Prune scopes whose deps always invalidate
Implements the optimization described in the previous PR: if we know that a scope's dependency will _always_ invalidate (it is not memoized and it is guaranteed to be a new object if the instruction executes, such as an array or object literal), then we can prune that scope. The invalidation is transitive: we track always-invalidating types from within scopes, and if their scope gets invalidated we prune downstream scope that depend on them. ## Test Plan Tested via #2639 - see https://fburl.com/everpaste/3e3hjpjs. 91 files change output due to reactive scopes which would always invalidate due to always invalidating dependencies.
Joe Savona committed
Mar 6, 2024 at 09:28 UTC
f4455ef51a11570a397c341a4119eab76955f267
5 files changed
+132
-60
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+8
@@ -63,6 +63,7 @@ import {
63
pruneUnusedScopes,
64
renameVariables,
65
} from "../ReactiveScopes";
66
+import { pruneAlwaysInvalidatingScopes } from "../ReactiveScopes/PruneAlwaysInvalidatingScopes";
67
import { eliminateRedundantPhi, enterSSA, leaveSSA } from "../SSA";
68
import { inferTypes } from "../TypeInference";
69
import {
@@ -319,6 +320,13 @@ function* runWithEnvironment(
320
value: reactiveFunction,
321
});
322
323
+ pruneAlwaysInvalidatingScopes(reactiveFunction);
324
+ yield log({
325
+ kind: "reactive",
326
+ name: "PruneAlwaysInvalidatingScopes",
327
+ value: reactiveFunction,
328
+ });
329
+
330
promoteUsedTemporaries(reactiveFunction);
331
yield log({
332
kind: "reactive",
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneAlwaysInvalidatingScopes.ts
new
+115
@@ -0,0 +1,115 @@
1
+/*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {
9
+ ReactiveFunctionTransform,
10
+ Transformed,
11
+ visitReactiveFunction,
12
+} from ".";
13
+import {
14
+ IdentifierId,
15
+ ReactiveFunction,
16
+ ReactiveInstruction,
17
+ ReactiveScopeBlock,
18
+ ReactiveStatement,
19
+} from "../HIR";
20
+
21
+/**
22
+ * Some instructions will *always* produce a new value, and unless memoized will *always*
23
+ * invalidate downstream reactive scopes. This pass finds such values and prunes downstream
24
+ * memoization.
25
+ *
26
+ * NOTE: function calls are an edge-case: function calls *may* return primitives, so this
27
+ * pass optimistically assumes they do. Therefore, unmemoized function calls will *not*
28
+ * prune downstream memoization. Only guaranteed new allocations, such as object and array
29
+ * literals, will cause pruning.
30
+ */
31
+export function pruneAlwaysInvalidatingScopes(fn: ReactiveFunction): void {
32
+ visitReactiveFunction(fn, new Transform(), false);
33
+}
34
+
35
+class Transform extends ReactiveFunctionTransform<boolean> {
36
+ alwaysInvalidatingValues: Set<IdentifierId> = new Set();
37
+ unmemoizedValues: Set<IdentifierId> = new Set();
38
+
39
+ override transformInstruction(
40
+ instruction: ReactiveInstruction,
41
+ withinScope: boolean
42
+ ): Transformed<ReactiveStatement> {
43
+ this.visitInstruction(instruction, withinScope);
44
+
45
+ const { lvalue, value } = instruction;
46
+ switch (value.kind) {
47
+ case "ArrayExpression":
48
+ case "ObjectExpression":
49
+ case "JsxExpression":
50
+ case "JsxFragment":
51
+ case "NewExpression": {
52
+ if (lvalue !== null) {
53
+ this.alwaysInvalidatingValues.add(lvalue.identifier.id);
54
+ if (!withinScope) {
55
+ this.unmemoizedValues.add(lvalue.identifier.id);
56
+ }
57
+ }
58
+ break;
59
+ }
60
+ case "StoreLocal": {
61
+ if (this.alwaysInvalidatingValues.has(value.value.identifier.id)) {
62
+ this.alwaysInvalidatingValues.add(value.lvalue.place.identifier.id);
63
+ }
64
+ if (this.unmemoizedValues.has(value.value.identifier.id)) {
65
+ this.unmemoizedValues.add(value.lvalue.place.identifier.id);
66
+ }
67
+ break;
68
+ }
69
+ case "LoadLocal": {
70
+ if (
71
+ lvalue !== null &&
72
+ this.alwaysInvalidatingValues.has(value.place.identifier.id)
73
+ ) {
74
+ this.alwaysInvalidatingValues.add(lvalue.identifier.id);
75
+ }
76
+ if (
77
+ lvalue !== null &&
78
+ this.unmemoizedValues.has(value.place.identifier.id)
79
+ ) {
80
+ this.unmemoizedValues.add(lvalue.identifier.id);
81
+ }
82
+ break;
83
+ }
84
+ }
85
+ return { kind: "keep" };
86
+ }
87
+
88
+ override transformScope(
89
+ scopeBlock: ReactiveScopeBlock,
90
+ _withinScope: boolean
91
+ ): Transformed<ReactiveStatement> {
92
+ this.visitScope(scopeBlock, true);
93
+
94
+ for (const dep of scopeBlock.scope.dependencies) {
95
+ if (this.unmemoizedValues.has(dep.identifier.id)) {
96
+ /*
97
+ * This scope depends on an always-invalidating value so the scope will always invalidate:
98
+ * prune it to avoid wasted comparisons
99
+ */
100
+ for (const [id, _decl] of scopeBlock.scope.declarations) {
101
+ if (this.alwaysInvalidatingValues.has(id)) {
102
+ this.unmemoizedValues.add(id);
103
+ }
104
+ }
105
+ for (const identifier of scopeBlock.scope.reassignments) {
106
+ if (this.alwaysInvalidatingValues.has(identifier.id)) {
107
+ this.unmemoizedValues.add(identifier.id);
108
+ }
109
+ }
110
+ return { kind: "replace-many", value: scopeBlock.instructions };
111
+ }
112
+ }
113
+ return { kind: "keep" };
114
+ }
115
+}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-array.expect.md
+3
-20
@@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = {
24
## Code
25
26
```javascript
27
-import { unstable_useMemoCache as useMemoCache } from "react";
27
import { useHook } from "shared-runtime";
28
29
function Component(props) {
31
- const $ = useMemoCache(4);
30
const x = [];
31
useHook();
32
x.push(props.value);
35
- let t0;
36
- if ($[0] !== x) {
37
- t0 = [x];
38
- $[0] = x;
39
- $[1] = t0;
40
- } else {
41
- t0 = $[1];
42
- }
43
- const y = t0;
44
- let t1;
45
- if ($[2] !== y) {
46
- t1 = [y];
47
- $[2] = y;
48
- $[3] = t1;
49
- } else {
50
- t1 = $[3];
51
- }
52
- return t1;
33
+
34
+ const y = [x];
35
+ return [y];
36
}
37
38
export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-new.expect.md
+3
-20
@@ -26,32 +26,15 @@ export const FIXTURE_ENTRYPOINT = {
26
## Code
27
28
```javascript
29
-import { unstable_useMemoCache as useMemoCache } from "react";
29
import { useHook } from "shared-runtime";
30
31
function Component(props) {
33
- const $ = useMemoCache(4);
32
const x = new Foo();
33
useHook();
34
x.value = props.value;
37
- let t0;
38
- if ($[0] !== x) {
39
- t0 = { x };
40
- $[0] = x;
41
- $[1] = t0;
42
- } else {
43
- t0 = $[1];
44
- }
45
- const y = t0;
46
- let t1;
47
- if ($[2] !== y) {
48
- t1 = { y };
49
- $[2] = y;
50
- $[3] = t1;
51
- } else {
52
- t1 = $[3];
53
- }
54
- return t1;
35
+
36
+ const y = { x };
37
+ return { y };
38
}
39
40
class Foo {}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-object.expect.md
+3
-20
@@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = {
24
## Code
25
26
```javascript
27
-import { unstable_useMemoCache as useMemoCache } from "react";
27
import { useHook } from "shared-runtime";
28
29
function Component(props) {
31
- const $ = useMemoCache(4);
30
const x = {};
31
useHook();
32
x.value = props.value;
35
- let t0;
36
- if ($[0] !== x) {
37
- t0 = { x };
38
- $[0] = x;
39
- $[1] = t0;
40
- } else {
41
- t0 = $[1];
42
- }
43
- const y = t0;
44
- let t1;
45
- if ($[2] !== y) {
46
- t1 = { y };
47
- $[2] = y;
48
- $[3] = t1;
49
- } else {
50
- t1 = $[3];
51
- }
52
- return t1;
33
+
34
+ const y = { x };
35
+ return { y };
36
}
37
38
export const FIXTURE_ENTRYPOINT = {