Skip to content

Commit f4455ef

Browse files
committed
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.
1 parent 48e08c4 commit f4455ef

5 files changed

Lines changed: 132 additions & 60 deletions

File tree

compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
pruneUnusedScopes,
6464
renameVariables,
6565
} from "../ReactiveScopes";
66+
import { pruneAlwaysInvalidatingScopes } from "../ReactiveScopes/PruneAlwaysInvalidatingScopes";
6667
import { eliminateRedundantPhi, enterSSA, leaveSSA } from "../SSA";
6768
import { inferTypes } from "../TypeInference";
6869
import {
@@ -319,6 +320,13 @@ function* runWithEnvironment(
319320
value: reactiveFunction,
320321
});
321322

323+
pruneAlwaysInvalidatingScopes(reactiveFunction);
324+
yield log({
325+
kind: "reactive",
326+
name: "PruneAlwaysInvalidatingScopes",
327+
value: reactiveFunction,
328+
});
329+
322330
promoteUsedTemporaries(reactiveFunction);
323331
yield log({
324332
kind: "reactive",
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = {
2424
## Code
2525

2626
```javascript
27-
import { unstable_useMemoCache as useMemoCache } from "react";
2827
import { useHook } from "shared-runtime";
2928

3029
function Component(props) {
31-
const $ = useMemoCache(4);
3230
const x = [];
3331
useHook();
3432
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];
5336
}
5437

5538
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-new.expect.md

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,15 @@ export const FIXTURE_ENTRYPOINT = {
2626
## Code
2727

2828
```javascript
29-
import { unstable_useMemoCache as useMemoCache } from "react";
3029
import { useHook } from "shared-runtime";
3130

3231
function Component(props) {
33-
const $ = useMemoCache(4);
3432
const x = new Foo();
3533
useHook();
3634
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 };
5538
}
5639

5740
class Foo {}

compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-object.expect.md

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = {
2424
## Code
2525

2626
```javascript
27-
import { unstable_useMemoCache as useMemoCache } from "react";
2827
import { useHook } from "shared-runtime";
2928

3029
function Component(props) {
31-
const $ = useMemoCache(4);
3230
const x = {};
3331
useHook();
3432
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 };
5336
}
5437

5538
export const FIXTURE_ENTRYPOINT = {

0 commit comments

Comments
 (0)