perf(ivy): add performance counters in ngDevMode (#23385)

PR Close #23385
This commit is contained in:
Misko Hevery 2018-04-14 11:52:53 -07:00 committed by Igor Minar
parent fb41b7dc30
commit b76f5a6a7d
6 changed files with 226 additions and 18 deletions

View File

@ -566,6 +566,7 @@ export function elementStart(
assertEqual( assertEqual(
currentView.bindingStartIndex, -1, 'elements should be created before any bindings'); currentView.bindingStartIndex, -1, 'elements should be created before any bindings');
ngDevMode && ngDevMode.rendererCreateElement++;
const native: RElement = renderer.createElement(name); const native: RElement = renderer.createElement(name);
const node: LElementNode = createLNode(index, LNodeType.Element, native !, null); const node: LElementNode = createLNode(index, LNodeType.Element, native !, null);
@ -580,6 +581,7 @@ function createDirectivesAndLocals(
localRefs: string[] | null | undefined, containerData: TView[] | null) { localRefs: string[] | null | undefined, containerData: TView[] | null) {
const node = previousOrParentNode; const node = previousOrParentNode;
if (firstTemplatePass) { if (firstTemplatePass) {
ngDevMode && ngDevMode.firstTemplatePass++;
ngDevMode && assertDataInRange(index - 1); ngDevMode && assertDataInRange(index - 1);
node.tNode = tData[index] = createTNode(name, attrs || null, containerData); node.tNode = tData[index] = createTNode(name, attrs || null, containerData);
cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null); cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null);
@ -756,6 +758,7 @@ function getOrCreateTView(
/** Creates a TView instance */ /** Creates a TView instance */
export function createTView( export function createTView(
defs: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null): TView { defs: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null): TView {
ngDevMode && ngDevMode.tView++;
return { return {
data: [], data: [],
directives: null, directives: null,
@ -784,6 +787,7 @@ function setUpAttributes(native: RElement, attrs: string[]): void {
const attrName = attrs[i]; const attrName = attrs[i];
if (attrName !== NG_PROJECT_AS_ATTR_NAME) { if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
const attrVal = attrs[i + 1]; const attrVal = attrs[i + 1];
ngDevMode && ngDevMode.rendererSetAttribute++;
isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) : isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) :
native.setAttribute(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 // In order to match current behavior, native DOM event listeners must be added for all
// events (including outputs). // events (including outputs).
const cleanupFns = cleanup || (cleanup = currentView.cleanup = []); const cleanupFns = cleanup || (cleanup = currentView.cleanup = []);
ngDevMode && ngDevMode.rendererAddEventListener++;
if (isProceduralRenderer(renderer)) { if (isProceduralRenderer(renderer)) {
const wrappedListener = wrapListenerWithDirtyLogic(currentView, listenerFn); const wrappedListener = wrapListenerWithDirtyLogic(currentView, listenerFn);
const cleanupFn = renderer.listen(native, eventName, wrappedListener); const cleanupFn = renderer.listen(native, eventName, wrappedListener);
@ -931,9 +936,11 @@ export function elementAttribute(
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const element: LElementNode = data[index]; const element: LElementNode = data[index];
if (value == null) { if (value == null) {
ngDevMode && ngDevMode.rendererRemoveAttribute++;
isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) : isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) :
element.native.removeAttribute(name); element.native.removeAttribute(name);
} else { } else {
ngDevMode && ngDevMode.rendererSetAttribute++;
const strValue = sanitizer == null ? stringify(value) : sanitizer(value); const strValue = sanitizer == null ? stringify(value) : sanitizer(value);
isProceduralRenderer(renderer) ? renderer.setAttribute(element.native, name, strValue) : isProceduralRenderer(renderer) ? renderer.setAttribute(element.native, name, strValue) :
element.native.setAttribute(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. // is risky, so sanitization can be done without further checks.
value = sanitizer != null ? (sanitizer(value) as any) : value; value = sanitizer != null ? (sanitizer(value) as any) : value;
const native = node.native; const native = node.native;
ngDevMode && ngDevMode.rendererSetProperty++;
isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) : isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) :
(native.setProperty ? native.setProperty(propName, value) : (native.setProperty ? native.setProperty(propName, value) :
(native as any)[propName] = value); (native as any)[propName] = value);
@ -994,6 +1002,7 @@ export function elementProperty<T>(
*/ */
function createTNode( function createTNode(
tagName: string | null, attrs: string[] | null, data: TContainer | null): TNode { tagName: string | null, attrs: string[] | null, data: TContainer | null): TNode {
ngDevMode && ngDevMode.tNode++;
return { return {
flags: 0, flags: 0,
tagName: tagName, tagName: tagName,
@ -1067,10 +1076,12 @@ export function elementClassNamed<T>(index: number, className: string, value: T
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const lElement = data[index] as LElementNode; const lElement = data[index] as LElementNode;
if (value) { if (value) {
ngDevMode && ngDevMode.rendererAddClass++;
isProceduralRenderer(renderer) ? renderer.addClass(lElement.native, className) : isProceduralRenderer(renderer) ? renderer.addClass(lElement.native, className) :
lElement.native.classList.add(className); lElement.native.classList.add(className);
} else { } else {
ngDevMode && ngDevMode.rendererRemoveClass++;
isProceduralRenderer(renderer) ? renderer.removeClass(lElement.native, className) : isProceduralRenderer(renderer) ? renderer.removeClass(lElement.native, className) :
lElement.native.classList.remove(className); lElement.native.classList.remove(className);
} }
@ -1095,6 +1106,7 @@ export function elementClass<T>(index: number, value: T | NO_CHANGE): void {
// future // future
// we will add logic here which would work with the animation code. // we will add logic here which would work with the animation code.
const lElement: LElementNode = data[index]; const lElement: LElementNode = data[index];
ngDevMode && ngDevMode.rendererSetClassName++;
isProceduralRenderer(renderer) ? renderer.setProperty(lElement.native, 'className', value) : isProceduralRenderer(renderer) ? renderer.setProperty(lElement.native, 'className', value) :
lElement.native['className'] = stringify(value); lElement.native['className'] = stringify(value);
} }
@ -1121,6 +1133,7 @@ export function elementStyleNamed<T>(
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const lElement: LElementNode = data[index]; const lElement: LElementNode = data[index];
if (value == null) { if (value == null) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) : renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
lElement.native['style'].removeProperty(styleName); lElement.native['style'].removeProperty(styleName);
@ -1128,6 +1141,7 @@ export function elementStyleNamed<T>(
let strValue = let strValue =
typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value); typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value);
if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer; if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer;
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) : renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) :
lElement.native['style'].setProperty(styleName, strValue); 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. // we will add logic here which would work with the animation code.
const lElement = data[index] as LElementNode; const lElement = data[index] as LElementNode;
if (isProceduralRenderer(renderer)) { if (isProceduralRenderer(renderer)) {
ngDevMode && ngDevMode.rendererSetStyle++;
renderer.setProperty(lElement.native, 'style', value); renderer.setProperty(lElement.native, 'style', value);
} else { } else {
const style = lElement.native['style']; const style = lElement.native['style'];
for (let i = 0, keys = Object.keys(value); i < keys.length; i++) { for (let i = 0, keys = Object.keys(value); i < keys.length; i++) {
const styleName: string = keys[i]; const styleName: string = keys[i];
const styleValue: any = (value as any)[styleName]; const styleValue: any = (value as any)[styleName];
styleValue == null ? style.removeProperty(styleName) : if (styleValue == null) {
style.setProperty(styleName, styleValue); 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 && ngDevMode &&
assertEqual( assertEqual(
currentView.bindingStartIndex, -1, 'text nodes should be created before bindings'); currentView.bindingStartIndex, -1, 'text nodes should be created before bindings');
ngDevMode && ngDevMode.rendererCreateTextNode++;
const textNode = createTextNode(value, renderer); const textNode = createTextNode(value, renderer);
const node = createLNode(index, LNodeType.Element, textNode); const node = createLNode(index, LNodeType.Element, textNode);
// Text nodes are self closing. // 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; let existingNode = data[index] as LTextNode;
ngDevMode && assertNotNull(existingNode, 'LNode should exist'); ngDevMode && assertNotNull(existingNode, 'LNode should exist');
ngDevMode && assertNotNull(existingNode.native, 'native element should exist'); ngDevMode && assertNotNull(existingNode.native, 'native element should exist');
value !== NO_CHANGE && if (existingNode.native) {
(isProceduralRenderer(renderer) ? renderer.setValue(existingNode.native, stringify(value)) : // If DOM node exists and value changed, update textContent
existingNode.native.textContent = stringify(value)); 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. * @param localRefs A set of local reference bindings on the element.
*/ */
export function container( 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 { localRefs?: string[] | null): void {
ngDevMode && assertEqual( ngDevMode && assertEqual(
currentView.bindingStartIndex, -1, currentView.bindingStartIndex, -1,

View File

@ -8,15 +8,51 @@
declare global { 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; declare let global: any;
export const ngDevModeResetPerfCounters: () => void =
if (typeof ngDevMode == 'undefined') { (typeof ngDevMode == 'undefined' && (function(global: {ngDevMode: NgDevModePerfCounters}) {
if (typeof window != 'undefined') (window as any).ngDevMode = true; function ngDevModeResetPerfCounters() {
if (typeof self != 'undefined') (self as any).ngDevMode = true; global['ngDevMode'] = {
if (typeof global != 'undefined') (global as any).ngDevMode = true; firstTemplatePass: 0,
} tNode: 0,
tView: 0,
export const _ngDevMode = true; 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;

View File

@ -6,12 +6,17 @@
* found in the LICENSE file at https://angular.io/license * 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 {LElementNode, LNode} from '../../src/render3/interfaces/node';
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; 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', () => { describe('instructions', () => {
function createDiv() { function createDiv() {
@ -30,6 +35,13 @@ describe('instructions', () => {
() => elementAttribute( () => elementAttribute(
0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl)); 0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl));
expect(t.html).toEqual('<div title="javascript:true"></div>'); 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( () => elementProperty(
0, 'title', bypassSanitizationTrustUrl('javascript:false'), sanitizeUrl)); 0, 'title', bypassSanitizationTrustUrl('javascript:false'), sanitizeUrl));
expect(t.html).toEqual('<div title="javascript:false"></div>'); 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', () => { it('should not stringify non string values', () => {
@ -52,6 +70,13 @@ describe('instructions', () => {
t.update(() => elementProperty(0, 'hidden', false)); t.update(() => elementProperty(0, 'hidden', false));
// The hidden property would be true if `false` was stringified into `"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((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>'); 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,
});
});
});
}); });

View File

@ -28,6 +28,12 @@ describe('render3 integration test', () => {
elementEnd(); elementEnd();
} }
} }
expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1,
tNode: 1,
tView: 1,
rendererCreateElement: 1,
});
}); });
it('should render and update basic "Hello, World" template', () => { it('should render and update basic "Hello, World" template', () => {

View File

@ -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\'.'
});
});
});

6
packages/types.d.ts vendored
View File

@ -19,3 +19,9 @@
declare let isNode: boolean; declare let isNode: boolean;
declare let isBrowser: boolean; declare let isBrowser: boolean;
declare namespace jasmine {
interface Matchers {
toHaveProperties(obj: any): boolean;
}
}