-
Notifications
You must be signed in to change notification settings - Fork 783
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
Resolve Check.run immediately if not async #1172
Conversation
I suspect this was originally done to prevent browsers from freezing up. Chrome is pretty good about that sort of thing, but others aren't. Firefox for one starts complaining if a script runs for more than a few seconds. Whether or not that's the reason behind this code is just a guess. @dylanb might know more. Either way we'd need to test this change in all supported browsers to make sure none of them choke on a single long execution time. |
Ok, this was added to give the frame messages time to bubble up. Without that timeout, documents with iframes do not collect results from the iframes consistently. Specifically, we saw problems in Firefox. I cannot recall whether the problems also occurred in Chrome. @paulirish I am interested in your suggestion on the "collection of callbacks", can you elaborate? |
@paulirish Another option is to only do the setTimeout if we are checking frames. This would be compatible with the current code but provide callers who know they are not interested in iframes the ability to get the performance improvements |
hey guys, thanks for the quick feedback.
Yeah I thought that might be the case, too. But in this scenario, the deferred work is basically 1 fn call and 1 assignment. And, as it turns out, calling setTimeout is already more expensive than just doing the original work. 😆
I don't 100% understand the aXe runtime, so this could easily make zero sense... Checks happen within rules, right? // at the end of Check.prototype.run ...
thisCheck.parentRule.postCheckCallbacks.push(_ => {
resolve(checkResult);
});
// ...
// then somewhere on the Rule-level you'd schedule the timer
setTimeout(_ => {
rule.postCheckCallbacks.forEach(cb => cb());
}, 0); This would mean just a total of ~40 timers installed rather than 10,000+. I also think it's worthwhile trying to verify the iframe race condition issue is still present, as that seems like the kind of issue Mozilla would address in the past 4 years. (Task scheduling is far more specced now than it was then). |
I talked briefly to @stephenmathieson about this idea and he said he'd be up for looking into a |
First off, I'm sorry for the delay, @paulirish. I haven't had time to take a look here until now. I've pulled this branch down locally and have done some primitive manual testing. Removing the Digging further into that ~18s, I see lots of time spent creating selectors for results (~100ms per invocation). Removing the Thoughts @paulirish @dylanb @WilcoFiers? |
@stephenmathieson First up, I 100% support working on the biggest bottleneck.. though I'm not seeing much of a perf win with Regardless... 'FIX THE PERF OF ALL THE THINGS!!!1' ya?? :) Here's how I did my benchmarking: yarn build; cat axe.js | pbcopy Then I copied axe into a devtools snippet. I evaluated it in this page. In devtools settings, I check "disable async stack traces" because it adds some overhead. Then I ran this in the console: console.time('axe');
await window.axe.run(document, {
elementRef: true,
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa']
},
resultTypes: ['violations', 'inapplicable'],
});
console.timeEnd('axe');
So it's worth ~570ms on this page on my machine. Still big enough that I think it's worth it. I tested your 'fake selector' solution and that looks more like a savings of ~90ms in my setup: |
Looking at the logic in my original PR here I don't see how this
@dylanb Do we have any more details? I'm willing to see if there is a difference in results in Firefox. |
I did my benchmarking nearly the same way, but without any additional axe configuration.
Haha, absolutely!
Agreed that half a second is worth shaving off. 👍
Interesting, I'll take another look there and will provide a more detailed update today. |
Wrapping axe.utils.getSelector = (...args) => {
function createUniqueSelector(...){...}
console.time('getSelector')
const selector = createUniqueSelector(...args)
console.timeEnd('getSelector')
return selector
} Shows between 10-100ms per invocation and a total of about 15s (~15441.0485239ms... 16133.1251224012ms, 14866.4254564811ms, 15289.6802264376ms, 15474.9632903336ms) spent on my machine 🤷♀️ The ticket referenced when this
I briefly tested removing the I'll test more throughly (with the Firefox extension) on Monday. |
sg. for avoiding the slow script warning I think it's reasonable to setTimeout0 between running each Rule. also yah . that's a lot of time generating selectors. funky! |
Additionally, the suggested A |
yes. I looked into the same thing yesterday and agree totally. |
Good and bad news. Bad first: removing the However, the good news is adding I'm going to do some more testing on this, but think this is an acceptable solution to the original problem. |
This patch removes `setTimeout()` call from `Check#run()` and adds it to `Rule#run()`. The `setTimeout()` was originally added in 4657dc5 in order to prevent browsers from "freezing up" while running 1000s of synchronous checks. This had the downside of creating ~10k timers which slows down `axe.run()` considerably. Moving the `setTimeout()` to `Rule#run()` ensures we continue to run asynchronously to prevent "unresponsive script" warnings, but we defer as little as possible. With this patch, we should see ~40 timers created rather than 10k+ since we now create one per Rule rather than one per Check. Hat tip to @paulirish for starting this conversation in #1172.
closing in favor of #1308 |
This patch removes `setTimeout()` call from `Check#run()` and adds it to `Rule#run()`. The `setTimeout()` was originally added in 4657dc5 in order to prevent browsers from "freezing up" while running 1000s of synchronous checks. This had the downside of creating ~10k timers which slows down `axe.run()` considerably. Moving the `setTimeout()` to `Rule#run()` ensures we continue to run asynchronously to prevent "unresponsive script" warnings, but we defer as little as possible. With this patch, we should see ~40 timers created rather than 10k+ since we now create one per Rule rather than one per Check. Hat tip to @paulirish for starting this conversation in #1172. ## Reviewer checks **Required fields, to be filled out by PR reviewer(s)** - [x] Follows the commit message policy, appropriate for next version - [x] Has documentation updated, a DU ticket, or requires no documentation change - [x] Includes new tests, or was unnecessary - [x] Code is reviewed for security by: @WilcoFiers
With performanceTimer on, we get some nice usertiming measures added. But in #701, some _jerk_ picked an endpoint for rules that I, personally, think can be improved. :) This moves the end mark of each rule to when the checks have synchronously finished. This change means we don't include the asynchronous bit afterwards, but that async bit is the subject of #1172. For accurate timing, it makes more sense to keep these measures synchronous. It also makes reading the flame chart more logical. Screenshots of before and after: ![image](https://user-images.githubusercontent.com/39191/50669976-6f0d9300-0f7d-11e9-8f60-24122a577084.png) ## Reviewer checks **Required fields, to be filled out by PR reviewer(s)** - [x] Follows the commit message policy, appropriate for next version - [x] Has documentation updated, a DU ticket, or requires no documentation change - [x] Includes new tests, or was unnecessary - [x] Code is reviewed for security by: @WilcoFiers
Running axe on this URL takes ~13s. But towards the end of the profile is an interesting block of time:
Zoomed out it looks like a lot of work, but it's actually quite sparse when you zoom in:
It's 1.3s of timers firing. :) Turns out each timer fire has about 0.1ms of overhead, and this was ~13,000 timers.
I traced it back to the setTimeout here, which doesn't appear to be necessary. But there may be context for why it was originally added that I don't know.
If it is necessary to keep it deferred, you could collect all these as callbacks and then schedule one setTimeout which will then execute all of them. That'd be just as fast.