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": { |     "master": { | ||||||
|       "uncompressed": { |       "uncompressed": { | ||||||
|         "runtime-es2015": 2285, |         "runtime-es2015": 2285, | ||||||
|         "main-es2015": 240874, |         "main-es2015": 240883, | ||||||
|         "polyfills-es2015": 36975, |         "polyfills-es2015": 36975, | ||||||
|         "5-es2015": 753 |         "5-es2015": 753 | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -279,6 +279,7 @@ export { | |||||||
| export { | export { | ||||||
|   compilePipe as ɵcompilePipe, |   compilePipe as ɵcompilePipe, | ||||||
| } from './render3/jit/pipe'; | } from './render3/jit/pipe'; | ||||||
|  | export { Profiler as ɵProfiler, ProfilerEvent as ɵProfilerEvent } from './render3/profiler'; | ||||||
| export { | export { | ||||||
|   publishDefaultGlobalUtils as ɵpublishDefaultGlobalUtils |   publishDefaultGlobalUtils as ɵpublishDefaultGlobalUtils | ||||||
| , | , | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import {NgOnChangesFeatureImpl} from './features/ng_onchanges_feature'; | |||||||
| import {DirectiveDef} from './interfaces/definition'; | import {DirectiveDef} from './interfaces/definition'; | ||||||
| import {TNode} from './interfaces/node'; | import {TNode} from './interfaces/node'; | ||||||
| import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view'; | import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view'; | ||||||
|  | import {profiler, ProfilerEvent} from './profiler'; | ||||||
| import {isInCheckNoChangesMode} from './state'; | import {isInCheckNoChangesMode} from './state'; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -256,9 +257,19 @@ function callHook(currentView: LView, initPhase: InitPhaseState, arr: HookData, | |||||||
|             (currentView[PREORDER_HOOK_FLAGS] >> PreOrderHookFlags.NumberOfInitHooksCalledShift) && |             (currentView[PREORDER_HOOK_FLAGS] >> PreOrderHookFlags.NumberOfInitHooksCalledShift) && | ||||||
|         (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) { |         (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) { | ||||||
|       currentView[FLAGS] += LViewFlags.IndexWithinInitPhaseIncrementer; |       currentView[FLAGS] += LViewFlags.IndexWithinInitPhaseIncrementer; | ||||||
|       hook.call(directive); |       profiler(ProfilerEvent.LifecycleHookStart, directive, hook); | ||||||
|  |       try { | ||||||
|  |         hook.call(directive); | ||||||
|  |       } finally { | ||||||
|  |         profiler(ProfilerEvent.LifecycleHookEnd, directive, hook); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     hook.call(directive); |     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 {GlobalTargetResolver, isProceduralRenderer, Renderer3} from '../interfaces/renderer'; | ||||||
| import {RElement} from '../interfaces/renderer_dom'; | import {RElement} from '../interfaces/renderer_dom'; | ||||||
| import {isDirectiveHost} from '../interfaces/type_checks'; | 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 {assertTNodeType} from '../node_assert'; | ||||||
|  | import {profiler, ProfilerEvent} from '../profiler'; | ||||||
| import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state'; | import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state'; | ||||||
| import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils'; | import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils'; | ||||||
| 
 | 
 | ||||||
| @ -121,6 +122,7 @@ function listenerInternal( | |||||||
|   const isTNodeDirectiveHost = isDirectiveHost(tNode); |   const isTNodeDirectiveHost = isDirectiveHost(tNode); | ||||||
|   const firstCreatePass = tView.firstCreatePass; |   const firstCreatePass = tView.firstCreatePass; | ||||||
|   const tCleanup: false|any[] = firstCreatePass && getOrCreateTViewCleanup(tView); |   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
 |   // 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
 |   // 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:
 |         // 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)
 |         // - either a target name (as a string) in case of global target (window, document, body)
 | ||||||
|         // - or element reference (in all other cases)
 |         // - 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); |         const cleanupFn = renderer.listen(resolved.name || target, eventName, listenerFn); | ||||||
|         ngDevMode && ngDevMode.rendererAddEventListener++; |         ngDevMode && ngDevMode.rendererAddEventListener++; | ||||||
| 
 | 
 | ||||||
| @ -186,7 +188,7 @@ function listenerInternal( | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|     } else { |     } else { | ||||||
|       listenerFn = wrapListener(tNode, lView, listenerFn, true /** preventDefault */); |       listenerFn = wrapListener(tNode, lView, context, listenerFn, true /** preventDefault */); | ||||||
|       target.addEventListener(eventName, listenerFn, useCapture); |       target.addEventListener(eventName, listenerFn, useCapture); | ||||||
|       ngDevMode && ngDevMode.rendererAddEventListener++; |       ngDevMode && ngDevMode.rendererAddEventListener++; | ||||||
| 
 | 
 | ||||||
| @ -196,7 +198,7 @@ function listenerInternal( | |||||||
|   } else { |   } else { | ||||||
|     // Even if there is no native listener to add, we still need to wrap the listener so that OnPush
 |     // 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.
 |     // 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
 |   // subscribe to directive outputs
 | ||||||
| @ -227,13 +229,16 @@ function listenerInternal( | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function executeListenerWithErrorHandling( | function executeListenerWithErrorHandling( | ||||||
|     lView: LView, listenerFn: (e?: any) => any, e: any): boolean { |     lView: LView, context: {}|null, listenerFn: (e?: any) => any, e: any): boolean { | ||||||
|   try { |   try { | ||||||
|  |     profiler(ProfilerEvent.OutputStart, context, listenerFn); | ||||||
|     // Only explicitly returning false from a listener should preventDefault
 |     // Only explicitly returning false from a listener should preventDefault
 | ||||||
|     return listenerFn(e) !== false; |     return listenerFn(e) !== false; | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     handleError(lView, error); |     handleError(lView, error); | ||||||
|     return false; |     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) |  * (the procedural renderer does this already, so in those cases, we should skip) | ||||||
|  */ |  */ | ||||||
| function wrapListener( | function wrapListener( | ||||||
|     tNode: TNode, lView: LView, listenerFn: (e?: any) => any, |     tNode: TNode, lView: LView, context: {}|null, listenerFn: (e?: any) => any, | ||||||
|     wrapWithPreventDefault: boolean): EventListener { |     wrapWithPreventDefault: boolean): EventListener { | ||||||
|   // Note: we are performing most of the work in the listener function itself
 |   // Note: we are performing most of the work in the listener function itself
 | ||||||
|   // to optimize listener registration.
 |   // to optimize listener registration.
 | ||||||
| @ -270,13 +275,13 @@ function wrapListener( | |||||||
|       markViewDirty(startView); |       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
 |     // A just-invoked listener function might have coalesced listeners so we need to check for
 | ||||||
|     // their presence and invoke as needed.
 |     // their presence and invoke as needed.
 | ||||||
|     let nextListenerFn = (<any>wrapListenerIn_markDirtyAndPreventDefault).__ngNextListenerFn__; |     let nextListenerFn = (<any>wrapListenerIn_markDirtyAndPreventDefault).__ngNextListenerFn__; | ||||||
|     while (nextListenerFn) { |     while (nextListenerFn) { | ||||||
|       // We should prevent default if any of the listeners explicitly return false
 |       // 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__; |       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 {assertPureTNodeType, assertTNodeType} from '../node_assert'; | ||||||
| import {updateTextNode} from '../node_manipulation'; | import {updateTextNode} from '../node_manipulation'; | ||||||
| import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher'; | 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 {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 {NO_CHANGE} from '../tokens'; | ||||||
| import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils'; | import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils'; | ||||||
| @ -501,16 +502,25 @@ export function renderComponentOrTemplate<T>( | |||||||
| function executeTemplate<T>( | function executeTemplate<T>( | ||||||
|     tView: TView, lView: LView, templateFn: ComponentTemplate<T>, rf: RenderFlags, context: T) { |     tView: TView, lView: LView, templateFn: ComponentTemplate<T>, rf: RenderFlags, context: T) { | ||||||
|   const prevSelectedIndex = getSelectedIndex(); |   const prevSelectedIndex = getSelectedIndex(); | ||||||
|  |   const isUpdatePhase = rf & RenderFlags.Update; | ||||||
|   try { |   try { | ||||||
|     setSelectedIndex(-1); |     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
 |       // When we're updating, inherently select 0 so we don't
 | ||||||
|       // have to generate that instruction for most update blocks.
 |       // have to generate that instruction for most update blocks.
 | ||||||
|       selectIndexInternal(tView, lView, HEADER_OFFSET, isInCheckNoChangesMode()); |       selectIndexInternal(tView, lView, HEADER_OFFSET, isInCheckNoChangesMode()); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     const preHookType = | ||||||
|  |         isUpdatePhase ? ProfilerEvent.TemplateUpdateStart : ProfilerEvent.TemplateCreateStart; | ||||||
|  |     profiler(preHookType, context); | ||||||
|     templateFn(rf, context); |     templateFn(rf, context); | ||||||
|   } finally { |   } finally { | ||||||
|     setSelectedIndex(prevSelectedIndex); |     setSelectedIndex(prevSelectedIndex); | ||||||
|  | 
 | ||||||
|  |     const postHookType = | ||||||
|  |         isUpdatePhase ? ProfilerEvent.TemplateUpdateEnd : ProfilerEvent.TemplateCreateEnd; | ||||||
|  |     profiler(postHookType, context); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1157,4 +1157,4 @@ export interface NodeInjectorDebug { | |||||||
|    * Location of the parent `TNode`. |    * Location of the parent `TNode`. | ||||||
|    */ |    */ | ||||||
|   parentInjectorIndex: number; |   parentInjectorIndex: number; | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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 {assertDefined} from '../../util/assert'; | ||||||
| import {global} from '../../util/global'; | import {global} from '../../util/global'; | ||||||
|  | import {setProfiler} from '../profiler'; | ||||||
| import {applyChanges} from './change_detection_utils'; | import {applyChanges} from './change_detection_utils'; | ||||||
| import {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from './discovery_utils'; | import {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from './discovery_utils'; | ||||||
| 
 | 
 | ||||||
| @ -40,6 +41,13 @@ let _published = false; | |||||||
| export function publishDefaultGlobalUtils() { | export function publishDefaultGlobalUtils() { | ||||||
|   if (!_published) { |   if (!_published) { | ||||||
|     _published = true; |     _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('getComponent', getComponent); | ||||||
|     publishGlobalUtil('getContext', getContext); |     publishGlobalUtil('getContext', getContext); | ||||||
|     publishGlobalUtil('getListeners', getListeners); |     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
 |  * 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 {applyChanges} from '../../src/render3/util/change_detection_utils'; | ||||||
| import {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from '../../src/render3/util/discovery_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'; | 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', () => { |     it('should publish applyChanges', () => { | ||||||
|       assertPublished('applyChanges', applyChanges); |       assertPublished('applyChanges', applyChanges); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     it('should publish ɵsetProfiler', () => { | ||||||
|  |       assertPublished('ɵsetProfiler', setProfiler); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user