perf(ivy): chain multiple property instructions (#31078)

Currently each property binding generates an instruction like this:

```
property('prop1', ctx.value1);
property('prop2', ctx.value2);
```

The problem is that we're repeating the call to `property` for each of the properties. Since the `property` instruction returns itself, we can chain all of the calls which is more compact and it looks like this:

```
property('prop1', ctx.value1)('prop2', ctx.value2);
```

These changes implement the chaining behavior for regular property bindings and for synthetic ones, however interpolated ones are still handled like before, because they use a separate instruction.

This PR resolves FW-1389.

PR Close #31078
This commit is contained in:
crisbeto
2019-06-19 19:50:42 +02:00
committed by Kara Erickson
parent ab86a58f27
commit 4b16d98955
4 changed files with 305 additions and 25 deletions

View File

@ -712,6 +712,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// special value to symbolize that there is no RHS to this binding
// TODO (matsko): revisit this once FW-959 is approached
const emptyValueBindInstruction = o.literal(undefined);
const propertyBindings: ChainablePropertyBinding[] = [];
// Generate element input bindings
allOtherInputs.forEach((input: t.BoundAttribute) => {
@ -729,14 +730,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// defined in...
const hasValue = value instanceof LiteralPrimitive ? !!value.value : true;
this.allocateBindingSlots(value);
const bindingName = prepareSyntheticPropertyName(input.name);
this.updateInstruction(elementIndex, input.sourceSpan, R3.property, () => {
return [
o.literal(bindingName),
(hasValue ? this.convertPropertyBinding(value, /* skipBindFn */ true) :
emptyValueBindInstruction),
];
propertyBindings.push({
name: prepareSyntheticPropertyName(input.name),
input,
value: () => hasValue ? this.convertPropertyBinding(value, /* skipBindFn */ true) :
emptyValueBindInstruction
});
} else {
// we must skip attributes with associated i18n context, since these attributes are handled
@ -771,8 +770,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
params);
} else {
// [prop]="value"
this.boundUpdateInstruction(
R3.property, elementIndex, attrName, input, value, params);
// Collect all the properties so that we can chain into a single function at the end.
propertyBindings.push({
name: attrName,
input,
value: () => this.convertPropertyBinding(value, true), params
});
}
} else if (inputType === BindingType.Attribute) {
if (value instanceof Interpolation && getInterpolationArgsLength(value) > 1) {
@ -799,6 +802,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
});
if (propertyBindings.length > 0) {
this.propertyInstructionChain(elementIndex, propertyBindings);
}
// Traverse element child nodes
t.visitAll(this, element.children);
@ -912,12 +919,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
// handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al;
this.templatePropertyBindings(template, templateIndex, template.templateAttrs);
this.templatePropertyBindings(templateIndex, template.templateAttrs);
// Only add normal input/output binding instructions on explicit ng-template elements.
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
// Add the input bindings
this.templatePropertyBindings(template, templateIndex, template.inputs);
this.templatePropertyBindings(templateIndex, template.inputs);
// Generate listeners for directive output
template.outputs.forEach((outputAst: t.BoundEvent) => {
this.creationInstruction(
@ -1024,21 +1031,26 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private bindingContext() { return `${this._bindingContext++}`; }
private templatePropertyBindings(
template: t.Template, templateIndex: number, attrs: (t.BoundAttribute|t.TextAttribute)[]) {
templateIndex: number, attrs: (t.BoundAttribute|t.TextAttribute)[]) {
const propertyBindings: ChainablePropertyBinding[] = [];
attrs.forEach(input => {
if (input instanceof t.BoundAttribute) {
const value = input.value.visit(this._valueConverter);
if (value !== undefined) {
this.allocateBindingSlots(value);
this.updateInstruction(
templateIndex, template.sourceSpan, R3.property,
() =>
[o.literal(input.name),
this.convertPropertyBinding(value, /* skipBindFn */ true)]);
propertyBindings.push({
name: input.name,
input,
value: () => this.convertPropertyBinding(value, /* skipBindFn */ true)
});
}
}
});
if (propertyBindings.length > 0) {
this.propertyInstructionChain(templateIndex, propertyBindings);
}
}
// Bindings must only be resolved after all local refs have been visited, so all
@ -1077,13 +1089,37 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private updateInstruction(
nodeIndex: number, span: ParseSourceSpan|null, reference: o.ExternalReference,
paramsOrFn?: o.Expression[]|(() => o.Expression[])) {
this.addSelectInstructionIfNecessary(nodeIndex, span);
this.instructionFn(this._updateCodeFns, span, reference, paramsOrFn || []);
}
private updateInstructionChain(
nodeIndex: number, span: ParseSourceSpan|null, reference: o.ExternalReference,
callsOrFn?: o.Expression[][]|(() => o.Expression[][])) {
this.addSelectInstructionIfNecessary(nodeIndex, span);
this._updateCodeFns.push(() => {
const calls = typeof callsOrFn === 'function' ? callsOrFn() : callsOrFn;
return chainedInstruction(span, reference, calls || []).toStmt();
});
}
private propertyInstructionChain(
nodeIndex: number, propertyBindings: ChainablePropertyBinding[]) {
this.updateInstructionChain(
nodeIndex, propertyBindings.length ? propertyBindings[0].input.sourceSpan : null,
R3.property, () => {
return propertyBindings.map(
property => [o.literal(property.name), property.value(), ...(property.params || [])]);
});
}
private addSelectInstructionIfNecessary(nodeIndex: number, span: ParseSourceSpan|null) {
if (this._lastNodeIndexWithFlush < nodeIndex) {
if (nodeIndex > 0) {
this.instructionFn(this._updateCodeFns, span, R3.select, [o.literal(nodeIndex)]);
}
this._lastNodeIndexWithFlush = nodeIndex;
}
this.instructionFn(this._updateCodeFns, span, reference, paramsOrFn || []);
}
private allocatePureFunctionSlots(numSlots: number): number {
@ -1388,6 +1424,22 @@ function instruction(
return o.importExpr(reference, null, span).callFn(params, span);
}
function chainedInstruction(
span: ParseSourceSpan | null, reference: o.ExternalReference, calls: o.Expression[][]) {
let expression = o.importExpr(reference, null, span) as o.Expression;
if (calls.length > 0) {
for (let i = 0; i < calls.length; i++) {
expression = expression.callFn(calls[i], span);
}
} else {
// Add a blank invocation, in case the `calls` array is empty.
expression = expression.callFn([], span);
}
return expression;
}
// e.g. x(2);
function generateNextContextExpr(relativeLevelDiff: number): o.Expression {
return o.importExpr(R3.nextContext)
@ -1979,3 +2031,10 @@ function isTextNode(node: t.Node): boolean {
function hasTextChildrenOnly(children: t.Node[]): boolean {
return children.every(isTextNode);
}
interface ChainablePropertyBinding {
name: string;
input: t.BoundAttribute;
value: () => o.Expression;
params?: any[];
}