perf(ivy): chain styling instructions (#33837)

Adds support for chaining of `styleProp`, `classProp` and `stylePropInterpolateX` instructions whenever possible which should help generate less code. Note that one complication here is for `stylePropInterpolateX` instructions where we have to break into multiple chains if there are other styling instructions inbetween the interpolations which helps maintain the execution order.

PR Close #33837
This commit is contained in:
Kristiyan Kostadinov
2019-11-18 20:13:23 +01:00
committed by Alex Rickabaugh
parent f69c6e204a
commit 8a052dc858
6 changed files with 465 additions and 94 deletions

View File

@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api';
import {StylingBuilder, StylingInstruction} from './styling_builder';
import {StylingBuilder, StylingInstructionCall} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@ -652,8 +652,12 @@ function createHostBindingsFunction(
// collected earlier.
const hostAttrs = convertAttributesToExpressions(hostBindingsMetadata.attributes);
const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool);
if (hostInstruction) {
createStatements.push(createStylingStmt(hostInstruction, bindingContext, bindingFn));
if (hostInstruction && hostInstruction.calls.length > 0) {
createStatements.push(
chainedInstruction(
hostInstruction.reference,
hostInstruction.calls.map(call => convertStylingCall(call, bindingContext, bindingFn)))
.toStmt());
}
if (styleBuilder.hasBindings) {
@ -661,11 +665,18 @@ function createHostBindingsFunction(
// the update block of a component/directive templateFn/hostBindingsFn so that the bindings
// are evaluated and updated for the element.
styleBuilder.buildUpdateLevelInstructions(getValueConverter()).forEach(instruction => {
// we subtract a value of `1` here because the binding slot was already
// allocated at the top of this method when all the input bindings were
// counted.
totalHostVarsCount += Math.max(instruction.allocateBindingSlots - 1, 0);
updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn));
if (instruction.calls.length > 0) {
const calls: o.Expression[][] = [];
instruction.calls.forEach(call => {
// we subtract a value of `1` here because the binding slot was already allocated
// at the top of this method when all the input bindings were counted.
totalHostVarsCount += Math.max(call.allocateBindingSlots - 1, 0);
calls.push(convertStylingCall(call, bindingContext, bindingFn));
});
updateStatements.push(chainedInstruction(instruction.reference, calls).toStmt());
}
});
}
@ -699,12 +710,9 @@ function bindingFn(implicit: any, value: AST) {
null, implicit, value, 'b', BindingForm.TrySimple, () => error('Unexpected interpolation'));
}
function createStylingStmt(
instruction: StylingInstruction, bindingContext: any, bindingFn: Function): o.Statement {
const params = instruction.params(value => bindingFn(bindingContext, value).currValExpr);
return o.importExpr(instruction.reference, null, instruction.sourceSpan)
.callFn(params, instruction.sourceSpan)
.toStmt();
function convertStylingCall(
call: StylingInstructionCall, bindingContext: any, bindingFn: Function) {
return call.params(value => bindingFn(bindingContext, value).currValExpr);
}
function getBindingNameAndInstruction(binding: ParsedProperty):

View File

@ -11,7 +11,6 @@ import {AST, ASTWithSource, BindingPipe, BindingType, Interpolation} from '../..
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import {isEmptyExpression} from '../../template_parser/template_parser';
import {error} from '../../util';
import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';
@ -25,10 +24,15 @@ const IMPORTANT_FLAG = '!important';
* A styling expression summary that is to be processed by the compiler
*/
export interface StylingInstruction {
sourceSpan: ParseSourceSpan|null;
reference: o.ExternalReference;
allocateBindingSlots: number;
/** Calls to individual styling instructions. Used when chaining calls to the same instruction. */
calls: StylingInstructionCall[];
}
export interface StylingInstructionCall {
sourceSpan: ParseSourceSpan|null;
supportsInterpolation?: boolean;
allocateBindingSlots: number;
params: ((convertFn: (value: any) => o.Expression | o.Expression[]) => o.Expression[]);
}
@ -280,17 +284,19 @@ export class StylingBuilder {
constantPool: ConstantPool): StylingInstruction|null {
if (this._directiveExpr && (attrs.length || this._hasInitialValues)) {
return {
sourceSpan,
reference: R3.elementHostAttrs,
allocateBindingSlots: 0,
params: () => {
// params => elementHostAttrs(attrs)
this.populateInitialStylingAttrs(attrs);
const attrArray = !attrs.some(attr => attr instanceof o.WrappedNodeExpr) ?
getConstantLiteralFromArray(constantPool, attrs) :
o.literalArr(attrs);
return [attrArray];
}
calls: [{
sourceSpan,
allocateBindingSlots: 0,
params: () => {
// params => elementHostAttrs(attrs)
this.populateInitialStylingAttrs(attrs);
const attrArray = !attrs.some(attr => attr instanceof o.WrappedNodeExpr) ?
getConstantLiteralFromArray(constantPool, attrs) :
o.literalArr(attrs);
return [attrArray];
}
}]
};
}
return null;
@ -344,14 +350,16 @@ export class StylingBuilder {
}
return {
sourceSpan: stylingInput.sourceSpan,
reference,
allocateBindingSlots: totalBindingSlotsRequired,
supportsInterpolation: isClassBased,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
const convertResult = convertFn(mapValue);
return Array.isArray(convertResult) ? convertResult : [convertResult];
}
calls: [{
supportsInterpolation: isClassBased,
sourceSpan: stylingInput.sourceSpan,
allocateBindingSlots: totalBindingSlotsRequired,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
const convertResult = convertFn(mapValue);
return Array.isArray(convertResult) ? convertResult : [convertResult];
}
}]
};
}
@ -360,25 +368,27 @@ export class StylingBuilder {
allowUnits: boolean, valueConverter: ValueConverter,
getInterpolationExpressionFn?: (value: Interpolation) => o.ExternalReference):
StylingInstruction[] {
let totalBindingSlotsRequired = 0;
return inputs.map(input => {
const value = input.value.visit(valueConverter);
const instructions: StylingInstruction[] = [];
// each styling binding value is stored in the LView
let totalBindingSlotsRequired = 1;
inputs.forEach(input => {
const previousInstruction: StylingInstruction|undefined =
instructions[instructions.length - 1];
const value = input.value.visit(valueConverter);
let referenceForCall = reference;
let totalBindingSlotsRequired = 1; // each styling binding value is stored in the LView
if (value instanceof Interpolation) {
totalBindingSlotsRequired += value.expressions.length;
if (getInterpolationExpressionFn) {
reference = getInterpolationExpressionFn(value);
referenceForCall = getInterpolationExpressionFn(value);
}
}
return {
const call = {
sourceSpan: input.sourceSpan,
allocateBindingSlots: totalBindingSlotsRequired,
supportsInterpolation: !!getInterpolationExpressionFn,
allocateBindingSlots: totalBindingSlotsRequired, reference,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
// params => stylingProp(propName, value)
const params: o.Expression[] = [];
@ -398,7 +408,20 @@ export class StylingBuilder {
return params;
}
};
// If we ended up generating a call to the same instruction as the previous styling property
// we can chain the calls together safely to save some bytes, otherwise we have to generate
// a separate instruction call. This is primarily a concern with interpolation instructions
// where we may start off with one `reference`, but end up using another based on the
// number of interpolations.
if (previousInstruction && previousInstruction.reference === referenceForCall) {
previousInstruction.calls.push(call);
} else {
instructions.push({reference: referenceForCall, calls: [call]});
}
});
return instructions;
}
private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] {
@ -420,10 +443,12 @@ export class StylingBuilder {
private _buildSanitizerFn(): StylingInstruction {
return {
sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
reference: R3.styleSanitizer,
allocateBindingSlots: 0,
params: () => [o.importExpr(R3.defaultStyleSanitizer)]
calls: [{
sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
allocateBindingSlots: 0,
params: () => [o.importExpr(R3.defaultStyleSanitizer)]
}]
};
}
@ -476,20 +501,6 @@ function getConstantLiteralFromArray(
return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR;
}
/**
* Simple helper function that adds a parameter or does nothing at all depending on the provided
* predicate and totalExpectedArgs values
*/
function addParam(
params: o.Expression[], predicate: any, value: o.Expression | null, argNumber: number,
totalExpectedArgs: number) {
if (predicate && value) {
params.push(value);
} else if (argNumber < totalExpectedArgs) {
params.push(o.NULL_EXPR);
}
}
export function parseProperty(name: string):
{property: string, unit: string, hasOverrideFlag: boolean} {
let hasOverrideFlag = false;

View File

@ -708,8 +708,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const limit = stylingInstructions.length - 1;
for (let i = 0; i <= limit; i++) {
const instruction = stylingInstructions[i];
this._bindingSlots += instruction.allocateBindingSlots;
this.processStylingInstruction(elementIndex, instruction, false);
this._bindingSlots += this.processStylingUpdateInstruction(elementIndex, instruction);
}
// the reason why `undefined` is used is because the renderer understands this as a
@ -1071,25 +1070,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
}
private processStylingInstruction(
elementIndex: number, instruction: StylingInstruction|null, createMode: boolean) {
private processStylingUpdateInstruction(
elementIndex: number, instruction: StylingInstruction|null) {
let allocateBindingSlots = 0;
if (instruction) {
if (createMode) {
this.creationInstruction(instruction.sourceSpan, instruction.reference, () => {
return instruction.params(value => this.convertPropertyBinding(value)) as o.Expression[];
});
} else {
this.updateInstructionWithAdvance(
elementIndex, instruction.sourceSpan, instruction.reference, () => {
return instruction
.params(value => {
return (instruction.supportsInterpolation && value instanceof Interpolation) ?
const calls: ChainableBindingInstruction[] = [];
instruction.calls.forEach(call => {
allocateBindingSlots += call.allocateBindingSlots;
calls.push({
sourceSpan: call.sourceSpan,
value: () => {
return call
.params(
value => (call.supportsInterpolation && value instanceof Interpolation) ?
this.getUpdateInstructionArguments(value) :
this.convertPropertyBinding(value);
}) as o.Expression[];
});
}
this.convertPropertyBinding(value)) as o.Expression[];
}
});
});
this.updateInstructionChainWithAdvance(elementIndex, instruction.reference, calls);
}
return allocateBindingSlots;
}
private creationInstruction(
@ -1127,8 +1131,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this._updateCodeFns.push(() => {
const calls = bindings.map(property => {
const fnParams = [property.value(), ...(property.params || [])];
const value = property.value();
const fnParams = Array.isArray(value) ? value : [value];
if (property.params) {
fnParams.push(...property.params);
}
if (property.name) {
// We want the property name to always be the first function parameter.
fnParams.unshift(o.literal(property.name));
}
return fnParams;
@ -2045,7 +2054,7 @@ function hasTextChildrenOnly(children: t.Node[]): boolean {
interface ChainableBindingInstruction {
name?: string;
sourceSpan: ParseSourceSpan|null;
value: () => o.Expression;
value: () => o.Expression | o.Expression[];
params?: any[];
}