Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
feat(eventListener): fix #798, improve EventTarget.addEventListener p…
Browse files Browse the repository at this point in the history
…erformance (#812)

* feat(performance): fix #798, improve EventTarget.addEventListener performance

* add test cases

* add comment to explain the implementations

* add test cases for resschedule eventTask

* add IE/edge and cross context check

* modify document for extra flags

* minor changes, define some const, move logic into __load_patch

* let nodejs use new addListener/removeListener method

* add nodejs test example

* add eventListeners/removeAllListeners test cases for browser
  • Loading branch information
JiaLiPassion authored and mhevery committed Jun 30, 2017
1 parent 98f3903 commit b3a76d3
Show file tree
Hide file tree
Showing 18 changed files with 1,219 additions and 593 deletions.
2 changes: 2 additions & 0 deletions MODULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Below is the full list of current support modules.
|timers|setTimeout/setInterval/setImmediate will be patched as Zone MacroTask|__Zone_disable_timer = true|
|blocking|alert/prompt/confirm will be patched as Zone.run|__Zone_disable_blocking = true|
|EventTarget|target.addEventListener will be patched as Zone aware EventTask|__Zone_disable_EventTarget = true|
|IE BrowserTools check|in IE, browser tool will not use zone patched eventListener|__Zone_disable_IE_check = true|
|CrossContext check|in webdriver, enable check event listener is cross context|__Zone_enable_cross_context_check = true|
|XHR|XMLHttpRequest will be patched as Zone aware MacroTask|__Zone_disable_XHR = true|
|geolocation|navigator.geolocation's prototype will be patched as Zone.run|__Zone_disable_geolocation = true|
|PromiseRejectionEvent|PromiseRejectEvent will fire when ZoneAwarePromise has unhandled error|__Zone_disable_PromiseRejectionEvent = true|
Expand Down
2 changes: 1 addition & 1 deletion example/benchmarks/addEventListener.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>Zone.js addEventListenerBenchmark</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="../css/style.css">
<script src="../../dist/zone.js"></script>
</head>
<body>
Expand Down
50 changes: 50 additions & 0 deletions example/benchmarks/event_emitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @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
*/
const events = require('events');
const EventEmitter = events.EventEmitter;
require('../../dist/zone-node');

const emitters = [];
const callbacks = [];
const size = 100000;
for (let i = 0; i < size; i++) {
const emitter = new EventEmitter();
const callback = (function (i) { return function () { console.log(i); }; })(i);
emitters[i] = emitter;
callbacks[i] = callback;
}

function addRemoveCallback(reuse, useZone) {
const start = new Date();
let callback = callbacks[0];
for (let i = 0; i < size; i++) {
const emitter = emitters[i];
if (!reuse) callback = callbacks[i];
if (useZone)
emitter.on('msg', callback);
else
emitter.__zone_symbol__addListener('msg', callback);
}

for (let i = 0; i < size; i++) {
const emitter = emitters[i];
if (!reuse) callback = callbacks[i];
if (useZone)
emitter.removeListener('msg', callback);
else
emitter.__zone_symbol__removeListener('msg', callback);
}
const end = new Date();
console.log(useZone ? 'use zone' : 'native', reuse ? 'reuse' : 'new');
console.log("Execution time: %dms", end - start);
}

addRemoveCallback(false, false);
addRemoveCallback(false, true);
addRemoveCallback(true, false);
addRemoveCallback(true, true);
11 changes: 6 additions & 5 deletions lib/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {findEventTasks} from '../common/events';
import {patchTimer} from '../common/timers';
import {findEventTask, patchClass, patchEventTargetMethods, patchMacroTask, patchMethod, patchOnProperties, patchPrototype, zoneSymbol} from '../common/utils';
import {patchClass, patchMacroTask, patchMethod, patchOnProperties, patchPrototype, zoneSymbol} from '../common/utils';

import {propertyPatch} from './define-property';
import {eventTargetPatch} from './event-target';
Expand Down Expand Up @@ -38,11 +39,12 @@ Zone.__load_patch('blocking', (global: any, Zone: ZoneType, api: _ZonePrivate) =
});

