Skip to content

Conversation

@topaxi
Copy link
Contributor

@topaxi topaxi commented Jan 14, 2016

The event dispatch method is not optimizable by modern JS engines. This is especially crucial if a lot of events are dispatched (mousemove and touchmove come to mind).

no-param-reassign
Fixed by not reassigning function param

dont-pass-arguments
Fixed by not pass arguments object to other Array#slice()

Now optimizable by v8, spidermonkey and most probably all the other engines:
optimizable

\o/

@mention-bot
Copy link

By analyzing the blame information on this pull request, we identified @dmethvin, @timmywil and @gibson042 to be potential reviewers

@jquerybot
Copy link

Thank you for your pull request. It looks like this may be your first contribution to a jQuery Foundation project, if so we need you to sign our Contributor License Agreement (CLA).

📝 Please visit http://contribute.jquery.org/CLA/ to sign.

After you signed, the PR is checked again automatically after a minute. If there's still an issue, please reply here to let us know.


If you've already signed our CLA, it's possible your git author information doesn't match your CLA signature (both your name and email have to match), for more information, check the status of your CLA check.

@dmethvin
Copy link
Member

@topaxi topaxi force-pushed the optimizable-event-dispatch branch from 0dfba7b to 94ac452 Compare January 14, 2016 20:22
- Do not assign to function parameters
- Do not pass arguments object to other functions
@topaxi topaxi force-pushed the optimizable-event-dispatch branch from 94ac452 to 247589e Compare January 14, 2016 20:24
@topaxi
Copy link
Contributor Author

topaxi commented Jan 14, 2016

No real perf data yet. Can look into it tomorrow :)

@topaxi
Copy link
Contributor Author

topaxi commented Jan 15, 2016

So I'm not sure how to build actual meaningful perf benchmarks.
I used jsperf.com with the following setup: http://jsperf.com/jquery-optimized-event-dispatch

While in the devtools the event.dispatch method shows up a lot less and faster, the overall test seems to run at the same speed or even slower in the "optimized" version.

Chrome Profile:
jsperf-jquery-event-dispatch chrome

Firefox Profile:
jsperf-jquery-event-dispatch ff

@topaxi topaxi force-pushed the optimizable-event-dispatch branch from 900cae0 to 76e710d Compare January 15, 2016 08:27
@markelog
Copy link
Member

I really, really hate this kind of hacks.

What articles like https://github.com/petkaantonov/bluebird/wiki/Optimization-killers forget to tell you, is that you need to improve performance only those parts of code that are slow, but not those parts that could be optimized - if you can, doesn't mean you should do... the definition of premature opmitization.

Like what if we do this - increase the size, introduce additional complexity, all that. But the next day, new Chrome comes out and with it, new v8 and with it, new Crankshaft/TurboFan, which do the optimization for arguments value but in the same time... new FF comes out and with it, new spiderMonkey and with it, new IonMonkey/or any other monkey, which bails on functions where arguments object is used in loops or something.

So now we left with increased size, convoluted logic and without any speed improvements in Chrome but with decreased speed in FF.

Bluebird is the most obfuscated, weird and unsupported code there is on JS, pretty much no one understand it, except for its author... and that for lib that doesn't have to be that fast because it deals with async operations anyways, just like jQuery doesn't have to that fast because it deals with DOM which is always the slower part of any front-end JS there is, this is probably because you don't see any "improvements" in your devtools.

Since there is no argument for this change i.e. -

run at the same speed or even slower in the "optimized" version

I'm closing it

@markelog markelog closed this Jan 15, 2016
@topaxi
Copy link
Contributor Author

topaxi commented Jan 15, 2016

this is probably because you don't see any "improvements" in your devtools

Well I do see improvements in the devtools, I just don't see them in my pseudo-benchmark on jsperf.com. I'm not even sure if the jsperf.com benchmarks are reliable for these kind of things.

When I use this change with mousemove events for drawing a rectangle in my browser I even see more frames in Firefox.

I'm currently programming a "selectable" like extension for ember here: https://github.com/topaxi/ember-snip

Using https://github.com/mrdoob/stats.js/ I see the following FPS on my notebook while randomly dragging around on the screen:

Firefox:
master: 10-25fps
this PR: 25-40fps

Chrome:
Both: 60fps

I suspect this change might even have a bigger impact on mobile devices.

If these "hacks" are not welcome, feel free to leave this PR closed.

@markelog
Copy link
Member

I'm not even sure if the jsperf.com benchmarks are reliable for these kind of things.

It is reliable, it is specifically build for these kind of things.

I see the following FPS on my notebook while randomly dragging around on the screen:

If you could put together something that we could check ourselves?

I'm still strongly against these changes, but it is enough evidence for re-opining

@topaxi
Copy link
Contributor Author

topaxi commented Jan 15, 2016

After reading through http://blog.methvin.com/2013/12/please-stop-jsperfcom-abuse.html by @dmethvin I don't feel very confident about making a real test-case based on jsperf.com.

@dmethvin Would you have time to create some benchmark with me this weekend or some time next week? You can reach me on twitter (@topaxi) or irc.freenode.org (topaxi).

Edit: Updated JSPerf for the dispatch function only: http://jsperf.com/jquery-optimized-event-dispatch/4
This shows ~40-50% speedups in Firefox, ~32% Mobile Firefox, ~15% in Chrome, ~23% in Mobile Chrome.

@markelog
Copy link
Member

Nice job of figuring out how to do a bench test, your results are compelling, added couple more browsers to your data, they all show either same results (however i suspect this is benchmark.js infelicity, not by much, but i think it should be around 1-3% faster
) or improved performance.

However, jQuery.event.dispatch is still private API (since it is not documented). Prime consumers of which is jQuery#trigger and real events.

We can't test real events (if would like to came up with some test-suite that would be great though), we can test jQuery#trigger though and those results are quite the opposite of yours - http://jsperf.com/trigger-vs-opt-trigger/3 or more so, since now all browsers show speed decrease from 15-50%

@topaxi
Copy link
Contributor Author

topaxi commented Jan 15, 2016

Yes this is the same result I mentioned in my first (well second really) comment. This is why I added a jQuery.event.dispatch() only test. While I know that actual events should be measured and our tests probably aren't near real-world enough. This is why I asked @dmethvin for help, to do a proper benchmark.

The jQuery#trigger() slowdown is quite interesting, I guess the engines are trying too hard/long to optimize?

@topaxi
Copy link
Contributor Author

topaxi commented Jan 15, 2016

@markelog I updated your bench using a mousemove event (which I observed the real-world speedups in my browsers), as triggering a click event (which occurs not very much, usually...) has a special case in https://github.com/jquery/jquery/blob/master/src/event.js#L505.

http://jsperf.com/trigger-vs-opt-trigger/4

Chrome seems slower on my desktop, but faster on mobile.
Firefox again seems much much faster.

@dmethvin
Copy link
Member

I can't look at this in detail right now but we would definitely want to see the full path with real events and/or .trigger(). Just for kicks I did a profiler run on this page (the Github one you're on now) after spinning the mouse around for a while and jQuery.event.dispatch does end up high on the list.
image

@markelog
Copy link
Member

Added couple more browsers to your example, on my chrome mobile (although "mouse" event on mobile?) it doesn't show any improvements, latest IE (those three dots), Safari (just like FF but in the opposite way) (and even sometimes FF) mobile Safari (expectedly, {although "mouse" event on mobile?}) still show significant speed decrease.

Whenever or not or our click hook in anyway responsable for difference is a mystery, we also have mouse hooks, might they related to speed improve in FF for mousemove event?

