From c9cd12240daa2284e92edf925b584edee7bf1b46 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sun, 4 Feb 2018 14:12:40 -0500 Subject: [PATCH] fix(perf): memoize axe.utils.select --- lib/core/base/audit.js | 2 ++ lib/core/utils/select.js | 38 +++++++++++++++++++++--------- test/core/base/audit.js | 49 +++++++++++++++++++++++++++++++++++++++ test/core/utils/select.js | 12 ++++++++++ 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index 0bc9b9809a..e371e55195 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -145,6 +145,7 @@ Audit.prototype.run = function (context, options, resolve, reject) { this.validateOptions(options); axe._tree = axe.utils.getFlattenedTree(document.documentElement); //cache the flattened tree + axe._selectCache = []; var q = axe.utils.queue(); this.rules.forEach(function (rule) { if (axe.utils.ruleShouldRun(rule, context, options)) { @@ -180,6 +181,7 @@ Audit.prototype.run = function (context, options, resolve, reject) { }); q.then(function (results) { axe._tree = undefined; // empty the tree + axe._selectCache = undefined; // remove the cache resolve(results.filter(function (result) { return !!result; })); }).catch(reject); }; diff --git a/lib/core/utils/select.js b/lib/core/utils/select.js index a751df14ab..9ce44e125c 100644 --- a/lib/core/utils/select.js +++ b/lib/core/utils/select.js @@ -45,16 +45,10 @@ function isNodeInContext(node, context) { * @param {Array} nodes The list of nodes to push * @param {Object} context The "resolved" context object, @see resolveContext */ -function pushNode(result, nodes, context) { +function pushNode(result, nodes) { 'use strict'; var temp; - var curried = (function (context) { - return function (node) { - return isNodeInContext(node, context); - }; - })(context); - nodes = nodes.filter(curried); if (result.length === 0) { return nodes; @@ -96,6 +90,7 @@ function hasOverlappingIncludes(includes) { * @return {Array} Matching virtual DOM nodes sorted by DOM order */ axe.utils.select = function select(selector, context) { + //jshint maxstatements:20 'use strict'; var result = [], candidate; @@ -103,16 +98,37 @@ axe.utils.select = function select(selector, context) { context.include = Array.from(context.include); } context.include.sort(axe.utils.nodeSorter); // ensure that the order of the include nodes is document order - for (var i = 0, l = context.include.length; i < l; i++) { + if (axe._selectCache) { // if used outside of run, it will still work + for (var j = 0, l = axe._selectCache.length; j < l; j++) { + // First see whether the item exists in the cache + let item = axe._selectCache[j]; + if (item.selector === selector) { + return item.result; + } + } + } + var curried = (function (context) { + return function (node) { + return isNodeInContext(node, context); + }; + })(context); + for (var i = 0; i < context.include.length; i++) { candidate = context.include[i]; if (candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE && - axe.utils.matchesSelector(candidate.actualNode, selector)) { - result = pushNode(result, [candidate], context); + axe.utils.matchesSelector(candidate.actualNode, selector) && + curried(candidate)) { + result = pushNode(result, [candidate]); } - result = pushNode(result, axe.utils.querySelectorAll(candidate, selector), context); + result = pushNode(result, axe.utils.querySelectorAllFilter(candidate, selector, curried)); } if (context.include.length > 1 && hasOverlappingIncludes(context.include)) { result.sort(axe.utils.nodeSorter); } + if (axe._selectCache) { + axe._selectCache.push({ + selector: selector, + result: result + }); + } return result; }; diff --git a/test/core/base/audit.js b/test/core/base/audit.js index 4859716c48..2e054379cd 100644 --- a/test/core/base/audit.js +++ b/test/core/base/audit.js @@ -64,6 +64,7 @@ describe('Audit', function () { afterEach(function () { fixture.innerHTML = ''; axe._tree = undefined; + axe._selectCache = undefined; axe.utils.getFlattenedTree = getFlattenedTree; }); @@ -479,6 +480,54 @@ describe('Audit', function () { rules: {} }, function () { assert.isTrue(called); + axe.utils.getFlattenedTree = getFlattenedTree; + done(); + }, isNotCalled); + }); + it('should assign the result of getFlattenedTree to axe._tree', function (done) { + var thing = 'honey badger'; + var saved = axe.utils.ruleShouldRun; + axe.utils.ruleShouldRun = function () { + assert.equal(axe._tree, thing); + return false; + }; + axe.utils.getFlattenedTree = function () { + return thing; + }; + a.run({ include: [document] }, {}, function () { + axe.utils.ruleShouldRun = saved; + done(); + }, isNotCalled); + }); + it('should clear axe._tree', function (done) { + var thing = 'honey badger'; + axe.utils.getFlattenedTree = function () { + return thing; + }; + a.run({ include: [document] }, { + rules: {} + }, function () { + assert.isTrue(typeof axe._tree === 'undefined'); + axe.utils.getFlattenedTree = getFlattenedTree; + done(); + }, isNotCalled); + }); + it('should assign an empty array to axe._selectCache', function (done) { + var saved = axe.utils.ruleShouldRun; + axe.utils.ruleShouldRun = function () { + assert.equal(axe._selectCache.length, 0); + return false; + }; + a.run({ include: [document] }, {}, function () { + axe.utils.ruleShouldRun = saved; + done(); + }, isNotCalled); + }); + it('should clear axe._selectCache', function (done) { + a.run({ include: [document] }, { + rules: {} + }, function () { + assert.isTrue(typeof axe._selectCache === 'undefined'); done(); }, isNotCalled); }); diff --git a/test/core/utils/select.js b/test/core/utils/select.js index 2ac60392a6..aef282f264 100644 --- a/test/core/utils/select.js +++ b/test/core/utils/select.js @@ -10,6 +10,7 @@ describe('axe.utils.select', function () { afterEach(function () { fixture.innerHTML = ''; + axe._selectCache = undefined; }); @@ -148,6 +149,17 @@ describe('axe.utils.select', function () { assert.equal(result.length, 3); }); + it ('should return the cached result if one exists', function () { + fixture.innerHTML = '
' + + '
'; + axe._selectCache = [{ + selector: '.bananas', + result: 'fruit bat' + }]; + var result = axe.utils.select('.bananas', { include: [axe.utils.getFlattenedTree($id('zero'))[0]] }); + assert.equal(result, 'fruit bat'); + + }); }); \ No newline at end of file