summaryrefslogtreecommitdiffstats
path: root/toolkit/components/perfmonitoring/AddonWatcher.jsm
blob: 58decba857226381423659e1e67b30a4f1a60438 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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";

this.EXPORTED_SYMBOLS = ["AddonWatcher"];

const { classes: Cc, interfaces: Ci, utils: Cu } = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                  "resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
                                  "resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PerformanceWatcher",
                                  "resource://gre/modules/PerformanceWatcher.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
                                  "@mozilla.org/base/telemetry;1",
                                  Ci.nsITelemetry);
XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                  "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "IdleService",
                                   "@mozilla.org/widget/idleservice;1",
                                   Ci.nsIIdleService);

/**
 * Don't notify observers of slow add-ons if at least `SUSPICIOUSLY_MANY_ADDONS`
 * show up at the same time. We assume that this indicates that the system itself
 * is busy, and that add-ons are not responsible.
 */
let SUSPICIOUSLY_MANY_ADDONS = 5;

this.AddonWatcher = {
  /**
   * Watch this topic to be informed when a slow add-on is detected and should
   * be reported to the user.
   *
   * If you need finer-grained control, use PerformanceWatcher.jsm.
   */
  TOPIC_SLOW_ADDON_DETECTED: "addon-watcher-detected-slow-addon",

  init: function() {
    this._initializedTimeStamp = Cu.now();

    try {
      this._ignoreList = new Set(JSON.parse(Preferences.get("browser.addon-watch.ignore", null)));
    } catch (ex) {
      // probably some malformed JSON, ignore and carry on
      this._ignoreList = new Set();
    }

    this._warmupPeriod = Preferences.get("browser.addon-watch.warmup-ms", 60 * 1000 /* 1 minute */);
    this._idleThreshold = Preferences.get("browser.addon-watch.deactivate-after-idle-ms", 3000);
    this.paused = false;
  },
  uninit: function() {
    this.paused = true;
  },
  _initializedTimeStamp: 0,

  set paused(paused) {
    if (paused) {
      if (this._listener) {
        PerformanceWatcher.removePerformanceListener({addonId: "*"}, this._listener);
      }
      this._listener = null;
    } else {
      this._listener = this._onSlowAddons.bind(this);
      PerformanceWatcher.addPerformanceListener({addonId: "*"}, this._listener);
    }
  },
  get paused() {
    return !this._listener;
  },
  _listener: null,

  /**
   * Provide the following object for each addon:
   *  {number} occurrences The total number of performance alerts recorded for this addon.
   *  {number} occurrencesSinceLastNotification The number of performances alerts recorded
   *     since we last notified the user.
   *  {number} latestNotificationTimeStamp The timestamp of the latest user notification
   *     that this add-on is slow.
   */
  _getAlerts: function(addonId) {
    let alerts = this._alerts.get(addonId);
    if (!alerts) {
      alerts = {
        occurrences: 0,
        occurrencesSinceLastNotification: 0,
        latestNotificationTimeStamp: 0,
      };
      this._alerts.set(addonId, alerts);
    }
    return alerts;
  },
  _alerts: new Map(),
  _onSlowAddons: function(addons) {
    try {
      if (IdleService.idleTime >= this._idleThreshold) {
        // The application is idle. Maybe the computer is sleeping, or maybe
        // the user isn't in front of it. Regardless, the user doesn't care
        // about things that slow down her browser while she's not using it.
        return;
      }

      if (addons.length > SUSPICIOUSLY_MANY_ADDONS) {
        // Heuristic: if we are notified of many slow addons at once, the issue
        // is probably not with the add-ons themselves with the system. We may
        // for instance be waking up from hibernation, or the system may be
        // busy swapping.
        return;
      }

      let now = Cu.now();
      if (now - this._initializedTimeStamp < this._warmupPeriod) {
        // Heuristic: do not report slowdowns during or just after startup.
        return;
      }

      // Report immediately to Telemetry, regardless of whether we report to
      // the user.
      for (let {source: {addonId}, details} of addons) {
        Telemetry.getKeyedHistogramById("PERF_MONITORING_SLOW_ADDON_JANK_US").
          add(addonId, details.highestJank);
        Telemetry.getKeyedHistogramById("PERF_MONITORING_SLOW_ADDON_CPOW_US").
          add(addonId, details.highestCPOW);
      }

      // We expect that users don't care about real-time alerts unless their
      // browser is going very, very slowly. Therefore, we use the following
      // heuristic:
      // - if jank is above freezeThreshold (e.g. 5 seconds), report immediately; otherwise
      // - if jank is below jankThreshold (e.g. 128ms), disregard; otherwise
      // - if the latest jank was more than prescriptionDelay (e.g. 5 minutes) ago, reset number of occurrences;
      // - if we have had fewer than occurrencesBetweenAlerts janks (e.g. 3) since last alert, disregard; otherwise
      // - if we have displayed an alert for this add-on less than delayBetweenAlerts ago (e.g. 6h), disregard; otherwise
      // - also, don't report more than highestNumberOfAddonsToReport (e.g. 1) at once.
      let freezeThreshold = Preferences.get("browser.addon-watch.freeze-threshold-micros", /* 5 seconds */ 5000000);
      let jankThreshold = Preferences.get("browser.addon-watch.jank-threshold-micros", /* 256 ms == 8 frames*/ 256000);
      let occurrencesBetweenAlerts = Preferences.get("browser.addon-watch.occurrences-between-alerts", 3);
      let delayBetweenAlerts = Preferences.get("browser.addon-watch.delay-between-alerts-ms", 6 * 3600 * 1000 /* 6h */);
      let delayBetweenFreezeAlerts = Preferences.get("browser.addon-watch.delay-between-freeze-alerts-ms", 2 * 60 * 1000 /* 2 min */);
      let prescriptionDelay = Preferences.get("browser.addon-watch.prescription-delay", 5 * 60 * 1000 /* 5 minutes */);
      let highestNumberOfAddonsToReport = Preferences.get("browser.addon-watch.max-simultaneous-reports", 1);

      addons = addons.filter(x => x.details.highestJank >= jankThreshold).
        sort((a, b) => a.details.highestJank - b.details.highestJank);

      for (let {source: {addonId}, details} of addons) {
        if (highestNumberOfAddonsToReport <= 0) {
          return;
        }
        if (this._ignoreList.has(addonId)) {
          // Add-on is ignored.
          continue;
        }

        let alerts = this._getAlerts(addonId);
        if (now - alerts.latestOccurrence >= prescriptionDelay) {
          // While this add-on has already caused slownesss, this
          // was a long time ago, let's forgive.
          alerts.occurrencesSinceLastNotification = 0;
        }

        alerts.occurrencesSinceLastNotification++;
        alerts.occurrences++;

        if (details.highestJank < freezeThreshold) {
          if (alerts.occurrencesSinceLastNotification <= occurrencesBetweenAlerts) {
            // While the add-on has caused jank at least once, we are only
            // interested in repeat offenders. Store the data for future use.
            continue;
          }
          if (now - alerts.latestNotificationTimeStamp <= delayBetweenAlerts) {
            // We have already displayed an alert for this add-on recently.
            // Wait a little before displaying another one.
            continue;
          }
        } else if (now - alerts.latestNotificationTimeStamp <= delayBetweenFreezeAlerts) {
          // Even in case of freeze, we want to avoid needlessly spamming the user.
          // We have already displayed an alert for this add-on recently.
          // Wait a little before displaying another one.
          continue;
        }

        // Ok, time to inform the user.
        alerts.latestNotificationTimeStamp = now;
        alerts.occurrencesSinceLastNotification = 0;
        Services.obs.notifyObservers(null, this.TOPIC_SLOW_ADDON_DETECTED, addonId);

        highestNumberOfAddonsToReport--;
      }
    } catch (ex) {
      Cu.reportError("Error in AddonWatcher._onSlowAddons " + ex);
      Cu.reportError(Task.Debugging.generateReadableStack(ex.stack));
    }
  },

  ignoreAddonForSession: function(addonid) {
    this._ignoreList.add(addonid);
  },
  ignoreAddonPermanently: function(addonid) {
    this._ignoreList.add(addonid);
    try {
      let ignoreList = JSON.parse(Preferences.get("browser.addon-watch.ignore", "[]"))
      if (!ignoreList.includes(addonid)) {
        ignoreList.push(addonid);
        Preferences.set("browser.addon-watch.ignore", JSON.stringify(ignoreList));
      }
    } catch (ex) {
      Preferences.set("browser.addon-watch.ignore", JSON.stringify([addonid]));
    }
  },

  /**
   * The list of alerts for this session.
   *
   * @type {Map<String, Object>} A map associating addonId to
   *  objects with fields
   *  {number} occurrences The total number of performance alerts recorded for this addon.
   *  {number} occurrencesSinceLastNotification The number of performances alerts recorded
   *     since we last notified the user.
   *  {number} latestNotificationTimeStamp The timestamp of the latest user notification
   *     that this add-on is slow.
   */
  get alerts() {
    let result = new Map();
    for (let [k, v] of this._alerts) {
      result.set(k, Cu.cloneInto(v, this));
    }
    return result;
  },
};