Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ext/web): Copy EventTarget list before dispatch #19360

Merged
merged 4 commits into from
Jun 5, 2023
Merged

fix(ext/web): Copy EventTarget list before dispatch #19360

merged 4 commits into from
Jun 5, 2023

Conversation

vrugtehagel
Copy link
Contributor

@vrugtehagel vrugtehagel commented Jun 4, 2023

Related issue: #19358.

This is a regression that seems to have been introduced in #18905. It looks to have been a performance optimization.

The issue is probably easiest described with some code:

const target = new EventTarget();
const event = new Event("foo");
target.addEventListener("foo", () => {
  console.log('base');
  target.addEventListener("foo", () => {
    console.log('nested');
  });
});
target.dispatchEvent(event);

Essentially, the second event listener is being attached while the foo event is still being dispatched. It should then not fire that second event listener, but Deno currently does.

@CLAassistant
Copy link

CLAassistant commented Jun 4, 2023

CLA assistant check
All committers have signed the CLA.

@vrugtehagel vrugtehagel marked this pull request as draft June 4, 2023 09:37
@vrugtehagel vrugtehagel changed the title Copy EventTarget list before dispatch fix(ext/web) Copy EventTarget list before dispatch Jun 4, 2023
@vrugtehagel vrugtehagel changed the title fix(ext/web) Copy EventTarget list before dispatch fix(ext/web): Copy EventTarget list before dispatch Jun 4, 2023
@vrugtehagel vrugtehagel marked this pull request as ready for review June 4, 2023 13:11
if (handlers.length > 1) {
handlers = ArrayPrototypeSlice(targetListeners[type]);
}
const handlers = ArrayPrototypeSlice(targetListeners[type]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this was a useful optimization for us to keep our websocket perf up. Rather than reverting this and always cloning the array, I would suggest that we memoize the handlers.length instead so that in the case where we're not slicing the array, we'll only ever go through the loop once.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Github won't allow me to added suggestions on deleted lines, but I think it would look like the code below:

  let handlers = targetListeners[type];
  const handlersLength = handlers.length;
  // Copy event listeners before iterating since the list can be modified during the iteration.
  if (handlersLength > 1) {
    handlers = ArrayPrototypeSlice(targetListeners[type]);
  }
  
for (let i = 0; i < handlersLength; i++) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! Your suggestion has been committed 🙂

@vrugtehagel vrugtehagel requested a review from mmastrac June 4, 2023 19:55
@aapoalas
Copy link
Collaborator

aapoalas commented Jun 4, 2023

Does this bug appear in the reverse? If you remove a listener when firing an event?

Partially sounds to me like the listener list should be shallowe copied upon add/remove of listeners, and triggers should take an object reference to keep "snapshotted" list

@vrugtehagel
Copy link
Contributor Author

vrugtehagel commented Jun 4, 2023

Hey @aapoalas! No, it does not happen in reverse; it would if the bug existed in the general case, but that's not true. The optimization done (see link in PR description) was just "no shallow copy if there is only one listener attached" which breaks in the example from the test in this PR, but this also means that when adding two or more listeners, things behave like they should. That means that this bug does not exist in the "removal" variant, because the buggy path only exists for when there is one event listener in the first place.

The solution here (after the suggested change) is to

  • If there are more than one listeners, shallow copy (as before);
  • Otherwise, if there is only one, loop through only the first item.

This is functionally equivalent to shallow copying, except avoids shallow copying in the case where there is one listener only (as a performance optimization).

Copy link
Contributor

@mmastrac mmastrac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@mmastrac mmastrac merged commit adf41ed into denoland:main Jun 5, 2023
bartlomieju pushed a commit that referenced this pull request Jun 8, 2023
Related issue: #19358.

This is a regression that seems to have been introduced in
#18905. It looks to have been a
performance optimization.

The issue is probably easiest described with some code:
```ts
const target = new EventTarget();
const event = new Event("foo");
target.addEventListener("foo", () => {
  console.log('base');
  target.addEventListener("foo", () => {
    console.log('nested');
  });
});
target.dispatchEvent(event);
```
Essentially, the second event listener is being attached while the `foo`
event is still being dispatched. It should then not fire that second
event listener, but Deno currently does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants