Merge mozilla-central to autoland. CLOSED TREE
[gecko.git] / toolkit / components / timermanager / UpdateTimerManager.sys.mjs
blob14c34e88473b46380eef1c7ed6460e83bd4c11e7
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";
14 const lazy = {};
16 XPCOMUtils.defineLazyPreferenceGetter(
17   lazy,
18   "gLogEnabled",
19   PREF_APP_UPDATE_LOG,
20   false
23 /**
24  *  Logs a string to the error console.
25  *  @param   string
26  *           The string to write to the error console.
27  *  @param   bool
28  *           Whether to log even if logging is disabled.
29  */
30 function LOG(string, alwaysLog = false) {
31   if (alwaysLog || lazy.gLogEnabled) {
32     dump("*** UTM:SVC " + string + "\n");
33     Services.console.logStringMessage("UTM:SVC " + string);
34   }
37 /**
38  *  A manager for timers. Manages timers that fire over long periods of time
39  *  (e.g. days, weeks, months).
40  *  @constructor
41  */
42 export function TimerManager() {
43   Services.obs.addObserver(this, "profile-before-change");
46 TimerManager.prototype = {
47   /**
48    * nsINamed
49    */
50   name: "UpdateTimerManager",
52   /**
53    * The Checker Timer
54    */
55   _timer: null,
57   /**
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.
61    */
62   _timerMinimumDelay: null,
64   /**
65    * The set of registered timers.
66    */
67   _timers: {},
69   /**
70    * See nsIObserver.idl
71    */
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
76     // seconds.
77     var minFirstInterval = 10000;
78     switch (aTopic) {
79       case "utm-test-init":
80         // Enforce a minimum timer interval of 500 ms for tests and fall through
81         // to profile-after-change to initialize the timer.
82         minInterval = 500;
83         minFirstInterval = 500;
84       // fall through
85       case "profile-after-change":
86         this._timerMinimumDelay = Math.max(
87           1000 *
88             Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120),
89           minInterval
90         );
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),
97           minFirstInterval
98         );
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
103         // for tests.
104         this._canEnsureTimer = true;
105         this._ensureTimer(firstInterval);
106         break;
107       case "profile-before-change":
108         Services.obs.removeObserver(this, "profile-before-change");
110         // Release everything we hold onto.
111         this._cancelTimer();
112         for (var timerID in this._timers) {
113           delete this._timers[timerID];
114         }
115         this._timers = null;
116         break;
117     }
118   },
120   /**
121    * Called when the checking timer fires.
122    *
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.
126    *
127    * @param   timer
128    *          The checking timer that fired.
129    */
130   notify: function TM_notify(timer) {
131     var nextDelay = null;
132     function updateNextDelay(delay) {
133       if (nextDelay === null || delay < nextDelay) {
134         nextDelay = delay;
135       }
136     }
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 });
147       } else {
148         updateNextDelay(intendedTime - now);
149       }
150     }
152     for (let { value } of Services.catMan.enumerateCategory(
153       CATEGORY_UPDATE_TIMER
154     )) {
155       let [cid, method, timerID, prefInterval, defaultInterval, maxInterval] =
156         value.split(",");
158       defaultInterval = parseInt(defaultInterval);
159       // cid and method are validated below when calling notify.
160       if (!timerID || !defaultInterval || isNaN(defaultInterval)) {
161         LOG(
162           "TimerManager:notify - update-timer category registered" +
163             (cid ? " for " + cid : "") +
164             " without required parameters - " +
165             "skipping"
166         );
167         continue;
168       }
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);
176       }
177       let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
178         /%ID%/,
179         timerID
180       );
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) {
189         lastUpdateTime = 0;
190         Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
191       }
193       tryFire(
194         timerID,
195         function () {
196           ChromeUtils.idleDispatch(() => {
197             try {
198               let startTime = Cu.now();
199               Cc[cid][method](Ci.nsITimerCallback).notify(timer);
200               ChromeUtils.addProfilerMarker(
201                 "UpdateTimer",
202                 { category: "Timer", startTime },
203                 timerID
204               );
205               LOG("TimerManager:notify - notified " + cid);
206             } catch (e) {
207               LOG(
208                 "TimerManager:notify - error notifying component id: " +
209                   cid +
210                   " ,error: " +
211                   e
212               );
213             }
214           });
215           Services.prefs.setIntPref(prefLastUpdate, now);
216           updateNextDelay(interval);
217         },
218         lastUpdateTime + interval
219       );
220     }
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(
230           /%ID%/,
231           timerID
232         );
233         timerData.lastUpdateTime = 0;
234         Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime);
235       }
236       tryFire(
237         timerID,
238         function () {
239           if (timerData.callback && timerData.callback.notify) {
240             ChromeUtils.idleDispatch(() => {
241               try {
242                 let startTime = Cu.now();
243                 timerData.callback.notify(timer);
244                 ChromeUtils.addProfilerMarker(
245                   "UpdateTimer",
246                   { category: "Timer", startTime },
247                   timerID
248                 );
249                 LOG(`TimerManager:notify - notified timerID: ${timerID}`);
250               } catch (e) {
251                 LOG(
252                   `TimerManager:notify - error notifying timerID: ${timerID}, error: ${e}`
253                 );
254               }
255             });
256           } else {
257             LOG(
258               `TimerManager:notify - timerID: ${timerID} doesn't implement nsITimerCallback - skipping`
259             );
260           }
261           timerData.lastUpdateTime = now;
262           let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
263             /%ID%/,
264             timerID
265           );
266           Services.prefs.setIntPref(prefLastUpdate, now);
267           updateNextDelay(timerData.interval);
268         },
269         timerData.lastUpdateTime + timerData.interval
270       );
271     }
273     if (callbacksToFire.length) {
274       callbacksToFire.sort((a, b) => a.intendedTime - b.intendedTime);
275       for (let { intendedTime, timerID, callback } of callbacksToFire) {
276         LOG(
277           `TimerManager:notify - fire timerID: ${timerID} ` +
278             `intended time: ${intendedTime} (${new Date(
279               intendedTime * 1000
280             ).toISOString()})`
281         );
282         callback();
283       }
284     }
286     if (nextDelay !== null) {
287       timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay);
288       this.lastTimerReset = Date.now();
289     } else {
290       this._cancelTimer();
291     }
292   },
294   /**
295    * Starts the timer, if necessary, and ensures that it will fire soon enough
296    * to happen after time |interval| (in milliseconds).
297    */
298   _ensureTimer(interval) {
299     if (!this._canEnsureTimer) {
300       return;
301     }
302     if (!this._timer) {
303       this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
304       this._timer.initWithCallback(
305         this,
306         interval,
307         Ci.nsITimer.TYPE_REPEATING_SLACK
308       );
309       this.lastTimerReset = Date.now();
310     } else if (
311       Date.now() + interval <
312       this.lastTimerReset + this._timer.delay
313     ) {
314       this._timer.delay = Math.max(
315         this.lastTimerReset + interval - Date.now(),
316         0
317       );
318     }
319   },
321   /**
322    * Stops the timer, if it is running.
323    */
324   _cancelTimer() {
325     if (this._timer) {
326       this._timer.cancel();
327       this._timer = null;
328     }
329   },
331   /**
332    * See nsIUpdateTimerManager.idl
333    */
334   registerTimer: function TM_registerTimer(id, callback, interval, skipFirst) {
335     let markerText = `timerID: ${id} interval: ${interval}s`;
336     if (skipFirst) {
337       markerText += " skipFirst";
338     }
339     ChromeUtils.addProfilerMarker(
340       "RegisterUpdateTimer",
341       { category: "Timer" },
342       markerText
343     );
344     LOG(
345       `TimerManager:registerTimer - timerID: ${id} interval: ${interval} skipFirst: ${skipFirst}`
346     );
347     if (this._timers === null) {
348       // Use normal logging since reportError is not available while shutting
349       // down.
350       LOG(
351         "TimerManager:registerTimer called after profile-before-change " +
352           "notification. Ignoring timer registration for id: " +
353           id,
354         true
355       );
356       return;
357     }
358     if (id in this._timers && callback != this._timers[id].callback) {
359       LOG(
360         "TimerManager:registerTimer - Ignoring second registration for " + id
361       );
362       return;
363     }
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) {
370       lastUpdateTime = 0;
371     }
372     if (lastUpdateTime == 0) {
373       if (skipFirst) {
374         lastUpdateTime = now;
375       }
376       Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
377     }
378     this._timers[id] = { callback, interval, lastUpdateTime };
380     this._ensureTimer(interval * 1000);
381   },
383   unregisterTimer: function TM_unregisterTimer(id) {
384     ChromeUtils.addProfilerMarker(
385       "UnregisterUpdateTimer",
386       { category: "Timer" },
387       id
388     );
389     LOG("TimerManager:unregisterTimer - id: " + id);
390     if (id in this._timers) {
391       delete this._timers[id];
392     } else {
393       LOG(
394         "TimerManager:unregisterTimer - Ignoring unregistration request for " +
395           "unknown id: " +
396           id
397       );
398     }
399   },
401   classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"),
402   QueryInterface: ChromeUtils.generateQI([
403     "nsINamed",
404     "nsIObserver",
405     "nsITimerCallback",
406     "nsIUpdateTimerManager",
407   ]),