perf(ivy): speed up bindings when no directives are present (#32919)

Prior to this fix, whenever a style or class binding is present, the
binding application process would require an instance of `TStylingContext`
to be built regardless of whether or not any binding resolution is needed
(just so that it knows whether or not there are any collisions).
This check is, however, unnecessary because if (and only if) there
are directives present on the element then are collisions possible.

This patch removes the need for style/class bindings to register
themselves on to a `TStylingContext` if there are no directives and
present on an element. This means that all map and prop-based
style/class bindings are applied as soon as bindings are updated on
an element.

PR Close #32919
This commit is contained in:
Matias Niemelä 2019-09-27 11:57:50 -07:00 committed by Miško Hevery
parent 8d111da7f6
commit b2decf0266
8 changed files with 230 additions and 60 deletions

View File

@ -10,7 +10,8 @@ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {setInputsForProperty} from '../instructions/shared';
import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling';
import {StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext} from '../interfaces/styling';
import {isDirectiveHost} from '../interfaces/type_checks';
import {BINDING_INDEX, LView, RENDERER} from '../interfaces/view';
import {getActiveDirectiveId, getCurrentStyleSanitizer, getLView, getSelectedIndex, setCurrentStyleSanitizer, setElementExitFn} from '../state';
import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from '../styling/bindings';
@ -18,7 +19,7 @@ import {activateStylingMapFeature} from '../styling/map_based_bindings';
import {attachStylingDebugObject} from '../styling/styling_debug';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, setValue, stylingMapToString} from '../util/styling_utils';
import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, patchConfig, setValue, stylingMapToString} from '../util/styling_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
@ -163,12 +164,19 @@ function stylingProp(
const context = isClassBased ? getClassesContext(tNode) : getStylesContext(tNode);
const sanitizer = isClassBased ? null : getCurrentStyleSanitizer();
// we check for this in the instruction code so that the context can be notified
// about prop or map bindings so that the direct apply check can decide earlier
// if it allows for context resolution to be bypassed.
if (!isContextLocked(context, hostBindingsMode)) {
patchConfig(context, TStylingConfig.HasPropBindings);
}
// Direct Apply Case: bypass context resolution and apply the
// style/class value directly to the element
if (allowDirectStyling(context, hostBindingsMode)) {
const renderer = getRenderer(tNode, lView);
updated = applyStylingValueDirectly(
renderer, context, native, lView, bindingIndex, prop, value,
renderer, context, native, lView, bindingIndex, prop, value, isClassBased,
isClassBased ? setClass : setStyle, sanitizer);
} else {
// Context Resolution (or first update) Case: save the value
@ -315,6 +323,13 @@ function _stylingMap(
const hostBindingsMode = isHostStyling();
const sanitizer = getCurrentStyleSanitizer();
// we check for this in the instruction code so that the context can be notified
// about prop or map bindings so that the direct apply check can decide earlier
// if it allows for context resolution to be bypassed.
if (!isContextLocked(context, hostBindingsMode)) {
patchConfig(context, TStylingConfig.HasMapBindings);
}
const valueHasChanged = hasValueChanged(oldValue, value);
const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);
@ -325,7 +340,7 @@ function _stylingMap(
const renderer = getRenderer(tNode, lView);
updated = applyStylingMapDirectly(
renderer, context, native, lView, bindingIndex, stylingMapArr as StylingMapArray,
isClassBased ? setClass : setStyle, sanitizer, valueHasChanged);
isClassBased, isClassBased ? setClass : setStyle, sanitizer, valueHasChanged);
} else {
updated = valueHasChanged;
activateStylingMapFeature();
@ -500,10 +515,12 @@ function getClassesContext(tNode: TNode): TStylingContext {
function getContext(tNode: TNode, isClassBased: boolean): TStylingContext {
let context = isClassBased ? tNode.classes : tNode.styles;
if (!isStylingContext(context)) {
context = allocTStylingContext(context as StylingMapArray | null);
const hasDirectives = isDirectiveHost(tNode);
context = allocTStylingContext(context as StylingMapArray | null, hasDirectives);
if (ngDevMode) {
attachStylingDebugObject(context as TStylingContext);
}
if (isClassBased) {
tNode.classes = context;
} else {

View File

@ -341,7 +341,25 @@ export const enum TStylingConfig {
/**
* The initial state of the styling context config.
*/
Initial = 0b0000000,
Initial = 0b00000000,
/**
* Whether or not there are any directives on this element.
*
* This is used so that certain performance optimizations can
* take place (e.g. direct style/class binding application).
*
* Note that the presence of this flag doesn't guarantee the
* presence of host-level style or class bindings within any
* of the active directives on the element.
*
* Examples include:
* 1. `<div dir-one>`
* 2. `<div dir-one [dir-two]="x">`
* 3. `<comp>`
* 4. `<comp dir-one>`
*/
HasDirectives = 0b00000001,
/**
* Whether or not there are prop-based bindings present.
@ -352,7 +370,7 @@ export const enum TStylingConfig {
* 3. `@HostBinding('style.prop') x`
* 4. `@HostBinding('class.prop') x`
*/
HasPropBindings = 0b0000001,
HasPropBindings = 0b00000010,
/**
* Whether or not there are map-based bindings present.
@ -363,7 +381,7 @@ export const enum TStylingConfig {
* 3. `@HostBinding('style') x`
* 4. `@HostBinding('class') x`
*/
HasMapBindings = 0b0000010,
HasMapBindings = 0b00000100,
/**
* Whether or not there are map-based and prop-based bindings present.
@ -374,7 +392,7 @@ export const enum TStylingConfig {
* 3. `<div [style]="x" dir-that-sets-some-prop>`
* 4. `<div [class]="x" dir-that-sets-some-class>`
*/
HasPropAndMapBindings = 0b0000011,
HasPropAndMapBindings = HasPropBindings | HasMapBindings,
/**
* Whether or not there are two or more sources for a single property in the context.
@ -384,7 +402,7 @@ export const enum TStylingConfig {
* 2. map + prop: `<div [style]="x" [style.prop]>`
* 3. map + map: `<div [style]="x" dir-that-sets-style>`
*/
HasCollisions = 0b0000100,
HasCollisions = 0b00001000,
/**
* Whether or not the context contains initial styling values.
@ -395,7 +413,7 @@ export const enum TStylingConfig {
* 3. `@Directive({ host: { 'style': 'width:200px' } })`
* 4. `@Directive({ host: { 'class': 'one two three' } })`
*/
HasInitialStyling = 0b00001000,
HasInitialStyling = 0b000010000,
/**
* Whether or not the context contains one or more template bindings.
@ -406,7 +424,7 @@ export const enum TStylingConfig {
* 3. `<div [class]="x">`
* 4. `<div [class.name]="x">`
*/
HasTemplateBindings = 0b00010000,
HasTemplateBindings = 0b000100000,
/**
* Whether or not the context contains one or more host bindings.
@ -417,7 +435,7 @@ export const enum TStylingConfig {
* 3. `@HostBinding('class') x`
* 4. `@HostBinding('class.name') x`
*/
HasHostBindings = 0b00100000,
HasHostBindings = 0b001000000,
/**
* Whether or not the template bindings are allowed to be registered in the context.
@ -428,7 +446,7 @@ export const enum TStylingConfig {
*
* Note that this is only set once.
*/
TemplateBindingsLocked = 0b01000000,
TemplateBindingsLocked = 0b010000000,
/**
* Whether or not the host bindings are allowed to be registered in the context.
@ -439,13 +457,13 @@ export const enum TStylingConfig {
*
* Note that this is only set once.
*/
HostBindingsLocked = 0b10000000,
HostBindingsLocked = 0b100000000,
/** A Mask of all the configurations */
Mask = 0b11111111,
Mask = 0b111111111,
/** Total amount of configuration bits used */
TotalBits = 8,
TotalBits = 9,
}
/**

View File

@ -10,7 +10,7 @@ import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanit
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, getBindingValue, getConfig, getDefaultValue, getGuardMask, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {getStylingState, resetStylingState} from './state';
@ -143,7 +143,6 @@ function updateBindingData(
patchConfig(
context,
hostBindingsMode ? TStylingConfig.HasHostBindings : TStylingConfig.HasTemplateBindings);
patchConfig(context, prop ? TStylingConfig.HasPropBindings : TStylingConfig.HasMapBindings);
}
const changed = forceUpdate || hasValueChanged(data[bindingIndex], value);
@ -629,22 +628,58 @@ export function applyStylingViaContext(
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* See `allowDirectStylingApply`.
* This function has three different cases that can occur (for each item in the map):
*
* - Case 1: Attempt to apply the current value in the map to the element (if it's `non null`).
*
* - Case 2: If a map value fails to be applied then the algorithm will find a matching entry in
* the initial values present in the context and attempt to apply that.
*
* - Default Case: If the initial value cannot be applied then a default value of `null` will be
* applied (which will remove the style/class value from the element).
*
* See `allowDirectStylingApply` to learn the logic used to determine whether any style/class
* bindings can be directly applied.
*
* @returns whether or not the styling map was applied to the element.
*/
export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, map: StylingMapArray, applyFn: ApplyStylingFn,
bindingIndex: number, map: StylingMapArray, isClassBased: boolean, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) {
setValue(data, bindingIndex, map);
const initialStyles =
hasConfig(context, TStylingConfig.HasInitialStyling) ? getStylingMapArray(context) : null;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i);
applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
// case 1: apply the map value (if it exists)
let applied =
applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
// case 2: apply the initial value (if it exists)
if (!applied && initialStyles) {
applied = findAndApplyMapValue(
renderer, element, applyFn, initialStyles, prop, bindingIndex, sanitizer);
}
// default case: apply `null` to remove the value
if (!applied) {
applyFn(renderer, element, prop, null, bindingIndex);
}
}
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
if (isClassBased) {
state.lastDirectClassMap = map;
} else {
state.lastDirectStyleMap = map;
}
return true;
}
return false;
@ -657,47 +692,95 @@ export function applyStylingMapDirectly(
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* See `allowDirectStylingApply`.
* This function has four different cases that can occur:
*
* - Case 1: Apply the provided prop/value (style or class) entry to the element
* (if it is `non null`).
*
* - Case 2: If value does not get applied (because its `null` or `undefined`) then the algorithm
* will check to see if a styling map value was applied to the element as well just
* before this (via `styleMap` or `classMap`). If and when a map is present then the
* algorithm will find the matching property in the map and apply its value.
*
* - Case 3: If a map value fails to be applied then the algorithm will check to see if there
* are any initial values present and attempt to apply a matching value based on
* the target prop.
*
* - Default Case: If a matching initial value cannot be applied then a default value
* of `null` will be applied (which will remove the style/class value
* from the element).
*
* See `allowDirectStylingApply` to learn the logic used to determine whether any style/class
* bindings can be directly applied.
*
* @returns whether or not the prop/value styling was applied to the element.
*/
export function applyStylingValueDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, prop: string, value: any, applyFn: ApplyStylingFn,
bindingIndex: number, prop: string, value: any, isClassBased: boolean, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null): boolean {
let applied = false;
if (hasValueChanged(data[bindingIndex], value)) {
setValue(data, bindingIndex, value);
applyStylingValue(renderer, context, element, prop, value, applyFn, bindingIndex, sanitizer);
// case 1: apply the provided value (if it exists)
applied = applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
// case 2: find the matching property in a styling map and apply the detected value
if (!applied && hasConfig(context, TStylingConfig.HasMapBindings)) {
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
const map = isClassBased ? state.lastDirectClassMap : state.lastDirectStyleMap;
applied = map ?
findAndApplyMapValue(renderer, element, applyFn, map, prop, bindingIndex, sanitizer) :
false;
}
// case 3: apply the initial value (if it exists)
if (!applied && hasConfig(context, TStylingConfig.HasInitialStyling)) {
const map = getStylingMapArray(context);
applied =
map ? findAndApplyMapValue(renderer, element, applyFn, map, prop, bindingIndex) : false;
}
// default case: apply `null` to remove the value
if (!applied) {
applyFn(renderer, element, prop, null, bindingIndex);
}
}
return applied;
}
function applyStylingValue(
renderer: any, element: RElement, prop: string, value: any, applyFn: ApplyStylingFn,
bindingIndex: number, sanitizer?: StyleSanitizeFn | null): boolean {
let valueToApply: string|null = unwrapSafeValue(value);
if (isStylingValueDefined(valueToApply)) {
valueToApply =
sanitizer ? sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : valueToApply;
applyFn(renderer, element, prop, valueToApply, bindingIndex);
return true;
}
return false;
}
function applyStylingValue(
renderer: any, context: TStylingContext, element: RElement, prop: string, value: any,
applyFn: ApplyStylingFn, bindingIndex: number, sanitizer?: StyleSanitizeFn | null) {
let valueToApply: string|null = unwrapSafeValue(value);
if (isStylingValueDefined(valueToApply)) {
valueToApply =
sanitizer ? sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : valueToApply;
} else if (hasConfig(context, TStylingConfig.HasInitialStyling)) {
const initialStyles = getStylingMapArray(context);
if (initialStyles) {
valueToApply = findInitialStylingValue(initialStyles, prop);
}
}
applyFn(renderer, element, prop, valueToApply, bindingIndex);
}
function findInitialStylingValue(map: StylingMapArray, prop: string): string|null {
function findAndApplyMapValue(
renderer: any, element: RElement, applyFn: ApplyStylingFn, map: StylingMapArray, prop: string,
bindingIndex: number, sanitizer?: StyleSanitizeFn | null) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const p = getMapProp(map, i);
if (p >= prop) {
return p === prop ? getMapValue(map, i) : null;
if (p === prop) {
let valueToApply = getMapValue(map, i);
valueToApply =
sanitizer ? sanitizer(prop, valueToApply, StyleSanitizeMode.SanitizeOnly) : valueToApply;
applyFn(renderer, element, prop, valueToApply, bindingIndex);
return true;
}
if (p > prop) {
break;
}
}
return null;
return false;
}
function normalizeBitMaskValue(value: number | boolean): number {

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {RElement} from '../interfaces/renderer';
import {StylingMapArray} from '../interfaces/styling';
import {TEMPLATE_DIRECTIVE_INDEX} from '../util/styling_utils';
/**
@ -57,6 +58,26 @@ export interface StylingState {
/** The styles update bit index value that is processed during each style binding */
stylesIndex: number;
/**
* The last class map that was applied (i.e. `[class]="x"`).
*
* Note that this property is only populated when direct class values are applied
* (i.e. context resolution is not used).
*
* See `allowDirectStyling` for more info.
*/
lastDirectClassMap: StylingMapArray|null;
/**
* The last style map that was applied (i.e. `[style]="x"`)
*
* Note that this property is only populated when direct style values are applied
* (i.e. context resolution is not used).
*
* See `allowDirectStyling` for more info.
*/
lastDirectStyleMap: StylingMapArray|null;
}
// these values will get filled in the very first time this is accessed...
@ -68,6 +89,8 @@ const _state: StylingState = {
classesIndex: -1,
stylesBitMask: -1,
stylesIndex: -1,
lastDirectClassMap: null,
lastDirectStyleMap: null,
};
const BIT_MASK_START_VALUE = 0;
@ -99,6 +122,8 @@ export function getStylingState(element: RElement, directiveIndex: number): Styl
_state.classesIndex = INDEX_START_VALUE;
_state.stylesBitMask = BIT_MASK_START_VALUE;
_state.stylesIndex = INDEX_START_VALUE;
_state.lastDirectClassMap = null;
_state.lastDirectStyleMap = null;
} else if (_state.directiveIndex !== directiveIndex) {
_state.directiveIndex = directiveIndex;
_state.sourceIndex++;

View File

@ -41,12 +41,20 @@ export const DEFAULT_GUARD_MASK_VALUE = 0b1;
* tNode for styles and for classes. This function allocates a new instance of a
* `TStylingContext` with the initial values (see `interfaces.ts` for more info).
*/
export function allocTStylingContext(initialStyling?: StylingMapArray | null): TStylingContext {
export function allocTStylingContext(
initialStyling: StylingMapArray | null, hasDirectives: boolean): TStylingContext {
initialStyling = initialStyling || allocStylingMapArray();
let config = TStylingConfig.Initial;
if (hasDirectives) {
config |= TStylingConfig.HasDirectives;
}
if (initialStyling.length > StylingMapArrayIndex.ValuesStartPosition) {
config |= TStylingConfig.HasInitialStyling;
}
return [
TStylingConfig.Initial, // 1) config for the styling context
DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...)
initialStyling, // 3) initial styling values
config, // 1) config for the styling context
DEFAULT_TOTAL_SOURCES, // 2) total amount of styling sources (template, directives, etc...)
initialStyling, // 3) initial styling values
];
}
@ -66,15 +74,34 @@ export function hasConfig(context: TStylingContext, flag: TStylingConfig) {
* Determines whether or not to apply styles/classes directly or via context resolution.
*
* There are three cases that are matched here:
* 1. context is locked for template or host bindings (depending on `hostBindingsMode`)
* 2. There are no collisions (i.e. properties with more than one binding)
* 3. There are only "prop" or "map" bindings present, but not both
* 1. there are no directives present AND ngDevMode is falsy
* 2. context is locked for template or host bindings (depending on `hostBindingsMode`)
* 3. There are no collisions (i.e. properties with more than one binding) across multiple
* sources (i.e. template + directive, directive + directive, directive + component)
*/
export function allowDirectStyling(context: TStylingContext, hostBindingsMode: boolean): boolean {
let allow = false;
const config = getConfig(context);
return ((config & getLockedConfig(hostBindingsMode)) !== 0) &&
((config & TStylingConfig.HasCollisions) === 0) &&
((config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings);
const contextIsLocked = (config & getLockedConfig(hostBindingsMode)) !== 0;
const hasNoDirectives = (config & TStylingConfig.HasDirectives) === 0;
// if no directives are present then we do not need populate a context at all. This
// is because duplicate prop bindings cannot be registered through the template. If
// and when this happens we can safely apply the value directly without context
// resolution...
if (hasNoDirectives) {
// `ngDevMode` is required to be checked here because tests/debugging rely on the context being
// populated. If things are in production mode then there is no need to build a context
// therefore the direct apply can be allowed (even on the first update).
allow = ngDevMode ? contextIsLocked : true;
} else if (contextIsLocked) {
const hasNoCollisions = (config & TStylingConfig.HasCollisions) === 0;
const hasOnlyMapsOrOnlyProps =
(config & TStylingConfig.HasPropAndMapBindings) !== TStylingConfig.HasPropAndMapBindings;
allow = hasNoCollisions && hasOnlyMapsOrOnlyProps;
}
return allow;
}
export function setConfig(context: TStylingContext, value: TStylingConfig): void {

View File

@ -575,6 +575,9 @@
{
"name": "extractPipeDef"
},
{
"name": "findAndApplyMapValue"
},
{
"name": "findAttrIndexInNode"
},
@ -587,9 +590,6 @@
{
"name": "findExistingListener"
},
{
"name": "findInitialStylingValue"
},
{
"name": "findViaComponent"
},

View File

@ -110,7 +110,7 @@ describe('styling context', () => {
});
function makeContextWithDebug() {
const ctx = allocTStylingContext();
const ctx = allocTStylingContext(null, false);
return attachStylingDebugObject(ctx);
}

View File

@ -64,6 +64,6 @@ describe('styling debugging tools', () => {
});
function makeContextWithDebug() {
const ctx = allocTStylingContext();
const ctx = allocTStylingContext(null, false);
return attachStylingDebugObject(ctx);
}