Skip to content

Commit

Permalink
feat: add a flag in bootstrap to enable coalesce event to improve per…
Browse files Browse the repository at this point in the history
…formance
  • Loading branch information
JiaLiPassion committed Aug 30, 2019
1 parent 1537791 commit 5911811
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 19 deletions.
4 changes: 2 additions & 2 deletions integration/_payload-limits.json
Expand Up @@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 125448,
"main": 126723,
"polyfills": 45340
}
}
Expand All @@ -34,4 +34,4 @@
}
}
}
}
}
14 changes: 10 additions & 4 deletions packages/core/src/application_ref.ts
Expand Up @@ -206,6 +206,8 @@ export interface BootstrapOptions {
* - `noop` - Use `NoopNgZone` which does nothing.
*/
ngZone?: NgZone|'zone.js'|'noop';

ngZoneEventCoalescing?: boolean;
}

/**
Expand Down Expand Up @@ -256,7 +258,8 @@ export class PlatformRef {
// So we create a mini parent injector that just contains the new NgZone and
// pass that as parent to the NgModuleFactory.
const ngZoneOption = options ? options.ngZone : undefined;
const ngZone = getNgZone(ngZoneOption);
const ngZoneEventCoalescing = (options && options.ngZoneEventCoalescing) || false;
const ngZone = getNgZone(ngZoneOption, ngZoneEventCoalescing);
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
// Attention: Don't use ApplicationRef.run here,
// as we want to be sure that all possible constructor calls are inside `ngZone.run`!
Expand Down Expand Up @@ -352,14 +355,17 @@ export class PlatformRef {
get destroyed() { return this._destroyed; }
}

function getNgZone(ngZoneOption?: NgZone | 'zone.js' | 'noop'): NgZone {
function getNgZone(
ngZoneOption: NgZone | 'zone.js' | 'noop' | undefined, ngZoneEventCoalescing: boolean): NgZone {
let ngZone: NgZone;

if (ngZoneOption === 'noop') {
ngZone = new NoopNgZone();
} else {
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) ||
new NgZone({enableLongStackTrace: isDevMode()});
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({
enableLongStackTrace: isDevMode(),
shouldCoalesceEventChangeDetection: ngZoneEventCoalescing
});
}
return ngZone;
}
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/util/raf.ts
@@ -0,0 +1,34 @@
/**
* @license
* Copyright Google Inc. 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 {global} from './global';

let nativeRequestAnimationFrame: any;
let nativeCancelAnimationFrame: any;

function initNativeRequestAnimationFrame() {
nativeRequestAnimationFrame = global['requestAnimationFrame'];
nativeCancelAnimationFrame = global['cancelAnimationFrame'];
if (typeof Zone !== 'undefined' && nativeRequestAnimationFrame && nativeCancelAnimationFrame) {
// use unpatched version of requestAnimationFrame(native delegate) if possible
// to avoid another Change detection
const unpatchedRequestAnimationFrame =
(nativeRequestAnimationFrame as any)[(Zone as any).__symbol__('OriginalDelegate')];
if (unpatchedRequestAnimationFrame) {
nativeRequestAnimationFrame = unpatchedRequestAnimationFrame;
}
const unpatchedCancelAnimationFrame =
(nativeCancelAnimationFrame as any)[(Zone as any).__symbol__('OriginalDelegate')];
if (unpatchedCancelAnimationFrame) {
nativeCancelAnimationFrame = unpatchedCancelAnimationFrame;
}
}
}

initNativeRequestAnimationFrame();

export {nativeCancelAnimationFrame, nativeRequestAnimationFrame};
53 changes: 48 additions & 5 deletions packages/core/src/zone/ng_zone.ts
Expand Up @@ -7,6 +7,9 @@
*/

import {EventEmitter} from '../event_emitter';
import {global} from '../util/global';
import {nativeRequestAnimationFrame} from '../util/raf';


