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:

committed by
Alex Rickabaugh

parent
f69c6e204a
commit
8a052dc858
@ -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):
|
||||
|
@ -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;
|
||||
|
@ -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[];
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user