From 62f5d750fcab4812dfdee019bc57d9e87eeddd40 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 5 May 2025 15:05:10 -0600 Subject: [PATCH 1/2] faster selectors! --- lib/core/utils/get-selector.js | 290 +++++++++++++++++++++++++++++---- test/playground.html | 286 ++++++++++++++++++++++++++++++-- test/run.js | 100 ++++++++++++ test/time.xlsx | Bin 0 -> 17092 bytes 4 files changed, 629 insertions(+), 47 deletions(-) create mode 100755 test/run.js create mode 100644 test/time.xlsx diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index 16e156f5fa..c0e2450ab6 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -6,6 +6,7 @@ import isXHTML from './is-xhtml'; import getShadowSelector from './get-shadow-selector'; import memoize from './memoize'; import constants from '../../core/constants'; +import getNodeFromTree from './get-node-from-tree'; const ignoredAttributes = [ 'class', @@ -104,7 +105,10 @@ export function getSelectorData(domTree) { const data = { classes: {}, tags: {}, - attributes: {} + attributes: {}, + frequency: {}, + newFreq: {}, + otherFreq: {}, }; domTree = Array.isArray(domTree) ? domTree : [domTree]; @@ -113,6 +117,8 @@ export function getSelectorData(domTree) { while (currentLevel.length) { const current = currentLevel.pop(); const node = current.actualNode; + const values = {}; + const newVals = []; if (!!node.querySelectorAll) { // ignore #text nodes @@ -125,10 +131,24 @@ export function getSelectorData(domTree) { data.tags[tag] = 1; } + data.otherFreq[tag] ??= {}; + + if (node.id) { + values[`#${node.id}`] = 1; + newVals.push(`#${node.id}`) + + data.otherFreq[tag][`#${node.id}`] ??= new Set(); + data.otherFreq[tag][`#${node.id}`].add(node); + } + // count all the classes if (node.classList) { Array.from(node.classList).forEach(cl => { const ind = escapeSelector(cl); + values[`.${ind}`] = 1 + newVals.push(`.${ind}`) + data.otherFreq[tag][`.${ind}`] ??= new Set(); + data.otherFreq[tag][`.${ind}`].add(node); if (data.classes[ind]) { data.classes[ind]++; } else { @@ -144,6 +164,11 @@ export function getSelectorData(domTree) { .forEach(at => { const atnv = getAttributeNameValue(node, at); if (atnv) { + values[`[${atnv}]`] = 1; + newVals.push(`[${atnv}]`) + data.otherFreq[tag][`[${atnv}]`] ??= new Set(); + data.otherFreq[tag][`[${atnv}]`].add(node); + if (data.attributes[atnv]) { data.attributes[atnv]++; } else { @@ -152,6 +177,11 @@ export function getSelectorData(domTree) { } }); } + + data.frequency[tag] ??= new Map(); + data.frequency[tag].set(node, values); + data.newFreq[tag] ??= new Map(); + data.newFreq[tag].set(node, newVals); } if (current.children.length) { // "recurse" @@ -165,6 +195,175 @@ export function getSelectorData(domTree) { return data; } +function generateFrequency(nodes) { + const ret = {}; + + nodes.forEach(node => { + // count the tag + const tag = node.nodeName; + if (node.id) { + ret[`#${node.id}`] ??= []; + ret[`#${node.id}`].push(node); + } + + // count all the classes + if (node.classList) { + Array.from(node.classList).forEach(cl => { + const ind = escapeSelector(cl); + ret[`.${ind}`] ??= []; + ret[`.${ind}`].push(node); + }); + } + + // count all the filtered attributes + if (node.hasAttributes()) { + Array.from(node.attributes) + .filter(filterAttributes) + .forEach(at => { + const atnv = getAttributeNameValue(node, at); + if (atnv) { + ret[`[${atnv}]`] ??= []; + ret[`[${atnv}]`].push(node); + } + }); + } + }); + + return ret; +} + +function leastFrequent(node, freqList) { + // shadow dom stuff causes problems + if (!freqList) { + return { + selector: '', + unique: false + } + } + + let selector = node.nodeName.toLowerCase(); + + // unique by default + if (selector === 'body' || selector === 'html') { + return { + selector, + unique: true + } + } + + const values = freqList.get(node); + let freq = axe._selectorData.otherFreq[node.nodeName]; + let min = Infinity; + + while (min > 1) { + let group; + let value = ''; + for (let i = 0; i < values.length; i++) { + const count = freq[ values[i] ].size + if (count < min) { + min = count; + value = values[i]; + group = freq[ values[i] ]; + } + } + + selector += value; + + // TODO: does 1 more path + if (!group) { + return { + selector, + unique: false + }; + } + + if (min > 1) { + const newFreq = {} + for (let i = 0; i < values.length; i++) { + if (values[i] === value) { + newFreq[ values[i] ] = freq[ values[i] ] + } + else { + newFreq[ values[i] ] = freq[ values[i] ].intersection( freq[value] ) + } + } + freq = newFreq; + } + } + + return { + selector, + unique: true + }; +} + +// function leastFrequent(node, feqList) { +// let selector = ''; + +// // Insert all elements in hash. +// const map = new Map(feqList); +// const keys = JSON.parse(JSON.stringify(map.get(node))) + +// let min_count = Infinity; +// while (min_count > 1) { +// const count = new Map(); + +// map.forEach((values, i) => { +// let hasKey = 0; +// const arr = Object.keys(values).forEach(key => { +// if (!keys[key]) { +// return; +// } + +// hasKey = 1; +// if (count.has(key)) { +// let freq = count.get(key); +// freq++; +// count.set(key, freq); +// } else { +// count.set(key, 1); +// } +// }); + +// if (!hasKey) { +// map.delete(i); +// } +// }) + +// // find min frequency. +// let res = ''; +// for (let [key, val] of count.entries()) { +// if (min_count >= val) { +// res = key; +// min_count = val; +// } +// } + +// selector += res; +// delete keys[res]; + +// if (min_count > 1) { +// map.forEach((keys, i) => { +// if (!keys[res]) { +// map.delete(i) +// } +// }) +// } + +// if (Object.values(keys).length === 0) { +// return { +// selector: getBaseSelector(node) + selector, +// unique: map.size <= 1 +// }; +// } +// } + +// return { +// selector: getBaseSelector(node) + selector, +// unique: true +// } +// } + /** * Given a node and the statistics on class frequency on the page, * return all its uncommon class data sorted in order of decreasing uniqueness @@ -342,6 +541,8 @@ function getThreeLeastCommonFeatures(elm, selectorData) { }, '')); } + + /** * generates a single selector for an element * @param {Element} elm The element for which to generate a selector @@ -350,49 +551,78 @@ function getThreeLeastCommonFeatures(elm, selectorData) { * @returns {String} The selector */ function generateSelector(elm, options, doc) { + const vNode = getNodeFromTree(elm); + if (vNode._selector) { + return vNode._selector; + } + /*eslint no-loop-func:0*/ // TODO: es-modules_selectorData if (!axe._selectorData) { throw new Error('Expect axe._selectorData to be set up'); } const { toRoot = false } = options; - let selector; - let similar; + // let selector; + // let similar; /** * Try to find a unique selector by filtering out all the clashing * nodes by adding ancestor selectors iteratively. * This loop is much faster than recursing and using querySelectorAll */ - do { - let features = getElmId(elm); - if (!features) { - features = getThreeLeastCommonFeatures(elm, axe._selectorData); - features += getNthChildString(elm, features); + // do { + // let features = getElmId(elm); + // if (!features) { + // features = getThreeLeastCommonFeatures(elm, axe._selectorData); + // features += getNthChildString(elm, features); + // } + // // if (selector) { + // // selector = features + ' > ' + selector; + // // } else { + // // selector = features; + // // } + // // If there are too many similar element running QSA again is faster + // const similar = findSimilar(doc, features); + // if (similar.length === 1) { + // vNode._selector = features; + // return features; + // } + const { selector, unique } = leastFrequent(elm, axe._selectorData.newFreq[elm.nodeName]); + + if (unique) { + vNode._selector = selector; + return selector; } - if (selector) { - selector = features + ' > ' + selector; - } else { - selector = features; - } - // If there are too many similar element running QSA again is faster - if (!similar || similar.length > constants.selectorSimilarFilterLimit) { - similar = findSimilar(doc, selector); - } else { - similar = similar.filter(item => { - return matchesSelector(item, selector); - }); + else { + // would probably need to add :nth-child to the end since selector is not guaranteed to be unique + // to its siblings + return generateSelector(elm.parentElement, options, doc) + ' > ' + selector; } - elm = elm.parentElement; - } while ((similar.length > 1 || toRoot) && elm && elm.nodeType !== 11); - - if (similar.length === 1) { - return selector; - } else if (selector.indexOf(' > ') !== -1) { - // For the odd case that document doesn't have a unique selector - return ':root' + selector.substring(selector.indexOf(' > ')); - } - return ':root'; + + // else if (similar.length > 1) { + // return generateSelector(elm.parentElement, options, doc) + ' > ' + features; + // } + + // vNode._selector = ':root'; + // return ':root'; + + // if (!similar || similar.length > constants.selectorSimilarFilterLimit) { + // similar = findSimilar(doc, selector); + // } else { + // similar = similar.filter(item => { + // return matchesSelector(item, selector); + // }); + // } + // elm = elm.parentElement; + // } while ((similar.length > 1 || toRoot) && elm && elm.nodeType !== 11); + + // if (similar.length === 1) { + // return selector; + // } else if (selector.indexOf(' > ') !== -1) { + // // For the odd case that document doesn't have a unique selector + // return ':root' + selector.substring(selector.indexOf(' > ')); + // } + // return ':root'; } /** diff --git a/test/playground.html b/test/playground.html index 29b6d3837d..2a166612d9 100644 --- a/test/playground.html +++ b/test/playground.html @@ -3,27 +3,279 @@