-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
events: make abort_controller event trusted #35811
events: make abort_controller event trusted #35811
Conversation
lib/internal/event_target.js
Outdated
get: isTrusted, | ||
get() { | ||
return this[kTrustEvent]; | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid this:
- It tanks perf, see events: re-use the same isTrusted getter #34459 / https://twitter.com/tverwaes/status/1285497796540473344
- If we make
kTrustEvent
a symbol property, it’s not unforgeable anymore, which means we might as well stop using an own property here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax given these conflicting requirements (unforgeable and the fact we need some events to be trusted internally) - what should we do?
Regarding performance - I am not sure the performance of isTrusted for EventTarget is that important (is it?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax hmm, actually I can capture it from the closure - let me try and let me know what you think - though it will likely tank performance just as bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure the performance of isTrusted for EventTarget is that important (is it?)
It’s not the performance of isTrusted
, it’s the performance of new Event()
that matters here :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax please take a look. I think what I ended up doing shouldn't tank performance or introduce a closure. Should I run benchmarks?
(Also thanks, I didn't know this tanks perf 🙏 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I run benchmarks?
I think that would be helpful, yes :) And maybe also update the benchmarks to account for situations in which sometimes new Event()
creates a trusted event and sometimes not :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM if the benchmarks are good with it :)
Looks mostly OK: benjamingruenbaum@Benjamins-MBP node % git checkout fix-abortcontroller-trusted-event
Switched to branch 'fix-abortcontroller-trusted-event'
benjamingruenbaum@Benjamins-MBP node % make -j12 > /dev/null
benjamingruenbaum@Benjamins-MBP node % ./out/Release/node ./benchmark/events/eventtarget.js
events/eventtarget.js listeners=1 n=1000000: 3,067,260.432666775
events/eventtarget.js listeners=5 n=1000000: 2,797,382.2678592885
events/eventtarget.js listeners=10 n=1000000: 2,318,102.157338759
benjamingruenbaum@Benjamins-MBP node % git checkout master
Switched to branch 'master'
benjamingruenbaum@Benjamins-MBP node % make -j12 > /dev/null
benjamingruenbaum@Benjamins-MBP node % ./out/Release/node ./benchmark/events/eventtarget.js
events/eventtarget.js listeners=1 n=1000000: 3,144,785.5756887244
events/eventtarget.js listeners=5 n=1000000: 2,704,624.177365229
events/eventtarget.js listeners=10 n=1000000: 2,340,452.7737892433
benjamingruenbaum@Benjamins-MBP node % Just for sport - here are benchmarks with the "old" (property) fix: benjamingruenbaum@Benjamins-MBP node % ./out/Release/node ./benchmark/events/eventtarget.js
events/eventtarget.js listeners=1 n=1000000: 1,039,763.9506252201
events/eventtarget.js listeners=5 n=1000000: 937,062.0643370639
events/eventtarget.js listeners=10 n=1000000: 829,347.3311018195 I ran each a few times (but didn't run the t-test thing). Anyway - good catch @addaleax :] |
lib/internal/event_target.js
Outdated
const isTrustedFalse = () => false; | ||
const isTrustedTrue = () => true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use a SafeWeakSet
, so that the isTrusted
getter function between a trusted and untrusted event is the same:
Object.getOwnPropertyDescriptor(trustedEvent, "isTrusted").get === Object.getOwnPropertyDescriptor(untrustedEvent, 'isTrusted').get;
const isTrustedFalse = () => false; | |
const isTrustedTrue = () => true; | |
const isTrustedSet = new SafeWeakSet(); | |
const isTrusted = ObjectGetOwnPropertyDescriptor({ | |
get isTrusted() { | |
return isTrustedSet.has(this); | |
} | |
}, 'isTrusted').get; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see Anna's comment above - this would introduce a (rather big) performance regression. The initial implementation was a bit like this (except without a WeakSet which IIRC incurs its own separate performance penalty in 8).
That said - that's a very good point (regarding wanting the reference to be the same for false/true cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, @addaleax I would like to (if possible) make the issue @ExE-Boss raised work, namely:
Object.getOwnPropertyDescriptor(trustedEvent, "isTrusted").get === Object.getOwnPropertyDescriptor(untrustedEvent, 'isTrusted').get;
Any clever trick or idea for getting the reference of different functions to equal without creating an additional closure for every event target or running into the "have a non-configurable own accessor property on an object without the overhead of creating the dictionary for slow properties" thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm … I think the above suggestion by @ExE-Boss might work rather well, actually? If we always use the same getter, then performance-wise we’re good, and SafeWeakSet
should give us the guarantees we’d want here in terms of unforgeability
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I think I misunderstood the solution - this is actually pretty clever (creating the temp object and getting the reference to avoid the different property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Benchmarks look good.
lib/internal/event_target.js
Outdated
// isTrusted is special (LegacyUnforgeable) | ||
ObjectDefineProperty(this, 'isTrusted', { | ||
get: isTrusted, | ||
get: shouldTrust ? isTrustedTrue : isTrustedFalse, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get: shouldTrust ? isTrustedTrue : isTrustedFalse, | |
get: isTrusted, |
lib/internal/event_target.js
Outdated
@@ -75,9 +77,11 @@ class Event { | |||
this[kDefaultPrevented] = false; | |||
this[kTimestamp] = lazyNow(); | |||
this[kPropagationStopped] = false; | |||
const shouldTrust = (options != null && options[kTrustEvent]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const shouldTrust = (options != null && options[kTrustEvent]); | |
if (options != null && options[kTrustEvent]) { | |
isTrustedSet.add(this); | |
} |
PTAL @ExE-Boss - neat trick 🙏 |
d66fba0
to
2bc7831
Compare
P.S. since I applied the changes manually and I feel like your contribution to this PR was important - I have added you as Co-Authored-By (same as if I pressed ""Commit Suggestion" in GitHub when squashing). I have used the same username/email combo GitHub used for you when this happened in the past - but if you prefer a different email/name combo please let me know and I'll gladly change it to whatever. Alternatively if you want me to remove you as "Co-Authored-By" let me know and I will do that instead. Whatever you prefer. |
ebec379
to
7fc3d02
Compare
The docs should be updated also: See: https://nodejs.org/dist/latest-v15.x/docs/api/events.html#events_event_istrusted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with any necessary doc updates applied
Any idea on what the docs should say?
Or something? |
That works, I think. We can revisit it later if necessary. |
lib/internal/event_target.js
Outdated
Symbol, | ||
SymbolFor, | ||
SafeWeakSet, | ||
ObjectGetOwnPropertyDescriptor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be sorted alphabetically:
Symbol, | |
SymbolFor, | |
SafeWeakSet, | |
ObjectGetOwnPropertyDescriptor, | |
ObjectGetOwnPropertyDescriptor, | |
SafeWeakSet, | |
Symbol, | |
SymbolFor, |
get isTrusted() { | ||
return isTrustedSet.has(this); | ||
} | ||
}, 'isTrusted').get; | ||
|
||
class Event { | ||
constructor(type, options) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To ensure that the Event
constructor has the correct length
, options
should be initialised to null
:
constructor(type, options) { | |
constructor(type, options = null) { |
This also makes it possible to use strict equality comparison when checking options !== null
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like a separate change from this one - @jasnell given past experience - would you prefer it if I made it here or if we opened an issue and it went on a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either works I think. If you do it here make it a separate Commit but a separate pr also works
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I don't understand this - IIUC length
is determined by the number of arguments - so Event.length
should be 2 anyway.
I can add a test for that though - I'll push it in #35806
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The length
property defaults to the number of leading parameters without an initializer, and options = null
makes the parameter have an initializer, thus the length
will be 1
:
https://tc39.es/ecma262/#sec-function-definitions-static-semantics-expectedargumentcount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed it in the other PR :]
ed336bd
to
0e3da51
Compare
const isTrusted = ObjectGetOwnPropertyDescriptor({ | ||
get isTrusted() { | ||
return isTrustedSet.has(this); | ||
} | ||
}, 'isTrusted').get; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const isTrusted = ObjectGetOwnPropertyDescriptor({ | |
get isTrusted() { | |
return isTrustedSet.has(this); | |
} | |
}, 'isTrusted').get; | |
function isTrusted() { | |
return isTrustedSet.has(this); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That will make the isTrusted
getter function incorrectly have an own prototype
property and the [[Construct]]
internal method.
It will also cause it to have the wrong name
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ExE-Boss would you mind explaining why this doesn't work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the following will no longer throw a TypeError
:
new (Object.getOwnPropertyDescriptor(new Event('foo'), "isTrusted").get)();
And the following will return "object"
instead of "undefined"
:
typeof Object.getOwnPropertyDescriptor(new Event('foo'), "isTrusted").get.prototype;
And the following will return true
instead of false
:
Object.getOwnPropertyDescriptor(new Event('foo'), "isTrusted").get.hasOwnProperty("prototype");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a test for this across the EventTarget properties :]
565cb05
to
521bc99
Compare
doc/api/events.md
Outdated
@@ -1282,9 +1282,11 @@ This is not used in Node.js and is provided purely for completeness. | |||
added: v14.5.0 | |||
--> | |||
|
|||
* Type: {boolean} Always returns `false`. | |||
* Type: {boolean} Returns `false` for user constructed events and `true` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Type: {boolean} Returns `false` for user constructed events and `true` | |
* Type: {boolean} Value is `false` for user-constructed events and `true` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All other properties (like event.returnValue
) just use "True if..." so I will change to that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that works.
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/673/ Update: Results show no significant change:
|
@@ -4,8 +4,10 @@ | |||
const common = require('../common'); | |||
|
|||
const { ok, strictEqual } = require('assert'); | |||
const { Event } = require('internal/event_target'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is unnecessary, as the Event
constructor is available as a global property.
const { Event } = require('internal/event_target'); |
It also causes the test to fail without the ‑‑expose‑internals
flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Event is currently not exposed globally as far as I know. Good catch on the flag - I'll add that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Event is not exposed at all at the moment because as you can tell - there are still a bunch of rough edges we need to iron out - I intended to work on this a lot more a few months ago and am only getting to it now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Event
is exposed since #35496, which shipped in v15.0.0:
node/lib/internal/bootstrap/node.js
Lines 143 to 148 in 9ff25b1
const { | |
EventTarget, | |
Event, | |
} = require('internal/event_target'); | |
exposeInterface(global, 'EventTarget', EventTarget); | |
exposeInterface(global, 'Event', Event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/ping @benjamingr Good to make this change or not so much?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no strong feelings regarding this - I didn't realize that we expose event globally and totally missed it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ExE-Boss very cool I missed this!
The AbortController abort event is trusted, currently we fire all events with isTrusted: false. Allow dispatching events internally with `isTrusted: true` and add a test for it. Co-Authored-By: ExE Boss <[email protected]> Fixes: nodejs#35748
521bc99
to
5ccc2a9
Compare
Thanks for the review and benchmark run Trott - please take a look at the wording :] |
You can apply the I'll add the In general, there are a number of cases where The up-to-date process for landing a PR would ideally be in https://github.com/nodejs/node/blob/master/doc/guides/collaborator-guide.md#landing-pull-requests but I see that the |
@Trott ok - I see there are two failed tests in the CI that are unrelated - how should I proceed? |
Best thing to do with unrelated failures is to use "Resume Build" on the left nav. That will rebuild only the Jenkins subtasks that failed before. I'll go ahead and click "Resume Build" right now so you don't have to. Optional thing you can do if you want to be a good citizen: Check to make sure that there are open issues for the tests that failed.
|
Landed in 81ba3ae...f20a783 |
The AbortController abort event is trusted, currently we fire all events with isTrusted: false. Allow dispatching events internally with `isTrusted: true` and add a test for it. Co-Authored-By: ExE Boss <[email protected]> Fixes: #35748 PR-URL: #35811 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
@Trott it worked - this is pretty cool! |
The AbortController abort event is trusted, currently we fire all events with isTrusted: false. Allow dispatching events internally with `isTrusted: true` and add a test for it. Co-Authored-By: ExE Boss <[email protected]> Fixes: #35748 PR-URL: #35811 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
The AbortController abort event is trusted, currently we fire all events with isTrusted: false. Allow dispatching events internally with `isTrusted: true` and add a test for it. Co-Authored-By: ExE Boss <[email protected]> Fixes: nodejs#35748 PR-URL: nodejs#35811 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
The AbortController abort event is trusted, currently we fire all events with isTrusted: false. Allow dispatching events internally with `isTrusted: true` and add a test for it. Co-Authored-By: ExE Boss <[email protected]> Fixes: nodejs#35748 PR-URL: nodejs#35811 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
The AbortController abort event is trusted, currently we fire all events with isTrusted: false. Allow dispatching events internally with `isTrusted: true` and add a test for it. Co-Authored-By: ExE Boss <[email protected]> Fixes: nodejs#35748 PR-URL: nodejs#35811 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
The AbortController abort event is trusted, currently we fire all events with isTrusted: false. Allow dispatching events internally with `isTrusted: true` and add a test for it. Co-Authored-By: ExE Boss <[email protected]> Fixes: #35748 PR-URL: #35811 Backport-PR-URL: #38386 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
Fire
abort
event as trusted, fixes #35748Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes