-
Notifications
You must be signed in to change notification settings - Fork 793
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
[WIP] fix(qsa): greatly improve qsa performance #2679
Conversation
if (describeName === 'without cache') { | ||
it('should find nodes using id, but not in shadow DOM', function() { | ||
var result = axe.utils.querySelectorAllFilter( | ||
dom[0].children[0], | ||
'#one' | ||
); | ||
assert.equal(result.length, 1); | ||
}); | ||
it('should find nodes using id, within a shadow DOM', function() { | ||
var result = axe.utils.querySelectorAllFilter( | ||
dom[0].children[0].children[2], | ||
'#one' | ||
); | ||
assert.equal(result.length, 1); | ||
}); | ||
} |
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 wrapping if
was added.
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.
Why does this not get tested with the cache?
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 you cannot query by ID through the shadow dom
node | ||
) { | ||
return node.actualNode.nodeName !== 'UL'; | ||
|
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 file is mostly just adding the wrapping describe. The first 30 lines are new code, then one additional if statement added in the middle (marked in another comment). The rest is no change
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 tried really hard to make sense of this PR, but the way you're using global cache all over the place, and the reliance on things getting executed in a very specific order that is in no way enforce. I'm not confident that this code works, and I wouldn't trust myself to make changes to this.
Please put some effort into writing this in a way where each function has a single clear purpose, with a strong preference on pure functions, and where if there are any preconditions to execution, those preconditions are obvious and enforced.
// see if the passed in node is the root node of the tree and can | ||
// find nodes using the cache rather than looping through the | ||
// the entire tree | ||
const nodes = getNodesMatchingSelector(domTree, selector, filter); |
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.
From the look of it, you might as well pass in the expressions variable instead of the selector. That way you're not converting it twice, and it makes for a more consistent function signature.
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.
Passing an expression makes testing harder as instead of a simple string I need to parse the selector into the expression first then pass it to the function. I don't feel that passing an expression makes it any more consistent either.
// see if the passed in node is the root node of the tree and can | ||
// find nodes using the cache rather than looping through the | ||
// the entire tree | ||
const nodes = getNodesMatchingSelector(domTree, selector, filter); |
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 nodes = getNodesMatchingSelector(domTree, selector, filter); | |
if (axe._tree === domTree) { | |
return matchSelectorOnTreeRoot(domTree, selector, filter); | |
} |
const selectorMap = domTree[0]._selectorMap; | ||
if (!selectorMap) { | ||
return; | ||
} |
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 selectorMap = domTree[0]._selectorMap; | |
if (!selectorMap) { | |
return; | |
} | |
let selectorMap = cache.get('selectorMap') | |
if (!selectorMap || cache.get('selectorMapRoot') !== domTree) { | |
selectorMap = getSelectorMap(domTree) | |
cache.set('selectorMap', selectorMap) | |
cache.set('selectorMapRoot', domTree) | |
} |
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.
Concerned about use of the cache with shadow DOM given that a shadow DOM test case was excluded from that. Why is this ok?
Closing in favor of #3423 |
Following the QSA proposal, this greatly improves the speed of QSA as it no longer has to scan the entire DOM tree for top-level queries. For large sites the speed increase is substantial.
For https://js.tensorflow.org/api/latest/, the top slowest
performanceTimer
results were as follow:We can see that the
#gather
step is what causes most rules to be slow (and not so much the#runchecks
step), causing axe-core to take 25 seconds to just run through all the rules.By caching node information ahead of time, the top slowest results are now as follows:
This change shaves off 15 seconds from the time needed to run all of axe rules on the site.
In terms of memory, this only increases the size of an axe-core run by a few MB.
Reviewer checks
Required fields, to be filled out by PR reviewer(s)