That sort of inconsistency is what i was ranting about, you did chrome optimization and yet you saw FF only improvements. "Tomorrow" it might be the opposite. Just shot in the dark it seems.

I agree we need to improve performance of event handling if we can, but i'd argue that fast compiler optimizations is not the way.

@markelog
Copy link
Member

While I know that actual events should be measured and our tests probably aren't near real-world enough

Actually, triggering events is pretty common use-case, i would think they are reflect the "real world" cases.

@topaxi
Copy link
Contributor Author

topaxi commented Jan 15, 2016

Actually, triggering events is pretty common use-case, i would think they are reflect the "real world".

Yes, but not as frequent as mousemove or touchmove events, they fire very rapidly. If a click event or similar costs even 10ms more and a mouse- or touchmove costs 5ms less, it might actually be worth it.

@markelog
Copy link
Member

I was questioning the

aren't near real-world enough

part, not whenever or not it would be worth it. That would be a different discussion.

@topaxi
Copy link
Contributor Author

topaxi commented Jan 15, 2016

Anyways, what does the core team need to merge or close this request? I'll be happy to work out some approved benchmarks.

On all my observations and profiling, jQuery.event.dispatch() always shows up way less and way faster in my branch.
There are cases where this slows down, but I have no deeper knowledge of how big this impact is.
It might be more interesting to find out what causes the other cases to slow down, I haven't noticed any other functions slowing down by these changes. Maybe more time gets spent in the compilers?

I think it should be worthwhile to remove code that actually prevents engines from optimization. I wasn't targeting any engine as all of them have troubles with those two changes I made.
While the performance in this PR might not matter, by removing deoptimizations, engines could improve performance in future versions, without overcoming these (probably, I'm no JIT programmer) complex deoptimizations.
If adding one loop is too much complexity, then feel free to close this immediately. Personally I feel a for loop is way clearer than calling Array#slice() on arguments.

On a side-note, other projects like Ember.js even put this in their guidelines (I respect your choice to not add such rules to yours).
Ember.js STYLEGUIDE

@markelog
Copy link
Member

Anyways, what does the core team need to merge or close this request?

I'd say proof of speed increase, right now, we definitely established that it will cause speed decrease. Which is the opposite.

If real events would work faster, we would need to decide what is more important -
increase the speed of events dispatch or convolute the logic, increase the size and worsen the trigger() case.

Now of "how", is the part where you can get creative :-). There is no universally good way to do it.

One might argue that you can take real site, profile it, with latest version of jQuery with and without your changes, on all browsers you have access to. Or you can use the benchmarks.js with selenium and your layer, basically writing your bench suite that would be activated with real user events (wow!).

I think it should be worthwhile to remove code that actually prevents engines from optimization.

That is that part i disagree with, i don't care whenever code is optimized or not, i care if that code is fast enough.

If adding one loop is too much complexity, then feel free to close this immediately. Personally I feel a for loop is way clearer than calling Array#slice() on arguments.

On a side-note, other projects like Ember.js even put this in their guidelines (I respect your choice to not add such rules to yours).
Ember.js STYLEGUIDE

This is why i bring up Bluebird, a lot of people praised and impress with it. Without understanding it.

event module is one of the complicated modules we have and if we would add engine optimization code in there, like this - https://github.com/petkaantonov/bluebird/blob/dec7f0296209a8552c887421ecbec3d4b99565e6/src/util.js#L196 (Basically you need to write the whole article to explain every line of code, that is the article you use to argue your point actually) it might just became unmaintainable.

not add such rules to yours

We might just do that, if you will able to prove your point :-)

@dmethvin
Copy link
Member

The mousemove/pointermove scenarios (and maybe window.scroll) are the most plausible scenarios where the event subsystem might cause a bottleneck. Once you get all the other parts of the execution path involved, though, it may be that deoptimization of jQuery.event.dispatch isn't enough of a difference to be noticed. In that case it would be good to find the real bottlenecks and fix those, either in addition to or in place of this patch.

