diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 126e8fe7c..38ecfac43 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -19,6 +19,8 @@ Home: https://github.com/gorhill/uBlock */ +import * as scrmgr from './scripting-manager.js'; + import { MODE_BASIC, MODE_OPTIMAL, @@ -100,14 +102,13 @@ import { } from './debug.js'; import { dnr } from './ext-compat.js'; -import { registerInjectables } from './scripting-manager.js'; import { toggleToolbarIcon } from './action.js'; /******************************************************************************/ const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, '').toLowerCase(); - const canShowBlockedCount = typeof dnr.setExtensionActionOptions === 'function'; +const { registerInjectables } = scrmgr; let pendingPermissionRequest; @@ -710,6 +711,8 @@ async function start() { if ( process.wakeupRun === false ) { await startSession(); + } else { + scrmgr.onWakeupRun(); } toggleDeveloperMode(rulesetConfig.developerMode); diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js index a615aaf1c..2b2164656 100644 --- a/platform/mv3/extension/js/ext.js +++ b/platform/mv3/extension/js/ext.js @@ -100,6 +100,16 @@ export async function sessionRemove(key) { return browser.storage.session.remove(key); } +export async function sessionKeys() { + if ( notAnObject(browser?.storage?.session) ) { return; } + if ( browser.storage.session.getKeys ) { + return browser.storage.session.getKeys(); + } + const bin = await browser.storage.session.get(null); + if ( notAnObject(bin) ) { return; } + return Object.keys(bin); +} + export async function sessionAccessLevel(level) { try { browser.storage.session.setAccessLevel(level); diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index c52deffa6..291fc0c2d 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -24,7 +24,7 @@ import * as ut from './utils.js'; import { browser, localKeys, localRemove, localWrite, - sessionRemove, + sessionKeys, sessionRead, sessionRemove, sessionWrite, } from './ext.js'; import { ubolErr, ubolLog } from './debug.js'; @@ -94,6 +94,15 @@ const normalizeRegisteredContentScripts = registered => { /******************************************************************************/ +async function resetCSSCache() { + const keys = await sessionKeys(); + return Promise.all( + keys.filter(a => a.startsWith('cache.css.')).map(a => sessionRemove(a)) + ); +} + +/******************************************************************************/ + function registerHighGeneric(context, genericDetails) { const { before, filteringModeDetails, rulesetsDetails } = context; @@ -297,7 +306,6 @@ async function registerProcedural(context) { const keys = await localKeys(); for ( const key of keys ) { if ( key.startsWith('css.procedural.') === false ) { continue; } - sessionRemove(key); localRemove(key); } } @@ -322,7 +330,7 @@ async function registerProcedural(context) { for ( const id of rulesetIds ) { promises.push( fetchJSON(`/rulesets/scripting/procedural/${id}`).then(data => { - return localWrite(`css.procedural.json.${id}`, data); + return localWrite(`css.procedural.${id}`, data); }) ); } @@ -386,7 +394,6 @@ async function registerSpecific(context) { const keys = await localKeys(); for ( const key of keys ) { if ( key.startsWith('css.specific.') === false ) { continue; } - sessionRemove(key); localRemove(key); } } @@ -411,7 +418,7 @@ async function registerSpecific(context) { for ( const id of rulesetIds ) { promises.push( fetchJSON(`/rulesets/scripting/specific/${id}`).then(data => { - return localWrite(`css.specific.json.${id}`, data); + return localWrite(`css.specific.${id}`, data); }) ); } @@ -547,7 +554,7 @@ function registerScriptlet(context, scriptletDetails) { // Issue: Safari appears to completely ignore excludeMatches // https://github.com/radiolondra/ExcludeMatches-Test -async function registerInjectables() { +export async function registerInjectables() { if ( browser.scripting === undefined ) { return false; } if ( registerInjectables.barrier ) { return true; } @@ -612,6 +619,8 @@ async function registerInjectables() { } } + await resetCSSCache(); + registerInjectables.barrier = false; return true; @@ -619,6 +628,25 @@ async function registerInjectables() { /******************************************************************************/ -export { - registerInjectables -}; +export async function onWakeupRun() { + const cleanupTime = await sessionRead('scripting.manager.cleanup.time') || 0; + const now = Date.now(); + const since = now - cleanupTime; + if ( since < (15 * 60 * 1000) ) { return; } // 15 minutes + const MAX_CACHE_ENTRY_LOW = 256; + const MAX_CACHE_ENTRY_HIGH = MAX_CACHE_ENTRY_LOW + + Math.min(Math.round(MAX_CACHE_ENTRY_LOW + MAX_CACHE_ENTRY_LOW / 8), 1); + const keys = await sessionKeys() || []; + const cacheKeys = keys.filter(a => a.startsWith('cache.css.')); + if ( cacheKeys.length < MAX_CACHE_ENTRY_HIGH ) { return; } + const entries = await Promise.all(cacheKeys.map(async a => { + const entry = await sessionRead(a) || {}; + entry.key = a; + return entry; + })); + entries.sort((a, b) => b.t - a.t); + entries.slice(MAX_CACHE_ENTRY_LOW).map(a => sessionRemove(a.key)); + sessionWrite('scripting.manager.cleanup.time', now) +} + +/******************************************************************************/ diff --git a/platform/mv3/extension/js/scripting/css-procedural.js b/platform/mv3/extension/js/scripting/css-procedural.js index d2dae1f06..8a9684aae 100644 --- a/platform/mv3/extension/js/scripting/css-procedural.js +++ b/platform/mv3/extension/js/scripting/css-procedural.js @@ -30,18 +30,8 @@ self.proceduralImports = undefined; /******************************************************************************/ -const isolatedAPI = self.isolatedAPI; - -const selectors = []; - -const cachedCSS = await isolatedAPI.sessionGet('css.procedural.cache') || {}; -if ( cachedCSS[isolatedAPI.docHostname] ) { - selectors.push(...cachedCSS[isolatedAPI.docHostname]); -} else { - selectors.push(...await isolatedAPI.getSelectors('procedural', proceduralImports)); - cachedCSS[isolatedAPI.docHostname] = selectors; - isolatedAPI.sessionSet('css.procedural.cache', cachedCSS); -} +const selectors = await self.cosmeticAPI.getSelectors('procedural', proceduralImports); +self.cosmeticAPI.release(); if ( selectors.length === 0 ) { return; } proceduralImports.length = 0; diff --git a/platform/mv3/extension/js/scripting/css-specific.js b/platform/mv3/extension/js/scripting/css-specific.js index 0076bf56d..681cd41d2 100644 --- a/platform/mv3/extension/js/scripting/css-specific.js +++ b/platform/mv3/extension/js/scripting/css-specific.js @@ -30,24 +30,10 @@ self.specificImports = undefined; /******************************************************************************/ -const isolatedAPI = self.isolatedAPI; - -const selectors = []; - -self.cssAPI.update('*{visibility:hidden!important;}'); - -const cachedCSS = await isolatedAPI.sessionGet('css.specific.cache') || {}; -if ( cachedCSS[isolatedAPI.docHostname] ) { - selectors.push(...cachedCSS[isolatedAPI.docHostname]); -} else { - selectors.push(...await isolatedAPI.getSelectors('specific', specificImports)); - cachedCSS[isolatedAPI.docHostname] = selectors; - isolatedAPI.sessionSet('css.specific.cache', cachedCSS); -} -const insert = selectors.length !== 0 - ? `${Array.from(selectors).join(',\n')}{display:none!important;}` - : undefined; -self.cssAPI.update(insert, '*{visibility:hidden!important;}'); +const selectors = await self.cosmeticAPI.getSelectors('specific', specificImports); +self.cosmeticAPI.release(); +if ( selectors.length === 0 ) { return; } +self.cssAPI.update(`${selectors.join(',\n')}{display:none!important;}`); /******************************************************************************/ diff --git a/platform/mv3/extension/js/scripting/isolated-api.js b/platform/mv3/extension/js/scripting/isolated-api.js index 8c7fa7859..49c54064c 100644 --- a/platform/mv3/extension/js/scripting/isolated-api.js +++ b/platform/mv3/extension/js/scripting/isolated-api.js @@ -29,7 +29,6 @@ const hostnameStack = (( ) => { const docloc = document.location; - isolatedAPI.docHostname = docloc.hostname; const origins = [ docloc.origin ]; if ( docloc.ancestorOrigins ) { origins.push(...docloc.ancestorOrigins); @@ -75,7 +74,38 @@ } }; - isolatedAPI.binarySearch = (sorted, target) => { +})(self.isolatedAPI); + + +(api => { + if ( typeof api === 'object' ) { return; } + + const cosmeticAPI = self.cosmeticAPI = {}; + + const sessionRead = async function(key) { + try { + const bin = await chrome.storage.session.get(key); + return bin?.[key] ?? undefined; + } catch { + } + }; + + const sessionWrite = function(key, data) { + try { + chrome.storage.session.set({ [key]: data }); + } catch { + } + }; + + const localRead = async function(key) { + try { + const bin = await chrome.storage.local.get(key); + return bin?.[key] ?? undefined; + } catch { + } + }; + + const binarySearch = (sorted, target) => { let l = 0, i = 0, d = 0; let r = sorted.length; let candidate; @@ -96,57 +126,79 @@ return -1; }; - isolatedAPI.sessionGet = async function(key) { - try { - const bin = await chrome.storage.session.get(key); - return bin?.[key] ?? undefined; - } catch { + const lookupHostname = (hostname, data) => { + const listref = binarySearch(data.hostnames, hostname); + if ( listref === -1 ) { return; } + const ilist = data.selectorListRefs[listref]; + const list = JSON.parse(`[${data.selectorLists[ilist]}]`); + const { result } = data; + for ( const iselector of list ) { + if ( iselector >= 0 ) { + result.selectors.add(data.selectors[iselector]); + } else { + result.exceptions.add(data.selectors[~iselector]); + } } }; - isolatedAPI.sessionSet = function(key, data) { - try { - chrome.storage.session.set({ [key]: data }); - } catch { - } + const selectorsFromRuleset = async (realm, rulesetId, result) => { + const data = await localRead(`css.${realm}.${rulesetId}`); + if ( typeof data !== 'object' || data === null ) { return; } + data.result = result; + self.isolatedAPI.forEachHostname(lookupHostname, data); }; - isolatedAPI.localGet = async function(key) { - try { - const bin = await chrome.storage.local.get(key); - return bin?.[key] ?? undefined; - } catch { - } - }; - - isolatedAPI.getSelectors = async function(realm, details) { + const fillCache = async function(realm, rulesetIds) { const selectors = new Set(); const exceptions = new Set(); - const lookupHostname = (hostname, data) => { - const listref = isolatedAPI.binarySearch(data.hostnames, hostname); - if ( listref === -1 ) { return; } - const ilist = data.selectorListRefs[listref]; - const list = JSON.parse(`[${data.selectorLists[ilist]}]`); - for ( const iselector of list ) { - if ( iselector >= 0 ) { - selectors.add(data.selectors[iselector]); - } else { - exceptions.add(data.selectors[~iselector]); - } - } - }; - const selectorsFromRuleset = async rulesetId => { - const data = await isolatedAPI.localGet(`css.${realm}.json.${rulesetId}`); - isolatedAPI.forEachHostname(lookupHostname, data); - }; - await Promise.all(details.map(a => selectorsFromRuleset(a.rulesetId))); + const result = { selectors, exceptions }; + await Promise.all(rulesetIds.map(a => selectorsFromRuleset(realm, a, result))); for ( const selector of exceptions ) { selectors.delete(selector); } - return Array.from(selectors); + cacheEntry[cacheSlots[realm]] = Array.from(selectors).map(a => + a.startsWith('{') ? JSON.parse(a) : a + ); }; -})(self.isolatedAPI); + const readCache = async ( ) => { + cacheEntry = await sessionRead(cacheKey) || {}; + }; + + const cacheSlots = { 'specific': 's', 'procedural': 'p' }; + const cacheKey = `cache.css.${document.location.hostname || ''}`; + let clientCount = 0; + let cacheEntry; + + cosmeticAPI.getSelectors = async function(realm, rulesetIds) { + clientCount += 1; + const slot = cacheSlots[realm]; + if ( cacheEntry === undefined ) { + cacheEntry = readCache(); + } + if ( cacheEntry instanceof Promise ) { + await cacheEntry; + } + if ( cacheEntry[slot] === undefined ) { + cacheEntry[slot] = fillCache(realm, rulesetIds); + } + if ( cacheEntry[slot] instanceof Promise ) { + await cacheEntry[slot]; + } + return cacheEntry[slot]; + }; + + cosmeticAPI.release = function() { + clientCount -= 1; + if ( clientCount !== 0 ) { return; } + self.cosmeticAPI = undefined; + const now = Math.round(Date.now() / 15000); + const since = now - (cacheEntry.t || 0); + if ( since <= 1 ) { return; } + cacheEntry.t = now; + sessionWrite(cacheKey, cacheEntry); + }; +})(self.cosmeticAPI); /******************************************************************************/ diff --git a/platform/mv3/safari/css-api.js b/platform/mv3/safari/css-api.js index e9b8817f4..1055b8cd4 100644 --- a/platform/mv3/safari/css-api.js +++ b/platform/mv3/safari/css-api.js @@ -25,20 +25,22 @@ const inserted = new Set(); self.cssAPI = { - insert(css) { + update(insert, remove) { chrome.runtime.sendMessage({ - what: 'insertCSS', - css, + what: 'updateCSS', + insert, + remove, }).catch(( ) => { }); - inserted.add(css); + inserted.add(insert); + inserted.delete(remove); }, }; self.addEventListener('pageshow', ( ) => { chrome.runtime.sendMessage({ - what: 'insertCSS', - css: Array.from(inserted).join('\n'), + what: 'updateCSS', + insert: Array.from(inserted).join('\n'), }).catch(( ) => { }); }); diff --git a/platform/mv3/safari/css-user.js b/platform/mv3/safari/css-user.js index 0f5caedaf..63d1ad11e 100644 --- a/platform/mv3/safari/css-user.js +++ b/platform/mv3/safari/css-user.js @@ -43,8 +43,8 @@ if ( details?.plainSelectors?.length ) { const selectors = details.plainSelectors; self.addEventListener('pageshow', ( ) => { chrome.runtime.sendMessage({ - what: 'insertCSS', - css: `${selectors.join(',\n')}{display:none!important;}`, + what: 'updateCSS', + insert: `${selectors.join(',\n')}{display:none!important;}`, }).catch(( ) => { }); }); diff --git a/platform/mv3/scriptlets/css-procedural.template.js b/platform/mv3/scriptlets/css-procedural.template.js index b704f630b..e5fcacd86 100644 --- a/platform/mv3/scriptlets/css-procedural.template.js +++ b/platform/mv3/scriptlets/css-procedural.template.js @@ -30,7 +30,7 @@ if ( Boolean(chrome?.storage?.local) === false ) { return; } const rulesetId = self.$rulesetId$; self.proceduralImports = self.proceduralImports || []; -self.proceduralImports.push({ rulesetId }); +self.proceduralImports.push(rulesetId); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/css-specific.template.js b/platform/mv3/scriptlets/css-specific.template.js index cec9190a3..897510ef1 100644 --- a/platform/mv3/scriptlets/css-specific.template.js +++ b/platform/mv3/scriptlets/css-specific.template.js @@ -30,7 +30,7 @@ if ( Boolean(chrome?.storage?.local) === false ) { return; } const rulesetId = self.$rulesetId$; self.specificImports = self.specificImports || []; -self.specificImports.push({ rulesetId }); +self.specificImports.push(rulesetId); /******************************************************************************/