Count hidden elements on-demand only in popup panel

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

The badge value for the no-cosmetic-filtering
switch will be evaluated on-demand only, when
the user hover over the switch with the mouse
cursor.

For touch screen displays, a tap on the switch
will cause the badge to be rendered if not
already done, otherwise this will toggle the
switch as usual.
This commit is contained in:
Raymond Hill 2019-11-01 12:32:34 -04:00
parent 7c0294bd5f
commit c090d2fde4
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
4 changed files with 171 additions and 91 deletions

View file

@ -366,39 +366,34 @@ const popupDataFromRequest = async function(request) {
return popupDataFromTabId(tabId, tabTitle);
};
const getDOMStats = async function(tabId) {
const getElementCount = async function(tabId, what) {
const results = await vAPI.tabs.executeScript(tabId, {
allFrames: true,
file: '/js/scriptlets/dom-survey.js',
file: `/js/scriptlets/dom-survey-${what}.js`,
runAt: 'document_end',
});
let elementCount = 0;
let scriptCount = 0;
results.forEach(result => {
if ( result instanceof Object === false ) { return; }
if ( result.hiddenElementCount > 0 ) {
elementCount += result.hiddenElementCount;
}
if ( result.externalScriptCount > 0 ) {
scriptCount += result.externalScriptCount;
}
if ( result.inlineScriptCount > 0 ) {
scriptCount += 1;
}
let total = 0;
results.forEach(count => {
if ( typeof count !== 'number' ) { return; }
total += count;
});
return { elementCount, scriptCount };
return total;
};
const onMessage = function(request, sender, callback) {
let pageStore;
// Async
switch ( request.what ) {
case 'getPopupLazyData':
getDOMStats(request.tabId).then(results => {
callback(results);
case 'getHiddenElementCount':
getElementCount(request.tabId, 'elements').then(count => {
callback(count);
});
return;
case 'getScriptCount':
getElementCount(request.tabId, 'scripts').then(count => {
callback(count);
});
return;
@ -414,6 +409,7 @@ const onMessage = function(request, sender, callback) {
// Sync
let response;
let pageStore;
switch ( request.what ) {
case 'hasPopupContentChanged':

View file

@ -668,24 +668,48 @@ let renderOnce = function() {
/******************************************************************************/
const renderPopupLazy = async function() {
const result = await messaging.send('popupPanel', {
what: 'getPopupLazyData',
tabId: popupData.tabId,
});
if ( result instanceof Object === false ) { return; }
const renderPopupLazy = (( ) => {
let mustRenderCosmeticFilteringBadge = true;
let count = result.elementCount || 0;
uDom.nodeFromSelector('#no-cosmetic-filtering > span.fa-icon-badge')
.textContent = count !== 0
? Math.min(count, 99).toLocaleString()
: '';
count = result.scriptCount || 0;
uDom.nodeFromSelector('#no-scripting > span.fa-icon-badge')
.textContent = count !== 0
? Math.min(count, 99).toLocaleString()
: '';
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/756
// Launch potentially expensive hidden elements-counting scriptlet on
// demand only.
{
const sw = uDom.nodeFromId('no-cosmetic-filtering');
const badge = sw.querySelector(':scope > span.fa-icon-badge');
badge.textContent = '\u22EF';
const render = ( ) => {
if ( mustRenderCosmeticFilteringBadge === false ) { return; }
mustRenderCosmeticFilteringBadge = false;
if ( sw.classList.contains('hnSwitchBusy') ) { return; }
sw.classList.add('hnSwitchBusy');
messaging.send('popupPanel', {
what: 'getHiddenElementCount',
tabId: popupData.tabId,
}).then(count => {
badge.textContent = (count || 0) !== 0
? Math.min(count, 99).toLocaleString()
: '';
sw.classList.remove('hnSwitchBusy');
});
};
sw.addEventListener('mouseenter', render, { passive: true });
}
return async function() {
const count = await messaging.send('popupPanel', {
what: 'getScriptCount',
tabId: popupData.tabId,
});
uDom.nodeFromSelector('#no-scripting > span.fa-icon-badge')
.textContent = (count || 0) !== 0
? Math.min(count, 99).toLocaleString()
: '';
mustRenderCosmeticFilteringBadge = true;
};
})();
/******************************************************************************/
@ -954,6 +978,13 @@ const toggleHostnameSwitch = async function(ev) {
const target = ev.currentTarget;
const switchName = target.getAttribute('id');
if ( !switchName ) { return; }
// For touch displays, process click only if the switch is not "busy".
if (
vAPI.webextFlavor.soup.has('mobile') &&
target.classList.contains('hnSwitchBusy')
) {
return;
}
target.classList.toggle('on');
renderTooltips('#' + switchName);

View file

@ -0,0 +1,96 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2015-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/******************************************************************************/
// https://github.com/uBlockOrigin/uBlock-issues/issues/756
// Keep in mind CPU usage with large DOM and/or filterset.
(( ) => {
if ( typeof vAPI !== 'object' ) { return; }
const t0 = Date.now();
if ( vAPI.domSurveyElements instanceof Object === false ) {
vAPI.domSurveyElements = {
busy: false,
hiddenElementCount: -1,
surveyTime: t0,
};
}
const surveyResults = vAPI.domSurveyElements;
if ( surveyResults.busy ) { return; }
surveyResults.busy = true;
if ( surveyResults.surveyTime < vAPI.domMutationTime ) {
surveyResults.hiddenElementCount = -1;
}
surveyResults.surveyTime = t0;
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;
})();
}
surveyResults.busy = false;
// IMPORTANT: This is returned to the injector, so this MUST be
// the last statement.
return surveyResults.hiddenElementCount;
})();

View file

@ -23,31 +23,28 @@
/******************************************************************************/
// https://github.com/uBlockOrigin/uBlock-issues/issues/756
// Keep in mind CPU usage witj large DOM and/or filterset.
// Scriptlets to count the number of script tags in a document.
(( ) => {
if ( typeof vAPI !== 'object' ) { return; }
const t0 = Date.now();
const tMax = t0 + 60;
const tMax = t0 + 50;
if ( vAPI.domSurveyResults instanceof Object === false ) {
vAPI.domSurveyResults = {
if ( vAPI.domSurveyScripts instanceof Object === false ) {
vAPI.domSurveyScripts = {
busy: false,
hiddenElementCount: -1,
inlineScriptCount: -1,
externalScriptCount: -1,
surveyTime: t0,
};
}
const surveyResults = vAPI.domSurveyResults;
const surveyResults = vAPI.domSurveyScripts;
if ( surveyResults.busy ) { return; }
surveyResults.busy = true;
if ( surveyResults.surveyTime < vAPI.domMutationTime ) {
surveyResults.hiddenElementCount = -1;
surveyResults.inlineScriptCount = -1;
surveyResults.externalScriptCount = -1;
}
@ -71,46 +68,6 @@
surveyResults.externalScriptCount = externalScriptCount;
}
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.
@ -164,9 +121,9 @@
// IMPORTANT: This is returned to the injector, so this MUST be
// the last statement.
return {
hiddenElementCount: surveyResults.hiddenElementCount,
inlineScriptCount: surveyResults.inlineScriptCount,
externalScriptCount: surveyResults.externalScriptCount,
};
let total = surveyResults.externalScriptCount;
if ( surveyResults.inlineScriptCount !== -1 ) {
total += surveyResults.inlineScriptCount;
}
return total;
})();