diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/timermanager/UpdateTimerManager.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/timermanager/UpdateTimerManager.sys.mjs')
-rw-r--r-- | toolkit/components/timermanager/UpdateTimerManager.sys.mjs | 408 |
1 files changed, 408 insertions, 0 deletions
diff --git a/toolkit/components/timermanager/UpdateTimerManager.sys.mjs b/toolkit/components/timermanager/UpdateTimerManager.sys.mjs new file mode 100644 index 0000000000..607a3b0e71 --- /dev/null +++ b/toolkit/components/timermanager/UpdateTimerManager.sys.mjs @@ -0,0 +1,408 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%"; +const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; +const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval"; +const PREF_APP_UPDATE_LOG = "app.update.log"; + +const CATEGORY_UPDATE_TIMER = "update-timer"; + +const lazy = {}; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gLogEnabled", + PREF_APP_UPDATE_LOG, + false +); + +/** + * Logs a string to the error console. + * @param string + * The string to write to the error console. + * @param bool + * Whether to log even if logging is disabled. + */ +function LOG(string, alwaysLog = false) { + if (alwaysLog || lazy.gLogEnabled) { + dump("*** UTM:SVC " + string + "\n"); + Services.console.logStringMessage("UTM:SVC " + string); + } +} + +/** + * A manager for timers. Manages timers that fire over long periods of time + * (e.g. days, weeks, months). + * @constructor + */ +export function TimerManager() { + Services.obs.addObserver(this, "profile-before-change"); +} + +TimerManager.prototype = { + /** + * nsINamed + */ + name: "UpdateTimerManager", + + /** + * The Checker Timer + */ + _timer: null, + + /** + * The Checker Timer minimum delay interval as specified by the + * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay + * pref doesn't exist this will default to 120000. + */ + _timerMinimumDelay: null, + + /** + * The set of registered timers. + */ + _timers: {}, + + /** + * See nsIObserver.idl + */ + observe: function TM_observe(aSubject, aTopic, aData) { + // Prevent setting the timer interval to a value of less than 30 seconds. + var minInterval = 30000; + // Prevent setting the first timer interval to a value of less than 10 + // seconds. + var minFirstInterval = 10000; + switch (aTopic) { + case "utm-test-init": + // Enforce a minimum timer interval of 500 ms for tests and fall through + // to profile-after-change to initialize the timer. + minInterval = 500; + minFirstInterval = 500; + // fall through + case "profile-after-change": + this._timerMinimumDelay = Math.max( + 1000 * + Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120), + minInterval + ); + // Prevent the timer delay between notifications to other consumers from + // being greater than 5 minutes which is 300000 milliseconds. + this._timerMinimumDelay = Math.min(this._timerMinimumDelay, 300000); + // Prevent the first interval from being less than the value of minFirstInterval + let firstInterval = Math.max( + Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERFIRSTINTERVAL, 30000), + minFirstInterval + ); + // Prevent the first interval from being greater than 2 minutes which is + // 120000 milliseconds. + firstInterval = Math.min(firstInterval, 120000); + // Cancel the timer if it has already been initialized. This is primarily + // for tests. + this._canEnsureTimer = true; + this._ensureTimer(firstInterval); + break; + case "profile-before-change": + Services.obs.removeObserver(this, "profile-before-change"); + + // Release everything we hold onto. + this._cancelTimer(); + for (var timerID in this._timers) { + delete this._timers[timerID]; + } + this._timers = null; + break; + } + }, + + /** + * Called when the checking timer fires. + * + * We only fire one notification each time, so that the operations are + * staggered. We don't want too many to happen at once, which could + * negatively impact responsiveness. + * + * @param timer + * The checking timer that fired. + */ + notify: function TM_notify(timer) { + var nextDelay = null; + function updateNextDelay(delay) { + if (nextDelay === null || delay < nextDelay) { + nextDelay = delay; + } + } + + // Each timer calls tryFire(), which figures out which is the one that + // wanted to be called earliest. That one will be fired; the others are + // skipped and will be done later. + var now = Math.round(Date.now() / 1000); + + var callbacksToFire = []; + function tryFire(timerID, callback, intendedTime) { + if (intendedTime <= now) { + callbacksToFire.push({ timerID, callback, intendedTime }); + } else { + updateNextDelay(intendedTime - now); + } + } + + for (let { value } of Services.catMan.enumerateCategory( + CATEGORY_UPDATE_TIMER + )) { + let [cid, method, timerID, prefInterval, defaultInterval, maxInterval] = + value.split(","); + + defaultInterval = parseInt(defaultInterval); + // cid and method are validated below when calling notify. + if (!timerID || !defaultInterval || isNaN(defaultInterval)) { + LOG( + "TimerManager:notify - update-timer category registered" + + (cid ? " for " + cid : "") + + " without required parameters - " + + "skipping" + ); + continue; + } + + let interval = Services.prefs.getIntPref(prefInterval, defaultInterval); + // Allow the update-timer category to specify a maximum value to prevent + // values larger than desired. + maxInterval = parseInt(maxInterval); + if (maxInterval && !isNaN(maxInterval)) { + interval = Math.min(interval, maxInterval); + } + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace( + /%ID%/, + timerID + ); + // Initialize the last update time to 0 when the preference isn't set so + // the timer will be notified soon after a new profile's first use. + let lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0); + + // If the last update time is greater than the current time then reset + // it to 0 and the timer manager will correct the value when it fires + // next for this consumer. + if (lastUpdateTime > now) { + lastUpdateTime = 0; + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); + } + + tryFire( + timerID, + function () { + ChromeUtils.idleDispatch(() => { + try { + let startTime = Cu.now(); + Cc[cid][method](Ci.nsITimerCallback).notify(timer); + ChromeUtils.addProfilerMarker( + "UpdateTimer", + { category: "Timer", startTime }, + timerID + ); + LOG("TimerManager:notify - notified " + cid); + } catch (e) { + LOG( + "TimerManager:notify - error notifying component id: " + + cid + + " ,error: " + + e + ); + } + }); + Services.prefs.setIntPref(prefLastUpdate, now); + updateNextDelay(interval); + }, + lastUpdateTime + interval + ); + } + + for (let _timerID in this._timers) { + let timerID = _timerID; // necessary for the closure to work properly + let timerData = this._timers[timerID]; + // If the last update time is greater than the current time then reset + // it to 0 and the timer manager will correct the value when it fires + // next for this consumer. + if (timerData.lastUpdateTime > now) { + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace( + /%ID%/, + timerID + ); + timerData.lastUpdateTime = 0; + Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime); + } + tryFire( + timerID, + function () { + if (timerData.callback && timerData.callback.notify) { + ChromeUtils.idleDispatch(() => { + try { + let startTime = Cu.now(); + timerData.callback.notify(timer); + ChromeUtils.addProfilerMarker( + "UpdateTimer", + { category: "Timer", startTime }, + timerID + ); + LOG(`TimerManager:notify - notified timerID: ${timerID}`); + } catch (e) { + LOG( + `TimerManager:notify - error notifying timerID: ${timerID}, error: ${e}` + ); + } + }); + } else { + LOG( + `TimerManager:notify - timerID: ${timerID} doesn't implement nsITimerCallback - skipping` + ); + } + timerData.lastUpdateTime = now; + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace( + /%ID%/, + timerID + ); + Services.prefs.setIntPref(prefLastUpdate, now); + updateNextDelay(timerData.interval); + }, + timerData.lastUpdateTime + timerData.interval + ); + } + + if (callbacksToFire.length) { + callbacksToFire.sort((a, b) => a.intendedTime - b.intendedTime); + for (let { intendedTime, timerID, callback } of callbacksToFire) { + LOG( + `TimerManager:notify - fire timerID: ${timerID} ` + + `intended time: ${intendedTime} (${new Date( + intendedTime * 1000 + ).toISOString()})` + ); + callback(); + } + } + + if (nextDelay !== null) { + timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay); + this.lastTimerReset = Date.now(); + } else { + this._cancelTimer(); + } + }, + + /** + * Starts the timer, if necessary, and ensures that it will fire soon enough + * to happen after time |interval| (in milliseconds). + */ + _ensureTimer(interval) { + if (!this._canEnsureTimer) { + return; + } + if (!this._timer) { + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback( + this, + interval, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + this.lastTimerReset = Date.now(); + } else if ( + Date.now() + interval < + this.lastTimerReset + this._timer.delay + ) { + this._timer.delay = Math.max( + this.lastTimerReset + interval - Date.now(), + 0 + ); + } + }, + + /** + * Stops the timer, if it is running. + */ + _cancelTimer() { + if (this._timer) { + this._timer.cancel(); + this._timer = null; + } + }, + + /** + * See nsIUpdateTimerManager.idl + */ + registerTimer: function TM_registerTimer(id, callback, interval, skipFirst) { + let markerText = `timerID: ${id} interval: ${interval}s`; + if (skipFirst) { + markerText += " skipFirst"; + } + ChromeUtils.addProfilerMarker( + "RegisterUpdateTimer", + { category: "Timer" }, + markerText + ); + LOG( + `TimerManager:registerTimer - timerID: ${id} interval: ${interval} skipFirst: ${skipFirst}` + ); + if (this._timers === null) { + // Use normal logging since reportError is not available while shutting + // down. + LOG( + "TimerManager:registerTimer called after profile-before-change " + + "notification. Ignoring timer registration for id: " + + id, + true + ); + return; + } + if (id in this._timers && callback != this._timers[id].callback) { + LOG( + "TimerManager:registerTimer - Ignoring second registration for " + id + ); + return; + } + let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id); + // Initialize the last update time to 0 when the preference isn't set so + // the timer will be notified soon after a new profile's first use. + let lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0); + let now = Math.round(Date.now() / 1000); + if (lastUpdateTime > now) { + lastUpdateTime = 0; + } + if (lastUpdateTime == 0) { + if (skipFirst) { + lastUpdateTime = now; + } + Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime); + } + this._timers[id] = { callback, interval, lastUpdateTime }; + + this._ensureTimer(interval * 1000); + }, + + unregisterTimer: function TM_unregisterTimer(id) { + ChromeUtils.addProfilerMarker( + "UnregisterUpdateTimer", + { category: "Timer" }, + id + ); + LOG("TimerManager:unregisterTimer - id: " + id); + if (id in this._timers) { + delete this._timers[id]; + } else { + LOG( + "TimerManager:unregisterTimer - Ignoring unregistration request for " + + "unknown id: " + + id + ); + } + }, + + classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"), + QueryInterface: ChromeUtils.generateQI([ + "nsINamed", + "nsIObserver", + "nsITimerCallback", + "nsIUpdateTimerManager", + ]), +}; |