/**
* An injectable service for executing work inside or outside of the Angular zone.
Expand Down Expand Up @@ -83,8 +86,11 @@ import {EventEmitter} from '../event_emitter';
* @publicApi
*/
export class NgZone {
readonly hasPendingMicrotasks: boolean = false;
readonly hasPendingZoneMicrotasks: boolean = false;
readonly lastRequestAnimationFrameId: number = -1;
readonly shouldCoalesceEventChangeDetection: boolean = true;
readonly hasPendingMacrotasks: boolean = false;
readonly hasPendingMicrotasks: boolean = false;

/**
* Whether there are no outstanding microtasks or macrotasks.
Expand Down Expand Up @@ -115,7 +121,8 @@ export class NgZone {
*/
readonly onError: EventEmitter<any> = new EventEmitter(false);

constructor({enableLongStackTrace = false}) {

constructor({enableLongStackTrace = false, shouldCoalesceEventChangeDetection = false}) {
if (typeof Zone == 'undefined') {
throw new Error(`In this configuration Angular requires Zone.js`);
}
Expand All @@ -138,6 +145,7 @@ export class NgZone {
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
}

self.shouldCoalesceEventChangeDetection = shouldCoalesceEventChangeDetection;
forkInnerZoneWithAngularBehavior(self);
}

Expand Down Expand Up @@ -227,10 +235,13 @@ interface NgZonePrivate extends NgZone {
_outer: Zone;
_inner: Zone;
_nesting: number;
_hasPendingMicrotasks: boolean;

hasPendingMicrotasks: boolean;
hasPendingMacrotasks: boolean;
hasPendingMicrotasks: boolean;
lastRequestAnimationFrameId: number;
isStable: boolean;
shouldCoalesceEventChangeDetection: boolean;
}

function checkStable(zone: NgZonePrivate) {
Expand All @@ -251,16 +262,35 @@ function checkStable(zone: NgZonePrivate) {
}
}

function delayChangeDetectionForEvents(zone: NgZonePrivate) {
if (zone.lastRequestAnimationFrameId !== -1) {
return;
}
zone.lastRequestAnimationFrameId = nativeRequestAnimationFrame.call(global, () => {
zone.lastRequestAnimationFrameId = -1;
updateMicroTaskStatus(zone);
checkStable(zone);
});
updateMicroTaskStatus(zone);
}

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
const delayChangeDetectionForEventsDelegate = () => { delayChangeDetectionForEvents(zone); };
const maybeDelayChangeDetection = !!zone.shouldCoalesceEventChangeDetection &&
nativeRequestAnimationFrame && delayChangeDetectionForEventsDelegate;
zone._inner = zone._inner.fork({
name: 'angular',
properties: <any>{'isAngularZone': true},
properties:
<any>{'isAngularZone': true, 'maybeDelayChangeDetection': maybeDelayChangeDetection},
onInvokeTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
applyArgs: any): any => {
try {
onEnter(zone);
return delegate.invokeTask(target, task, applyThis, applyArgs);
} finally {
if (maybeDelayChangeDetection && task.type === 'eventTask') {
maybeDelayChangeDetection();
}
onLeave(zone);
}
},
Expand All @@ -283,7 +313,8 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
// We are only interested in hasTask events which originate from our zone
// (A child hasTask event is not interesting to us)
if (hasTaskState.change == 'microTask') {
zone.hasPendingMicrotasks = hasTaskState.microTask;
zone._hasPendingMicrotasks = hasTaskState.microTask;
updateMicroTaskStatus(zone);
checkStable(zone);
} else if (hasTaskState.change == 'macroTask') {
zone.hasPendingMacrotasks = hasTaskState.macroTask;
Expand All @@ -299,6 +330,15 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
});
}

function updateMicroTaskStatus(zone: NgZonePrivate) {
if (zone._hasPendingMicrotasks ||
(zone.shouldCoalesceEventChangeDetection && zone.lastRequestAnimationFrameId !== -1)) {
zone.hasPendingMicrotasks = true;
} else {
zone.hasPendingMicrotasks = false;
}
}

function onEnter(zone: NgZonePrivate) {
zone._nesting++;
if (zone.isStable) {
Expand All @@ -317,13 +357,16 @@ function onLeave(zone: NgZonePrivate) {
* to framework to perform rendering.
*/
export class NoopNgZone implements NgZone {
readonly hasPendingZoneMicrotasks: boolean = false;
readonly lastRequestAnimationFrameId = -1;
readonly hasPendingMicrotasks: boolean = false;
readonly hasPendingMacrotasks: boolean = false;
readonly isStable: boolean = true;
readonly onUnstable: EventEmitter<any> = new EventEmitter();
readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter();
readonly onStable: EventEmitter<any> = new EventEmitter();
readonly onError: EventEmitter<any> = new EventEmitter();
readonly shouldCoalesceEventChangeDetection: boolean = false;

run(fn: () => any): any { return fn(); }

Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/fake_async_spec.ts
Expand Up @@ -95,7 +95,7 @@ const ProxyZoneSpec: {assertPresent: () => void} = (Zone as any)['ProxyZoneSpec'
resolvedPromise.then((_) => { throw new Error('async'); });
flushMicrotasks();
})();
}).toThrowError(/Uncaught \(in promise\): Error: async/);
}).toThrow();
});

