summaryrefslogtreecommitdiff
path: root/toolkit/devtools/profiler/profiler.js
blob: 2e0ffad146aff0ae3ae2d5d9e0c1fd791920e3dc (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
/* 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";

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

Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");

devtools.lazyRequireGetter(this, "Services");
devtools.lazyRequireGetter(this, "promise");
devtools.lazyRequireGetter(this, "EventEmitter",
  "devtools/toolkit/event-emitter");
devtools.lazyRequireGetter(this, "DevToolsUtils",
  "devtools/toolkit/DevToolsUtils");
devtools.lazyRequireGetter(this, "FramerateFront",
  "devtools/server/actors/framerate", true);

devtools.lazyRequireGetter(this, "L10N",
  "devtools/shared/profiler/global", true);
devtools.lazyRequireGetter(this, "CATEGORIES",
  "devtools/shared/profiler/global", true);
devtools.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
  "devtools/shared/profiler/global", true);
devtools.lazyRequireGetter(this, "CATEGORY_OTHER",
  "devtools/shared/profiler/global", true);
devtools.lazyRequireGetter(this, "ThreadNode",
  "devtools/shared/profiler/tree-model", true);
devtools.lazyRequireGetter(this, "CallView",
  "devtools/shared/profiler/tree-view", true);

devtools.lazyImporter(this, "FileUtils",
  "resource://gre/modules/FileUtils.jsm");
devtools.lazyImporter(this, "NetUtil",
  "resource://gre/modules/NetUtil.jsm");
devtools.lazyImporter(this, "LineGraphWidget",
  "resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "BarGraphWidget",
  "resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "CanvasGraphUtils",
  "resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "SideMenuWidget",
  "resource:///modules/devtools/SideMenuWidget.jsm");

const RECORDING_DATA_DISPLAY_DELAY = 10; // ms
const FRAMERATE_CALC_INTERVAL = 16; // ms
const FRAMERATE_GRAPH_HEIGHT = 60; // px
const CATEGORIES_GRAPH_HEIGHT = 60; // px
const CATEGORIES_GRAPH_MIN_BARS_WIDTH = 3; // px
const CALL_VIEW_FOCUS_EVENTS_DRAIN = 10; // ms
const GRAPH_SCROLL_EVENTS_DRAIN = 50; // ms
const GRAPH_ZOOM_MIN_TIMESPAN = 20; // ms

// This identifier string is used to tentatively ascertain whether or not
// a JSON loaded from disk is actually something generated by this tool.
// It isn't, of course, a definitive verification, but a Good Enough™
// approximation before continuing the import. Don't localize this.
const PROFILE_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
const PROFILE_SERIALIZER_VERSION = 1;

// The panel's window global is an EventEmitter firing the following events:
const EVENTS = {
  // When a recording is started or stopped, via the `stopwatch` button, or
  // when `console.profile` and `console.profileEnd` is invoked.
  RECORDING_STARTED: "Profiler:RecordingStarted",
  RECORDING_ENDED: "Profiler:RecordingEnded",

  // When a recording is abruptly ended, either because the built-in profiler
  // module is stopped by a third party, or because the recordings list is
  // cleared while there's one in progress.
  RECORDING_LOST: "Profiler:RecordingCancelled",

  // When a recording is displayed in the ProfileView.
  RECORDING_DISPLAYED: "Profiler:RecordingDisplayed",

  // When a new tab is spawned in the ProfileView from a graphs selection.
  TAB_SPAWNED_FROM_SELECTION: "Profiler:TabSpawnedFromSelection",

  // When a new tab is spawned in the ProfileView from a node in the tree.
  TAB_SPAWNED_FROM_FRAME_NODE: "Profiler:TabSpawnedFromFrameNode",

  // When different panels in the ProfileView are shown.
  EMPTY_NOTICE_SHOWN: "Profiler:EmptyNoticeShown",
  RECORDING_NOTICE_SHOWN: "Profiler:RecordingNoticeShown",
  LOADING_NOTICE_SHOWN: "Profiler:LoadingNoticeShown",
  TABBED_BROWSER_SHOWN: "Profiler:TabbedBrowserShown",

  // When a source is shown in the JavaScript Debugger at a specific location.
  SOURCE_SHOWN_IN_JS_DEBUGGER: "Profiler:SourceShownInJsDebugger",
  SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Profiler:SourceNotFoundInJsDebugger"
};

/**
 * The current target and the profiler connection, set by this tool's host.
 */
let gToolbox, gTarget, gFront;

/**
 * Initializes the profiler controller and views.
 */
let startupProfiler = Task.async(function*() {
  yield promise.all([
    PrefObserver.register(),
    EventsHandler.initialize(),
    RecordingsListView.initialize(),
    ProfileView.initialize()
  ]);

  // Profiles may have been created before this tool was opened, e.g. via
  // `console.profile` and `console.profileEnd(). Populate the UI with them.
  for (let recordingData of gFront.finishedConsoleRecordings) {
    let profileLabel = recordingData.profilerData.profileLabel;
    let recordingItem = RecordingsListView.addEmptyRecording(profileLabel);
    RecordingsListView.customizeRecording(recordingItem, recordingData);
  }
  for (let { profileLabel } of gFront.pendingConsoleRecordings) {
    RecordingsListView.handleRecordingStarted(profileLabel);
  }

  // Select the first recording, if available.
  RecordingsListView.selectedIndex = 0;
});

/**
 * Destroys the profiler controller and views.
 */
let shutdownProfiler = Task.async(function*() {
  yield promise.all([
    PrefObserver.unregister(),
    EventsHandler.destroy(),
    RecordingsListView.destroy(),
    ProfileView.destroy()
  ]);
});

/**
 * Observes pref changes on the devtools.profiler branch and triggers the
 * required frontend modifications.
 */
let PrefObserver = {
  register: function() {
    this.branch = Services.prefs.getBranch("devtools.profiler.");
    this.branch.addObserver("", this, false);
  },
  unregister: function() {
    this.branch.removeObserver("", this);
  },
  observe: function(subject, topic, pref) {
    Prefs.refresh();

    if (pref == "ui.show-platform-data") {
      RecordingsListView.forceSelect(RecordingsListView.selectedItem);
    }
  }
};

/**
 * Functions handling target-related lifetime events.
 */
let EventsHandler = {
  /**
   * Listen for events emitted by the current tab target.
   */
  initialize: function() {
    this._onConsoleProfileStart = this._onConsoleProfileStart.bind(this);
    this._onConsoleProfileEnd = this._onConsoleProfileEnd.bind(this);

    gFront.on("profile", this._onConsoleProfileStart);
    gFront.on("profileEnd", this._onConsoleProfileEnd);
    gFront.on("profiler-unexpectedly-stopped", this._onProfilerDeactivated);
  },

  /**
   * Remove events emitted by the current tab target.
   */
  destroy: function() {
    gFront.off("profile", this._onConsoleProfileStart);
    gFront.off("profileEnd", this._onConsoleProfileEnd);
    gFront.off("profiler-unexpectedly-stopped", this._onProfilerDeactivated);
  },

  /**
   * Invoked whenever `console.profile` is called.
   *
   * @param string profileLabel
   *        The provided string argument if available, undefined otherwise.
   */
  _onConsoleProfileStart: function(event, profileLabel) {
    RecordingsListView.handleRecordingStarted(profileLabel);
  },

  /**
   * Invoked whenever `console.profileEnd` is called.
   *
   * @param object recordingData
   *        The profiler and refresh driver ticks data received from the front.
   */
  _onConsoleProfileEnd: function(event, recordingData) {
    RecordingsListView.handleRecordingEnded(recordingData);
  },

  /**
   * Invoked whenever the built-in profiler module is deactivated.
   * @see ProfilerConnection.prototype._onProfilerUnexpectedlyStopped
   */
  _onProfilerDeactivated: function() {
    RecordingsListView.removeForPredicate(e => e.isRecording);
    RecordingsListView.handleRecordingCancelled();
  }
};

/**
 * Shortcuts for accessing various profiler preferences.
 */
const Prefs = new ViewHelpers.Prefs("devtools.profiler", {
  showPlatformData: ["Bool", "ui.show-platform-data"]
});

/**
 * Convenient way of emitting events from the panel window.
 */
EventEmitter.decorate(this);

/**
 * DOM query helpers.
 */
function $(selector, target = document) {
  return target.querySelector(selector);
}
function $$(selector, target = document) {
  return target.querySelectorAll(selector);
}