summaryrefslogtreecommitdiffstats
path: root/toolkit/components/perfmonitoring/PerformanceWatcher.jsm
blob: d0d034974967df91796c718b6504e7f16c767d73 (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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
// -*- 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";

/**
 * An API for being informed of slow add-ons and tabs.
 *
 * Generally, this API is both more CPU-efficient and more battery-efficient
 * than PerformanceStats. As PerformanceStats, this API does not provide any
 * information during the startup or shutdown of Firefox.
 *
 * = Examples =
 *
 * Example use: reporting whenever a specific add-on slows down Firefox.
 * let listener = function(source, details) {
 *   // This listener is triggered whenever the addon causes Firefox to miss
 *   // frames. Argument `source` contains information about the source of the
 *   // slowdown (including the process in which it happens), while `details`
 *   // contains performance statistics.
 *   console.log(`Oops, add-on ${source.addonId} seems to be slowing down Firefox.`, details);
 * };
 * PerformanceWatcher.addPerformanceListener({addonId: "myaddon@myself.name"}, listener);
 *
 * Example use: reporting whenever any webpage slows down Firefox.
 * let listener = function(alerts) {
 *   // This listener is triggered whenever any window causes Firefox to miss
 *   // frames. FieldArgument `source` contains information about the source of the
 *   // slowdown (including the process in which it happens), while `details`
 *   // contains performance statistics.
 *   for (let {source, details} of alerts) {
 *     console.log(`Oops, window ${source.windowId} seems to be slowing down Firefox.`, details);
 * };
 * // Special windowId 0 lets us to listen to all webpages.
 * PerformanceWatcher.addPerformanceListener({windowId: 0}, listener);
 *
 *
 * = How this works =
 *
 * This high-level API is based on the lower-level nsIPerformanceStatsService.
 * At the end of each event (including micro-tasks), the nsIPerformanceStatsService
 * updates its internal performance statistics and determines whether any
 * add-on/window in the current process has exceeded the jank threshold.
 *
 * The PerformanceWatcher maintains low-level performance observers in each
 * process and forwards alerts to the main process. Internal observers collate
 * low-level main process alerts and children process alerts and notify clients
 * of this API.
 */

this.EXPORTED_SYMBOLS = ["PerformanceWatcher"];

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

let { PerformanceStats, performanceStatsService } = Cu.import("resource://gre/modules/PerformanceStats.jsm", {});
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});

// `true` if the code is executed in content, `false` otherwise
let isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;

if (!isContent) {
  // Initialize communication with children.
  //
  // To keep the protocol simple, the children inform the parent whenever a slow
  // add-on/tab is detected. We do not attempt to implement thresholds.
  Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceWatcher-content.js",
    true/* including future processes*/);

  Services.ppmm.addMessageListener("performancewatcher-propagate-notifications",
    (...args) => ChildManager.notifyObservers(...args)
  );
}

// Configure the performance stats service to inform us in case of jank.
performanceStatsService.jankAlertThreshold = 64000 /* us */;


/**
 * Handle communications with child processes. Handle listening to
 * either a single add-on id (including the special add-on id "*",
 * which is notified for all add-ons) or a single window id (including
 * the special window id 0, which is notified for all windows).
 *
 * Acquire through `ChildManager.getAddon` and `ChildManager.getWindow`.
 */
function ChildManager(map, key) {
  this.key = key;
  this._map = map;
  this._listeners = new Set();
}
ChildManager.prototype = {
  /**
   * Add a listener, which will be notified whenever a child process
   * reports a slow performance alert for this addon/window.
   */
  addListener: function(listener) {
    this._listeners.add(listener);
  },
  /**
   * Remove a listener.
   */
  removeListener: function(listener) {
    let deleted = this._listeners.delete(listener);
    if (!deleted) {
      throw new Error("Unknown listener");
    }
  },

  listeners: function() {
    return this._listeners.values();
  }
};

