Improve efficiency of per-site switches badge code

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/756

This is the code used to find out the count values
displayed as badge on the cosmetic filtering and
scripting per-site switches in the popup panel.

The issue is that document.querySelector*() -- used to
find out the number of hidden elements -- is unduly
expensive on large DOM.

The changes in this commit have focused on avoiding the
use of document.querySelector*() as much as possible.

Also, the results are cached for reuse unless DOM
mutations are detected.
This commit is contained in:
Raymond Hill 2019-10-31 11:07:11 -04:00
parent 11da758d47
commit d8975ee580
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 148 additions and 35 deletions

View File

@ -268,6 +268,7 @@ vAPI.SafeAnimationFrame.prototype = {
/******************************************************************************/
vAPI.domWatcher = (( ) => {
vAPI.domMutationTime = Date.now();
const addedNodeLists = [];
const removedNodeLists = [];
@ -282,9 +283,7 @@ vAPI.domWatcher = (( ) => {
safeObserverHandlerTimer;
const safeObserverHandler = function() {
//console.time('dom watcher/safe observer handler');
let i = addedNodeLists.length,
j = addedNodes.length;
let i = addedNodeLists.length;
while ( i-- ) {
const nodeList = addedNodeLists[i];
let iNode = nodeList.length;
@ -293,7 +292,7 @@ vAPI.domWatcher = (( ) => {
if ( node.nodeType !== 1 ) { continue; }
if ( ignoreTags.has(node.localName) ) { continue; }
if ( node.parentElement === null ) { continue; }
addedNodes[j++] = node;
addedNodes.push(node);
}
}
addedNodeLists.length = 0;
@ -308,7 +307,6 @@ vAPI.domWatcher = (( ) => {
}
}
removedNodeLists.length = 0;
//console.timeEnd('dom watcher/safe observer handler');
if ( addedNodes.length === 0 && removedNodes === false ) { return; }
for ( const listener of getListenerIterator() ) {
try { listener.onDOMChanged(addedNodes, removedNodes); }
@ -316,12 +314,12 @@ vAPI.domWatcher = (( ) => {
}
addedNodes.length = 0;
removedNodes = false;
vAPI.domMutationTime = Date.now();
};
// https://github.com/chrisaljoudi/uBlock/issues/205
// Do not handle added node directly from within mutation observer.
const observerHandler = function(mutations) {
//console.time('dom watcher/observer handler');
let i = mutations.length;
while ( i-- ) {
const mutation = mutations[i];
@ -339,7 +337,6 @@ vAPI.domWatcher = (( ) => {
addedNodeLists.length < 100 ? 1 : undefined
);
}
//console.timeEnd('dom watcher/observer handler');
};
const startMutationObserver = function() {

View File

@ -377,8 +377,15 @@ const getDOMStats = async function(tabId) {
let scriptCount = 0;
results.forEach(result => {
if ( result instanceof Object === false ) { return; }
elementCount += result.elementCount;
scriptCount += result.scriptCount;
if ( result.hiddenElementCount > 0 ) {
elementCount += result.hiddenElementCount;
}
if ( result.externalScriptCount > 0 ) {
scriptCount += result.externalScriptCount;
}
if ( result.inlineScriptCount > 0 ) {
scriptCount += 1;
}
});
return { elementCount, scriptCount };

View File

@ -23,41 +23,150 @@
/******************************************************************************/
// https://github.com/uBlockOrigin/uBlock-issues/issues/756
// Keep in mind CPU usage witj large DOM and/or filterset.
(( ) => {
if ( typeof vAPI !== 'object' ) { return; }
// https://github.com/gorhill/httpswitchboard/issues/25
//
// https://github.com/gorhill/httpswitchboard/issues/131
// Looks for inline javascript also in at least one a[href] element.
//
// https://github.com/gorhill/uMatrix/issues/485
// Mind "on..." attributes.
//
// https://github.com/gorhill/uMatrix/issues/924
// Report inline styles.
let inlineScriptCount = 0;
if (
document.querySelector('script:not([src])') !== null ||
document.querySelector('script[src^="data:"]') !== null ||
document.querySelector('script[src^="blob:"]') !== null ||
document.querySelector('a[href^="javascript:"]') !== null ||
document.querySelector('[onabort],[onblur],[oncancel],[oncanplay],[oncanplaythrough],[onchange],[onclick],[onclose],[oncontextmenu],[oncuechange],[ondblclick],[ondrag],[ondragend],[ondragenter],[ondragexit],[ondragleave],[ondragover],[ondragstart],[ondrop],[ondurationchange],[onemptied],[onended],[onerror],[onfocus],[oninput],[oninvalid],[onkeydown],[onkeypress],[onkeyup],[onload],[onloadeddata],[onloadedmetadata],[onloadstart],[onmousedown],[onmouseenter],[onmouseleave],[onmousemove],[onmouseout],[onmouseover],[onmouseup],[onwheel],[onpause],[onplay],[onplaying],[onprogress],[onratechange],[onreset],[onresize],[onscroll],[onseeked],[onseeking],[onselect],[onshow],[onstalled],[onsubmit],[onsuspend],[ontimeupdate],[ontoggle],[onvolumechange],[onwaiting],[onafterprint],[onbeforeprint],[onbeforeunload],[onhashchange],[onlanguagechange],[onmessage],[onoffline],[ononline],[onpagehide],[onpageshow],[onrejectionhandled],[onpopstate],[onstorage],[onunhandledrejection],[onunload],[oncopy],[oncut],[onpaste]') !== null
) {
inlineScriptCount = 1;
const t0 = Date.now();
const tMax = t0 + 60;
if ( vAPI.domSurveyResults instanceof Object === false ) {
vAPI.domSurveyResults = {
busy: false,
hiddenElementCount: -1,
inlineScriptCount: -1,
externalScriptCount: -1,
surveyTime: t0,
};
}
const surveyResults = vAPI.domSurveyResults;
if ( surveyResults.busy ) { return; }
surveyResults.busy = true;
if ( surveyResults.surveyTime < vAPI.domMutationTime ) {
surveyResults.hiddenElementCount = -1;
surveyResults.inlineScriptCount = -1;
surveyResults.externalScriptCount = -1;
}
surveyResults.surveyTime = t0;
if ( surveyResults.externalScriptCount === -1 ) {
const reInlineScript = /^(data:|blob:|$)/;
let inlineScriptCount = 0;
let externalScriptCount = 0;
for ( const script of document.scripts ) {
if ( reInlineScript.test(script.src) ) {
inlineScriptCount = 1;
continue;
}
externalScriptCount += 1;
if ( externalScriptCount === 99 ) { break; }
}
if ( inlineScriptCount !== 0 || externalScriptCount === 99 ) {
surveyResults.inlineScriptCount = inlineScriptCount;
}
surveyResults.externalScriptCount = externalScriptCount;
}
const scriptTags = document.querySelectorAll('script[src]');
let elementCount = 0;
if ( vAPI.domFilterer ) {
elementCount = vAPI.domFilterer.getFilteredElementCount();
if ( surveyResults.hiddenElementCount === -1 ) {
surveyResults.hiddenElementCount = (( ) => {
if ( vAPI.domFilterer instanceof Object === false ) { return 0; }
const details = vAPI.domFilterer.getAllSelectors_(true);
if ( Array.isArray(details.declarative) === false ) { return 0; }
const selectors = details.declarative.map(entry => entry[0]);
const simple = [], complex = [];
for ( const selectorStr of selectors ) {
for ( const selector of selectorStr.split(',\n') ) {
if ( /[ +>~]/.test(selector) ) {
complex.push(selector);
} else {
simple.push(selector);
}
}
}
const simpleStr = simple.join(',\n');
const complexStr = complex.join(',\n');
const nodeIter = document.createNodeIterator(
document.body,
NodeFilter.SHOW_ELEMENT
);
const matched = new Set();
for (;;) {
const node = nodeIter.nextNode();
if ( node === null ) { break; }
if ( node.offsetParent !== null ) { continue; }
if (
node.matches(simpleStr) === false &&
node.closest(complexStr) !== node
) {
continue;
}
matched.add(node);
if ( matched.size === 99 ) { break; }
}
return matched.size;
})();
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/756
// Keep trying to find inline script-like instances but only if we
// have the time-budget to do so.
if ( surveyResults.inlineScriptCount === -1 && Date.now() < tMax ) {
if ( document.querySelector('a[href^="javascript:"]') !== null ) {
surveyResults.inlineScriptCount = 1;
}
}
if ( surveyResults.inlineScriptCount === -1 && Date.now() < tMax ) {
surveyResults.inlineScriptCount = 0;
const onHandlers = new Set([
'onabort', 'onblur', 'oncancel', 'oncanplay',
'oncanplaythrough', 'onchange', 'onclick', 'onclose',
'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag',
'ondragend', 'ondragenter', 'ondragexit', 'ondragleave',
'ondragover', 'ondragstart', 'ondrop', 'ondurationchange',
'onemptied', 'onended', 'onerror', 'onfocus',
'oninput', 'oninvalid', 'onkeydown', 'onkeypress',
'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata',
'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave',
'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup',
'onwheel', 'onpause', 'onplay', 'onplaying',
'onprogress', 'onratechange', 'onreset', 'onresize',
'onscroll', 'onseeked', 'onseeking', 'onselect',
'onshow', 'onstalled', 'onsubmit', 'onsuspend',
'ontimeupdate', 'ontoggle', 'onvolumechange', 'onwaiting',
'onafterprint', 'onbeforeprint', 'onbeforeunload', 'onhashchange',
'onlanguagechange', 'onmessage', 'onoffline', 'ononline',
'onpagehide', 'onpageshow', 'onrejectionhandled', 'onpopstate',
'onstorage', 'onunhandledrejection', 'onunload',
'oncopy', 'oncut', 'onpaste'
]);
const nodeIter = document.createNodeIterator(
document.body,
NodeFilter.SHOW_ELEMENT
);
for (;;) {
const node = nodeIter.nextNode();
if ( node === null ) { break; }
if ( node.hasAttributes() === false ) { continue; }
for ( const attr of node.getAttributeNames() ) {
if ( onHandlers.has(attr) === false ) { continue; }
surveyResults.inlineScriptCount = 1;
break;
}
}
}
surveyResults.busy = false;
// IMPORTANT: This is returned to the injector, so this MUST be
// the last statement.
return {
elementCount,
scriptCount: inlineScriptCount + scriptTags.length,
hiddenElementCount: surveyResults.hiddenElementCount,
inlineScriptCount: surveyResults.inlineScriptCount,
externalScriptCount: surveyResults.externalScriptCount,
};
})();