mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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.
This commit is contained in:
@@ -63,6 +63,7 @@ import {
|
||||
pruneUnusedScopes,
|
||||
renameVariables,
|
||||
} from "../ReactiveScopes";
|
||||
import { pruneAlwaysInvalidatingScopes } from "../ReactiveScopes/PruneAlwaysInvalidatingScopes";
|
||||
import { eliminateRedundantPhi, enterSSA, leaveSSA } from "../SSA";
|
||||
import { inferTypes } from "../TypeInference";
|
||||
import {
|
||||
@@ -319,6 +320,13 @@ function* runWithEnvironment(
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneAlwaysInvalidatingScopes(reactiveFunction);
|
||||
yield log({
|
||||
kind: "reactive",
|
||||
name: "PruneAlwaysInvalidatingScopes",
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
yield log({
|
||||
kind: "reactive",
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
visitReactiveFunction,
|
||||
} from ".";
|
||||
import {
|
||||
IdentifierId,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
} from "../HIR";
|
||||
|
||||
/**
|
||||
* Some instructions will *always* produce a new value, and unless memoized will *always*
|
||||
* invalidate downstream reactive scopes. This pass finds such values and prunes downstream
|
||||
* memoization.
|
||||
*
|
||||
* NOTE: function calls are an edge-case: function calls *may* return primitives, so this
|
||||
* pass optimistically assumes they do. Therefore, unmemoized function calls will *not*
|
||||
* prune downstream memoization. Only guaranteed new allocations, such as object and array
|
||||
* literals, will cause pruning.
|
||||
*/
|
||||
export function pruneAlwaysInvalidatingScopes(fn: ReactiveFunction): void {
|
||||
visitReactiveFunction(fn, new Transform(), false);
|
||||
}
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
alwaysInvalidatingValues: Set<IdentifierId> = new Set();
|
||||
unmemoizedValues: Set<IdentifierId> = new Set();
|
||||
|
||||
override transformInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
withinScope: boolean
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitInstruction(instruction, withinScope);
|
||||
|
||||
const { lvalue, value } = instruction;
|
||||
switch (value.kind) {
|
||||
case "ArrayExpression":
|
||||
case "ObjectExpression":
|
||||
case "JsxExpression":
|
||||
case "JsxFragment":
|
||||
case "NewExpression": {
|
||||
if (lvalue !== null) {
|
||||
this.alwaysInvalidatingValues.add(lvalue.identifier.id);
|
||||
if (!withinScope) {
|
||||
this.unmemoizedValues.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "StoreLocal": {
|
||||
if (this.alwaysInvalidatingValues.has(value.value.identifier.id)) {
|
||||
this.alwaysInvalidatingValues.add(value.lvalue.place.identifier.id);
|
||||
}
|
||||
if (this.unmemoizedValues.has(value.value.identifier.id)) {
|
||||
this.unmemoizedValues.add(value.lvalue.place.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "LoadLocal": {
|
||||
if (
|
||||
lvalue !== null &&
|
||||
this.alwaysInvalidatingValues.has(value.place.identifier.id)
|
||||
) {
|
||||
this.alwaysInvalidatingValues.add(lvalue.identifier.id);
|
||||
}
|
||||
if (
|
||||
lvalue !== null &&
|
||||
this.unmemoizedValues.has(value.place.identifier.id)
|
||||
) {
|
||||
this.unmemoizedValues.add(lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { kind: "keep" };
|
||||
}
|
||||
|
||||
override transformScope(
|
||||
scopeBlock: ReactiveScopeBlock,
|
||||
_withinScope: boolean
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitScope(scopeBlock, true);
|
||||
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
if (this.unmemoizedValues.has(dep.identifier.id)) {
|
||||
/*
|
||||
* This scope depends on an always-invalidating value so the scope will always invalidate:
|
||||
* prune it to avoid wasted comparisons
|
||||
*/
|
||||
for (const [id, _decl] of scopeBlock.scope.declarations) {
|
||||
if (this.alwaysInvalidatingValues.has(id)) {
|
||||
this.unmemoizedValues.add(id);
|
||||
}
|
||||
}
|
||||
for (const identifier of scopeBlock.scope.reassignments) {
|
||||
if (this.alwaysInvalidatingValues.has(identifier.id)) {
|
||||
this.unmemoizedValues.add(identifier.id);
|
||||
}
|
||||
}
|
||||
return { kind: "replace-many", value: scopeBlock.instructions };
|
||||
}
|
||||
}
|
||||
return { kind: "keep" };
|
||||
}
|
||||
}
|
||||
+3
-20
@@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react";
|
||||
import { useHook } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = useMemoCache(4);
|
||||
const x = [];
|
||||
useHook();
|
||||
x.push(props.value);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = [x];
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const y = t0;
|
||||
let t1;
|
||||
if ($[2] !== y) {
|
||||
t1 = [y];
|
||||
$[2] = y;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
|
||||
const y = [x];
|
||||
return [y];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+3
-20
@@ -26,32 +26,15 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react";
|
||||
import { useHook } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = useMemoCache(4);
|
||||
const x = new Foo();
|
||||
useHook();
|
||||
x.value = props.value;
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = { x };
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const y = t0;
|
||||
let t1;
|
||||
if ($[2] !== y) {
|
||||
t1 = { y };
|
||||
$[2] = y;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
|
||||
const y = { x };
|
||||
return { y };
|
||||
}
|
||||
|
||||
class Foo {}
|
||||
|
||||
+3
-20
@@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react";
|
||||
import { useHook } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = useMemoCache(4);
|
||||
const x = {};
|
||||
useHook();
|
||||
x.value = props.value;
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = { x };
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const y = t0;
|
||||
let t1;
|
||||
if ($[2] !== y) {
|
||||
t1 = { y };
|
||||
$[2] = y;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
|
||||
const y = { x };
|
||||
return { y };
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
Reference in New Issue
Block a user