One real perf problem that we know of is that with real events we currently copy and normalize the event object as soon as the event arrives, through jQuery.event.fix. This incurs a constant-time overhead up front, which can be large for mouse/pointer events because referencing some of those properties can force layout. We should really try to get #2337 landed, @jbedard had a great approach for fixing that.

I would love to see some kind of real-world repeatable benchmark we could use to judge whether we're really improving things or just optimizing code for no reason.

@topaxi
Copy link
Contributor Author

topaxi commented Jan 16, 2016

Just a small update, I took the benchmark of @markelog and reversed the examples (http://jsperf.com/trigger-vs-opt-trigger/5), seems like the second one is always slower. Even putting the same two test-cases causes the second one to slow down by around 20%. I guess some GC behaviour is interfering here.

I'll look into some other way of doing the benchmarks later this evening. Even if this means I have to do one test-case at a time and do a Ops/sec table by hand.

@markelog
Copy link
Member

@markelog
Copy link
Member

Gotta say, i hated proving opposite point :/ it is hard to be impartial

@markelog
Copy link
Member

Hm, runned tests couple more times, results began to differ o_O.

@jdalton little help here?

How it could be that those tests results are different - 1, 2

@topaxi
Copy link
Contributor Author

topaxi commented Jan 18, 2016

So I made a single test for each case and ran each test 3 times. This is the result I got on my notebook:

Direct call to event dispatch

Original: http://jsperf.com/jquery-optimized-event-dispatch/6
Chrome: 39,013 Ops/sec (3 Runs)
Firefox: 47,880 Ops/sec (3 Runs)

Optimized: http://jsperf.com/jquery-optimized-event-dispatch/7
Chrome: 43,820 Ops/sec (3 Runs)
Firefox: 65,056 Ops/sec (3 Runs)

Diff for direct call to dispatch:
Chrome: 12.3% faster
Firefox: 35.8% faster

Trigger click

Original: http://jsperf.com/jquery-optimized-event-dispatch/8
Chrome: 10,367 Ops/sec (3 Runs)
Firefox: 8,493 Ops/sec (3 Runs)

Optimized: http://jsperf.com/jquery-optimized-event-dispatch/9
Chrome: 14,120 Ops/sec (3 Runs)
Firefox: 12,638 Ops/sec (3 Runs)

Diff for trigger click:
Chrome: 36.2% faster
Firefox: 48.8% faster

Trigger mousemove

Original: http://jsperf.com/jquery-optimized-event-dispatch/10
Chrome: 9,921 Ops/sec (3 Runs)
Firefox: 7,242 Ops/sec (3 Runs)

Optimized: http://jsperf.com/jquery-optimized-event-dispatch/11
Chrome: 15,732 Ops/sec (3 Runs)
Firefox: 20,139 Ops/sec (3 Runs)

Diff for trigger mousemove:
Chrome: 58.6%
Firefox: 178.1%

@timmywil
Copy link
Member

@dmethvin What you do think? Should we land?

@dmethvin
Copy link
Member

So the con to landing it is that it's a Chrome-specific optimization, I don't know if other browsers disable optimization when arguments passed to another function. There is a danger that we end up with cargo-cult patterns that we can't explain later, which is I think was the concern @markelog had.

On the pro side, we already do browser-specific things like isolate try/catch into a function because of Chrome, even though other browsers like Edge will optimize such functions. Our team has been stable enough (yay us!) that we'll have enough organizational memory to come back and change things if it turns out it is no longer needed. Plus it's nice to see the dispatch speed double for cases like mousemove and pointermove which can be bottlenecks.

So what the heck, let's land it. Nice work @topaxi!

Since @topaxi already has a framework to test perf for these cases, I'd love to see #2337 comment happen in 3.0 so that jQuery.event.fix could be faster and we could support more properties directly on our event without up-front copy overyead. @jbedard do you see any problems with trying to freshen up your old code? Maybe @topaxi could give you some help there.

@jbedard
Copy link
Contributor

jbedard commented Jan 22, 2016

Here's an updated version of my change which @dmethvin mentioned if anyone wants to try it out. There were definitely still a few questions about that though. Especially if the perf hit of getter/setters is worth avoiding the fix-loop... I seem to recall the set on first get was also questionable.

There also appears to be an argument assignment in jQuery.event.fix that might be worth changing in this PR...

@dmethvin
Copy link
Member

@jbedard your link isn't working for me. Can you open a new PR? we can discuss/experiment there. Nice catch on the arg assignment!

@jbedard
Copy link
Contributor

jbedard commented Jan 23, 2016

Opps, I've fixed the link in the comment. Also opened #2860.

@markelog
Copy link
Member

markelog commented Jan 23, 2016

I would ask for some clarification comment before the loop.

Also, i'm not sure if actually see speed increase, right now, proof seems inconclusive, in one test we see speed decrease in the other speed increase but for Firefox only, whereas this should be v8 specific opt. Without any explanation.

Would still love some explanation from the @jdalton and his overall opinion about it.
@dmethvin if you certain this is right move, would you mind profile it, i would feel much more certain about it.

@jbedard
Copy link
Contributor

jbedard commented Jan 23, 2016

I'd be curious if only removing the argument assignment would make a difference, but keeping the slice.call( arguments ). Those are separate "not optimized" warnings and might deserve separate fixes/discussions...

@topaxi
Copy link
Contributor Author

topaxi commented Jan 25, 2016

@jbedard param assignments are only bad if you do this while also using an arguments object in your function (at least that's how I'm understanding it and observed so far, but I'm definitely not 100% sure here).

The issue is that the arguments and param interaction in JavaScript is quite weird and imho very dangerous.

A small example:

function foo(bar, baz) {
  console.log(arguments[0] === 'bar')
  console.log(arguments[1] === 'baz')
  // so far so good.. but if we assign something to arguments
  arguments[0] = 'foo'
  console.log(arguments[0] === 'foo') // still good
  console.log(bar === 'foo') // Oops, by assigning arguments[0] we also reassigned bar
  // this also works the other way around
  baz = 'foo'
  console.log(baz === 'foo') // Okay, this is what we wanted
  console.log(arguments[1] === 'foo') // Oops, we somehow changed something in arguments
}

foo('bar', 'baz') // this logs 6 times `true`

And this is why all engines (to date) deoptimize if we pass arguments around or assign to params when an arguments object is being used.

So to avoid these mistakes, some guidelines agreed to avoid param reassigned at all times, while it (at least to my knowledge) is only bad while using arguments in the same function.

To summarize params and arguments optimization issues:

// OK, param reassignment but no arguments object
function(foo) {
  foo = somethingelse()
}
// Bad, param reassignment with arguments object
function(foo) {
  foo = somethingelse()
  console.log(arguments[0])
}
// Bad, passing arguments to an other function
function(foo) {
  var args = Array.prototype.slice.call(arguments)
  console.log(args[0])
}
// Good, converting arguments to array
function(foo) {
  var args = new Array(arguments.length)
  for (var i = 0; i < arguments.length; i++) {
    args[i] = arguments[i]
  }
  console.log(args[0])
}

@jbedard
Copy link
Contributor

jbedard commented Jan 26, 2016

Still sounds like a good thing to do, and it can be done without the slice change...

@dmethvin
Copy link
Member

I'd like to land this, with a small addition of a comment that says we're doing the explicit loop to allow JS engines to optimize the function. Any objections?

@mgol
Copy link
Member

mgol commented Apr 26, 2016

@dmethvin SGTM.

@dmethvin dmethvin closed this in 9f268ca Apr 27, 2016
@lock lock bot locked as resolved and limited conversation to collaborators Jan 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

8 participants