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); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										101
									
								
								packages/core/src/render3/profiler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								packages/core/src/render3/profiler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|  | ||||
							
								
								
									
										320
									
								
								packages/core/test/acceptance/profiler_spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								packages/core/test/acceptance/profiler_spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user