From c090d2fde45a8b3ee9f558d17f162dec90aef7ca Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 1 Nov 2019 12:32:34 -0400 Subject: [PATCH] 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. --- src/js/messaging.js | 38 ++++---- src/js/popup.js | 65 +++++++++---- src/js/scriptlets/dom-survey-elements.js | 96 +++++++++++++++++++ .../{dom-survey.js => dom-survey-scripts.js} | 63 ++---------- 4 files changed, 171 insertions(+), 91 deletions(-) create mode 100644 src/js/scriptlets/dom-survey-elements.js rename src/js/scriptlets/{dom-survey.js => dom-survey-scripts.js} (68%) diff --git a/src/js/messaging.js b/src/js/messaging.js index b29140cc6..42a45fb37 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -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': diff --git a/src/js/popup.js b/src/js/popup.js index ea9c4da34..8f99a70eb 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -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); diff --git a/src/js/scriptlets/dom-survey-elements.js b/src/js/scriptlets/dom-survey-elements.js new file mode 100644 index 000000000..70416bbab --- /dev/null +++ b/src/js/scriptlets/dom-survey-elements.js @@ -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; +})(); diff --git a/src/js/scriptlets/dom-survey.js b/src/js/scriptlets/dom-survey-scripts.js similarity index 68% rename from src/js/scriptlets/dom-survey.js rename to src/js/scriptlets/dom-survey-scripts.js index 3640ae236..17698333a 100644 --- a/src/js/scriptlets/dom-survey.js +++ b/src/js/scriptlets/dom-survey-scripts.js @@ -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; })();