perf(ivy): add performance counters in ngDevMode (#23385)
PR Close #23385
This commit is contained in:
parent
fb41b7dc30
commit
b76f5a6a7d
|
@ -566,6 +566,7 @@ export function elementStart(
|
|||
assertEqual(
|
||||
currentView.bindingStartIndex, -1, 'elements should be created before any bindings');
|
||||
|
||||
ngDevMode && ngDevMode.rendererCreateElement++;
|
||||
const native: RElement = renderer.createElement(name);
|
||||
const node: LElementNode = createLNode(index, LNodeType.Element, native !, null);
|
||||
|
||||
|
@ -580,6 +581,7 @@ function createDirectivesAndLocals(
|
|||
localRefs: string[] | null | undefined, containerData: TView[] | null) {
|
||||
const node = previousOrParentNode;
|
||||
if (firstTemplatePass) {
|
||||
ngDevMode && ngDevMode.firstTemplatePass++;
|
||||
ngDevMode && assertDataInRange(index - 1);
|
||||
node.tNode = tData[index] = createTNode(name, attrs || null, containerData);
|
||||
cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null);
|
||||
|
@ -756,6 +758,7 @@ function getOrCreateTView(
|
|||
/** Creates a TView instance */
|
||||
export function createTView(
|
||||
defs: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null): TView {
|
||||
ngDevMode && ngDevMode.tView++;
|
||||
return {
|
||||
data: [],
|
||||
directives: null,
|
||||
|
@ -784,6 +787,7 @@ function setUpAttributes(native: RElement, attrs: string[]): void {
|
|||
const attrName = attrs[i];
|
||||
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
|
||||
const attrVal = attrs[i + 1];
|
||||
ngDevMode && ngDevMode.rendererSetAttribute++;
|
||||
isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) :
|
||||
native.setAttribute(attrName, attrVal);
|
||||
}
|
||||
|
@ -867,6 +871,7 @@ export function listener(
|
|||
// In order to match current behavior, native DOM event listeners must be added for all
|
||||
// events (including outputs).
|
||||
const cleanupFns = cleanup || (cleanup = currentView.cleanup = []);
|
||||
ngDevMode && ngDevMode.rendererAddEventListener++;
|
||||
if (isProceduralRenderer(renderer)) {
|
||||
const wrappedListener = wrapListenerWithDirtyLogic(currentView, listenerFn);
|
||||
const cleanupFn = renderer.listen(native, eventName, wrappedListener);
|
||||
|
@ -931,9 +936,11 @@ export function elementAttribute(
|
|||
if (value !== NO_CHANGE) {
|
||||
const element: LElementNode = data[index];
|
||||
if (value == null) {
|
||||
ngDevMode && ngDevMode.rendererRemoveAttribute++;
|
||||
isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) :
|
||||
element.native.removeAttribute(name);
|
||||
} else {
|
||||
ngDevMode && ngDevMode.rendererSetAttribute++;
|
||||
const strValue = sanitizer == null ? stringify(value) : sanitizer(value);
|
||||
isProceduralRenderer(renderer) ? renderer.setAttribute(element.native, name, strValue) :
|
||||
element.native.setAttribute(name, strValue);
|
||||
|
@ -977,6 +984,7 @@ export function elementProperty<T>(
|
|||
// is risky, so sanitization can be done without further checks.
|
||||
value = sanitizer != null ? (sanitizer(value) as any) : value;
|
||||
const native = node.native;
|
||||
ngDevMode && ngDevMode.rendererSetProperty++;
|
||||
isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) :
|
||||
(native.setProperty ? native.setProperty(propName, value) :
|
||||
(native as any)[propName] = value);
|
||||
|
@ -994,6 +1002,7 @@ export function elementProperty<T>(
|
|||
*/
|
||||
function createTNode(
|
||||
tagName: string | null, attrs: string[] | null, data: TContainer | null): TNode {
|
||||
ngDevMode && ngDevMode.tNode++;
|
||||
return {
|
||||
flags: 0,
|
||||
tagName: tagName,
|
||||
|
@ -1067,10 +1076,12 @@ export function elementClassNamed<T>(index: number, className: string, value: T
|
|||
if (value !== NO_CHANGE) {
|
||||
const lElement = data[index] as LElementNode;
|
||||
if (value) {
|
||||
ngDevMode && ngDevMode.rendererAddClass++;
|
||||
isProceduralRenderer(renderer) ? renderer.addClass(lElement.native, className) :
|
||||
lElement.native.classList.add(className);
|
||||
|
||||
} else {
|
||||
ngDevMode && ngDevMode.rendererRemoveClass++;
|
||||
isProceduralRenderer(renderer) ? renderer.removeClass(lElement.native, className) :
|
||||
lElement.native.classList.remove(className);
|
||||
}
|
||||
|
@ -1095,6 +1106,7 @@ export function elementClass<T>(index: number, value: T | NO_CHANGE): void {
|
|||
// future
|
||||
// we will add logic here which would work with the animation code.
|
||||
const lElement: LElementNode = data[index];
|
||||
ngDevMode && ngDevMode.rendererSetClassName++;
|
||||
isProceduralRenderer(renderer) ? renderer.setProperty(lElement.native, 'className', value) :
|
||||
lElement.native['className'] = stringify(value);
|
||||
}
|
||||
|
@ -1121,6 +1133,7 @@ export function elementStyleNamed<T>(
|
|||
if (value !== NO_CHANGE) {
|
||||
const lElement: LElementNode = data[index];
|
||||
if (value == null) {
|
||||
ngDevMode && ngDevMode.rendererRemoveStyle++;
|
||||
isProceduralRenderer(renderer) ?
|
||||
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
|
||||
lElement.native['style'].removeProperty(styleName);
|
||||
|
@ -1128,6 +1141,7 @@ export function elementStyleNamed<T>(
|
|||
let strValue =
|
||||
typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value);
|
||||
if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer;
|
||||
ngDevMode && ngDevMode.rendererSetStyle++;
|
||||
isProceduralRenderer(renderer) ?
|
||||
renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) :
|
||||
lElement.native['style'].setProperty(styleName, strValue);
|
||||
|
@ -1155,14 +1169,20 @@ export function elementStyle<T>(
|
|||
// we will add logic here which would work with the animation code.
|
||||
const lElement = data[index] as LElementNode;
|
||||
if (isProceduralRenderer(renderer)) {
|
||||
ngDevMode && ngDevMode.rendererSetStyle++;
|
||||
renderer.setProperty(lElement.native, 'style', value);
|
||||
} else {
|
||||
const style = lElement.native['style'];
|
||||
for (let i = 0, keys = Object.keys(value); i < keys.length; i++) {
|
||||
const styleName: string = keys[i];
|
||||
const styleValue: any = (value as any)[styleName];
|
||||
styleValue == null ? style.removeProperty(styleName) :
|
||||
style.setProperty(styleName, styleValue);
|
||||
if (styleValue == null) {
|
||||
ngDevMode && ngDevMode.rendererRemoveStyle++;
|
||||
style.removeProperty(styleName);
|
||||
} else {
|
||||
ngDevMode && ngDevMode.rendererSetStyle++;
|
||||
style.setProperty(styleName, styleValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1184,6 +1204,7 @@ export function text(index: number, value?: any): void {
|
|||
ngDevMode &&
|
||||
assertEqual(
|
||||
currentView.bindingStartIndex, -1, 'text nodes should be created before bindings');
|
||||
ngDevMode && ngDevMode.rendererCreateTextNode++;
|
||||
const textNode = createTextNode(value, renderer);
|
||||
const node = createLNode(index, LNodeType.Element, textNode);
|
||||
// Text nodes are self closing.
|
||||
|
@ -1203,9 +1224,18 @@ export function textBinding<T>(index: number, value: T | NO_CHANGE): void {
|
|||
let existingNode = data[index] as LTextNode;
|
||||
ngDevMode && assertNotNull(existingNode, 'LNode should exist');
|
||||
ngDevMode && assertNotNull(existingNode.native, 'native element should exist');
|
||||
value !== NO_CHANGE &&
|
||||
(isProceduralRenderer(renderer) ? renderer.setValue(existingNode.native, stringify(value)) :
|
||||
existingNode.native.textContent = stringify(value));
|
||||
if (existingNode.native) {
|
||||
// If DOM node exists and value changed, update textContent
|
||||
ngDevMode && ngDevMode.rendererSetText++;
|
||||
value !== NO_CHANGE &&
|
||||
(isProceduralRenderer(renderer) ? renderer.setValue(existingNode.native, stringify(value)) :
|
||||
existingNode.native.textContent = stringify(value));
|
||||
} else {
|
||||
// Node was created but DOM node creation was delayed. Create and append now.
|
||||
ngDevMode && ngDevMode.rendererCreateTextNode++;
|
||||
existingNode.native = createTextNode(value, renderer);
|
||||
insertChild(existingNode, currentView);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
|
@ -1407,7 +1437,7 @@ export function createLContainer(
|
|||
* @param localRefs A set of local reference bindings on the element.
|
||||
*/
|
||||
export function container(
|
||||
index: number, template?: ComponentTemplate<any>, tagName?: string, attrs?: string[],
|
||||
index: number, template?: ComponentTemplate<any>, tagName?: string | null, attrs?: string[],
|
||||
localRefs?: string[] | null): void {
|
||||
ngDevMode && assertEqual(
|
||||
currentView.bindingStartIndex, -1,
|
||||
|
|
|
@ -8,15 +8,51 @@
|
|||
|
||||
|
||||
declare global {
|
||||
const ngDevMode: boolean;
|
||||
const ngDevMode: null|NgDevModePerfCounters;
|
||||
interface NgDevModePerfCounters {
|
||||
firstTemplatePass: number;
|
||||
tNode: number;
|
||||
tView: number;
|
||||
rendererCreateTextNode: number;
|
||||
rendererSetText: number;
|
||||
rendererCreateElement: number;
|
||||
rendererAddEventListener: number;
|
||||
rendererSetAttribute: number;
|
||||
rendererRemoveAttribute: number;
|
||||
rendererSetProperty: number;
|
||||
rendererSetClassName: number;
|
||||
rendererAddClass: number;
|
||||
rendererRemoveClass: number;
|
||||
rendererSetStyle: number;
|
||||
rendererRemoveStyle: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
declare let global: any;
|
||||
|
||||
if (typeof ngDevMode == 'undefined') {
|
||||
if (typeof window != 'undefined') (window as any).ngDevMode = true;
|
||||
if (typeof self != 'undefined') (self as any).ngDevMode = true;
|
||||
if (typeof global != 'undefined') (global as any).ngDevMode = true;
|
||||
}
|
||||
|
||||
export const _ngDevMode = true;
|
||||
export const ngDevModeResetPerfCounters: () => void =
|
||||
(typeof ngDevMode == 'undefined' && (function(global: {ngDevMode: NgDevModePerfCounters}) {
|
||||
function ngDevModeResetPerfCounters() {
|
||||
global['ngDevMode'] = {
|
||||
firstTemplatePass: 0,
|
||||
tNode: 0,
|
||||
tView: 0,
|
||||
rendererCreateTextNode: 0,
|
||||
rendererSetText: 0,
|
||||
rendererCreateElement: 0,
|
||||
rendererAddEventListener: 0,
|
||||
rendererSetAttribute: 0,
|
||||
rendererRemoveAttribute: 0,
|
||||
rendererSetProperty: 0,
|
||||
rendererSetClassName: 0,
|
||||
rendererAddClass: 0,
|
||||
rendererRemoveClass: 0,
|
||||
rendererSetStyle: 0,
|
||||
rendererRemoveStyle: 0,
|
||||
};
|
||||
}
|
||||
ngDevModeResetPerfCounters();
|
||||
return ngDevModeResetPerfCounters;
|
||||
})(typeof window != 'undefined' && window || typeof self != 'undefined' && self ||
|
||||
typeof global != 'undefined' && global)) as() => void;
|
||||
|
|
|
@ -6,12 +6,17 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, renderTemplate} from '../../src/render3/instructions';
|
||||
import {NgForOfContext} from '@angular/common';
|
||||
|
||||
import {RenderFlags, directiveInject} from '../../src/render3';
|
||||
import {defineComponent} from '../../src/render3/definition';
|
||||
import {bind, container, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions';
|
||||
import {LElementNode, LNode} from '../../src/render3/interfaces/node';
|
||||
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||
import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
|
||||
|
||||
import {TemplateFixture} from './render_util';
|
||||
import {NgForOf} from './common_with_def';
|
||||
import {ComponentFixture, TemplateFixture} from './render_util';
|
||||
|
||||
describe('instructions', () => {
|
||||
function createDiv() {
|
||||
|
@ -30,6 +35,13 @@ describe('instructions', () => {
|
|||
() => elementAttribute(
|
||||
0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl));
|
||||
expect(t.html).toEqual('<div title="javascript:true"></div>');
|
||||
expect(ngDevMode).toHaveProperties({
|
||||
firstTemplatePass: 1,
|
||||
tNode: 1,
|
||||
tView: 1,
|
||||
rendererCreateElement: 1,
|
||||
rendererSetAttribute: 2
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -44,6 +56,12 @@ describe('instructions', () => {
|
|||
() => elementProperty(
|
||||
0, 'title', bypassSanitizationTrustUrl('javascript:false'), sanitizeUrl));
|
||||
expect(t.html).toEqual('<div title="javascript:false"></div>');
|
||||
expect(ngDevMode).toHaveProperties({
|
||||
firstTemplatePass: 1,
|
||||
tNode: 1,
|
||||
tView: 1,
|
||||
rendererCreateElement: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not stringify non string values', () => {
|
||||
|
@ -52,6 +70,13 @@ describe('instructions', () => {
|
|||
t.update(() => elementProperty(0, 'hidden', false));
|
||||
// The hidden property would be true if `false` was stringified into `"false"`.
|
||||
expect((t.hostNode.native as HTMLElement).querySelector('div') !.hidden).toEqual(false);
|
||||
expect(ngDevMode).toHaveProperties({
|
||||
firstTemplatePass: 1,
|
||||
tNode: 1,
|
||||
tView: 1,
|
||||
rendererCreateElement: 1,
|
||||
rendererSetProperty: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -93,4 +118,63 @@ describe('instructions', () => {
|
|||
expect(fixture.html).toEqual('<div class="multiple classes"></div>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('performance counters', () => {
|
||||
it('should create tViews only once for each nested level', () => {
|
||||
const _c0 = ['ngFor', '', 'ngForOf', ''];
|
||||
/**
|
||||
* <ul *ngFor="let row of rows">
|
||||
* <li *ngFor="let col of row.cols">{{col}}</li>
|
||||
* </ul>
|
||||
*/
|
||||
class NestedLoops {
|
||||
rows = [['a', 'b'], ['A', 'B'], ['a', 'b'], ['A', 'B']];
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: NestedLoops,
|
||||
selectors: [['todo-app']],
|
||||
factory: function ToDoAppComponent_Factory() { return new NestedLoops(); },
|
||||
template: function ToDoAppComponent_Template(rf: RenderFlags, ctx: NestedLoops) {
|
||||
if (rf & 1) {
|
||||
container(0, ToDoAppComponent_NgForOf_Template_0, null, _c0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
elementProperty(0, 'ngForOf', bind(ctx.rows));
|
||||
}
|
||||
function ToDoAppComponent_NgForOf_Template_0(
|
||||
rf: RenderFlags, ctx0: NgForOfContext<any>) {
|
||||
if (rf & 1) {
|
||||
elementStart(0, 'ul');
|
||||
container(1, ToDoAppComponent_NgForOf_NgForOf_Template_1, null, _c0);
|
||||
elementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
const row_r2 = ctx0.$implicit;
|
||||
elementProperty(1, 'ngForOf', bind(row_r2));
|
||||
}
|
||||
function ToDoAppComponent_NgForOf_NgForOf_Template_1(
|
||||
rf: RenderFlags, ctx1: NgForOfContext<any>) {
|
||||
if (rf & 1) {
|
||||
elementStart(0, 'li');
|
||||
text(1);
|
||||
elementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
const col_r3 = ctx1.$implicit;
|
||||
textBinding(1, interpolation1('', col_r3, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
directives: [NgForOf]
|
||||
});
|
||||
}
|
||||
const fixture = new ComponentFixture(NestedLoops);
|
||||
expect(ngDevMode).toHaveProperties({
|
||||
// Expect: host view + component + *ngForRow + *ngForCol
|
||||
tView: 7, // should be: 4,
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,12 @@ describe('render3 integration test', () => {
|
|||
elementEnd();
|
||||
}
|
||||
}
|
||||
expect(ngDevMode).toHaveProperties({
|
||||
firstTemplatePass: 1,
|
||||
tNode: 1,
|
||||
tView: 1,
|
||||
rendererCreateElement: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render and update basic "Hello, World" template', () => {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ngDevModeResetPerfCounters} from '../../src/render3/ng_dev_mode';
|
||||
|
||||
beforeEach(ngDevModeResetPerfCounters);
|
||||
beforeEach(() => {
|
||||
jasmine.addMatchers({
|
||||
toHaveProperties: function(util, customEqualityTesters) {
|
||||
return {compare: toHavePropertiesCompare};
|
||||
}
|
||||
});
|
||||
});
|
||||
function toHavePropertiesCompare(actual: any, expected: any) {
|
||||
let pass = true;
|
||||
let errors = [];
|
||||
for (let key of Object.keys(actual)) {
|
||||
if (expected.hasOwnProperty(key)) {
|
||||
if (actual[key] !== expected[key]) {
|
||||
pass = false;
|
||||
errors.push(`Expected '${key}' to be '${expected[key]}' but was '${actual[key]}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {pass: pass, message: errors.join('\n')};
|
||||
}
|
||||
|
||||
describe('toHaveProperties', () => {
|
||||
it('should pass', () => {
|
||||
expect({tNode: 1}).toHaveProperties({});
|
||||
expect({tNode: 2}).toHaveProperties({tNode: 2});
|
||||
});
|
||||
|
||||
it('should fail', () => {
|
||||
expect(toHavePropertiesCompare({tNode: 2, tView: 4}, {tNode: 3, tView: 5})).toEqual({
|
||||
pass: false,
|
||||
message:
|
||||
'Expected \'tNode\' to be \'3\' but was \'2\'.\nExpected \'tView\' to be \'5\' but was \'4\'.'
|
||||
});
|
||||
});
|
||||
});
|
|
@ -19,3 +19,9 @@
|
|||
|
||||
declare let isNode: boolean;
|
||||
declare let isBrowser: boolean;
|
||||
|
||||
declare namespace jasmine {
|
||||
interface Matchers {
|
||||
toHaveProperties(obj: any): boolean;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue