Bug 1539166 - Refactor the session scheduler into its own module r=chutten
authorJan-Erik Rediger <jrediger@mozilla.com>
Fri, 05 Apr 2019 16:17:42 +0000
changeset 468188 2c0325bf2543d560379cf85e53d43cef772c1914
parent 468187 1922e2efbb65df48de5da978b9db4530dc77bc4a
child 468189 e66a133f9624e19fc1f44eb8e84f2dc8e705cf04
push id35822
push usershindli@mozilla.com
push dateFri, 05 Apr 2019 21:47:45 +0000
treeherdermozilla-central@98064c475d2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerschutten
bugs1539166
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1539166 - Refactor the session scheduler into its own module r=chutten This is in preparation for further logic refactoring in later commits. Differential Revision: https://phabricator.services.mozilla.com/D26147
toolkit/components/telemetry/app/TelemetryScheduler.jsm
toolkit/components/telemetry/moz.build
toolkit/components/telemetry/pings/TelemetrySession.jsm
toolkit/components/telemetry/tests/unit/head.js
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryScheduler.jsm
@@ -0,0 +1,372 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "TelemetryScheduler",
+];
+
+const {Log} = ChromeUtils.import("resource://gre/modules/Log.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {TelemetrySession} = ChromeUtils.import("resource://gre/modules/TelemetrySession.jsm");
+const {TelemetryUtils} = ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
+const {clearTimeout, setTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+  idleService: ["@mozilla.org/widget/idleservice;1", "nsIIdleService"],
+});
+
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+
+const MIN_SUBSESSION_LENGTH_MS = Services.prefs.getIntPref("toolkit.telemetry.minSubsessionLength", 5 * 60) * 1000;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+
+// Seconds of idle time before pinging.
+// On idle-daily a gather-telemetry notification is fired, during it probes can
+// start asynchronous tasks to gather data.
+const IDLE_TIMEOUT_SECONDS = Services.prefs.getIntPref("toolkit.telemetry.idleTimeout", 5 * 60);
+
+// Execute a scheduler tick every 5 minutes.
+const SCHEDULER_TICK_INTERVAL_MS = Services.prefs.getIntPref("toolkit.telemetry.scheduler.tickInterval", 5 * 60) * 1000;
+// When user is idle, execute a scheduler tick every 60 minutes.
+const SCHEDULER_TICK_IDLE_INTERVAL_MS = Services.prefs.getIntPref("toolkit.telemetry.scheduler.idleTickInterval", 60 * 60) * 1000;
+
+// The maximum time (ms) until the tick should moved from the idle
+// queue to the regular queue if it hasn't been executed yet.
+const SCHEDULER_TICK_MAX_IDLE_DELAY_MS = 60 * 1000;
+
+// The frequency at which we persist session data to the disk to prevent data loss
+// in case of aborted sessions (currently 5 minutes).
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+// The tolerance we have when checking if it's midnight (15 minutes).
+const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+var Policy = {
+  now: () => new Date(),
+  setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+  clearSchedulerTickTimeout: id => clearTimeout(id),
+  prioEncode: (batchID, prioParams) => PrioEncoder.encode(batchID, prioParams),
+};
+
+/**
+ * TelemetryScheduler contains a single timer driving all regularly-scheduled
+ * Telemetry related jobs. Having a single place with this logic simplifies
+ * reasoning about scheduling actions in a single place, making it easier to
+ * coordinate jobs and coalesce them.
+ */
+var TelemetryScheduler = {
+  _lastDailyPingTime: 0,
+  _lastSessionCheckpointTime: 0,
+
+  // For sanity checking.
+  _lastAdhocPingTime: 0,
+  _lastTickTime: 0,
+
+  _log: null,
+
+  // The timer which drives the scheduler.
+  _schedulerTimer: null,
+  // The interval used by the scheduler timer.
+  _schedulerInterval: 0,
+  _shuttingDown: true,
+  _isUserIdle: false,
+
+  /**
+   * Initialises the scheduler and schedules the first daily/aborted session pings.
+   */
+  init() {
+    this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::");
+    this._log.trace("init");
+    this._shuttingDown = false;
+    this._isUserIdle = false;
+
+    // Initialize the last daily ping and aborted session last due times to the current time.
+    // Otherwise, we might end up sending daily pings even if the subsession is not long enough.
+    let now = Policy.now();
+    this._lastDailyPingTime = now.getTime();
+    this._lastSessionCheckpointTime = now.getTime();
+    this._rescheduleTimeout();
+
+    idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+    Services.obs.addObserver(this, "wake_notification");
+  },
+
+  /**
+   * Stops the scheduler.
+   */
+  shutdown() {
+    if (this._shuttingDown) {
+      if (this._log) {
+        this._log.error("shutdown - Already shut down");
+      } else {
+        Cu.reportError("TelemetryScheduler.shutdown - Already shut down");
+      }
+      return;
+    }
+
+    this._log.trace("shutdown");
+    if (this._schedulerTimer) {
+      Policy.clearSchedulerTickTimeout(this._schedulerTimer);
+      this._schedulerTimer = null;
+    }
+
+    idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+    Services.obs.removeObserver(this, "wake_notification");
+
+    this._shuttingDown = true;
+  },
+
+  _clearTimeout() {
+    if (this._schedulerTimer) {
+      Policy.clearSchedulerTickTimeout(this._schedulerTimer);
+    }
+  },
+
+  /**
+   * Reschedules the tick timer.
+   */
+  _rescheduleTimeout() {
+    this._log.trace("_rescheduleTimeout - isUserIdle: " + this._isUserIdle);
+    if (this._shuttingDown) {
+      this._log.warn("_rescheduleTimeout - already shutdown");
+      return;
+    }
+
+    this._clearTimeout();
+
+    const now = Policy.now();
+    let timeout = SCHEDULER_TICK_INTERVAL_MS;
+
+    // When the user is idle we want to fire the timer less often.
+    if (this._isUserIdle) {
+      timeout = SCHEDULER_TICK_IDLE_INTERVAL_MS;
+      // We need to make sure though that we don't miss sending pings around
+      // midnight when we use the longer idle intervals.
+      const nextMidnight = TelemetryUtils.getNextMidnight(now);
+      timeout = Math.min(timeout, nextMidnight.getTime() - now.getTime());
+    }
+
+    this._log.trace("_rescheduleTimeout - scheduling next tick for " + new Date(now.getTime() + timeout));
+    this._schedulerTimer =
+      Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), timeout);
+  },
+
+  _sentDailyPingToday(nowDate) {
+    // This is today's date and also the previous midnight (0:00).
+    const todayDate = TelemetryUtils.truncateToDays(nowDate);
+    // We consider a ping sent for today if it occured after or at 00:00 today.
+    return (this._lastDailyPingTime >= todayDate.getTime());
+  },
+
+  /**
+   * Checks if we can send a daily ping or not.
+   * @param {Object} nowDate A date object.
+   * @return {Boolean} True if we can send the daily ping, false otherwise.
+   */
+  _isDailyPingDue(nowDate) {
+    // The daily ping is not due if we already sent one today.
+    if (this._sentDailyPingToday(nowDate)) {
+      this._log.trace("_isDailyPingDue - already sent one today");
+      return false;
+    }
+
+    // Avoid overly short sessions.
+    const timeSinceLastDaily = nowDate.getTime() - this._lastDailyPingTime;
+    if (timeSinceLastDaily < MIN_SUBSESSION_LENGTH_MS) {
+      this._log.trace("_isDailyPingDue - delaying daily to keep minimum session length");
+      return false;
+    }
+
+    this._log.trace("_isDailyPingDue - is due");
+    return true;
+  },
+
+  /**
+   * An helper function to save an aborted-session ping.
+   * @param {Number} now The current time, in milliseconds.
+   * @param {Object} [competingPayload=null] If we are coalescing the daily and the
+   *                 aborted-session pings, this is the payload for the former. Note
+   *                 that the reason field of this payload will be changed.
+   * @return {Promise} A promise resolved when the ping is saved.
+   */
+  _saveAbortedPing(now, competingPayload = null) {
+    this._lastSessionCheckpointTime = now;
+    return TelemetrySession.saveAbortedSessionPing(competingPayload)
+                .catch(e => this._log.error("_saveAbortedPing - Failed", e));
+  },
+
+  /**
+   * The notifications handler.
+   */
+  observe(aSubject, aTopic, aData) {
+    this._log.trace("observe - aTopic: " + aTopic);
+    switch (aTopic) {
+      case "idle":
+        // If the user is idle, increase the tick interval.
+        this._isUserIdle = true;
+        return this._onSchedulerTick();
+      case "active":
+        // User is back to work, restore the original tick interval.
+        this._isUserIdle = false;
+        return this._onSchedulerTick(true);
+      case "wake_notification":
+        // The machine woke up from sleep, trigger a tick to avoid sessions
+        // spanning more than a day.
+        // This is needed because sleep time does not count towards timeouts
+        // on Mac & Linux - see bug 1262386, bug 1204823 et al.
+        return this._onSchedulerTick(true);
+    }
+    return undefined;
+  },
+
+  /**
+   * Creates an object with a method `dispatch` that will call `dispatchFn` unless
+   * the method `cancel` is called beforehand.
+   *
+   * This is used to wrap main thread idle dispatch since it does not provide a
+   * cancel mechanism.
+   */
+  _makeIdleDispatch(dispatchFn) {
+    this._log.trace("_makeIdleDispatch");
+    let fn = dispatchFn;
+    let l = (msg) => this._log.trace(msg); // need to bind `this`
+    return {
+      cancel() {
+        fn = undefined;
+      },
+      dispatch(resolve, reject) {
+        l("_makeIdleDispatch.dispatch - !!fn: " + !!fn);
+        if (!fn) {
+          return Promise.resolve().then(resolve, reject);
+        }
+        return fn(resolve, reject);
+      },
+    };
+  },
+
+  /**
+   * Performs a scheduler tick. This function manages Telemetry recurring operations.
+   * @param {Boolean} [dispatchOnIdle=false] If true, the tick is dispatched in the
+   *                  next idle cycle of the main thread.
+   * @return {Promise} A promise, only used when testing, resolved when the scheduled
+   *                   operation completes.
+   */
+  _onSchedulerTick(dispatchOnIdle = false) {
+    this._log.trace("_onSchedulerTick - dispatchOnIdle: " + dispatchOnIdle);
+    // This call might not be triggered from a timeout. In that case we don't want to
+    // leave any previously scheduled timeouts pending.
+    this._clearTimeout();
+
+    if (this._idleDispatch) {
+      this._idleDispatch.cancel();
+    }
+
+    if (this._shuttingDown) {
+      this._log.warn("_onSchedulerTick - already shutdown.");
+      return Promise.reject(new Error("Already shutdown."));
+    }
+
+    let promise = Promise.resolve();
+    try {
+      if (dispatchOnIdle) {
+        this._idleDispatch = this._makeIdleDispatch((resolve, reject) => {
+          this._log.trace("_onSchedulerTick - ildeDispatchToMainThread dispatch");
+          return this._schedulerTickLogic().then(resolve, reject);
+        });
+        promise = new Promise((resolve, reject) =>
+          Services.tm.idleDispatchToMainThread(() => {
+            return this._idleDispatch
+              ? this._idleDispatch.dispatch(resolve, reject)
+              : Promise.resolve().then(resolve, reject);
+            },
+            SCHEDULER_TICK_MAX_IDLE_DELAY_MS));
+      } else {
+        promise = this._schedulerTickLogic();
+      }
+    } catch (e) {
+      Telemetry.getHistogramById("TELEMETRY_SCHEDULER_TICK_EXCEPTION").add(1);
+      this._log.error("_onSchedulerTick - There was an exception", e);
+    } finally {
+      this._rescheduleTimeout();
+    }
+
+    // This promise is returned to make testing easier.
+    return promise;
+  },
+
+  /**
+   * Implements the scheduler logic.
+   * @return {Promise} Resolved when the scheduled task completes. Only used in tests.
+   */
+  _schedulerTickLogic() {
+    this._log.trace("_schedulerTickLogic");
+
+    let nowDate = Policy.now();
+    let now = nowDate.getTime();
+
+    if ((now - this._lastTickTime) > (1.1 * SCHEDULER_TICK_INTERVAL_MS) &&
+        (this._lastTickTime != 0)) {
+      Telemetry.getHistogramById("TELEMETRY_SCHEDULER_WAKEUP").add(1);
+      this._log.trace("_schedulerTickLogic - First scheduler tick after sleep.");
+    }
+    this._lastTickTime = now;
+
+    // Check if the daily ping is due.
+    const shouldSendDaily = this._isDailyPingDue(nowDate);
+
+    if (shouldSendDaily) {
+      this._log.trace("_schedulerTickLogic - Daily ping due.");
+      this._lastDailyPingTime = now;
+      return TelemetrySession.sendDailyPing();
+    }
+
+    // Check if the aborted-session ping is due. If a daily ping was saved above, it was
+    // already duplicated as an aborted-session ping.
+    const isAbortedPingDue =
+      (now - this._lastSessionCheckpointTime) >= ABORTED_SESSION_UPDATE_INTERVAL_MS;
+    if (isAbortedPingDue) {
+      this._log.trace("_schedulerTickLogic - Aborted session ping due.");
+      return this._saveAbortedPing(now);
+    }
+
+    // No ping is due.
+    this._log.trace("_schedulerTickLogic - No ping due.");
+    return Promise.resolve();
+  },
+
+  /**
+   * Update the scheduled pings if some other ping was sent.
+   * @param {String} reason The reason of the ping that was sent.
+   * @param {Object} [competingPayload=null] The payload of the ping that was sent.
+   */
+  reschedulePings(reason, competingPayload = null) {
+    if (this._shuttingDown) {
+      this._log.error("reschedulePings - already shutdown");
+      return;
+    }
+
+    this._log.trace("reschedulePings - reason: " + reason);
+    let now = Policy.now();
+    this._lastAdhocPingTime = now.getTime();
+    if (reason == REASON_ENVIRONMENT_CHANGE) {
+      // We just generated an environment-changed ping, save it as an aborted session and
+      // update the schedules.
+      this._saveAbortedPing(now.getTime(), competingPayload);
+      // If we're close to midnight, skip today's daily ping and reschedule it for tomorrow.
+      let nearestMidnight = TelemetryUtils.getNearestMidnight(now, SCHEDULER_MIDNIGHT_TOLERANCE_MS);
+      if (nearestMidnight) {
+        this._lastDailyPingTime = now.getTime();
+      }
+    }
+
+    this._rescheduleTimeout();
+  },
+};
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -100,16 +100,17 @@ XPCOM_MANIFESTS += [
     'core/components.conf',
 ]
 
 EXTRA_JS_MODULES += [
     'app/TelemetryArchive.jsm',
     'app/TelemetryController.jsm',
     'app/TelemetryEnvironment.jsm',
     'app/TelemetryReportingPolicy.jsm',
+    'app/TelemetryScheduler.jsm',
     'app/TelemetrySend.jsm',
     'app/TelemetryStorage.jsm',
     'app/TelemetryTimestamps.jsm',
     'app/TelemetryUtils.jsm',
     'other/GCTelemetry.jsm',
     'other/UITelemetry.jsm',
     'pings/CoveragePing.jsm',
     'pings/EcosystemTelemetry.jsm',
--- a/toolkit/components/telemetry/pings/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/pings/TelemetrySession.jsm
@@ -3,28 +3,28 @@
  * 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/. */
 
 "use strict";
 
 const {Log} = ChromeUtils.import("resource://gre/modules/Log.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm", this);
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
-const {clearTimeout, setTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm");
 ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this);
 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
   TelemetryController: "resource://gre/modules/TelemetryController.jsm",
   TelemetryStorage: "resource://gre/modules/TelemetryStorage.jsm",
   UITelemetry: "resource://gre/modules/UITelemetry.jsm",
   GCTelemetry: "resource://gre/modules/GCTelemetry.jsm",
   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
   TelemetryReportingPolicy: "resource://gre/modules/TelemetryReportingPolicy.jsm",
+  TelemetryScheduler: "resource://gre/modules/TelemetryScheduler.jsm",
 });
 
 const Utils = TelemetryUtils;
 
 const myScope = this;
 
 // When modifying the payload in incompatible ways, please bump this version number
 const PAYLOAD_VERSION = 4;
@@ -46,60 +46,36 @@ const MIN_SUBSESSION_LENGTH_MS = Service
 
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetrySession" + (Utils.isContentProcess ? "#content::" : "::");
 
 // Whether the FHR/Telemetry unification features are enabled.
 // Changing this pref requires a restart.
 const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(TelemetryUtils.Preferences.Unified, false);
 
-// Execute a scheduler tick every 5 minutes.
-const SCHEDULER_TICK_INTERVAL_MS = Services.prefs.getIntPref("toolkit.telemetry.scheduler.tickInterval", 5 * 60) * 1000;
-// When user is idle, execute a scheduler tick every 60 minutes.
-const SCHEDULER_TICK_IDLE_INTERVAL_MS = Services.prefs.getIntPref("toolkit.telemetry.scheduler.idleTickInterval", 60 * 60) * 1000;
-
-// The tolerance we have when checking if it's midnight (15 minutes).
-const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
-
-// The maximum time (ms) until the tick should moved from the idle
-// queue to the regular queue if it hasn't been executed yet.
-const SCHEDULER_TICK_MAX_IDLE_DELAY_MS = 60 * 1000;
-
-// Seconds of idle time before pinging.
-// On idle-daily a gather-telemetry notification is fired, during it probes can
-// start asynchronous tasks to gather data.
-const IDLE_TIMEOUT_SECONDS = Services.prefs.getIntPref("toolkit.telemetry.idleTimeout", 5 * 60);
-
-// The frequency at which we persist session data to the disk to prevent data loss
-// in case of aborted sessions (currently 5 minutes).
-const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
-
 var gWasDebuggerAttached = false;
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"],
-  idleService: ["@mozilla.org/widget/idleservice;1", "nsIIdleService"],
 });
 
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
 /**
  * This is a policy object used to override behavior for testing.
  */
 var Policy = {
   now: () => new Date(),
   monotonicNow: Utils.monotonicNow,
   generateSessionUUID: () => generateUUID(),
   generateSubsessionUUID: () => generateUUID(),
-  setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
-  clearSchedulerTickTimeout: id => clearTimeout(id),
 };
 
 /**
  * Get the ping type based on the payload.
  * @param {Object} aPayload The ping payload.
  * @return {String} A string representing the ping type.
  */
 function getPingType(aPayload) {
@@ -169,332 +145,16 @@ var processInfo = {
     }
     let io = new this._IO_COUNTERS();
     if (!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address()))
       return null;
     return [parseInt(io.readBytes), parseInt(io.writeBytes)];
   },
 };
 
-/**
- * TelemetryScheduler contains a single timer driving all regularly-scheduled
- * Telemetry related jobs. Having a single place with this logic simplifies
- * reasoning about scheduling actions in a single place, making it easier to
- * coordinate jobs and coalesce them.
- */
-var TelemetryScheduler = {
-  _lastDailyPingTime: 0,
-  _lastSessionCheckpointTime: 0,
-
-  // For sanity checking.
-  _lastAdhocPingTime: 0,
-  _lastTickTime: 0,
-
-  _log: null,
-
-  // The timer which drives the scheduler.
-  _schedulerTimer: null,
-  // The interval used by the scheduler timer.
-  _schedulerInterval: 0,
-  _shuttingDown: true,
-  _isUserIdle: false,
-
-  /**
-   * Initialises the scheduler and schedules the first daily/aborted session pings.
-   */
-  init() {
-    this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::");
-    this._log.trace("init");
-    this._shuttingDown = false;
-    this._isUserIdle = false;
-
-    // Initialize the last daily ping and aborted session last due times to the current time.
-    // Otherwise, we might end up sending daily pings even if the subsession is not long enough.
-    let now = Policy.now();
-    this._lastDailyPingTime = now.getTime();
-    this._lastSessionCheckpointTime = now.getTime();
-    this._rescheduleTimeout();
-
-    idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
-    Services.obs.addObserver(this, "wake_notification");
-  },
-
-  /**
-   * Stops the scheduler.
-   */
-  shutdown() {
-    if (this._shuttingDown) {
-      if (this._log) {
-        this._log.error("shutdown - Already shut down");
-      } else {
-        Cu.reportError("TelemetryScheduler.shutdown - Already shut down");
-      }
-      return;
-    }
-
-    this._log.trace("shutdown");
-    if (this._schedulerTimer) {
-      Policy.clearSchedulerTickTimeout(this._schedulerTimer);
-      this._schedulerTimer = null;
-    }
-
-    idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
-    Services.obs.removeObserver(this, "wake_notification");
-
-    this._shuttingDown = true;
-  },
-
-  _clearTimeout() {
-    if (this._schedulerTimer) {
-      Policy.clearSchedulerTickTimeout(this._schedulerTimer);
-    }
-  },
-
-  /**
-   * Reschedules the tick timer.
-   */
-  _rescheduleTimeout() {
-    this._log.trace("_rescheduleTimeout - isUserIdle: " + this._isUserIdle);
-    if (this._shuttingDown) {
-      this._log.warn("_rescheduleTimeout - already shutdown");
-      return;
-    }
-
-    this._clearTimeout();
-
-    const now = Policy.now();
-    let timeout = SCHEDULER_TICK_INTERVAL_MS;
-
-    // When the user is idle we want to fire the timer less often.
-    if (this._isUserIdle) {
-      timeout = SCHEDULER_TICK_IDLE_INTERVAL_MS;
-      // We need to make sure though that we don't miss sending pings around
-      // midnight when we use the longer idle intervals.
-      const nextMidnight = Utils.getNextMidnight(now);
-      timeout = Math.min(timeout, nextMidnight.getTime() - now.getTime());
-    }
-
-    this._log.trace("_rescheduleTimeout - scheduling next tick for " + new Date(now.getTime() + timeout));
-    this._schedulerTimer =
-      Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), timeout);
-  },
-
-  _sentDailyPingToday(nowDate) {
-    // This is today's date and also the previous midnight (0:00).
-    const todayDate = Utils.truncateToDays(nowDate);
-    // We consider a ping sent for today if it occured after or at 00:00 today.
-    return (this._lastDailyPingTime >= todayDate.getTime());
-  },
-
-  /**
-   * Checks if we can send a daily ping or not.
-   * @param {Object} nowDate A date object.
-   * @return {Boolean} True if we can send the daily ping, false otherwise.
-   */
-  _isDailyPingDue(nowDate) {
-    // The daily ping is not due if we already sent one today.
-    if (this._sentDailyPingToday(nowDate)) {
-      this._log.trace("_isDailyPingDue - already sent one today");
-      return false;
-    }
-
-    // Avoid overly short sessions.
-    const timeSinceLastDaily = nowDate.getTime() - this._lastDailyPingTime;
-    if (timeSinceLastDaily < MIN_SUBSESSION_LENGTH_MS) {
-      this._log.trace("_isDailyPingDue - delaying daily to keep minimum session length");
-      return false;
-    }
-
-    this._log.trace("_isDailyPingDue - is due");
-    return true;
-  },
-
-  /**
-   * An helper function to save an aborted-session ping.
-   * @param {Number} now The current time, in milliseconds.
-   * @param {Object} [competingPayload=null] If we are coalescing the daily and the
-   *                 aborted-session pings, this is the payload for the former. Note
-   *                 that the reason field of this payload will be changed.
-   * @return {Promise} A promise resolved when the ping is saved.
-   */
-  _saveAbortedPing(now, competingPayload = null) {
-    this._lastSessionCheckpointTime = now;
-    return Impl._saveAbortedSessionPing(competingPayload)
-                .catch(e => this._log.error("_saveAbortedPing - Failed", e));
-  },
-
-  /**
-   * The notifications handler.
-   */
-  observe(aSubject, aTopic, aData) {
-    this._log.trace("observe - aTopic: " + aTopic);
-    switch (aTopic) {
-      case "idle":
-        // If the user is idle, increase the tick interval.
-        this._isUserIdle = true;
-        return this._onSchedulerTick();
-      case "active":
-        // User is back to work, restore the original tick interval.
-        this._isUserIdle = false;
-        return this._onSchedulerTick(true);
-      case "wake_notification":
-        // The machine woke up from sleep, trigger a tick to avoid sessions
-        // spanning more than a day.
-        // This is needed because sleep time does not count towards timeouts
-        // on Mac & Linux - see bug 1262386, bug 1204823 et al.
-        return this._onSchedulerTick(true);
-    }
-    return undefined;
-  },
-
-  /**
-   * Creates an object with a method `dispatch` that will call `dispatchFn` unless
-   * the method `cancel` is called beforehand.
-   *
-   * This is used to wrap main thread idle dispatch since it does not provide a
-   * cancel mechanism.
-   */
-  _makeIdleDispatch(dispatchFn) {
-    this._log.trace("_makeIdleDispatch");
-    let fn = dispatchFn;
-    let l = (msg) => this._log.trace(msg); // need to bind `this`
-    return {
-      cancel() {
-        fn = undefined;
-      },
-      dispatch(resolve, reject) {
-        l("_makeIdleDispatch.dispatch - !!fn: " + !!fn);
-        if (!fn) {
-          return Promise.resolve().then(resolve, reject);
-        }
-        return fn(resolve, reject);
-      },
-    };
-  },
-
-  /**
-   * Performs a scheduler tick. This function manages Telemetry recurring operations.
-   * @param {Boolean} [dispatchOnIdle=false] If true, the tick is dispatched in the
-   *                  next idle cycle of the main thread.
-   * @return {Promise} A promise, only used when testing, resolved when the scheduled
-   *                   operation completes.
-   */
-  _onSchedulerTick(dispatchOnIdle = false) {
-    this._log.trace("_onSchedulerTick - dispatchOnIdle: " + dispatchOnIdle);
-    // This call might not be triggered from a timeout. In that case we don't want to
-    // leave any previously scheduled timeouts pending.
-    this._clearTimeout();
-
-    if (this._idleDispatch) {
-      this._idleDispatch.cancel();
-    }
-
-    if (this._shuttingDown) {
-      this._log.warn("_onSchedulerTick - already shutdown.");
-      return Promise.reject(new Error("Already shutdown."));
-    }
-
-    let promise = Promise.resolve();
-    try {
-      if (dispatchOnIdle) {
-        this._idleDispatch = this._makeIdleDispatch((resolve, reject) => {
-          this._log.trace("_onSchedulerTick - ildeDispatchToMainThread dispatch");
-          return this._schedulerTickLogic().then(resolve, reject);
-        });
-        promise = new Promise((resolve, reject) =>
-          Services.tm.idleDispatchToMainThread(() => {
-            return this._idleDispatch
-              ? this._idleDispatch.dispatch(resolve, reject)
-              : Promise.resolve().then(resolve, reject);
-            },
-            SCHEDULER_TICK_MAX_IDLE_DELAY_MS));
-      } else {
-        promise = this._schedulerTickLogic();
-      }
-    } catch (e) {
-      Telemetry.getHistogramById("TELEMETRY_SCHEDULER_TICK_EXCEPTION").add(1);
-      this._log.error("_onSchedulerTick - There was an exception", e);
-    } finally {
-      this._rescheduleTimeout();
-    }
-
-    // This promise is returned to make testing easier.
-    return promise;
-  },
-
-  /**
-   * Implements the scheduler logic.
-   * @return {Promise} Resolved when the scheduled task completes. Only used in tests.
-   */
-  _schedulerTickLogic() {
-    this._log.trace("_schedulerTickLogic");
-
-    let nowDate = Policy.now();
-    let now = nowDate.getTime();
-
-    if ((now - this._lastTickTime) > (1.1 * SCHEDULER_TICK_INTERVAL_MS) &&
-        (this._lastTickTime != 0)) {
-      Telemetry.getHistogramById("TELEMETRY_SCHEDULER_WAKEUP").add(1);
-      this._log.trace("_schedulerTickLogic - First scheduler tick after sleep.");
-    }
-    this._lastTickTime = now;
-
-    // Check if the daily ping is due.
-    const shouldSendDaily = this._isDailyPingDue(nowDate);
-
-    if (shouldSendDaily) {
-      this._log.trace("_schedulerTickLogic - Daily ping due.");
-      this._lastDailyPingTime = now;
-      return Impl._sendDailyPing();
-    }
-
-    // Check if the aborted-session ping is due. If a daily ping was saved above, it was
-    // already duplicated as an aborted-session ping.
-    const isAbortedPingDue =
-      (now - this._lastSessionCheckpointTime) >= ABORTED_SESSION_UPDATE_INTERVAL_MS;
-    if (isAbortedPingDue) {
-      this._log.trace("_schedulerTickLogic - Aborted session ping due.");
-      return this._saveAbortedPing(now);
-    }
-
-    // No ping is due.
-    this._log.trace("_schedulerTickLogic - No ping due.");
-    return Promise.resolve();
-  },
-
-  /**
-   * Update the scheduled pings if some other ping was sent.
-   * @param {String} reason The reason of the ping that was sent.
-   * @param {Object} [competingPayload=null] The payload of the ping that was sent. The
-   *                 reason of this payload will be changed.
-   */
-  reschedulePings(reason, competingPayload = null) {
-    if (this._shuttingDown) {
-      this._log.error("reschedulePings - already shutdown");
-      return;
-    }
-
-    this._log.trace("reschedulePings - reason: " + reason);
-    let now = Policy.now();
-    this._lastAdhocPingTime = now.getTime();
-    if (reason == REASON_ENVIRONMENT_CHANGE) {
-      // We just generated an environment-changed ping, save it as an aborted session and
-      // update the schedules.
-      this._saveAbortedPing(now.getTime(), competingPayload);
-      // If we're close to midnight, skip today's daily ping and reschedule it for tomorrow.
-      let nearestMidnight = Utils.getNearestMidnight(now, SCHEDULER_MIDNIGHT_TOLERANCE_MS);
-      if (nearestMidnight) {
-        this._lastDailyPingTime = now.getTime();
-      }
-    }
-
-    this._rescheduleTimeout();
-  },
-};
-
 var EXPORTED_SYMBOLS = ["TelemetrySession"];
 
 var TelemetrySession = Object.freeze({
   /**
    * Send a ping to a test server. Used only for testing.
    */
   testPing() {
     return Impl.testPing();
@@ -617,16 +277,25 @@ var TelemetrySession = Object.freeze({
    * Please note that the returned value is trustworthy only after the delayed setup.
    *
    * @return {Boolean} True if the new profile ping was sent on this profile,
    *         false otherwise.
    */
   get newProfilePingSent() {
     return Impl._newProfilePingSent;
   },
+
+
+  saveAbortedSessionPing(aProvidedPayload) {
+    return Impl._saveAbortedSessionPing(aProvidedPayload);
+  },
+
+  sendDailyPing() {
+    return Impl._sendDailyPing();
+  },
 });
 
 var Impl = {
   _initialized: false,
   _logger: null,
   _slowSQLStartup: {},
   // The activity state for the user. If false, don't count the next
   // active tick. Otherwise, increment the active ticks as usual.
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -257,19 +257,19 @@ var gAppInfo = null;
 function createAppInfo(ID = "xpcshell@tests.mozilla.org", name = "XPCShell",
                        version = "1.0", platformVersion = "1.0") {
   AddonTestUtils.createAppInfo(ID, name, version, platformVersion);
   gAppInfo = AddonTestUtils.appInfo;
 }
 
 // Fake the timeout functions for the TelemetryScheduler.
 function fakeSchedulerTimer(set, clear) {
-  let session = ChromeUtils.import("resource://gre/modules/TelemetrySession.jsm", null);
-  session.Policy.setSchedulerTickTimeout = set;
-  session.Policy.clearSchedulerTickTimeout = clear;
+  let scheduler = ChromeUtils.import("resource://gre/modules/TelemetryScheduler.jsm", null);
+  scheduler.Policy.setSchedulerTickTimeout = set;
+  scheduler.Policy.clearSchedulerTickTimeout = clear;
 }
 
 /* global TelemetrySession:false, TelemetryEnvironment:false, TelemetryController:false,
           TelemetryStorage:false, TelemetrySend:false, TelemetryReportingPolicy:false
  */
 
 /**
  * Fake the current date.
@@ -282,16 +282,17 @@ function fakeNow(...args) {
   const date = new Date(...args);
   const modules = [
     ChromeUtils.import("resource://gre/modules/TelemetrySession.jsm", null),
     ChromeUtils.import("resource://gre/modules/TelemetryEnvironment.jsm", null),
     ChromeUtils.import("resource://gre/modules/TelemetryController.jsm", null),
     ChromeUtils.import("resource://gre/modules/TelemetryStorage.jsm", null),
     ChromeUtils.import("resource://gre/modules/TelemetrySend.jsm", null),
     ChromeUtils.import("resource://gre/modules/TelemetryReportingPolicy.jsm", null),
+    ChromeUtils.import("resource://gre/modules/TelemetryScheduler.jsm", null),
   ];
 
   for (let m of modules) {
     m.Policy.now = () => date;
   }
 
   return new Date(date);
 }
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -78,18 +78,18 @@ function sendPing() {
 
 function fakeGenerateUUID(sessionFunc, subsessionFunc) {
   let session = ChromeUtils.import("resource://gre/modules/TelemetrySession.jsm", null);
   session.Policy.generateSessionUUID = sessionFunc;
   session.Policy.generateSubsessionUUID = subsessionFunc;
 }
 
 function fakeIdleNotification(topic) {
-  let session = ChromeUtils.import("resource://gre/modules/TelemetrySession.jsm", null);
-  return session.TelemetryScheduler.observe(null, topic, null);
+  let scheduler = ChromeUtils.import("resource://gre/modules/TelemetryScheduler.jsm", null);
+  return scheduler.TelemetryScheduler.observe(null, topic, null);
 }
 
 function setupTestData() {
   Services.startup.interrupted = true;
   let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
   h2.add();
 
   let k1 = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");