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:
mgechev 2021-03-11 18:43:23 -08:00 committed by atscott
parent a43f36babd
commit 520ff69854
10 changed files with 474 additions and 13 deletions

View File

@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2285,
"main-es2015": 240874,
"main-es2015": 240883,
"polyfills-es2015": 36975,
"5-es2015": 753
}

View File

@ -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
,

View File

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

View File

@ -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__;
}

View File

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

View 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);
}
};

View File

@ -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);

View 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();
});
});

View File

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