1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%";
8 const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay";
9 const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval";
10 const PREF_APP_UPDATE_LOG = "app.update.log";
12 const CATEGORY_UPDATE_TIMER = "update-timer";
16 XPCOMUtils.defineLazyPreferenceGetter(
24 * Logs a string to the error console.
26 * The string to write to the error console.
28 * Whether to log even if logging is disabled.
30 function LOG(string, alwaysLog = false) {
31 if (alwaysLog || lazy.gLogEnabled) {
32 dump("*** UTM:SVC " + string + "\n");
33 Services.console.logStringMessage("UTM:SVC " + string);
38 * A manager for timers. Manages timers that fire over long periods of time
39 * (e.g. days, weeks, months).
42 export function TimerManager() {
43 Services.obs.addObserver(this, "profile-before-change");
46 TimerManager.prototype = {
50 name: "UpdateTimerManager",
58 * The Checker Timer minimum delay interval as specified by the
59 * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay
60 * pref doesn't exist this will default to 120000.
62 _timerMinimumDelay: null,
65 * The set of registered timers.
72 observe: function TM_observe(aSubject, aTopic) {
73 // Prevent setting the timer interval to a value of less than 30 seconds.
74 var minInterval = 30000;
75 // Prevent setting the first timer interval to a value of less than 10
77 var minFirstInterval = 10000;
80 // Enforce a minimum timer interval of 500 ms for tests and fall through
81 // to profile-after-change to initialize the timer.
83 minFirstInterval = 500;
85 case "profile-after-change":
86 this._timerMinimumDelay = Math.max(
88 Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120),
91 // Prevent the timer delay between notifications to other consumers from
92 // being greater than 5 minutes which is 300000 milliseconds.
93 this._timerMinimumDelay = Math.min(this._timerMinimumDelay, 300000);
94 // Prevent the first interval from being less than the value of minFirstInterval
95 let firstInterval = Math.max(
96 Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERFIRSTINTERVAL, 30000),
99 // Prevent the first interval from being greater than 2 minutes which is
100 // 120000 milliseconds.
101 firstInterval = Math.min(firstInterval, 120000);
102 // Cancel the timer if it has already been initialized. This is primarily
104 this._canEnsureTimer = true;
105 this._ensureTimer(firstInterval);
107 case "profile-before-change":
108 Services.obs.removeObserver(this, "profile-before-change");
110 // Release everything we hold onto.
112 for (var timerID in this._timers) {
113 delete this._timers[timerID];
121 * Called when the checking timer fires.
123 * We only fire one notification each time, so that the operations are
124 * staggered. We don't want too many to happen at once, which could
125 * negatively impact responsiveness.
128 * The checking timer that fired.
130 notify: function TM_notify(timer) {
131 var nextDelay = null;
132 function updateNextDelay(delay) {
133 if (nextDelay === null || delay < nextDelay) {
138 // Each timer calls tryFire(), which figures out which is the one that
139 // wanted to be called earliest. That one will be fired; the others are
140 // skipped and will be done later.
141 var now = Math.round(Date.now() / 1000);
143 var callbacksToFire = [];
144 function tryFire(timerID, callback, intendedTime) {
145 if (intendedTime <= now) {
146 callbacksToFire.push({ timerID, callback, intendedTime });
148 updateNextDelay(intendedTime - now);
152 for (let { value } of Services.catMan.enumerateCategory(
153 CATEGORY_UPDATE_TIMER
155 let [cid, method, timerID, prefInterval, defaultInterval, maxInterval] =
158 defaultInterval = parseInt(defaultInterval);
159 // cid and method are validated below when calling notify.
160 if (!timerID || !defaultInterval || isNaN(defaultInterval)) {
162 "TimerManager:notify - update-timer category registered" +
163 (cid ? " for " + cid : "") +
164 " without required parameters - " +
170 let interval = Services.prefs.getIntPref(prefInterval, defaultInterval);
171 // Allow the update-timer category to specify a maximum value to prevent
172 // values larger than desired.
173 maxInterval = parseInt(maxInterval);
174 if (maxInterval && !isNaN(maxInterval)) {
175 interval = Math.min(interval, maxInterval);
177 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
181 // Initialize the last update time to 0 when the preference isn't set so
182 // the timer will be notified soon after a new profile's first use.
183 let lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0);
185 // If the last update time is greater than the current time then reset
186 // it to 0 and the timer manager will correct the value when it fires
187 // next for this consumer.
188 if (lastUpdateTime > now) {
190 Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
196 ChromeUtils.idleDispatch(() => {
198 let startTime = Cu.now();
199 Cc[cid][method](Ci.nsITimerCallback).notify(timer);
200 ChromeUtils.addProfilerMarker(
202 { category: "Timer", startTime },
205 LOG("TimerManager:notify - notified " + cid);
208 "TimerManager:notify - error notifying component id: " +
215 Services.prefs.setIntPref(prefLastUpdate, now);
216 updateNextDelay(interval);
218 lastUpdateTime + interval
222 for (let _timerID in this._timers) {
223 let timerID = _timerID; // necessary for the closure to work properly
224 let timerData = this._timers[timerID];
225 // If the last update time is greater than the current time then reset
226 // it to 0 and the timer manager will correct the value when it fires
227 // next for this consumer.
228 if (timerData.lastUpdateTime > now) {
229 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
233 timerData.lastUpdateTime = 0;
234 Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime);
239 if (timerData.callback && timerData.callback.notify) {
240 ChromeUtils.idleDispatch(() => {
242 let startTime = Cu.now();
243 timerData.callback.notify(timer);
244 ChromeUtils.addProfilerMarker(
246 { category: "Timer", startTime },
249 LOG(`TimerManager:notify - notified timerID: ${timerID}`);
252 `TimerManager:notify - error notifying timerID: ${timerID}, error: ${e}`
258 `TimerManager:notify - timerID: ${timerID} doesn't implement nsITimerCallback - skipping`
261 timerData.lastUpdateTime = now;
262 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
266 Services.prefs.setIntPref(prefLastUpdate, now);
267 updateNextDelay(timerData.interval);
269 timerData.lastUpdateTime + timerData.interval
273 if (callbacksToFire.length) {
274 callbacksToFire.sort((a, b) => a.intendedTime - b.intendedTime);
275 for (let { intendedTime, timerID, callback } of callbacksToFire) {
277 `TimerManager:notify - fire timerID: ${timerID} ` +
278 `intended time: ${intendedTime} (${new Date(
286 if (nextDelay !== null) {
287 timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay);
288 this.lastTimerReset = Date.now();
295 * Starts the timer, if necessary, and ensures that it will fire soon enough
296 * to happen after time |interval| (in milliseconds).
298 _ensureTimer(interval) {
299 if (!this._canEnsureTimer) {
303 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
304 this._timer.initWithCallback(
307 Ci.nsITimer.TYPE_REPEATING_SLACK
309 this.lastTimerReset = Date.now();
311 Date.now() + interval <
312 this.lastTimerReset + this._timer.delay
314 this._timer.delay = Math.max(
315 this.lastTimerReset + interval - Date.now(),
322 * Stops the timer, if it is running.
326 this._timer.cancel();
332 * See nsIUpdateTimerManager.idl
334 registerTimer: function TM_registerTimer(id, callback, interval, skipFirst) {
335 let markerText = `timerID: ${id} interval: ${interval}s`;
337 markerText += " skipFirst";
339 ChromeUtils.addProfilerMarker(
340 "RegisterUpdateTimer",
341 { category: "Timer" },
345 `TimerManager:registerTimer - timerID: ${id} interval: ${interval} skipFirst: ${skipFirst}`
347 if (this._timers === null) {
348 // Use normal logging since reportError is not available while shutting
351 "TimerManager:registerTimer called after profile-before-change " +
352 "notification. Ignoring timer registration for id: " +
358 if (id in this._timers && callback != this._timers[id].callback) {
360 "TimerManager:registerTimer - Ignoring second registration for " + id
364 let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id);
365 // Initialize the last update time to 0 when the preference isn't set so
366 // the timer will be notified soon after a new profile's first use.
367 let lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0);
368 let now = Math.round(Date.now() / 1000);
369 if (lastUpdateTime > now) {
372 if (lastUpdateTime == 0) {
374 lastUpdateTime = now;
376 Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
378 this._timers[id] = { callback, interval, lastUpdateTime };
380 this._ensureTimer(interval * 1000);
383 unregisterTimer: function TM_unregisterTimer(id) {
384 ChromeUtils.addProfilerMarker(
385 "UnregisterUpdateTimer",
386 { category: "Timer" },
389 LOG("TimerManager:unregisterTimer - id: " + id);
390 if (id in this._timers) {
391 delete this._timers[id];
394 "TimerManager:unregisterTimer - Ignoring unregistration request for " +
401 classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"),
402 QueryInterface: ChromeUtils.generateQI([
406 "nsIUpdateTimerManager",