perf(core): add private hooks around user code executed by the runtime (#41255)
Introduces an **internal**, **experimental** `profiler` function, which the runtime invokes around user code, including before and after: - Running the template function of a component - Executing a lifecycle hook - Evaluating an output handler The `profiler` function invokes a callback set with the global `ng.ɵsetProfiler`. This API is **private** and **experimental** and could be removed or changed at any time. This implementation is cheap and available in production. It's cheap because the `profiler` function is simple, which allows the JiT compiler to inline it in the callsites. It also doesn't add up much to the production bundle. To listen for profiler events: ```ts ng.ɵsetProfiler((event, ...args) => { // monitor user code execution }); ``` PR Close #41255
This commit is contained in:
parent
a43f36babd
commit
520ff69854
|
@ -39,7 +39,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2285,
|
||||
"main-es2015": 240874,
|
||||
"main-es2015": 240883,
|
||||
"polyfills-es2015": 36975,
|
||||
"5-es2015": 753
|
||||
}
|
||||
|
|
|
@ -279,6 +279,7 @@ export {
|
|||
export {
|
||||
compilePipe as ɵcompilePipe,
|
||||
} from './render3/jit/pipe';
|
||||
export { Profiler as ɵProfiler, ProfilerEvent as ɵProfilerEvent } from './render3/profiler';
|
||||
export {
|
||||
publishDefaultGlobalUtils as ɵpublishDefaultGlobalUtils
|
||||
,
|
||||
|
|
|
@ -13,6 +13,7 @@ import {NgOnChangesFeatureImpl} from './features/ng_onchanges_feature';
|
|||
import {DirectiveDef} from './interfaces/definition';
|
||||
import {TNode} from './interfaces/node';
|
||||
import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view';
|
||||
import {profiler, ProfilerEvent} from './profiler';
|
||||
import {isInCheckNoChangesMode} from './state';
|
||||
|
||||
|
||||
|
@ -256,9 +257,19 @@ function callHook(currentView: LView, initPhase: InitPhaseState, arr: HookData,
|
|||
(currentView[PREORDER_HOOK_FLAGS] >> PreOrderHookFlags.NumberOfInitHooksCalledShift) &&
|
||||
(currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) {
|
||||
currentView[FLAGS] += LViewFlags.IndexWithinInitPhaseIncrementer;
|
||||
profiler(ProfilerEvent.LifecycleHookStart, directive, hook);
|
||||
try {
|
||||
hook.call(directive);
|
||||
} finally {
|
||||
profiler(ProfilerEvent.LifecycleHookEnd, directive, hook);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
profiler(ProfilerEvent.LifecycleHookStart, directive, hook);
|
||||
try {
|
||||
hook.call(directive);
|
||||
} finally {
|
||||
profiler(ProfilerEvent.LifecycleHookEnd, directive, hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@ import {PropertyAliasValue, TNode, TNodeFlags, TNodeType} from '../interfaces/no
|
|||
import {GlobalTargetResolver, isProceduralRenderer, Renderer3} from '../interfaces/renderer';
|
||||
import {RElement} from '../interfaces/renderer_dom';
|
||||
import {isDirectiveHost} from '../interfaces/type_checks';
|
||||
import {CLEANUP, FLAGS, LView, LViewFlags, RENDERER, TView} from '../interfaces/view';
|
||||
import {CLEANUP, CONTEXT, FLAGS, LView, LViewFlags, RENDERER, TView} from '../interfaces/view';
|
||||
import {assertTNodeType} from '../node_assert';
|
||||
import {profiler, ProfilerEvent} from '../profiler';
|
||||
import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state';
|
||||
import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils';
|
||||
|
||||
|
@ -121,6 +122,7 @@ function listenerInternal(
|
|||
const isTNodeDirectiveHost = isDirectiveHost(tNode);
|
||||
const firstCreatePass = tView.firstCreatePass;
|
||||
const tCleanup: false|any[] = firstCreatePass && getOrCreateTViewCleanup(tView);
|
||||
const context = lView[CONTEXT];
|
||||
|
||||
// When the ɵɵlistener instruction was generated and is executed we know that there is either a
|
||||
// native listener or a directive output on this element. As such we we know that we will have to
|
||||
|
@ -177,7 +179,7 @@ function listenerInternal(
|
|||
// The first argument of `listen` function in Procedural Renderer is:
|
||||
// - either a target name (as a string) in case of global target (window, document, body)
|
||||
// - or element reference (in all other cases)
|
||||
listenerFn = wrapListener(tNode, lView, listenerFn, false /** preventDefault */);
|
||||
listenerFn = wrapListener(tNode, lView, context, listenerFn, false /** preventDefault */);
|
||||
const cleanupFn = renderer.listen(resolved.name || target, eventName, listenerFn);
|
||||
ngDevMode && ngDevMode.rendererAddEventListener++;
|
||||
|
||||
|
@ -186,7 +188,7 @@ function listenerInternal(
|
|||
}
|
||||
|
||||
} else {
|
||||
listenerFn = wrapListener(tNode, lView, listenerFn, true /** preventDefault */);
|
||||
listenerFn = wrapListener(tNode, lView, context, listenerFn, true /** preventDefault */);
|
||||
target.addEventListener(eventName, listenerFn, useCapture);
|
||||
ngDevMode && ngDevMode.rendererAddEventListener++;
|
||||
|
||||
|
@ -196,7 +198,7 @@ function listenerInternal(
|
|||
} else {
|
||||
// Even if there is no native listener to add, we still need to wrap the listener so that OnPush
|
||||
// ancestors are marked dirty when an event occurs.
|
||||
listenerFn = wrapListener(tNode, lView, listenerFn, false /** preventDefault */);
|
||||
listenerFn = wrapListener(tNode, lView, context, listenerFn, false /** preventDefault */);
|
||||
}
|
||||
|
||||
// subscribe to directive outputs
|
||||
|
@ -227,13 +229,16 @@ function listenerInternal(
|
|||
}
|
||||
|
||||
function executeListenerWithErrorHandling(
|
||||
lView: LView, listenerFn: (e?: any) => any, e: any): boolean {
|
||||
lView: LView, context: {}|null, listenerFn: (e?: any) => any, e: any): boolean {
|
||||
try {
|
||||
profiler(ProfilerEvent.OutputStart, context, listenerFn);
|
||||
// Only explicitly returning false from a listener should preventDefault
|
||||
return listenerFn(e) !== false;
|
||||
} catch (error) {
|
||||
handleError(lView, error);
|
||||
return false;
|
||||
} finally {
|
||||
profiler(ProfilerEvent.OutputEnd, context, listenerFn);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,7 +253,7 @@ function executeListenerWithErrorHandling(
|
|||
* (the procedural renderer does this already, so in those cases, we should skip)
|
||||
*/
|
||||
function wrapListener(
|
||||
tNode: TNode, lView: LView, listenerFn: (e?: any) => any,
|
||||
tNode: TNode, lView: LView, context: {}|null, listenerFn: (e?: any) => any,
|
||||
wrapWithPreventDefault: boolean): EventListener {
|
||||
// Note: we are performing most of the work in the listener function itself
|
||||
// to optimize listener registration.
|
||||
|
@ -270,13 +275,13 @@ function wrapListener(
|
|||
markViewDirty(startView);
|
||||
}
|
||||
|
||||
let result = executeListenerWithErrorHandling(lView, listenerFn, e);
|
||||
let result = executeListenerWithErrorHandling(lView, context, listenerFn, e);
|
||||
// A just-invoked listener function might have coalesced listeners so we need to check for
|
||||
// their presence and invoke as needed.
|
||||
let nextListenerFn = (<any>wrapListenerIn_markDirtyAndPreventDefault).__ngNextListenerFn__;
|
||||
while (nextListenerFn) {
|
||||
// We should prevent default if any of the listeners explicitly return false
|
||||
result = executeListenerWithErrorHandling(lView, nextListenerFn, e) && result;
|
||||
result = executeListenerWithErrorHandling(lView, context, nextListenerFn, e) && result;
|
||||
nextListenerFn = (<any>nextListenerFn).__ngNextListenerFn__;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DE
|
|||
import {assertPureTNodeType, assertTNodeType} from '../node_assert';
|
||||
import {updateTextNode} from '../node_manipulation';
|
||||
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
|
||||
import {profiler, ProfilerEvent} from '../profiler';
|
||||
import {enterView, getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, leaveView, setBindingIndex, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setIsInCheckNoChangesMode, setSelectedIndex} from '../state';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils';
|
||||
|
@ -501,16 +502,25 @@ export function renderComponentOrTemplate<T>(
|
|||
function executeTemplate<T>(
|
||||
tView: TView, lView: LView, templateFn: ComponentTemplate<T>, rf: RenderFlags, context: T) {
|
||||
const prevSelectedIndex = getSelectedIndex();
|
||||
const isUpdatePhase = rf & RenderFlags.Update;
|
||||
try {
|
||||
setSelectedIndex(-1);
|
||||
if (rf & RenderFlags.Update && lView.length > HEADER_OFFSET) {
|
||||
if (isUpdatePhase && lView.length > HEADER_OFFSET) {
|
||||
// When we're updating, inherently select 0 so we don't
|
||||
// have to generate that instruction for most update blocks.
|
||||
selectIndexInternal(tView, lView, HEADER_OFFSET, isInCheckNoChangesMode());
|
||||
}
|
||||
|
||||
const preHookType =
|
||||
isUpdatePhase ? ProfilerEvent.TemplateUpdateStart : ProfilerEvent.TemplateCreateStart;
|
||||
profiler(preHookType, context);
|
||||
templateFn(rf, context);
|
||||
} finally {
|
||||
setSelectedIndex(prevSelectedIndex);
|
||||
|
||||
const postHookType =
|
||||
isUpdatePhase ? ProfilerEvent.TemplateUpdateEnd : ProfilerEvent.TemplateCreateEnd;
|
||||
profiler(postHookType, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Profiler events is an enum used by the profiler to distinguish between different calls of user
|
||||
* code invoked throughout the application lifecycle.
|
||||
*/
|
||||
export const enum ProfilerEvent {
|
||||
/**
|
||||
* Corresponds to the point in time before the runtime has called the template function of a
|
||||
* component with `RenderFlags.Create`.
|
||||
*/
|
||||
TemplateCreateStart,
|
||||
|
||||
/**
|
||||
* Corresponds to the point in time after the runtime has called the template function of a
|
||||
* component with `RenderFlags.Create`.
|
||||
*/
|
||||
TemplateCreateEnd,
|
||||
|
||||
/**
|
||||
* Corresponds to the point in time before the runtime has called the template function of a
|
||||
* component with `RenderFlags.Update`.
|
||||
*/
|
||||
TemplateUpdateStart,
|
||||
|
||||
/**
|
||||
* Corresponds to the point in time after the runtime has called the template function of a
|
||||
* component with `RenderFlags.Update`.
|
||||
*/
|
||||
TemplateUpdateEnd,
|
||||
|
||||
/**
|
||||
* Corresponds to the point in time before the runtime has called a lifecycle hook of a component
|
||||
* or directive.
|
||||
*/
|
||||
LifecycleHookStart,
|
||||
|
||||
/**
|
||||
* Corresponds to the point in time after the runtime has called a lifecycle hook of a component
|
||||
* or directive.
|
||||
*/
|
||||
LifecycleHookEnd,
|
||||
|
||||
/**
|
||||
* Corresponds to the point in time before the runtime has evaluated an expression associated with
|
||||
* an event or an output.
|
||||
*/
|
||||
OutputStart,
|
||||
|
||||
/**
|
||||
* Corresponds to the point in time after the runtime has evaluated an expression associated with
|
||||
* an event or an output.
|
||||
*/
|
||||
OutputEnd,
|
||||
}
|
||||
|
||||
/**
|
||||
* Profiler function which the runtime will invoke before and after user code.
|
||||
*/
|
||||
export interface Profiler {
|
||||
(event: ProfilerEvent, instance: {}|null, hookOrListener?: (e?: any) => any): void;
|
||||
}
|
||||
|
||||
|
||||
let profilerCallback: Profiler|null = null;
|
||||
|
||||
/**
|
||||
* Sets the callback function which will be invoked before and after performing certain actions at
|
||||
* runtime (for example, before and after running change detection).
|
||||
*
|
||||
* Warning: this function is *INTERNAL* and should not be relied upon in application's code.
|
||||
* The contract of the function might be changed in any release and/or the function can be removed
|
||||
* completely.
|
||||
*
|
||||
* @param profiler function provided by the caller or null value to disable profiling.
|
||||
*/
|
||||
export const setProfiler = (profiler: Profiler|null) => {
|
||||
profilerCallback = profiler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Profiler function which wraps user code executed by the runtime.
|
||||
*
|
||||
* @param event ProfilerEvent corresponding to the execution context
|
||||
* @param instance component instance
|
||||
* @param hookOrListener lifecycle hook function or output listener. The value depends on the
|
||||
* execution context
|
||||
* @returns
|
||||
*/
|
||||
export const profiler: Profiler = function(
|
||||
event: ProfilerEvent, instance: {}|null, hookOrListener?: (e?: any) => any) {
|
||||
if (profilerCallback != null /* both `null` and `undefined` */) {
|
||||
profilerCallback(event, instance, hookOrListener);
|
||||
}
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
import {assertDefined} from '../../util/assert';
|
||||
import {global} from '../../util/global';
|
||||
import {setProfiler} from '../profiler';
|
||||
import {applyChanges} from './change_detection_utils';
|
||||
import {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from './discovery_utils';
|
||||
|
||||
|
@ -40,6 +41,13 @@ let _published = false;
|
|||
export function publishDefaultGlobalUtils() {
|
||||
if (!_published) {
|
||||
_published = true;
|
||||
|
||||
/**
|
||||
* Warning: this function is *INTERNAL* and should not be relied upon in application's code.
|
||||
* The contract of the function might be changed in any release and/or the function can be
|
||||
* removed completely.
|
||||
*/
|
||||
publishGlobalUtil('ɵsetProfiler', setProfiler);
|
||||
publishGlobalUtil('getComponent', getComponent);
|
||||
publishGlobalUtil('getContext', getContext);
|
||||
publishGlobalUtil('getListeners', getListeners);
|
||||
|
|
|
@ -0,0 +1,320 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {ProfilerEvent, setProfiler} from '@angular/core/src/render3/profiler';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {expect} from '@angular/core/testing/src/testing_internal';
|
||||
import {onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, DoCheck, ErrorHandler, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild} from '../../src/core';
|
||||
|
||||
|
||||
onlyInIvy('Ivy-specific functionality').describe('profiler', () => {
|
||||
class Profiler {
|
||||
profile() {}
|
||||
}
|
||||
|
||||
let profilerSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
const profiler = new Profiler();
|
||||
profilerSpy = spyOn(profiler, 'profile').and.callThrough();
|
||||
setProfiler(profiler.profile);
|
||||
});
|
||||
|
||||
afterAll(() => setProfiler(null));
|
||||
|
||||
function findProfilerCall(condition: ProfilerEvent|((args: any[]) => boolean)) {
|
||||
let predicate: (args: any[]) => boolean = _ => true;
|
||||
if (typeof condition !== 'function') {
|
||||
predicate = (args: any[]) => args[0] === condition;
|
||||
} else {
|
||||
predicate = condition;
|
||||
}
|
||||
return profilerSpy.calls.all().map((call: any) => call.args).find(predicate);
|
||||
}
|
||||
|
||||
describe('change detection hooks', () => {
|
||||
it('should call the profiler for creation and change detection', () => {
|
||||
@Component({selector: 'my-comp', template: '<button (click)="onClick()"></button>'})
|
||||
class MyComponent {
|
||||
onClick() {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyComponent]});
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
|
||||
expect(profilerSpy).toHaveBeenCalled();
|
||||
|
||||
const templateCreateStart = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.TemplateCreateStart &&
|
||||
args[1] === fixture.componentInstance);
|
||||
const templateCreateEnd = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.TemplateCreateEnd && args[1] === fixture.componentInstance);
|
||||
|
||||
expect(templateCreateStart).toBeTruthy();
|
||||
expect(templateCreateEnd).toBeTruthy();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const templateUpdateStart = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.TemplateUpdateStart &&
|
||||
args[1] === fixture.componentInstance);
|
||||
const templateUpdateEnd = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.TemplateUpdateEnd && args[1] === fixture.componentInstance);
|
||||
|
||||
expect(templateUpdateStart).toBeTruthy();
|
||||
expect(templateUpdateEnd).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should invoke the profiler when the template throws', () => {
|
||||
@Component({selector: 'my-comp', template: '{{ throw() }}'})
|
||||
class MyComponent {
|
||||
throw() {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyComponent]});
|
||||
|
||||
let myComp: MyComponent;
|
||||
expect(() => {
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
myComp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}).toThrow();
|
||||
|
||||
expect(profilerSpy).toHaveBeenCalled();
|
||||
|
||||
const templateCreateStart = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.TemplateCreateStart && args[1] === myComp);
|
||||
const templateCreateEnd = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.TemplateCreateEnd && args[1] === myComp);
|
||||
|
||||
expect(templateCreateStart).toBeTruthy();
|
||||
expect(templateCreateEnd).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('outputs and events', () => {
|
||||
it('should invoke the profiler on event handler', () => {
|
||||
@Component({selector: 'my-comp', template: '<button (click)="onClick()"></button>'})
|
||||
class MyComponent {
|
||||
onClick() {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyComponent]});
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
const myComp = fixture.componentInstance;
|
||||
|
||||
const clickSpy = spyOn(myComp, 'onClick');
|
||||
const button = fixture.nativeElement.querySelector('button')!;
|
||||
|
||||
button.click();
|
||||
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
|
||||
const outputStart = findProfilerCall(ProfilerEvent.OutputStart);
|
||||
const outputEnd = findProfilerCall(ProfilerEvent.OutputEnd);
|
||||
|
||||
expect(outputStart[1]).toEqual(myComp!);
|
||||
expect(outputEnd[1]).toEqual(myComp!);
|
||||
});
|
||||
|
||||
it('should invoke the profiler on event handler even when it throws', () => {
|
||||
@Component({selector: 'my-comp', template: '<button (click)="onClick()"></button>'})
|
||||
class MyComponent {
|
||||
onClick() {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
const handler = new ErrorHandler();
|
||||
const errorSpy = spyOn(handler, 'handleError');
|
||||
|
||||
TestBed.configureTestingModule(
|
||||
{declarations: [MyComponent], providers: [{provide: ErrorHandler, useValue: handler}]});
|
||||
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
const myComp = fixture.componentInstance;
|
||||
const button = fixture.nativeElement.querySelector('button')!;
|
||||
|
||||
button.click();
|
||||
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
|
||||
const outputStart = findProfilerCall(ProfilerEvent.OutputStart);
|
||||
const outputEnd = findProfilerCall(ProfilerEvent.OutputEnd);
|
||||
|
||||
expect(outputStart[1]).toEqual(myComp!);
|
||||
expect(outputEnd[1]).toEqual(myComp!);
|
||||
});
|
||||
|
||||
it('should invoke the profiler on output handler execution', async () => {
|
||||
@Component({selector: 'child', template: ''})
|
||||
class Child {
|
||||
@Output() childEvent = new EventEmitter();
|
||||
}
|
||||
|
||||
@Component({selector: 'my-comp', template: '<child (childEvent)="onEvent()"></child>'})
|
||||
class MyComponent {
|
||||
@ViewChild(Child) child!: Child;
|
||||
onEvent() {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyComponent, Child]});
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
const myComp = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
myComp.child!.childEvent.emit();
|
||||
|
||||
const outputStart = findProfilerCall(ProfilerEvent.OutputStart);
|
||||
const outputEnd = findProfilerCall(ProfilerEvent.OutputEnd);
|
||||
|
||||
expect(outputStart[1]).toEqual(myComp!);
|
||||
expect(outputEnd[1]).toEqual(myComp!);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle hooks', () => {
|
||||
it('should call the profiler on lifecycle execution', () => {
|
||||
@Component({selector: 'my-comp', template: '{{prop}}'})
|
||||
class MyComponent implements OnInit, AfterViewInit, AfterViewChecked, AfterContentInit,
|
||||
AfterContentChecked, OnChanges, DoCheck {
|
||||
@Input() prop = 1;
|
||||
|
||||
ngOnInit() {}
|
||||
ngDoCheck() {}
|
||||
ngOnChanges() {}
|
||||
ngAfterViewInit() {}
|
||||
ngAfterViewChecked() {}
|
||||
ngAfterContentInit() {}
|
||||
ngAfterContentChecked() {}
|
||||
}
|
||||
|
||||
@Component({selector: 'my-parent', template: '<my-comp [prop]="prop"></my-comp>'})
|
||||
class MyParent {
|
||||
prop = 1;
|
||||
@ViewChild(MyComponent) child!: MyComponent;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyParent, MyComponent]});
|
||||
const fixture = TestBed.createComponent(MyParent);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const myParent = fixture.componentInstance;
|
||||
const myComp = fixture.componentInstance.child;
|
||||
|
||||
const ngOnInitStart = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookStart && args[2] === myComp.ngOnInit);
|
||||
const ngOnInitEnd = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookEnd && args[2] === myComp.ngOnInit);
|
||||
|
||||
expect(ngOnInitStart).toBeTruthy();
|
||||
expect(ngOnInitEnd).toBeTruthy();
|
||||
|
||||
const ngOnDoCheckStart = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookStart && args[2] === myComp.ngDoCheck);
|
||||
const ngOnDoCheckEnd = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookEnd && args[2] === myComp.ngDoCheck);
|
||||
|
||||
expect(ngOnDoCheckStart).toBeTruthy();
|
||||
expect(ngOnDoCheckEnd).toBeTruthy();
|
||||
|
||||
const ngAfterViewInitStart = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookStart && args[2] === myComp.ngAfterViewInit);
|
||||
const ngAfterViewInitEnd = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookEnd && args[2] === myComp.ngAfterViewInit);
|
||||
|
||||
expect(ngAfterViewInitStart).toBeTruthy();
|
||||
expect(ngAfterViewInitEnd).toBeTruthy();
|
||||
|
||||
const ngAfterViewCheckedStart = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.LifecycleHookStart &&
|
||||
args[2] === myComp.ngAfterViewChecked);
|
||||
const ngAfterViewCheckedEnd = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookEnd && args[2] === myComp.ngAfterViewChecked);
|
||||
|
||||
expect(ngAfterViewCheckedStart).toBeTruthy();
|
||||
expect(ngAfterViewCheckedEnd).toBeTruthy();
|
||||
|
||||
const ngAfterContentInitStart = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.LifecycleHookStart &&
|
||||
args[2] === myComp.ngAfterContentInit);
|
||||
const ngAfterContentInitEnd = findProfilerCall(
|
||||
(args: any[]) =>
|
||||
args[0] === ProfilerEvent.LifecycleHookEnd && args[2] === myComp.ngAfterContentInit);
|
||||
|
||||
expect(ngAfterContentInitStart).toBeTruthy();
|
||||
expect(ngAfterContentInitEnd).toBeTruthy();
|
||||
|
||||
const ngAfterContentCheckedStart = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.LifecycleHookStart &&
|
||||
args[2] === myComp.ngAfterContentChecked);
|
||||
const ngAfterContentChecked = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.LifecycleHookEnd &&
|
||||
args[2] === myComp.ngAfterContentChecked);
|
||||
|
||||
expect(ngAfterContentCheckedStart).toBeTruthy();
|
||||
expect(ngAfterContentChecked).toBeTruthy();
|
||||
|
||||
|
||||
// Verify we call `ngOnChanges` and the corresponding profiler hooks
|
||||
const onChangesSpy = spyOn(myComp, 'ngOnChanges');
|
||||
profilerSpy.calls.reset();
|
||||
|
||||
myParent.prop = 2;
|
||||
fixture.detectChanges();
|
||||
|
||||
const ngOnChangesStart = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.LifecycleHookStart && args[2] &&
|
||||
args[2].name && args[2].name.indexOf('OnChangesHook') >= 0);
|
||||
const ngOnChangesEnd = findProfilerCall(
|
||||
(args: any[]) => args[0] === ProfilerEvent.LifecycleHookEnd && args[2] && args[2].name &&
|
||||
args[2].name.indexOf('OnChangesHook') >= 0);
|
||||
|
||||
expect(onChangesSpy).toHaveBeenCalled();
|
||||
expect(ngOnChangesStart).toBeTruthy();
|
||||
expect(ngOnChangesEnd).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the profiler on lifecycle execution even after error', () => {
|
||||
@Component({selector: 'my-comp', template: ''})
|
||||
class MyComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyComponent]});
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
|
||||
expect(() => {
|
||||
fixture.detectChanges();
|
||||
}).toThrow();
|
||||
|
||||
const lifecycleStart = findProfilerCall(ProfilerEvent.LifecycleHookStart);
|
||||
const lifecycleEnd = findProfilerCall(ProfilerEvent.LifecycleHookEnd);
|
||||
|
||||
expect(lifecycleStart).toBeTruthy();
|
||||
expect(lifecycleEnd).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {setProfiler} from '@angular/core/src/render3/profiler';
|
||||
import {applyChanges} from '../../src/render3/util/change_detection_utils';
|
||||
import {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from '../../src/render3/util/discovery_utils';
|
||||
import {GLOBAL_PUBLISH_EXPANDO_KEY, GlobalDevModeContainer, publishDefaultGlobalUtils, publishGlobalUtil} from '../../src/render3/util/global_utils';
|
||||
|
@ -60,6 +61,10 @@ describe('global utils', () => {
|
|||
it('should publish applyChanges', () => {
|
||||
assertPublished('applyChanges', applyChanges);
|
||||
});
|
||||
|
||||
it('should publish ɵsetProfiler', () => {
|
||||
assertPublished('ɵsetProfiler', setProfiler);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue