From a615de5b6ffe7531cd5c129ea51b080795603062 Mon Sep 17 00:00:00 2001 From: mauro-balades Date: Thu, 26 Sep 2024 18:44:22 +0200 Subject: [PATCH] Refactor ZenTabUnloader to add support for unloading inactive tabs This commit refactors the ZenTabUnloader class to add support for unloading inactive tabs. It introduces a new class called ZenTabsIntervalUnloader, which periodically checks for inactive tabs and discards them if they meet certain criteria. The criteria include being inactive for a specified timeout period and not meeting any exclusion URLs. The commit also adds a new class called ZenTabsObserver, which listens to various tab events and notifies the ZenTabUnloader when a tab's activity changes. These changes improve the efficiency of tab management and help reduce resource usage by unloading tabs that are not actively being used. --- src/ZenTabUnloader.mjs | 251 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 src/ZenTabUnloader.mjs diff --git a/src/ZenTabUnloader.mjs b/src/ZenTabUnloader.mjs new file mode 100644 index 0000000..7f4132b --- /dev/null +++ b/src/ZenTabUnloader.mjs @@ -0,0 +1,251 @@ + +{ + const ZEN_TAB_UNLOADER_PREF = "zen.tab-unloader.enabled"; + const ZEN_TAB_UNLOADER_TIMEOUT_PREF = "zen.tab-unloader.timeout"; + + const lazy = {}; + + XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "zenTabUnloaderEnabled", + "zen.tab-unloader.enabled", + false + ); + + XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "zenTabUnloaderTimeout", + "zen.tab-unloader.timeout-minutes", + 5 + ); + + XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "zenTabUnloaderExcludedUrls", + "zen.tab-unloader.excluded-urls", + "" + ); + + const ZEN_TAB_UNLOADER_DEFAULT_EXCLUDED_URLS = [ + "^about:", + "^chrome:", + "^devtools:", + "^file:", + "^resource:", + "^view-source:", + "^view-image:", + ]; + + class ZenTabsObserver { + static ALL_EVENTS = [ + "TabAttrModified", + "TabPinned", + "TabUnpinned", + "TabBrowserInserted", + "TabBrowserDiscarded", + "TabShow", + "TabHide", + "TabOpen", + "TabClose", + "TabSelect", + "TabMultiSelect", + ] + + #listeners = []; + + constructor() { + this.#listenAllEvents(); + } + + #listenAllEvents() { + const eventListener = this.#eventListener.bind(this); + for (const event of ZenTabsObserver.ALL_EVENTS) { + window.addEventListener(event, eventListener); + } + window.addEventListener("unload", () => { + for (const event of ZenTabsObserver.ALL_EVENTS) { + window.removeEventListener(event, eventListener); + } + }); + } + + #eventListener(event) { + for (const listener of this.#listeners) { + listener(event.type, event); + } + } + + addTabsListener(listener) { + this.#listeners.push(listener); + } + } + + class ZenTabsIntervalUnloader { + static INTERVAL = 1000 * 60; // 1 minute + + interval = null; + unloader = null; + + #excludedUrls = []; + #compiledExcludedUrls = []; + + constructor(unloader) { + this.unloader = unloader; + this.interval = setInterval(this.intervalListener.bind(this), ZenTabsIntervalUnloader.INTERVAL); + this.#excludedUrls = this.lazyExcludeUrls; + } + + get lazyExcludeUrls() { + return [ + ...ZEN_TAB_UNLOADER_DEFAULT_EXCLUDED_URLS, + ...lazy.zenTabUnloaderExcludedUrls.split(",").map(url => url.trim()) + ]; + } + + arraysEqual(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + // If you don't care about the order of the elements inside + // the array, you should sort both arrays here. + // Please note that calling sort on an array will modify that array. + // you might want to clone your array first. + + for (var i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; + } + + get excludedUrls() { + // Check if excludedrls is the same as the pref value + const excludedUrls = this.lazyExcludeUrls; + if (!this.arraysEqual(this.#excludedUrls, excludedUrls) || !this.#compiledExcludedUrls.length) { + this.#excludedUrls = excludedUrls; + this.#compiledExcludedUrls = excludedUrls.map(url => new RegExp(url)); + } + return this.#compiledExcludedUrls; + } + + intervalListener() { + const currentTimestamp = Date.now(); + const excludedUrls = this.excludedUrls; + for (const tab of this.unloader.tabs) { + if (this.unloader.canUnloadTab(tab, currentTimestamp, excludedUrls)) { + console.debug("ZenTabUnloader: Discarding tab", tab); + tab.ownerGlobal.gBrowser.discardBrowser(tab); + } + } + } + } + + + class ZenTabUnloader { + static ACTIVITY_MODIFIERS = [ + "muted", + "soundplaying", + "label", + "attention", + ] + + allTabs = []; + constructor() { + if (!lazy.zenTabUnloaderEnabled) { + return; + } + this.observer = new ZenTabsObserver(); + this.intervalUnloader = new ZenTabsIntervalUnloader(this); + this.allTabs = gBrowser.tabs; + this.observer.addTabsListener(this.onTabEvent.bind(this)); + } + + onTabEvent(action, event) { + const tab = event.target; + switch (action) { + case "TabPinned": + case "TabUnpinned": + case "TabBrowserInserted": + case "TabBrowserDiscarded": + case "TabShow": + case "TabHide": + break; + case "TabAttrModified": + this.handleTabAttrModified(tab, event); + break; + case "TabOpen": + this.handleTabOpen(tab); + break; + case "TabClose": + this.handleTabClose(tab); + break; + case "TabSelect": + case "TabMultiSelect": + this.updateTabActivity(tab); + break; + default: + console.warn("ZenTabUnloader: Unhandled tab event", action); + break; + } + } + + onLocationChange(browser) { + const tab = browser.ownerGlobal.gBrowser.getTabForBrowser(browser); + this.updateTabActivity(tab); + } + + handleTabClose(tab) { + this.allTabs = this.allTabs.filter(t => t !== tab); + } + + handleTabOpen(tab) { + if (!lazy.zenTabUnloaderEnabled) { + return; + } + if (this.allTabs.includes(tab)) { + return; + } + this.allTabs.push(tab); + this.updateTabActivity(tab); + } + + handleTabAttrModified(tab, event) { + for (const modifier of ZenTabUnloader.ACTIVITY_MODIFIERS) { + if (event.detail.changed.includes(modifier)) { + this.updateTabActivity(tab); + break; + } + } + } + + updateTabActivity(tab) { + const currentTimestamp = Date.now(); + tab.lastActivity = currentTimestamp; + } + + get tabs() { + return this.allTabs; + } + + canUnloadTab(tab, currentTimestamp, excludedUrls) { + if (tab.pinned || tab.selected || tab.multiselected + || tab.hasAttribute("busy") || tab.hasAttribute("pending") + || !tab.linkedPanel || tab.splitView || tab.attention + || excludedUrls.some(url => url.test(tab.linkedBrowser.currentURI.spec))) { + return false; + } + const lastActivity = tab.lastActivity; + if (!lastActivity) { + return false; + } + const diff = currentTimestamp - lastActivity; + // Check if the tab has been inactive for more than the timeout + if (diff < lazy.zenTabUnloaderTimeout * 60 * 1000) { + return false; + } + return true; + } + } + + window.gZenTabUnloader = new ZenTabUnloader(); +}