/**
 * Dispatch child alerts to observers.
 *
 * Triggered by messages from content processes.
 */
ChildManager.notifyObservers = function({data: {addons, windows}}) {
  if (addons && addons.length > 0) {
    // Dispatch the entire list to universal listeners
    this._notify(ChildManager.getAddon("*").listeners(), addons);

    // Dispatch individual alerts to individual listeners
    for (let {source, details} of addons) {
      this._notify(ChildManager.getAddon(source.addonId).listeners(), source, details);
    }
  }
  if (windows && windows.length > 0) {
    // Dispatch the entire list to universal listeners
    this._notify(ChildManager.getWindow(0).listeners(), windows);

    // Dispatch individual alerts to individual listeners
    for (let {source, details} of windows) {
      this._notify(ChildManager.getWindow(source.windowId).listeners(), source, details);
    }
  }
};

ChildManager._notify = function(targets, ...args) {
  for (let target of targets) {
    target(...args);
  }
};

ChildManager.getAddon = function(key) {
  return this._get(this._addons, key);
};
ChildManager._addons = new Map();

ChildManager.getWindow = function(key) {
  return this._get(this._windows, key);
};
ChildManager._windows = new Map();

ChildManager._get = function(map, key) {
  let result = map.get(key);
  if (!result) {
    result = new ChildManager(map, key);
    map.set(key, result);
  }
  return result;
};
let gListeners = new WeakMap();

/**
 * An object in charge of managing all the observables for a single
 * target (window/addon/all windows/all addons).
 *
 * In a content process, a target is represented by a single observable.
 * The situation is more sophisticated in a parent process, as a target
 * has both an in-process observable and several observables across children
 * processes.
 *
 * This class abstracts away the difference to simplify the work of
 * (un)registering observers for targets.
 *
 * @param {object} target The target being observed, as an object
 * with one of the following fields:
 *   - {string} addonId Either "*" for the universal add-on observer
 *     or the add-on id of an addon. Note that this class does not
 *     check whether the add-on effectively exists, and that observers
 *     may be registered for an add-on before the add-on is installed
 *     or started.
 *   - {xul:tab} tab A single tab. It must already be initialized.
 *   - {number} windowId Either 0 for the universal window observer
 *     or the outer window id of the window.
 */
function Observable(target) {
  // A mapping from `listener` (function) to `Observer`.
  this._observers = new Map();
  if ("addonId" in target) {
    this._key = `addonId: ${target.addonId}`;
    this._process = performanceStatsService.getObservableAddon(target.addonId);
    this._children = isContent ? null : ChildManager.getAddon(target.addonId);
    this._isBuffered = target.addonId == "*";
  } else if ("tab" in target || "windowId" in target) {
    let windowId;
    if ("tab" in target) {
      windowId = target.tab.linkedBrowser.outerWindowID;
      // By convention, outerWindowID may not be 0.
    } else if ("windowId" in target) {
      windowId = target.windowId;
    }
    if (windowId == undefined || windowId == null) {
      throw new TypeError(`No outerWindowID. Perhaps the target is a tab that is not initialized yet.`);
    }
    this._key = `tab-windowId: ${windowId}`;
    this._process = performanceStatsService.getObservableWindow(windowId);
    this._children = isContent ? null : ChildManager.getWindow(windowId);
    this._isBuffered = windowId == 0;
  } else {
    throw new TypeError("Unexpected target");
  }
}
Observable.prototype = {
  addJankObserver: function(listener) {
    if (this._observers.has(listener)) {
      throw new TypeError(`Listener already registered for target ${this._key}`);
    }
    if (this._children) {
      this._children.addListener(listener);
    }
    let observer = this._isBuffered ? new BufferedObserver(listener)
      : new Observer(listener);
    // Store the observer to be able to call `this._process.removeJankObserver`.
    this._observers.set(listener, observer);

    this._process.addJankObserver(observer);
  },
  removeJankObserver: function(listener) {
    let observer = this._observers.get(listener);
    if (!observer) {
      throw new TypeError(`No listener for target ${this._key}`);
    }
    this._observers.delete(listener);

    if (this._children) {
      this._children.removeListener(listener);
    }

    this._process.removeJankObserver(observer);
    observer.dispose();
  },
};

