Reduce React bloat on equivalent objects with similar temporal alias trees (#2193)

Summary:
Release notes: none

In an attempt to pull out some of the work from https://github.com/facebook/prepack/pull/2148 to make things easier to consume and review. This PR is one part, where we check the temporal alias values (if an object value has it) and see if we can dedupe the trees. Example below:
Closes https://github.com/facebook/prepack/pull/2193

Differential Revision: D8732542

Pulled By: trueadm

fbshipit-source-id: 352d0048acfef69b5a257c0fe280435fa7baaa7f
This commit is contained in:
Dominic Gannaway 2018-07-05 05:07:43 -07:00 committed by Facebook Github Bot
parent ef7e1873fa
commit 3cd3dd8431
13 changed files with 629 additions and 14 deletions

View File

@ -11,6 +11,7 @@
import { Realm } from "../realm.js";
import {
AbstractObjectValue,
AbstractValue,
ArrayValue,
FunctionValue,
@ -54,12 +55,14 @@ export class ReactEquivalenceSet {
this.arrayRoot = new Map();
this.reactElementRoot = new Map();
this.reactPropsRoot = new Map();
this.temporalAliasRoot = new Map();
}
realm: Realm;
objectRoot: ReactSetKeyMap;
arrayRoot: ReactSetKeyMap;
reactElementRoot: ReactSetKeyMap;
reactPropsRoot: ReactSetKeyMap;
temporalAliasRoot: ReactSetKeyMap;
residualReactElementVisitor: ResidualReactElementVisitor;
_createNode(): ReactSetNode {
@ -118,10 +121,8 @@ export class ReactEquivalenceSet {
let temporalAlias = object.temporalAlias;
if (temporalAlias !== undefined) {
// Snapshotting uses temporalAlias to on ObjectValues, so if
// they have a temporalAlias then we need to treat it as a field
currentMap = this.getKey(temporalAliasSymbol, currentMap, visitedValues);
result = this.getValue(temporalAlias, currentMap, visitedValues);
result = this.getTemporalAliasValue(temporalAlias, currentMap, visitedValues);
}
if (result === undefined) {
@ -137,6 +138,85 @@ export class ReactEquivalenceSet {
return result.value;
}
_getTemporalValue(temporalAlias: AbstractObjectValue, visitedValues: Set<Value>): AbstractObjectValue {
// Check to ensure the temporal alias is definitely declared in the current scope
if (!this.residualReactElementVisitor.wasTemporalAliasDeclaredInCurrentScope(temporalAlias)) {
return temporalAlias;
}
let temporalBuildNodeEntryArgs = this.realm.getTemporalBuildNodeEntryArgsFromDerivedValue(temporalAlias);
if (temporalBuildNodeEntryArgs === undefined) {
return temporalAlias;
}
let temporalArgs = temporalBuildNodeEntryArgs.args;
if (temporalArgs.length === 0) {
return temporalAlias;
}
let currentMap = this.temporalAliasRoot;
let result;
for (let i = 0; i < temporalArgs.length; i++) {
let arg = temporalArgs[i];
let equivalenceArg;
if (arg instanceof ObjectValue && arg.temporalAlias === temporalAlias) {
continue;
}
if (arg instanceof ObjectValue && isReactElement(arg)) {
equivalenceArg = this.residualReactElementVisitor.reactElementEquivalenceSet.add(arg);
if (arg !== equivalenceArg) {
temporalArgs[i] = equivalenceArg;
}
} else if (arg instanceof AbstractObjectValue && !arg.values.isTop() && arg.kind !== "conditional") {
// Might be a temporal, so let's check
let childTemporalBuildNodeEntryArgs = this.realm.getTemporalBuildNodeEntryArgsFromDerivedValue(arg);
if (childTemporalBuildNodeEntryArgs !== undefined) {
equivalenceArg = this._getTemporalValue(arg, visitedValues);
invariant(equivalenceArg instanceof AbstractObjectValue);
if (equivalenceArg !== arg) {
temporalArgs[i] = equivalenceArg;
}
}
} else if (arg instanceof AbstractValue) {
equivalenceArg = this.residualReactElementVisitor.residualHeapVisitor.equivalenceSet.add(arg);
if (arg !== equivalenceArg) {
temporalArgs[i] = equivalenceArg;
}
}
currentMap = this.getKey(i, (currentMap: any), visitedValues);
invariant(arg instanceof Value && (equivalenceArg instanceof Value || equivalenceArg === undefined));
result = this.getValue(equivalenceArg || arg, currentMap, visitedValues);
currentMap = result.map;
}
invariant(result !== undefined);
if (result.value === null) {
result.value = temporalAlias;
}
// Check to ensure the equivalent temporal alias is definitely declared in the current scope
if (!this.residualReactElementVisitor.wasTemporalAliasDeclaredInCurrentScope(result.value)) {
result.value = temporalAlias;
return temporalAlias;
}
return result.value;
}
getTemporalAliasValue(
temporalAlias: AbstractObjectValue,
map: ReactSetValueMap,
visitedValues: Set<Value>
): ReactSetNode {
let result = this._getTemporalValue(temporalAlias, visitedValues);
invariant(result instanceof AbstractObjectValue);
if (!map.has(result)) {
map.set(result, this._createNode());
}
return ((map.get(result): any): ReactSetNode);
}
// for arrays: [0] -> [1] -> [2]... as nodes
_getArrayValue(array: ArrayValue, visitedValues: Set<Value>): ArrayValue {
if (visitedValues.has(array)) return array;

View File

@ -37,10 +37,9 @@ export class ReactPropsSet {
let temporalAlias = props.temporalAlias;
if (temporalAlias !== undefined) {
// Snapshotting uses temporalAlias to on ObjectValues, so if
// they have a temporalAlias then we need to treat it as a field
currentMap = reactEquivalenceSet.getKey(temporalAliasSymbol, currentMap, visitedValues);
result = reactEquivalenceSet.getValue(temporalAlias, currentMap, visitedValues);
result = reactEquivalenceSet.getTemporalAliasValue(temporalAlias, currentMap, visitedValues);
currentMap = result.map;
}
if (result === undefined) {

View File

@ -41,6 +41,9 @@ function createPropsObject(
config: ObjectValue | AbstractObjectValue,
children: void | Value
): { key: Value, ref: Value, props: ObjectValue } {
// If we're in "rendering" a React component tree, we should have an active reconciler
let activeReconciler = realm.react.activeReconciler;
let firstRenderOnly = activeReconciler !== undefined ? activeReconciler.componentTreeConfig.firstRenderOnly : false;
let defaultProps =
type instanceof ObjectValue || type instanceof AbstractObjectValue
? Get(realm, type, "defaultProps")
@ -82,7 +85,7 @@ function createPropsObject(
}
let possibleRef = Get(realm, config, "ref");
if (possibleRef !== realm.intrinsics.null && possibleRef !== realm.intrinsics.undefined) {
if (possibleRef !== realm.intrinsics.null && possibleRef !== realm.intrinsics.undefined && !firstRenderOnly) {
// if the config has been marked as having no partial key or ref and the possible ref
// is abstract, yet the config doesn't have a ref property, then the ref can remain null
let refNotNeeded =
@ -117,13 +120,11 @@ function createPropsObject(
(config instanceof AbstractObjectValue && config.isPartialObject()) ||
(config instanceof ObjectValue && config.isPartialObject() && config.isSimpleObject())
) {
let args = [];
args.push(config);
// create a new props object that will be the target of the Object.assign
props = Create.ObjectCreate(realm, realm.intrinsics.ObjectPrototype);
realm.react.reactProps.add(props);
applyObjectAssignConfigsForReactElement(realm, props, args);
applyObjectAssignConfigsForReactElement(realm, props, [config]);
props.makeFinal();
if (children !== undefined) {

View File

@ -10,7 +10,15 @@
/* @flow strict-local */
import { Realm } from "../realm.js";
import { AbstractValue, ObjectValue, StringValue, SymbolValue, Value } from "../values/index.js";
import {
AbstractObjectValue,
AbstractValue,
FunctionValue,
ObjectValue,
StringValue,
SymbolValue,
Value,
} from "../values/index.js";
import { ResidualHeapVisitor } from "./ResidualHeapVisitor.js";
import { determineIfReactElementCanBeHoisted } from "../react/hoisting.js";
import { traverseReactElement } from "../react/elements.js";
@ -21,6 +29,7 @@ import {
hardModifyReactObjectPropertyBinding,
} from "../react/utils.js";
import invariant from "../invariant.js";
import { TemporalBuildNodeEntry } from "../utils/generator.js";
import { ReactEquivalenceSet } from "../react/ReactEquivalenceSet.js";
import { ReactElementSet } from "../react/ReactElementSet.js";
import { ReactPropsSet } from "../react/ReactPropsSet.js";
@ -122,4 +131,27 @@ export class ResidualReactElementVisitor {
this.reactElementEquivalenceSet = reactElementEquivalenceSet;
this.reactPropsEquivalenceSet = reactPropsEquivalenceSet;
}
wasTemporalAliasDeclaredInCurrentScope(temporalAlias: AbstractObjectValue): boolean {
let scope = this.residualHeapVisitor.scope;
if (scope instanceof FunctionValue) {
return false;
}
// If the temporal has already been visited, then we know the temporal
// value was used and thus declared in another scope
if (this.residualHeapVisitor.values.has(temporalAlias)) {
return false;
}
// Otherwise, we check the current scope and see if the
// temporal value was declared in one of the entries
for (let i = 0; i < scope._entries.length; i++) {
let entry = scope._entries[i];
if (entry instanceof TemporalBuildNodeEntry) {
if (entry.declared === temporalAlias) {
return true;
}
}
}
return false;
}
}

View File

@ -125,7 +125,7 @@ export type TemporalBuildNodeEntryArgs = {
mutatesOnly?: Array<Value>,
};
class TemporalBuildNodeEntry extends GeneratorEntry {
export class TemporalBuildNodeEntry extends GeneratorEntry {
constructor(args: TemporalBuildNodeEntryArgs) {
super();
Object.assign(this, args);

View File

@ -94,3 +94,23 @@ it("Replace this in callbacks 2", () => {
it("Replace this in callbacks 3", () => {
runTest(__dirname + "/FirstRenderOnly/replace-this-in-callbacks3.js", { firstRenderOnly: true });
});
it("Equivalence of snapshotted node", () => {
runTest(__dirname + "/FirstRenderOnly/equivalence.js", { firstRenderOnly: true });
});
it("Equivalence of snapshotted node 2", () => {
runTest(__dirname + "/FirstRenderOnly/equivalence2.js", { firstRenderOnly: true });
});
it("Equivalence of snapshotted node 3", () => {
runTest(__dirname + "/FirstRenderOnly/equivalence3.js", { firstRenderOnly: true });
});
it("Equivalence of snapshotted node 4", () => {
runTest(__dirname + "/FirstRenderOnly/equivalence4.js", { firstRenderOnly: true });
});
it("Equivalence of snapshotted node 5", () => {
runTest(__dirname + "/FirstRenderOnly/equivalence5.js", { firstRenderOnly: true });
});

View File

@ -0,0 +1,27 @@
var React = require("react");
function App(props) {
var foo = Object.assign({}, props);
var a = <span {...foo} key={null} />;
var b = <span {...foo} key={null} />;
return [a, b];
}
App.getTrials = function(renderer, Root, data, isCompiled) {
if (isCompiled) {
const [a, b] = Root({});
if (a !== b) {
throw new Error("Equivalence check failed");
}
}
renderer.update(<Root />);
return [["equivalence render", renderer.toJSON()]];
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App, {
firstRenderOnly: true,
});
}
module.exports = App;

View File

@ -0,0 +1,28 @@
var React = require("react");
function App(props) {
var foo = { children: <div /> };
var foo2 = Object.assign({}, props, foo);
var a = <span {...foo2} key={null} />;
var b = <span {...foo2} key={null} />;
return [a, b];
}
App.getTrials = function(renderer, Root, data, isCompiled) {
if (isCompiled) {
const [a, b] = Root({});
if (a !== b) {
throw new Error("Equivalence check failed");
}
}
renderer.update(<Root />);
return [["equivalence render", renderer.toJSON()]];
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App, {
firstRenderOnly: true,
});
}
module.exports = App;

View File

@ -0,0 +1,34 @@
var React = require("react");
function App(props) {
var foo = props.foo;
var x = {
val: 1,
};
var y = {
val: 1,
};
var foo = Object.assign({}, props, foo ? x : y);
var a = <span {...foo} key={null} />;
var b = <span {...foo} key={null} />;
return [a, b];
}
App.getTrials = function(renderer, Root, data, isCompiled) {
if (isCompiled) {
const [a, b] = Root({ foo: false });
if (a !== b) {
throw new Error("Equivalence check failed");
}
}
renderer.update(<Root foo={false} />);
return [["equivalence render", renderer.toJSON()]];
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App, {
firstRenderOnly: true,
});
}
module.exports = App;

View File

@ -0,0 +1,24 @@
var React = require("react");
function App(props) {
var foo = Object.assign({}, props);
if (props.x) {
return <span {...foo} key={null} />;
} else {
return <span {...foo} key={null} />;
}
}
App.getTrials = function(renderer, Root, data, isCompiled) {
renderer.update(<Root x={false} className={"test"} />);
return [["equivalence render", renderer.toJSON()]];
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App, {
firstRenderOnly: true,
});
}
module.exports = App;

View File

@ -0,0 +1,30 @@
var React = require("react");
function App(props) {
var foo = Object.assign({}, props);
if (props.x) {
return [<span {...foo} key={null} />, <span {...foo} key={null} />];
} else {
return [<span {...foo} key={null} />, <span {...foo} key={null} />];
}
}
App.getTrials = function(renderer, Root, data, isCompiled) {
if (isCompiled) {
const [a, b] = Root({ x: false });
if (a !== b) {
throw new Error("Equivalence check failed");
}
}
renderer.update(<Root x={false} className={"test"} />);
return [["equivalence render", renderer.toJSON()]];
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App, {
firstRenderOnly: true,
});
}
module.exports = App;

View File

@ -1,5 +1,345 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Equivalence of snapshotted node 2: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 2: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 2: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 2: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 3: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 3: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 3: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 3: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 4: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 4: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 4: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 4: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 5: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 5: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 5: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node 5: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Equivalence of snapshotted node: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`React Context 2: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 4,

View File

@ -289,8 +289,8 @@ function setupReactTests() {
let { getTrials: getTrialsA, independent } = A;
let { getTrials: getTrialsB } = B;
// Run tests that assert the rendered output matches.
let resultA = getTrialsA(rendererA, A, data);
let resultB = independent ? getTrialsB(rendererB, B, data) : getTrialsA(rendererB, B, data);
let resultA = getTrialsA(rendererA, A, data, false);
let resultB = independent ? getTrialsB(rendererB, B, data, true) : getTrialsA(rendererB, B, data, false);
// The test has returned many values for us to check
for (let i = 0; i < resultA.length; i++) {