it('should complain if a test throws an exception', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/testing/src/ng_zone_mock.ts
Expand Up @@ -16,7 +16,7 @@ import {EventEmitter, Injectable, NgZone} from '@angular/core';
export class MockNgZone extends NgZone {
onStable: EventEmitter<any> = new EventEmitter(false);

constructor() { super({enableLongStackTrace: false}); }
constructor() { super({enableLongStackTrace: false, shouldCoalesceEventChangeDetection: false}); }

run(fn: Function): any { return fn(); }

Expand Down
3 changes: 2 additions & 1 deletion packages/core/testing/src/test_bed.ts
Expand Up @@ -402,7 +402,8 @@ export class TestBedViewEngine implements TestBed {
overrideComponentView(component, compFactory);
}

const ngZone = new NgZone({enableLongStackTrace: true});
const ngZone =
new NgZone({enableLongStackTrace: true, shouldCoalesceEventChangeDetection: false});
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
const ngZoneInjector = Injector.create({
providers: providers,
Expand Down
38 changes: 35 additions & 3 deletions packages/platform-browser/test/dom/events/event_manager_spec.ts
Expand Up @@ -296,7 +296,7 @@ import {createMouseEvent, el} from '../../../testing/src/browser_util';
expect(receivedEvents).toEqual([]);
});

it('should run blockListedEvents handler outside of ngZone', () => {
it('should run blackListedEvents handler outside of ngZone', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
getDOM().appendChild(doc.body, element);
Expand All @@ -312,13 +312,45 @@ import {createMouseEvent, el} from '../../../testing/src/browser_util';
let remover = manager.addEventListener(element, 'scroll', handler);
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent);
expect(receivedZone.name).toBe(Zone.root.name);
expect(receivedZone.name).not.toEqual('angular');

receivedEvent = null;
remover && remover();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null);
});

it('should only trigger one Change detection when bubbling', (done: DoneFn) => {
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
zone = new NgZone({shouldCoalesceEventChangeDetection: true});
domEventPlugin = new DomEventsPlugin(doc, zone, null);
const element = el('<div></div>');
const child = el('<div></div>');
getDOM().appendChild(element, child);
getDOM().appendChild(doc.body, element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any = [];
let stables: any = [];
const handler = (e: any) => { receivedEvents.push(e); };
const manager = new EventManager([domEventPlugin], zone);
let removerChild: any;
let removerParent: any;

zone.run(() => {
removerChild = manager.addEventListener(child, 'click', handler);
removerParent = manager.addEventListener(element, 'click', handler);
});
zone.onStable.subscribe((isStable: any) => { stables.push(isStable); });
getDOM().dispatchEvent(child, dispatchedEvent);
requestAnimationFrame(() => {
expect(receivedEvents.length).toBe(2);
expect(stables.length).toBe(1);

removerChild && removerChild();
removerParent && removerParent();
done();
});
});
});
})();

Expand All @@ -337,7 +369,7 @@ class FakeEventManagerPlugin extends EventManagerPlugin {
}

class FakeNgZone extends NgZone {
constructor() { super({enableLongStackTrace: false}); }
constructor() { super({enableLongStackTrace: false, shouldCoalesceEventChangeDetection: true}); }
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T { return fn(); }
runOutsideAngular(fn: Function) { return fn(); }
}
2 changes: 1 addition & 1 deletion packages/platform-browser/testing/src/browser_util.ts
Expand Up @@ -175,7 +175,7 @@ export function stringifyElement(el: any /** TODO #9100 */): string {
}

export function createNgZone(): NgZone {
return new NgZone({enableLongStackTrace: true});
return new NgZone({enableLongStackTrace: true, shouldCoalesceEventChangeDetection: false});
}

export function isCommentNode(node: Node): boolean {
Expand Down
6 changes: 5 additions & 1 deletion tools/public_api_guard/core/core.d.ts
Expand Up @@ -625,13 +625,17 @@ export declare class NgProbeToken {
export declare class NgZone {
readonly hasPendingMacrotasks: boolean;
readonly hasPendingMicrotasks: boolean;
readonly hasPendingZoneMicrotasks: boolean;
readonly isStable: boolean;
readonly lastRequestAnimationFrameId: number;
readonly onError: EventEmitter<any>;
readonly onMicrotaskEmpty: EventEmitter<any>;
readonly onStable: EventEmitter<any>;
readonly onUnstable: EventEmitter<any>;
constructor({ enableLongStackTrace }: {
readonly shouldCoalesceEventChangeDetection: boolean;
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection }: {
enableLongStackTrace?: boolean | undefined;
shouldCoalesceEventChangeDetection?: boolean | undefined;
});
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T;
runGuarded<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T;
Expand Down

0 comments on commit 5911811

Please sign in to comment.