@samitouri / QOS-React / commits / f4455ef51a

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 = {