Zone.__load_patch('EventTarget', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
eventTargetPatch(global);
eventTargetPatch(global, api);
// patch XMLHttpRequestEventTarget's addEventListener/removeEventListener
const XMLHttpRequestEventTarget = (global as any)['XMLHttpRequestEventTarget'];
if (XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype) {
patchEventTargetMethods(XMLHttpRequestEventTarget.prototype);
// TODO: @JiaLiPassion, add this back later.
api.patchEventTargetMethods(XMLHttpRequestEventTarget.prototype);
}
patchClass('MutationObserver');
patchClass('WebKitMutationObserver');
Expand Down Expand Up @@ -180,7 +182,7 @@ Zone.__load_patch('PromiseRejectionEvent', (global: any, Zone: ZoneType, api: _Z
// handle unhandled promise rejection
function findPromiseRejectionHandler(evtName: string) {
return function(e: any) {
const eventTasks = findEventTask(global, evtName);
const eventTasks = findEventTasks(global, evtName);
eventTasks.forEach(eventTask => {
// windows has added unhandledrejection event listener
// trigger the event listener
Expand All @@ -204,7 +206,6 @@ Zone.__load_patch('PromiseRejectionEvent', (global: any, Zone: ZoneType, api: _Z


Zone.__load_patch('util', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
api.patchEventTargetMethods = patchEventTargetMethods;
api.patchOnProperties = patchOnProperties;
api.patchMethod = patchMethod;
});
90 changes: 80 additions & 10 deletions lib/browser/event-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@
* found in the LICENSE file at https://angular.io/license
*/

import {patchEventTargetMethods} from '../common/utils';
import {FALSE_STR, globalSources, patchEventTargetMethods, TRUE_STR, ZONE_SYMBOL_PREFIX, zoneSymbolEventNames} from '../common/events';
import {attachOriginToPatched, isIEOrEdge, zoneSymbol} from '../common/utils';

const WTF_ISSUE_555 =
'Anchor,Area,Audio,BR,Base,BaseFont,Body,Button,Canvas,Content,DList,Directory,Div,Embed,FieldSet,Font,Form,Frame,FrameSet,HR,Head,Heading,Html,IFrame,Image,Input,Keygen,LI,Label,Legend,Link,Map,Marquee,Media,Menu,Meta,Meter,Mod,OList,Object,OptGroup,Option,Output,Paragraph,Pre,Progress,Quote,Script,Select,Source,Span,Style,TableCaption,TableCell,TableCol,Table,TableRow,TableSection,TextArea,Title,Track,UList,Unknown,Video';
const NO_EVENT_TARGET =
'ApplicationCache,EventSource,FileReader,InputMethodContext,MediaController,MessagePort,Node,Performance,SVGElementInstance,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebKitNamedFlow,Window,Worker,WorkerGlobalScope,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload,IDBRequest,IDBOpenDBRequest,IDBDatabase,IDBTransaction,IDBCursor,DBIndex,WebSocket'
.split(',');
const EVENT_TARGET = 'EventTarget';
import {eventNames} from './property-descriptor';

export function eventTargetPatch(_global: any, api: _ZonePrivate) {
const WTF_ISSUE_555 =
'Anchor,Area,Audio,BR,Base,BaseFont,Body,Button,Canvas,Content,DList,Directory,Div,Embed,FieldSet,Font,Form,Frame,FrameSet,HR,Head,Heading,Html,IFrame,Image,Input,Keygen,LI,Label,Legend,Link,Map,Marquee,Media,Menu,Meta,Meter,Mod,OList,Object,OptGroup,Option,Output,Paragraph,Pre,Progress,Quote,Script,Select,Source,Span,Style,TableCaption,TableCell,TableCol,Table,TableRow,TableSection,TextArea,Title,Track,UList,Unknown,Video';
const NO_EVENT_TARGET =
'ApplicationCache,EventSource,FileReader,InputMethodContext,MediaController,MessagePort,Node,Performance,SVGElementInstance,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebKitNamedFlow,Window,Worker,WorkerGlobalScope,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload,IDBRequest,IDBOpenDBRequest,IDBDatabase,IDBTransaction,IDBCursor,DBIndex,WebSocket'
.split(',');
const EVENT_TARGET = 'EventTarget';

export function eventTargetPatch(_global: any) {
let apis = [];
const isWtf = _global['wtf'];
const WTF_ISSUE_555_ARRAY = WTF_ISSUE_555.split(',');

if (isWtf) {
// Workaround for: https://github.com/google/tracing-framework/issues/555
apis = WTF_ISSUE_555.split(',').map((v) => 'HTML' + v + 'Element').concat(NO_EVENT_TARGET);
apis = WTF_ISSUE_555_ARRAY.map((v) => 'HTML' + v + 'Element').concat(NO_EVENT_TARGET);
} else if (_global[EVENT_TARGET]) {
apis.push(EVENT_TARGET);
} else {
Expand All @@ -29,8 +34,73 @@ export function eventTargetPatch(_global: any) {
apis = NO_EVENT_TARGET;
}

const isDisableIECheck = _global['__Zone_disable_IE_check'] || false;
const isEnableCrossContextCheck = _global['__Zone_enable_cross_context_check'] || false;
const ieOrEdge = isIEOrEdge();

const ADD_EVENT_LISTENER_SOURCE = '.addEventListener:';
const FUNCTION_WRAPPER = '[object FunctionWrapper]';
const BROWSER_TOOLS = 'function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }';

// predefine all __zone_symbol__ + eventName + true/false string
for (let i = 0; i < eventNames.length; i++) {
const eventName = eventNames[i];
const falseEventName = eventName + FALSE_STR;
const trueEventName = eventName + TRUE_STR;
const symbol = ZONE_SYMBOL_PREFIX + falseEventName;
const symbolCapture = ZONE_SYMBOL_PREFIX + trueEventName;
zoneSymbolEventNames[eventName] = {};
zoneSymbolEventNames[eventName][FALSE_STR] = symbol;
zoneSymbolEventNames[eventName][TRUE_STR] = symbolCapture;
}

// predefine all task.source string
for (let i = 0; i < WTF_ISSUE_555.length; i++) {
const target: any = WTF_ISSUE_555_ARRAY[i];
const targets: any = globalSources[target] = {};
for (let j = 0; j < eventNames.length; j++) {
const eventName = eventNames[j];
targets[eventName] = target + ADD_EVENT_LISTENER_SOURCE + eventName;
}
}

const checkIEAndCrossContext = function(
nativeDelegate: any, delegate: any, target: any, args: any) {
if (!isDisableIECheck && ieOrEdge) {
if (isEnableCrossContextCheck) {
try {
const testString = delegate.toString();
if ((testString === FUNCTION_WRAPPER || testString == BROWSER_TOOLS)) {
nativeDelegate.apply(target, args);
return false;
}
} catch (error) {
nativeDelegate.apply(target, args);
return false;
}
} else {
const testString = delegate.toString();
if ((testString === FUNCTION_WRAPPER || testString == BROWSER_TOOLS)) {
nativeDelegate.apply(target, args);
return false;
}
}
} else if (isEnableCrossContextCheck) {
try {
delegate.toString();
} catch (error) {
nativeDelegate.apply(target, args);
return false;
}
}
return true;
};

for (let i = 0; i < apis.length; i++) {
const type = _global[apis[i]];
patchEventTargetMethods(type && type.prototype);
patchEventTargetMethods(type && type.prototype, {validateHandler: checkIEAndCrossContext});
}

api.patchEventTargetMethods = patchEventTargetMethods;
return true;
}
2 changes: 1 addition & 1 deletion lib/browser/property-descriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ const IDBIndexEventNames =
['upgradeneeded', 'complete', 'abort', 'success', 'error', 'blocked', 'versionchange', 'close'];
const websocketEventNames = ['close', 'error', 'open', 'message'];

const eventNames = globalEventHandlersEventNames.concat(
export const eventNames = globalEventHandlersEventNames.concat(
webglEventNames, formEventNames, detailEventNames, documentEventNames, windowEventNames,
htmlElementEventNames, ieElementEventNames);

Expand Down
26 changes: 2 additions & 24 deletions lib/browser/webapis-media-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,6 @@ Zone.__load_patch('mediaQuery', (global: any, Zone: ZoneType, api: _ZonePrivate)
return;
}
api.patchEventTargetMethods(
global['MediaQueryList'].prototype, 'addListener', 'removeListener',
(self: any, args: any[]) => {
return {
useCapturing: false,
eventName: 'mediaQuery',
handler: args[0],
target: self || global,
name: 'mediaQuery',
invokeAddFunc: function(addFnSymbol: any, delegate: any) {
if (delegate && (<Task>delegate).invoke) {
return this.target[addFnSymbol]((<Task>delegate).invoke);
} else {
return this.target[addFnSymbol](delegate);
}
},
invokeRemoveFunc: function(removeFnSymbol: any, delegate: any) {
if (delegate && (<Task>delegate).invoke) {
return this.target[removeFnSymbol]((<Task>delegate).invoke);
} else {
return this.target[removeFnSymbol](delegate);
}
}
};
});
global['MediaQueryList'].prototype,
{addEventListenerFnName: 'addListener', removeEventListenerFnName: 'removeListener'});
});
3 changes: 2 additions & 1 deletion lib/browser/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {patchEventTargetMethods, patchOnProperties} from '../common/utils';
import {patchEventTargetMethods} from '../common/events';
import {patchOnProperties} from '../common/utils';

// we have to patch the instance since the proto is non-configurable
export function apply(_global: any) {
Expand Down
Loading

0 comments on commit b3a76d3

Please sign in to comment.