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:
Joe Savona
2024-03-06 09:28:02 -08:00
parent 48e08c42be
commit f4455ef51a
5 changed files with 132 additions and 60 deletions
@@ -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",
@@ -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" };
}
}
@@ -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 = {
@@ -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 {}
@@ -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 = {