More React reconciler fixes/debug output

Summary:
Release notes: none

More improvements the React reconciler so it gives better output and scan/tracks for more trees.
Closes https://github.com/facebook/prepack/pull/1539

Differential Revision: D7186071

Pulled By: trueadm

fbshipit-source-id: 8cf3c08e23a6d3b386917e58371b4f021a537d7e
This commit is contained in:
Dominic Gannaway 2018-03-07 16:55:51 -08:00 committed by Facebook Github Bot
parent e1c23e98c8
commit 81e47461e0
6 changed files with 1038 additions and 83 deletions

File diff suppressed because it is too large Load Diff

View File

@ -75,7 +75,9 @@ function printReactEvaluationGraph(evaluatedRootNode, depth) {
printReactEvaluationGraph(child, depth);
}
} else {
let line = `- ${evaluatedRootNode.name} (${evaluatedRootNode.status.toLowerCase()})`;
let status = evaluatedRootNode.status.toLowerCase();
let message = evaluatedRootNode.message !== "" ? `: ${evaluatedRootNode.message}` : "";
let line = `- ${evaluatedRootNode.name} (${status}${message})`;
console.log(line.padStart(line.length + depth));
printReactEvaluationGraph(evaluatedRootNode.children, depth + 2);
}

View File

@ -39,6 +39,7 @@ import {
createReactEvaluatedNode,
getComponentName,
sanitizeReactElementForFirstRenderOnly,
getValueFromRenderCall,
} from "./utils";
import { Get } from "../methods/index.js";
import invariant from "../invariant.js";
@ -54,7 +55,7 @@ import {
createClassInstanceForFirstRenderOnly,
} from "./components.js";
import { ExpectedBailOut, SimpleClassBailOut, NewComponentTreeBranch } from "./errors.js";
import { Completion } from "../completions.js";
import { AbruptCompletion } from "../completions.js";
import { Logger } from "../utils/logger.js";
import type { ClassComponentMetadata, ReactComponentTreeConfig } from "../types.js";
@ -130,33 +131,36 @@ export class Reconciler {
this.alreadyEvaluatedRootNodes.set(componentType, evaluatedRootNode);
return result;
} catch (error) {
if (error instanceof Completion) {
// this.logger.logCompletion(error);
evaluatedRootNode.status = "UNSUPPORTED_COMPLETION";
return this.realm.intrinsics.undefined;
} else {
// if we get an error and we're not dealing with the root
// rather than throw a FatalError, we log the error as a warning
// and continue with the other tree roots
// TODO: maybe control what levels gets treated as warning/error?
if (!isRoot) {
// if we get an error and we're not dealing with the root
// rather than throw a FatalError, we log the error as a warning
// and continue with the other tree roots
// TODO: maybe control what levels gets treated as warning/error?
if (!isRoot) {
if (error instanceof AbruptCompletion) {
this.logger.logWarning(
componentType,
`__optimizeReactComponentTree() React component tree (branch) failed due runtime runtime exception thrown`
);
evaluatedRootNode.status = "ABRUPT_COMPLETION";
} else {
this.logger.logWarning(
componentType,
`__optimizeReactComponentTree() React component tree (branch) failed due to - ${error.message}`
);
evaluatedRootNode.message = "evaluation failed on new component tree branch";
evaluatedRootNode.status = "BAIL-OUT";
return this.realm.intrinsics.undefined;
}
if (error instanceof ExpectedBailOut) {
let diagnostic = new CompilerDiagnostic(
`__optimizeReactComponentTree() React component tree (root) failed due to - ${error.message}`,
this.realm.currentLocation,
"PP0020",
"FatalError"
);
this.realm.handleError(diagnostic);
if (this.realm.handleError(diagnostic) === "Fail") throw new FatalError();
}
return this.realm.intrinsics.undefined;
}
if (error instanceof ExpectedBailOut) {
let diagnostic = new CompilerDiagnostic(
`__optimizeReactComponentTree() React component tree (root) failed due to - ${error.message}`,
this.realm.currentLocation,
"PP0020",
"FatalError"
);
this.realm.handleError(diagnostic);
if (this.realm.handleError(diagnostic) === "Fail") throw new FatalError();
}
throw error;
}
@ -228,12 +232,9 @@ export class Reconciler {
let instance = createClassInstance(this.realm, componentType, props, context, classMetadata);
// get the "render" method off the instance
let renderMethod = Get(this.realm, instance, "render");
invariant(
renderMethod instanceof ECMAScriptSourceFunctionValue && renderMethod.$Call,
"Expected render method to be a FunctionValue with $Call method"
);
invariant(renderMethod instanceof ECMAScriptSourceFunctionValue);
// the render method doesn't have any arguments, so we just assign the context of "this" to be the instance
return renderMethod.$Call(instance, []);
return getValueFromRenderCall(this.realm, renderMethod, instance, []);
}
_renderSimpleClassComponent(
@ -247,12 +248,9 @@ export class Reconciler {
let instance = createSimpleClassInstance(this.realm, componentType, props, context);
// get the "render" method off the instance
let renderMethod = Get(this.realm, instance, "render");
invariant(
renderMethod instanceof ECMAScriptSourceFunctionValue && renderMethod.$Call,
"Expected render method to be a FunctionValue with $Call method"
);
invariant(renderMethod instanceof ECMAScriptSourceFunctionValue);
// the render method doesn't have any arguments, so we just assign the context of "this" to be the instance
return renderMethod.$Call(instance, []);
return getValueFromRenderCall(this.realm, renderMethod, instance, []);
}
_renderFunctionalComponent(
@ -260,8 +258,7 @@ export class Reconciler {
props: ObjectValue | AbstractValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue
) {
invariant(componentType.$Call, "Expected componentType to be a FunctionValue with $Call method");
return componentType.$Call(this.realm.intrinsics.undefined, [props, context]);
return getValueFromRenderCall(this.realm, componentType, this.realm.intrinsics.undefined, [props, context]);
}
_getClassComponentMetadata(
@ -307,6 +304,8 @@ export class Reconciler {
// that we need to also evaluate. given we can't find those components
return renderResult;
}
} else {
this._findReactComponentTrees(props, evaluatedNode);
}
}
// this is the worst case, we were unable to find the render prop function
@ -392,12 +391,9 @@ export class Reconciler {
if (componentWillMount instanceof ECMAScriptSourceFunctionValue && componentWillMount.$Call) {
componentWillMount.$Call(instance, []);
}
invariant(
renderMethod instanceof ECMAScriptSourceFunctionValue && renderMethod.$Call,
"Expected render method to be a FunctionValue with $Call method"
);
invariant(renderMethod instanceof ECMAScriptSourceFunctionValue);
// the render method doesn't have any arguments, so we just assign the context of "this" to be the instance
return renderMethod.$Call(instance, []);
return getValueFromRenderCall(this.realm, renderMethod, instance, []);
}
_renderComponent(
@ -485,6 +481,33 @@ export class Reconciler {
return "NORMAL";
}
_resolveAbstractValue(
componentType: Value,
value: AbstractValue,
context: ObjectValue | AbstractObjectValue,
branchStatus: BranchStatusEnum,
branchState: BranchState | null,
evaluatedNode: ReactEvaluatedNode
) {
let length = value.args.length;
if (length > 0) {
let newBranchState = new BranchState();
// TODO investigate what other kinds than "conditional" might be safe to deeply resolve
for (let i = 0; i < length; i++) {
value.args[i] = this._resolveDeeply(
componentType,
value.args[i],
context,
"NEW_BRANCH",
newBranchState,
evaluatedNode
);
}
newBranchState.applyBranchedLogic(this.realm, this.reactSerializerState);
}
return value;
}
_resolveDeeply(
componentType: Value,
value: Value,
@ -503,23 +526,7 @@ export class Reconciler {
// terminal values
return value;
} else if (value instanceof AbstractValue) {
let length = value.args.length;
if (length > 0) {
let newBranchState = new BranchState();
// TODO investigate what other kinds than "conditional" might be safe to deeply resolve
for (let i = 0; i < length; i++) {
value.args[i] = this._resolveDeeply(
componentType,
value.args[i],
context,
"NEW_BRANCH",
newBranchState,
evaluatedNode
);
}
newBranchState.applyBranchedLogic(this.realm, this.reactSerializerState);
}
return value;
return this._resolveAbstractValue(componentType, value, context, branchStatus, branchState, evaluatedNode);
}
// TODO investigate what about other iterables type objects
if (value instanceof ArrayValue) {
@ -570,9 +577,11 @@ export class Reconciler {
if (!(refValue instanceof NullValue)) {
let evaluatedChildNode = createReactEvaluatedNode("BAIL-OUT", getComponentName(this.realm, typeValue));
evaluatedNode.children.push(evaluatedChildNode);
evaluatedChildNode.status = "BAIL-OUT";
let bailOutMessage = `refs are not supported on <Components />`;
evaluatedChildNode.message = bailOutMessage;
this._queueNewComponentTree(typeValue, evaluatedChildNode);
this._assignBailOutMessage(reactElement, `Bail-out: refs are not supported on <Components />`);
this._findReactComponentTrees(propsValue, evaluatedNode);
this._assignBailOutMessage(reactElement, bailOutMessage);
return reactElement;
}
if (
@ -584,7 +593,7 @@ export class Reconciler {
) {
this._assignBailOutMessage(
reactElement,
`Bail-out: props on <Component /> was not not an ObjectValue or an AbstractValue`
`props on <Component /> was not not an ObjectValue or an AbstractValue`
);
return reactElement;
}
@ -594,11 +603,18 @@ export class Reconciler {
renderStrategy === "NORMAL" &&
!(typeValue instanceof ECMAScriptSourceFunctionValue || valueIsKnownReactAbstraction(this.realm, typeValue))
) {
this._assignBailOutMessage(
reactElement,
`Bail-out: type on <Component /> was not a ECMAScriptSourceFunctionValue`
);
return reactElement;
this._findReactComponentTrees(propsValue, evaluatedNode);
if (typeValue instanceof AbstractValue) {
this._findReactComponentTrees(typeValue, evaluatedNode);
return reactElement;
} else {
let evaluatedChildNode = createReactEvaluatedNode("BAIL-OUT", getComponentName(this.realm, typeValue));
evaluatedNode.children.push(evaluatedChildNode);
let bailOutMessage = `type on <Component /> was not a ECMAScriptSourceFunctionValue`;
evaluatedChildNode.message = bailOutMessage;
this._assignBailOutMessage(reactElement, bailOutMessage);
return reactElement;
}
} else if (renderStrategy === "FRAGMENT") {
return resolveChildren();
}
@ -639,7 +655,12 @@ export class Reconciler {
}
if (result instanceof UndefinedValue) {
this._assignBailOutMessage(reactElement, `Bail-out: undefined was returned from render`);
let evaluatedChildNode = createReactEvaluatedNode("BAIL-OUT", getComponentName(this.realm, typeValue));
evaluatedNode.children.push(evaluatedChildNode);
let bailOutMessage = `undefined was returned from render`;
evaluatedChildNode.message = bailOutMessage;
this._assignBailOutMessage(reactElement, bailOutMessage);
this._findReactComponentTrees(propsValue, evaluatedNode);
if (branchStatus === "NEW_BRANCH" && branchState) {
return branchState.captureBranchedValue(typeValue, reactElement);
}
@ -654,15 +675,29 @@ export class Reconciler {
if (error instanceof NewComponentTreeBranch) {
// NO-OP (we don't queue a newComponentTree as this was already done)
} else {
let evaluatedChildNode = createReactEvaluatedNode("BAIL-OUT", getComponentName(this.realm, typeValue));
evaluatedNode.children.push(evaluatedChildNode);
this._queueNewComponentTree(typeValue, evaluatedChildNode);
if (error instanceof ExpectedBailOut) {
this._assignBailOutMessage(reactElement, "Bail-out: " + error.message);
} else if (error instanceof FatalError) {
this._assignBailOutMessage(reactElement, "Evaluation bail-out");
// handle abrupt completions
if (error instanceof AbruptCompletion) {
let evaluatedChildNode = createReactEvaluatedNode(
"ABRUPT_COMPLETION",
getComponentName(this.realm, typeValue)
);
evaluatedNode.children.push(evaluatedChildNode);
} else {
throw error;
let evaluatedChildNode = createReactEvaluatedNode("BAIL-OUT", getComponentName(this.realm, typeValue));
evaluatedNode.children.push(evaluatedChildNode);
this._queueNewComponentTree(typeValue, evaluatedChildNode);
this._findReactComponentTrees(propsValue, evaluatedNode);
if (error instanceof ExpectedBailOut) {
evaluatedChildNode.message = error.message;
this._assignBailOutMessage(reactElement, error.message);
} else if (error instanceof FatalError) {
let message = "evaluation failed";
evaluatedChildNode.message = message;
this._assignBailOutMessage(reactElement, message);
} else {
evaluatedChildNode.message = `unknown error`;
throw error;
}
}
}
// a child component bailed out during component folding, so return the function value and continue
@ -679,6 +714,7 @@ export class Reconciler {
_assignBailOutMessage(reactElement: ObjectValue, message: string): void {
// $BailOutReason is a field on ObjectValue that allows us to specify a message
// that gets serialized as a comment node during the ReactElement serialization stage
message = `Bail-out: ${message}`;
if (reactElement.$BailOutReason !== undefined) {
// merge bail out messages if one already exists
reactElement.$BailOutReason += `, ${message}`;
@ -718,4 +754,22 @@ export class Reconciler {
}
return false;
}
_findReactComponentTrees(value: Value, evaluatedNode: ReactEvaluatedNode): void {
if (value instanceof AbstractValue) {
for (let arg of value.args) {
this._findReactComponentTrees(arg, evaluatedNode);
}
} else if (value instanceof ObjectValue) {
for (let [propName, binding] of value.properties) {
if (binding && binding.descriptor && binding.enumerable) {
this._findReactComponentTrees(getProperty(this.realm, value, propName), evaluatedNode);
}
}
} else if (value instanceof ECMAScriptSourceFunctionValue || valueIsKnownReactAbstraction(this.realm, value)) {
let evaluatedChildNode = createReactEvaluatedNode("NEW_TREE", getComponentName(this.realm, value));
evaluatedNode.children.push(evaluatedChildNode);
this._queueNewComponentTree(value, evaluatedChildNode);
}
}
}

View File

@ -11,7 +11,7 @@
import { Realm, type Effects } from "../realm.js";
import { FunctionEnvironmentRecord, Reference } from "../environment.js";
import { Completion } from "../completions.js";
import { Completion, PossiblyNormalCompletion, AbruptCompletion } from "../completions.js";
import type { BabelNode, BabelNodeJSXIdentifier } from "babel-types";
import {
AbstractObjectValue,
@ -674,13 +674,22 @@ export function isRenderPropFunctionSelfContained(
}
export function createReactEvaluatedNode(
status: "ROOT" | "NEW_TREE" | "INLINED" | "BAIL-OUT" | "UNKNOWN_TYPE" | "RENDER_PROPS" | "UNSUPPORTED_COMPLETION",
status:
| "ROOT"
| "NEW_TREE"
| "INLINED"
| "BAIL-OUT"
| "UNKNOWN_TYPE"
| "RENDER_PROPS"
| "UNSUPPORTED_COMPLETION"
| "ABRUPT_COMPLETION",
name: string
): ReactEvaluatedNode {
return {
children: [],
message: "",
name,
status,
children: [],
};
}
@ -744,6 +753,37 @@ export function convertConfigObjectToReactComponentTreeConfig(
};
}
export function getValueFromRenderCall(
realm: Realm,
renderFunction: ECMAScriptSourceFunctionValue,
instance: ObjectValue | AbstractObjectValue | UndefinedValue,
args: Array<Value>
): Value {
invariant(renderFunction.$Call, "Expected render function to be a FunctionValue with $Call method");
let funcCall = renderFunction.$Call;
let effects;
try {
effects = realm.evaluateForEffects(() => funcCall(instance, args));
} catch (error) {
throw error;
}
let completion = effects[0];
if (completion instanceof PossiblyNormalCompletion) {
// in this case one of the branches may complete abruptly, which means that
// not all control flow branches join into one flow at this point.
// Consequently we have to continue tracking changes until the point where
// all the branches come together into one.
completion = realm.composeWithSavedCompletion(completion);
}
// Note that the effects of (non joining) abrupt branches are not included
// in joinedEffects, but are tracked separately inside completion.
realm.applyEffects(effects);
// return or throw completion
if (completion instanceof AbruptCompletion) throw completion;
invariant(completion instanceof Value);
return completion;
}
export function sanitizeReactElementForFirstRenderOnly(realm: Realm, reactElement: ObjectValue): ObjectValue {
let typeValue = Get(realm, reactElement, "type");

View File

@ -28,7 +28,12 @@ import {
import { Get } from "../methods/index.js";
import { ModuleTracer } from "../utils/modules.js";
import buildTemplate from "babel-template";
import { ReactStatistics, type ReactSerializerState, type AdditionalFunctionEffects } from "./types";
import {
ReactStatistics,
type ReactSerializerState,
type AdditionalFunctionEffects,
type ReactEvaluatedNode,
} from "./types";
import { Reconciler, type ComponentTreeState } from "../react/reconcilation.js";
import {
valueIsClassComponent,
@ -155,7 +160,8 @@ export class Functions {
_generateWriteEffectsForReactComponentTree(
componentType: ECMAScriptSourceFunctionValue,
effects: Effects,
componentTreeState: ComponentTreeState
componentTreeState: ComponentTreeState,
evaluatedNode: ReactEvaluatedNode
): void {
let additionalFunctionEffects = this._createAdditionalEffects(effects);
let value = effects[0];
@ -168,6 +174,7 @@ export class Functions {
if (value instanceof Completion) {
// TODO we don't support this yet, but will do very soon
// to unblock work, we'll just return at this point right now
evaluatedNode.status = "UNSUPPORTED_COMPLETION";
return;
}
invariant(value instanceof Value);
@ -222,7 +229,7 @@ export class Functions {
}
let effects = reconciler.render(componentType, null, null, true, evaluatedRootNode);
let componentTreeState = reconciler.componentTreeState;
this._generateWriteEffectsForReactComponentTree(componentType, effects, componentTreeState);
this._generateWriteEffectsForReactComponentTree(componentType, effects, componentTreeState, evaluatedRootNode);
// for now we just use abstract props/context, in the future we'll create a new branch with a new component
// that used the props/context. It will extend the original component and only have a render method
@ -240,7 +247,12 @@ export class Functions {
reconciler.clearComponentTreeState();
let branchEffects = reconciler.render(branchComponentType, null, null, false, evaluatedNode);
let branchComponentTreeState = reconciler.componentTreeState;
this._generateWriteEffectsForReactComponentTree(branchComponentType, branchEffects, branchComponentTreeState);
this._generateWriteEffectsForReactComponentTree(
branchComponentType,
branchEffects,
branchComponentTreeState,
evaluatedNode
);
});
}
if (this.realm.react.output === "bytecode") {

View File

@ -152,9 +152,18 @@ export class TimingStatistics {
}
export type ReactEvaluatedNode = {
name: string,
status: "ROOT" | "NEW_TREE" | "INLINED" | "BAIL-OUT" | "UNKNOWN_TYPE" | "RENDER_PROPS" | "UNSUPPORTED_COMPLETION",
children: Array<ReactEvaluatedNode>,
message: string,
name: string,
status:
| "ROOT"
| "NEW_TREE"
| "INLINED"
| "BAIL-OUT"
| "UNKNOWN_TYPE"
| "RENDER_PROPS"
| "UNSUPPORTED_COMPLETION"
| "ABRUPT_COMPLETION",
};
export class ReactStatistics {