-
Notifications
You must be signed in to change notification settings - Fork 407
Slow addEventListener performance. #798
Comments
@mhevery , I want to confirm whether I understand correctly or not. Suggested fix: Modification of Naive Fix.addEventListenerthe logic will look like const originalAddEventListener = document.addEventListener;
const originalRemoveEventListener = document.removeEventListener;
document.addEventListener = function(...) {
// make Zone aware will schedule an eventTask
const zoneAwareCallback = userCallback[Zone.current.__private_symbol] = makeZoneAware(userCallback);
originalAddEventListener.apply(this, [zoneAwareCallback]);
}
document.removeEventListener = function(...) {
// find all zoneAwareUserCallback attached to userCallback
for (let key in userCallback) {
if (key.startsWith('ZonePrivateSymbol')) {
originalRemoveEventListener.apply(this, [userCallback[key]]);
}
}
} // and makeZoneAware will not keep the addtional eventTasks array in zone.js memory https://github.com/angular/zone.js/blob/master/lib/common/utils.ts#L340 will be removed. Is that correct? |
@JiaLiPassion yes that looks right. But I have a question. Why do we have to loop in I guess there could be an issue if the But i want to make sure that the happy case is fast, with no loops. |
@mhevery , yeah, you are right, we may don't need to loop. And we may don't need a consider the case you mentioned above. Case1function userCallback() {....}
zoneA.run(() => div.addEventListener('click', userCallback);
zoneB.run(() => div.addEventListener('click', userCallback);
// then removeEventListener, this code may run in zoneA/zoneB or any other zone
div.removeEventListener('click', userCallback); Current version of zone.jsthe code above will have the following result. function userCallback() {....}
zoneA.run(() => div.addEventListener('click', userCallback);
// div will register a click eventListener which is a EventTask whose zone is zoneA
assert.toStrictEqual(div[zoneSymbol['eventTasks']].length, 1);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][0].zone, zoneA);
zoneB.run(() => div.addEventListener('click', userCallback);
// zone will first try to find the existing task, and will get the
// eventTask which created by zoneA, and it will not try to
// schedule a new eventTask, it will just use the existing one.
assert.toStrictEqual(div[zoneSymbol['eventTasks']].length, 1);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][0].zone, zoneA);
// then removeEventListener, this code may run in zoneA/zoneB or any other zone
div.removeEventListener('click', userCallback);
// the existing eventTask will be removed.
assert.toStrictEqual(div[zoneSymbol['eventTasks']].length, 0); So for this case, use function userCallback() {....}
userCallback.zoneAware = makeZoneAware(userCallback); because in this case, will actually will only have one zoneAware listener, Case2function userCallback() {....}
zoneA.run(() => div.addEventListener('click', userCallback, {useCapture: true});
zoneB.run(() => div.addEventListener('click', userCallback, {useCapture: false});
// then removeEventListener, this code may run in zoneA/zoneB or any other zone
div.removeEventListener('click', userCallback, {useCapture: true});
div.removeEventListener('click', userCallback, {useCapture: false}); Current version of zone.jsthe code above will have the following result. function userCallback() {....}
zoneA.run(() => div.addEventListener('click', userCallback, {useCapture: true});
// div will register a click eventListener which is a EventTask whose zone is zoneA
assert.toStrictEqual(div[zoneSymbol['eventTasks']].length, 1);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][0].zone, zoneA);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][0].data.options.useCapture, true);
zoneB.run(() => div.addEventListener('click', userCallback, {useCapture: false});
// zone will first try to find the existing task with the callback and useCapture
// there will no existing one, so it will try to
// schedule a new eventTask.
assert.toStrictEqual(div[zoneSymbol['eventTasks']].length, 2);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][1].zone, zoneB);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][1].data.options.useCapture, false);
// then removeEventListener, this code may run in zoneA/zoneB or any other zone
div.removeEventListener('click', userCallback, {useCapture: true});
// the existing eventTask which created by zoneA will be removed.
assert.toStrictEqual(div[zoneSymbol['eventTasks']].length, 1);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][1].zone, zoneB);
assert.toStrictEqual(div[zoneSymbol['eventTasks']][1].data.options.useCapture, false);
div.removeEventListener('click', userCallback, {useCapture: false});
// the existing eventTask which created by zoneB will be removed.
assert.toStrictEqual(div[zoneSymbol['eventTasks']].length, 0); in this case, the pure
so we may just need a different version of Suggested fix: Modification of Naive Fix. function userCallback() {....}
zoneA.run(() => {
userCallback[{useCapture: true}]= makeZoneAware(userCallback);
});
zoneB.run(() => {
userCallback[{useCapture: false}]= makeZoneAware(userCallback);
}); so the case2 will become function userCallback() {....}
zoneA.run(() => div.addEventListener('click', userCallback, {useCapture: true});
// div will register a click eventListener which is a EventTask whose zone is zoneA
assert.toStrictEqual(userCallback[{useCapture: true}].zone, zoneA);
assert.toStrictEqual(userCallback[{useCapture: true}].data.options.useCapture, true);
zoneB.run(() => div.addEventListener('click', userCallback, {useCapture: false});
// zone will first try to find the existing task with the callback and useCapture
// there will no existing one, so it will try to
// schedule a new eventTask.
assert.toStrictEqual(userCallback[{useCapture: false}].zone, zoneB);
assert.toStrictEqual(userCallback[{useCapture: false}].data.options.useCapture, false);
// then removeEventListener, this code may run in zoneA/zoneB or any other zone
div.removeEventListener('click', userCallback, {useCapture: true});
// the existing eventTask which created by zoneA will be removed.
assert.toStrictEqual(userCallback[{useCapture: true}], undefined);
assert.toStrictEqual(userCallback[{useCapture: false}].zone, zoneB);
div.removeEventListener('click', userCallback, {useCapture: false});
// the existing eventTask which created by zoneB will be removed.
assert.toStrictEqual(userCallback[{useCapture: false}], undefined); |
@JiaLiPassion yes that sounds right. To summarize:
Here is my take on pseudo code. // lazy create a zoneAwareCallback and cache it in a zone independent way.
function getZoneAwareCallback(callback) {
// se if we already have a callback
let zoneAwareCallback = callback.zoneReturnCallback;
if (!zoneReturnCallback) {
// Create a callback but don't associated it with a zone.
zoneAwareCallback = function(event) {
// retrieve the zone form the EventTarget using the event and our ID.
const zone = this[ZONE_INFO][zoneAwareCallback.symbol + event.name];
return zone.runGuarde(callback, this, [event]);
}
// create a unique symbol for this callback
zoneAwareCallback.symbol = '__zone_symbol__' + (counter++) + '_'
callback.zoneAwareCallback = zoneAwareCallaback;
}
return zoneAwareCallback;
}
function addEventListener(event, callback, options) {
const zoneAwareCallback = getZoneAwareCallback(callback);
// lazy create a ZoneInfo. We could patch the callback symbol directly, but that would make the
// EventTarget shape unpredictable and could negatively impact performance. So we do the safe
// thing and put it on a separate object.
let zoneInfo = this[ZONE_INFO];
if (!zoneInfo) zoneInfo = this[ZONE_INFO] = {};
const zoneEventSymbol = zoneAwerCallback + event
if (!zoneInfo[zoneEventSymbol]) {
zoneInfo[zoneEventSymbol] = Zone.current;
}
return nativeAddEventListener(event, zoneAwareCallback, options);
}
function removeEventListener(event, callback, options) {
const zoneAwareCallback = getZoneAwareCallback(callback);
let zoneInfo = this[ZONE_INFO];
if (zoneInfo) {
zoneInfo[zoneAwerCallback.symbol + event] = null;
}
return nativeRemoveEventListener(event, zoneAwareCallback, options);
} |
@mhevery .
Yes, I agree. but it seems
About the code, I have one question. in the case2 I listed above, function userCallback() {....}
zoneA.run(() => div.addEventListener('click', userCallback, {useCapture: true});
zoneB.run(() => div.addEventListener('click', userCallback, {useCapture: false});
// then removeEventListener, this code may run in zoneA/zoneB or any other zone
div.removeEventListener('click', userCallback, {useCapture: true});
div.removeEventListener('click', userCallback, {useCapture: false}); we should have 2 zoneAwareCallback attached to the And because // lazy create a zoneAwareCallback and cache it in a zone independent way.
function getZoneAwareCallback(callback, useCapture) {
// se if we already have a callback
let zoneAwareCallbacks = callback.zoneReturnCallbacks;
if (!zoneAwareCallbacks) {
zoneAwareCallbacks = {};
}
let zoneAwareCallback = zoneAwareCallbacks[useCapture.toString()];
if (!zoneReturnCallback) {
// Create a callback but don't associated it with a zone.
zoneAwareCallback = function(event) {
// retrieve the zone form the EventTarget using the event and our ID and useCapture flag.
const zone = this[ZONE_INFO][zoneAwareCallback.symbol + event.name + useCapture];
return zone.runGuarde(callback, this, [event]);
}
// create a unique symbol for this callback
zoneAwareCallback.symbol = '__zone_symbol__' + (counter++) + '_'
zoneAwareCallbacks[useCapture.toString()] = zoneAwareCallback;
}
return zoneAwareCallback;
} please review. |
I agree with this change: // retrieve the zone form the EventTarget using the event and our ID and useCapture flag.
const zone = this[ZONE_INFO][zoneAwareCallback.symbol + event.name + useCapture]; I would improve on this line as: if (useCapture) {
zoneAwareCallbacks.zoneAwareCaptureCallback= zoneAwareCallback;
} else {
zoneAwareCallbacks.zoneAwareCallback= zoneAwareCallback;
} Other than that this looks great. |
@mhevery , got it, I will implement this one! |
@mhevery , we discussed this one yesterday that we still need to schedule the function addEventListener(event, callback, options) {
const zoneAwareCallback = getZoneAwareCallback(callback);
// lazy create a ZoneInfo. We could patch the callback symbol directly, but that would make the
// EventTarget shape unpredictable and could negatively impact performance. So we do the safe
// thing and put it on a separate object.
const zone = Zone.current;
let zoneInfo = this[ZONE_INFO];
if (!zoneInfo) zoneInfo = this[ZONE_INFO] = {};
const zoneEventSymbol = zoneAwareCallback.symbol + event + useCapture;
if (!zoneInfo[zoneEventSymbol]) {
// save task information so we can get task when
// removeEventListener
zoneInfo[zoneEventSymbol] = zone;
}
Zone.current.scheduleEventTask(source, zoneAwareCallback, ....);
} But if we
|
I forget about the Tasks. Here it is rewritten with // lazy create a zoneAwareCallback and cache it in a zone independent way.
function getZoneAwareCallback(callback) {
// see if we already have a callback
let zoneAwareCallback = callback.zoneReturnCallback;
if (!zoneAwareCallback) {
// Create a callback but don't associated it with a zone.
zoneAwareCallback = function(event) {
// retrieve the zone form the EventTarget using the event and our ID.
const task = this[TASK_INFO][zoneAwareCallback.symbol + event.name];
return task.invoke.apply(this, arguments);
}
// create a unique symbol for this callback
zoneAwareCallback.symbol = '__zone_symbol__' + (counter++) + '_'
callback.zoneAwareCallback = zoneAwareCallback;
}
return zoneAwareCallback;
}
function customSchedule() {
nativeAddEventListener(this.data.event, this.data.zoneAwareCallback, this.data.options);
}
function customCancel() {
nativeRemoveEventListener(this.data.event, this.data.zoneAwareCallback, this.data.options);
}
function addEventListener(event, callback, options) {
const zoneAwareCallback = getZoneAwareCallback(callback);
// lazy create a ZoneInfo. We could patch the callback symbol directly, but that would make the
// EventTarget shape unpredictable and could negatively impact performance. So we do the safe
// thing and put it on a separate object.
let taskInfo = this[TASK_INFO];
if (!taskInfo) taskInfo = this[TASK_INFO] = {};
const zoneTaskSymbol = zoneAwareCallback.symbol + event
if (!taskInfo[zoneTaskSymbol]) {
taskInfo[zoneTaskSymbol] = Zone.current.scheduleMacroTask(
'...', callback, {event, zoneAwareCallback, options},
customSchedule, customCancel);
}
}
function removeEventListener(event, callback, options) {
const zoneAwareCallback = getZoneAwareCallback(callback);
let taskInfo = this[TASK_INFO];
if (taskInfo) {
const zoneTaskSymbol = zoneAwareCallback.symbol + event
if (taskInfo[zoneTaskSymbol]) {
const task: ZoneTask = taskInfo[zoneTaskSymbol];
task.customCancel();
taskInfo[zoneTaskSymbol] = null;
}
}
return nativeRemoveEventListener(event, zoneAwareCallback, options);
} |
…tener performance
…tener performance
…ner performance
…ner performance
…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
@JiaLiPassion @mhevery thnx so much for all the work done on this issue. I've just updated to the latest zone.js ( Before
After
SummaryWe can see 10+% improvement on the pureScript time which quite frankly is more than I've expected :-) But GC might be the factor here as GC amounts and times dropped dramatically! Once again, thnx for all the efforts here! |
@pkozlowski-opensource , thank you very much! very glad to see that the performance improved! |
@pkozlowski-opensource , could you post your sample app for testing the perf of angular? I just fixed some bugs because of this changes, I want to check the performance changes again, thank you. |
Issue
Currently calling
addEventListener
andremoveEventListener
is significantly slower than native version. This has a negative impact on Angular framework performance.Root Cause
Given
In order to make the zone propagate the
userCallback
has to be wrapped inzoneAwareUserCallback
like so:Notice that when the
removeEventListener
gets called it needs to get called withzoneAwareUserCallback
rather thanuserCallback
.The issue is that the
addEventListener
has to do the translation fromuserCallback
tozoneAwareUserCallback
internally (its monkey patched). And the same translation has to happen inremoveEventListener
. This translation has to be memoized for this is to work.Currently the cost of creating the delegate and book keeping is the source of the slowdown.
The current bookkeeping involves creating an array which is patched on the
EventTarget
instance which contains all of theuserCallback
s. For each one there is a correspondingzoneAwareUserCallback
. When subsequent call is made to theremoveEventListener
the array is first checked to see if it contains the callback, and if so we reuse its zone-aware version.Naive Approach
You may ask why is this not sufficient.
This would simply patch the zone-aware function on the function itself and it would greatly simplify the bookkeeping. The problem is that the zone-aware function is bound to a specific Zone. This means that it can't be reused for different zones.
This code would break:
The reason is that zone-aware callback is bound to zone, hence it would restore the wrong zone.
Suggested fix: Modification of Naive Fix.
Since the callback is zone specific, instead of patching to a well known name, we could patch to a name which is zone specific. (This assumes that each zone would have a private unique symbol)
This would greatly simplify the book-keeping and memory pressure which is the source of the performance slowdown.
Internally
addEventListener
/removeEventListener
can easily determine how to translate from user to zone-aware callback.Related Code
The text was updated successfully, but these errors were encountered: