toolkit/components/telemetry/TelemetryHealthPing.jsm
author Kris Maglione <maglione.k@gmail.com>
Tue, 24 Apr 2018 20:18:09 -0700
changeset 415693 95b7f4ae23fdaf6d806ffac8a402e5f043dbd1c5
parent 405168 b599a95ce057712c6b7804f7f108c9c4acff7e6d
child 428444 c68131530742b84ecd191860d0bb6b2dffbf56ee
permissions -rw-r--r--
Bug 1456686: Part 1 - Fix unused and shadowed explicit imports. r=standard8 These issues were previously ignored due to the nature of our global import rules. They need to be fixed before that rule can be updated. MozReview-Commit-ID: DCChktTc5TW

/* 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/. */

/*
 * This module collects data on send failures and other critical issues with Telemetry submissions.
 */

"use strict";

var EXPORTED_SYMBOLS = [
  "TelemetryHealthPing",
];

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);

ChromeUtils.defineModuleGetter(this, "TelemetryController", "resource://gre/modules/TelemetryController.jsm");
ChromeUtils.defineModuleGetter(this, "setTimeout", "resource://gre/modules/Timer.jsm");
ChromeUtils.defineModuleGetter(this, "clearTimeout", "resource://gre/modules/Timer.jsm");
ChromeUtils.defineModuleGetter(this, "TelemetryUtils", "resource://gre/modules/TelemetryUtils.jsm");
ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
ChromeUtils.defineModuleGetter(this, "Preferences", "resource://gre/modules/Preferences.jsm");

const Utils = TelemetryUtils;

const MS_IN_A_MINUTE = 60 * 1000;
const IS_HEALTH_PING_ENABLED = Preferences.get(TelemetryUtils.Preferences.HealthPingEnabled, true);

// Send health ping every hour
const SEND_TICK_DELAY = 60 * MS_IN_A_MINUTE;

// Send top 10 discarded pings only to minimize health ping size
const MAX_SEND_DISCARDED_PINGS = 10;

const LOGGER_NAME = "Toolkit.Telemetry";
const LOGGER_PREFIX = "TelemetryHealthPing::";

var Policy = {
  setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
  clearSchedulerTickTimeout: (id) => clearTimeout(id)
};

var TelemetryHealthPing = {
  Reason: Object.freeze({
    IMMEDIATE: "immediate", // Ping was sent immediately after recording with no delay.
    DELAYED: "delayed",     // Recorded data was sent after a delay.
    SHUT_DOWN: "shutdown",  // Recorded data was sent on shutdown.
  }),

  FailureType: Object.freeze({
    DISCARDED_FOR_SIZE: "pingDiscardedForSize",
    SEND_FAILURE: "sendFailure",
  }),

  OsInfo: Object.freeze({
    "name": Services.appinfo.OS,
    "version": Services.sysinfo.get("kernel_version") || Services.sysinfo.get("version")
  }),

  HEALTH_PING_TYPE: "health",

  _logger: null,

  // The health ping is sent every every SEND_TICK_DELAY.
  // Initialize this so that first failures are sent immediately.
  _lastSendTime: -SEND_TICK_DELAY,

  /**
   * This stores reported send failures with the following structure:
   * {
   *  type1: {
   *    subtype1: value,
   *    ...
   *    subtypeN: value
   *  },
   *  ...
   * }
   */
  _failures: {},
  _timeoutId: null,

  /**
   * Record a failure to send a ping out.
   * @param {String} failureType The type of failure (e.g. "timeout", ...).
   * @returns {Promise} Test-only, resolved when the ping is stored or sent.
   */
  recordSendFailure(failureType) {
    return this._addToFailure(this.FailureType.SEND_FAILURE, failureType);
  },

  /**
   * Record that a ping was discarded and its type.
   * @param {String} pingType The type of discarded ping (e.g. "main", ...).
   * @returns {Promise} Test-only, resolved when the ping is stored or sent.
   */
  recordDiscardedPing(pingType) {
    return this._addToFailure(this.FailureType.DISCARDED_FOR_SIZE, pingType);
  },

  /**
   * Assemble payload.
   * @param {String} reason A string indicating the triggering reason (e.g. "immediate", "delayed", "shutdown").
   * @returns {Object} The assembled payload.
   */
  _assemblePayload(reason) {
    this._log.trace("_assemblePayload()");
    let payload = {
      os: this.OsInfo,
      reason
    };

    for (let key of Object.keys(this._failures)) {
      if (key === this.FailureType.DISCARDED_FOR_SIZE) {
        payload[key] = this._getTopDiscardFailures(this._failures[key]);
      } else {
        payload[key] = this._failures[key];
      }
    }

    return payload;
  },

  /**
   * Sort input dictionary descending by value.
   * @param {Object} failures A dictionary of failures subtype and count.
   * @returns {Object} Sorted failures by value.
   */
  _getTopDiscardFailures(failures) {
    this._log.trace("_getTopDiscardFailures()");
    let sortedItems = Object.entries(failures).sort((first, second) => {
      return second[1] - first[1];
    });

    let result = {};
    sortedItems.slice(0, MAX_SEND_DISCARDED_PINGS).forEach(([key, value]) => {
      result[key] = value;
    });

    return result;
  },

  /**
   * Assemble the failure information and submit it.
   * @param {String} reason A string indicating the triggering reason (e.g. "immediate", "delayed", "shutdown").
   * @returns {Promise} Test-only promise that resolves when the ping was stored or sent (if any).
   */
  _submitPing(reason) {
    if (!IS_HEALTH_PING_ENABLED || !this._hasDataToSend()) {
      return Promise.resolve();
    }

    this._log.trace("_submitPing(" + reason + ")");
    let payload = this._assemblePayload(reason);
    this._clearData();
    this._lastSendTime = Utils.monotonicNow();

    let options = {
      addClientId: true,
      usePingSender: reason === this.Reason.SHUT_DOWN
    };

    return new Promise(r =>
      // If we submit the health ping immediately, the send task would be triggered again
      // before discarding oversized pings from the queue.
      // To work around this, we send the ping on the next tick.
      Services.tm.dispatchToMainThread(() => r(
        TelemetryController
          .submitExternalPing(this.HEALTH_PING_TYPE, payload, options))));
  },

  /**
   * Accumulate failure information and trigger a ping immediately or on timeout.
   * @param {String} failureType The type of failure (e.g. "timeout", ...).
   * @param {String} failureSubType The subtype of failure (e.g. ping type, ...).
   * @returns {Promise} Test-only, resolved when the ping is stored or sent.
   */
  _addToFailure(failureType, failureSubType) {
    this._log.trace("_addToFailure() - with type and subtype: " + failureType + " : " + failureSubType);

    if (!(failureType in this._failures)) {
      this._failures[failureType] = {};
    }

    let current = this._failures[failureType][failureSubType] || 0;
    this._failures[failureType][failureSubType] = current + 1;

    const now = Utils.monotonicNow();
    if ((now - this._lastSendTime) >= SEND_TICK_DELAY) {
      return this._submitPing(this.Reason.IMMEDIATE);
    }

    let submissionDelay = SEND_TICK_DELAY - now - this._lastSendTime;
    this._timeoutId =
      Policy.setSchedulerTickTimeout(() => TelemetryHealthPing._submitPing(this.Reason.DELAYED), submissionDelay);
    return Promise.resolve();
  },

  /**
   * @returns {boolean} Check the availability of recorded failures data.
   */
  _hasDataToSend() {
    return Object.keys(this._failures).length !== 0;
  },

  /**
   * Clear recorded failures data.
   */
  _clearData() {
    this._log.trace("_clearData()");
    this._failures = {};
  },

  /**
   * Clear and reset timeout.
   */
  _resetTimeout() {
    if (this._timeoutId) {
      Policy.clearSchedulerTickTimeout(this._timeoutId);
      this._timeoutId = null;
    }
  },

  /**
   * Submit latest ping on shutdown.
   * @returns {Promise} Test-only, resolved when the ping is stored or sent.
   */
  shutdown() {
    this._log.trace("shutdown()");
    this._resetTimeout();
    return this._submitPing(this.Reason.SHUT_DOWN);
  },

  /**
   * Test-only, restore to initial state.
   */
  testReset() {
    this._lastSendTime = -SEND_TICK_DELAY;
    this._clearData();
    this._resetTimeout();
  },

  get _log() {
    if (!this._logger) {
      this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX + "::");
    }

    return this._logger;
  },
};