/**
 * Get a cached observable for a given target.
 */
Observable.get = function(target) {
  let key;
  if ("addonId" in target) {
    key = target.addonId;
  } else if ("tab" in target) {
    // We do not want to use a tab as a key, as this would prevent it from
    // being garbage-collected.
    key = target.tab.linkedBrowser.outerWindowID;
  } else if ("windowId" in target) {
    key = target.windowId;
  }
  if (key == null) {
    throw new TypeError(`Could not extract a key from ${JSON.stringify(target)}. Could the target be an unitialized tab?`);
  }
  let observable = this._cache.get(key);
  if (!observable) {
    observable = new Observable(target);
    this._cache.set(key, observable);
  }
  return observable;
};
Observable._cache = new Map();

/**
 * Wrap a listener callback as an unbuffered nsIPerformanceObserver.
 *
 * Each observation is propagated immediately to the listener.
 */
function Observer(listener) {
  // Make sure that monitoring stays alive (in all processes) at least as
  // long as the observer.
  this._monitor = PerformanceStats.getMonitor(["jank", "cpow"]);
  this._listener = listener;
}
Observer.prototype = {
  observe: function(...args) {
    this._listener(...args);
  },
  dispose: function() {
    this._monitor.dispose();
    this.observe = function poison() {
      throw new Error("Internal error: I should have stopped receiving notifications");
    }
  },
};

/**
 * Wrap a listener callback as an buffered nsIPerformanceObserver.
 *
 * Observations are buffered and dispatch in the next tick to the listener.
 */
function BufferedObserver(listener) {
  Observer.call(this, listener);
  this._buffer = [];
  this._isDispatching = false;
  this._pending = null;
}
BufferedObserver.prototype = Object.create(Observer.prototype);
BufferedObserver.prototype.observe = function(source, details) {
  this._buffer.push({source, details});
  if (!this._isDispatching) {
    this._isDispatching = true;
    Services.tm.mainThread.dispatch(() => {
      // Grab buffer, in case something in the listener could modify it.
      let buffer = this._buffer;
      this._buffer = [];

      // As of this point, any further observations need to use the new buffer
      // and a new dispatcher.
      this._isDispatching = false;

      this._listener(buffer);
    }, Ci.nsIThread.DISPATCH_NORMAL);
  }
};

this.PerformanceWatcher = {
  /**
   * Add a listener informed whenever we receive a slow performance alert
   * in the application.
   *
   * @param {object} target An object with one of the following fields:
   *  - {string} addonId Either "*" to observe all add-ons or a full add-on ID.
   *      to observe a single add-on.
   *  - {number} windowId Either 0 to observe all windows or an outer window ID
   *      to observe a single tab.
   *  - {xul:browser} tab To observe a single tab.
   * @param {function} listener A function that will be triggered whenever
   *    the target causes a slow performance notification. The notification may
   *    have originated in any process of the application.
   *
   *    If the listener listens to a single add-on/webpage, it is triggered with
   *    the following arguments:
   *       source: {groupId, name, addonId, windowId, isSystem, processId}
   *         Information on the source of the notification.
   *       details: {reason, highestJank, highestCPOW} Information on the
   *         notification.
   *
   *    If the listener listens to all add-ons/all webpages, it is triggered with
   *    an array of {source, details}, as described above.
   */
  addPerformanceListener: function(target, listener) {
    if (typeof listener != "function") {
      throw new TypeError();
    }
    let observable = Observable.get(target);
    observable.addJankObserver(listener);
  },
  removePerformanceListener: function(target, listener) {
    if (typeof listener != "function") {
      throw new TypeError();
    }
    let observable = Observable.get(target);
    observable.removeJankObserver(listener);
  },
};