diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 1410f8ab36..917d0775ba 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -39,7 +39,7 @@ "master": { "uncompressed": { "runtime-es2015": 2285, - "main-es2015": 240874, + "main-es2015": 240883, "polyfills-es2015": 36975, "5-es2015": 753 } diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 93d713fe05..bae931b12c 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -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 , diff --git a/packages/core/src/render3/hooks.ts b/packages/core/src/render3/hooks.ts index 5199d2846e..55fb0363bf 100644 --- a/packages/core/src/render3/hooks.ts +++ b/packages/core/src/render3/hooks.ts @@ -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; - hook.call(directive); + profiler(ProfilerEvent.LifecycleHookStart, directive, hook); + try { + hook.call(directive); + } finally { + profiler(ProfilerEvent.LifecycleHookEnd, directive, hook); + } } } else { - hook.call(directive); + profiler(ProfilerEvent.LifecycleHookStart, directive, hook); + try { + hook.call(directive); + } finally { + profiler(ProfilerEvent.LifecycleHookEnd, directive, hook); + } } } diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts index 2c5efd8784..c469fd8ece 100644 --- a/packages/core/src/render3/instructions/listener.ts +++ b/packages/core/src/render3/instructions/listener.ts @@ -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 = (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 = (nextListenerFn).__ngNextListenerFn__; } diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 6bf02d590a..34d6ef6daf 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -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( function executeTemplate( tView: TView, lView: LView, templateFn: ComponentTemplate, 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); } } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 64e3b02052..ba16dcf3c9 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -1157,4 +1157,4 @@ export interface NodeInjectorDebug { * Location of the parent `TNode`. */ parentInjectorIndex: number; -} \ No newline at end of file +} diff --git a/packages/core/src/render3/profiler.ts b/packages/core/src/render3/profiler.ts new file mode 100644 index 0000000000..fd908f5db0 --- /dev/null +++ b/packages/core/src/render3/profiler.ts @@ -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); + } +}; diff --git a/packages/core/src/render3/util/global_utils.ts b/packages/core/src/render3/util/global_utils.ts index 489eb506ab..9e488d3f1a 100644 --- a/packages/core/src/render3/util/global_utils.ts +++ b/packages/core/src/render3/util/global_utils.ts @@ -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); diff --git a/packages/core/test/acceptance/profiler_spec.ts b/packages/core/test/acceptance/profiler_spec.ts new file mode 100644 index 0000000000..bec91977d4 --- /dev/null +++ b/packages/core/test/acceptance/profiler_spec.ts @@ -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: ''}) + 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: ''}) + 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: ''}) + 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: ''}) + 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: ''}) + 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(); + }); +}); diff --git a/packages/core/test/render3/global_utils_spec.ts b/packages/core/test/render3/global_utils_spec.ts index 577eddecf7..ea19965a1d 100644 --- a/packages/core/test/render3/global_utils_spec.ts +++ b/packages/core/test/render3/global_utils_spec.ts @@ -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); + }); }); });