Bug 1122058 - Add telemetry hooks to various performance tools actions. r=vp,mratcliffe
authorJordan Santell <jsantell@mozilla.com>
Wed, 09 Sep 2015 15:20:12 -0700
changeset 262182 5d0470be49b3e5efe101a48e9875cf4c7f9a3d5f
parent 262149 fd3764cfdcc535aafdf6b595304591c9c8dac3e7
child 262183 206f2986ff806ef4fb02170f321f110c04c64952
push id29363
push userphilringnalda@gmail.com
push dateSat, 12 Sep 2015 22:59:06 +0000
treeherdermozilla-central@68718290640b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp, mratcliffe
bugs1122058
milestone43.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 1122058 - Add telemetry hooks to various performance tools actions. r=vp,mratcliffe
browser/devtools/performance/events.js
browser/devtools/performance/modules/logic/telemetry.js
browser/devtools/performance/moz.build
browser/devtools/performance/performance-controller.js
browser/devtools/performance/test/browser_perf-telemetry.js
browser/devtools/shared/telemetry.js
toolkit/components/telemetry/Histograms.json
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/events.js
@@ -0,0 +1,106 @@
+/* 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";
+
+module.exports = {
+  // Fired by the PerformanceController and OptionsView when a pref changes.
+  PREF_CHANGED: "Performance:PrefChanged",
+
+  // Fired by the PerformanceController when the devtools theme changes.
+  THEME_CHANGED: "Performance:ThemeChanged",
+
+  // Emitted by the PerformanceView when the state (display mode) changes,
+  // for example when switching between "empty", "recording" or "recorded".
+  // This causes certain panels to be hidden or visible.
+  UI_STATE_CHANGED: "Performance:UI:StateChanged",
+
+  // Emitted by the PerformanceView on clear button click
+  UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings",
+
+  // Emitted by the PerformanceView on record button click
+  UI_START_RECORDING: "Performance:UI:StartRecording",
+  UI_STOP_RECORDING: "Performance:UI:StopRecording",
+
+  // Emitted by the PerformanceView on import button click
+  UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
+  // Emitted by the RecordingsView on export button click
+  UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
+
+  // When a new recording is being tracked in the panel.
+  NEW_RECORDING: "Performance:NewRecording",
+
+  // When a recording is started or stopped or stopping via the PerformanceController
+  RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
+
+  // Emitted by the PerformanceController or RecordingView
+  // when a recording model is selected
+  RECORDING_SELECTED: "Performance:RecordingSelected",
+
+  // When recordings have been cleared out
+  RECORDINGS_CLEARED: "Performance:RecordingsCleared",
+
+  // When a recording is exported via the PerformanceController
+  RECORDING_EXPORTED: "Performance:RecordingExported",
+
+  // Emitted by the PerformanceController when a recording is imported.
+  // Unless you're interested in specifically imported recordings, like in tests
+  // or telemetry, you should probably use the normal RECORDING_STATE_CHANGE in the UI.
+  RECORDING_IMPORTED: "Performance:RecordingImported",
+
+  // When the front has updated information on the profiler's circular buffer
+  PROFILER_STATUS_UPDATED: "Performance:BufferUpdated",
+
+  // When the PerformanceView updates the display of the buffer status
+  UI_BUFFER_STATUS_UPDATED: "Performance:UI:BufferUpdated",
+
+  // Emitted by the OptimizationsListView when it renders new optimization
+  // data and clears the optimization data
+  OPTIMIZATIONS_RESET: "Performance:UI:OptimizationsReset",
+  OPTIMIZATIONS_RENDERED: "Performance:UI:OptimizationsRendered",
+
+  // Emitted by the OverviewView when more data has been rendered
+  OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
+  FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered",
+  MARKERS_GRAPH_RENDERED: "Performance:UI:OverviewMarkersRendered",
+  MEMORY_GRAPH_RENDERED: "Performance:UI:OverviewMemoryRendered",
+
+  // Emitted by the OverviewView when a range has been selected in the graphs
+  OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected",
+
+  // Emitted by the DetailsView when a subview is selected
+  DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
+
+  // Emitted by the WaterfallView when it has been rendered
+  WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
+
+  // Emitted by the JsCallTreeView when a call tree has been rendered
+  JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered",
+
+  // Emitted by the JsFlameGraphView when it has been rendered
+  JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered",
+
+  // Emitted by the MemoryCallTreeView when a call tree has been rendered
+  MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered",
+
+  // Emitted by the MemoryFlameGraphView when it has been rendered
+  MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered",
+
+  // When a source is shown in the JavaScript Debugger at a specific location.
+  SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
+  SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
+
+  // These are short hands for the RECORDING_STATE_CHANGE event to make refactoring
+  // tests easier and in rare cases (telemetry). UI components should use
+  // RECORDING_STATE_CHANGE in almost all cases,
+  RECORDING_STARTED: "Performance:RecordingStarted",
+  RECORDING_WILL_STOP: "Performance:RecordingWillStop",
+  RECORDING_STOPPED: "Performance:RecordingStopped",
+
+  // Fired by the PerformanceController when `populateWithRecordings` is finished.
+  RECORDINGS_SEEDED: "Performance:RecordingsSeeded",
+
+  // Emitted by the PerformanceController when `PerformanceController.stopRecording()`
+  // is completed; used in tests, to know when a manual UI click is finished.
+  CONTROLLER_STOPPED_RECORDING: "Performance:Controller:StoppedRecording",
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/modules/logic/telemetry.js
@@ -0,0 +1,122 @@
+/* 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";
+
+loader.lazyRequireGetter(this, "Telemetry",
+  "devtools/shared/telemetry");
+loader.lazyRequireGetter(this, "Services",
+  "resource://gre/modules/Services.jsm", true);
+loader.lazyRequireGetter(this, "DevToolsUtils",
+  "devtools/toolkit/DevToolsUtils");
+loader.lazyRequireGetter(this, "EVENTS",
+  "devtools/performance/events");
+
+const EVENT_MAP_FLAGS = new Map([
+  [EVENTS.RECORDING_IMPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG"],
+  [EVENTS.RECORDING_EXPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG"],
+]);
+
+const RECORDING_FEATURES = [
+  "withMarkers", "withTicks", "withMemory", "withAllocations", "withJITOptimizations"
+];
+
+const SELECTED_VIEW_HISTOGRAM_NAME = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
+
+function PerformanceTelemetry (emitter) {
+  this._emitter = emitter;
+  this._telemetry = new Telemetry();
+  this.onFlagEvent = this.onFlagEvent.bind(this);
+  this.onRecordingStopped = this.onRecordingStopped.bind(this);
+  this.onViewSelected = this.onViewSelected.bind(this);
+
+  for (let [event] of EVENT_MAP_FLAGS) {
+    this._emitter.on(event, this.onFlagEvent);
+  }
+
+  this._emitter.on(EVENTS.RECORDING_STOPPED, this.onRecordingStopped);
+  this._emitter.on(EVENTS.DETAILS_VIEW_SELECTED, this.onViewSelected);
+
+  if (DevToolsUtils.testing) {
+    this.recordLogs();
+  }
+}
+
+PerformanceTelemetry.prototype.destroy = function () {
+  if (this._previousView) {
+    this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView);
+  }
+
+  this._telemetry.destroy();
+  for (let [event] of EVENT_MAP_FLAGS) {
+    this._emitter.off(event, this.onFlagEvent);
+  }
+  this._emitter.off(EVENTS.RECORDING_STOPPED, this.onRecordingStopped);
+  this._emitter.off(EVENTS.DETAILS_VIEW_SELECTED, this.onViewSelected);
+  this._emitter = null;
+};
+
+PerformanceTelemetry.prototype.onFlagEvent = function (eventName, ...data) {
+  this._telemetry.log(EVENT_MAP_FLAGS.get(eventName), true);
+};
+
+PerformanceTelemetry.prototype.onRecordingStopped = function (_, model) {
+  if (model.isConsole()) {
+    this._telemetry.log("DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT", true);
+  } else {
+    this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_COUNT", true);
+  }
+
+  this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS", model.getDuration());
+
+  let config = model.getConfiguration();
+  for (let k in config) {
+    if (RECORDING_FEATURES.indexOf(k) !== -1) {
+      this._telemetry.logKeyed("DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED", k, config[k]);
+    }
+  }
+};
+
+PerformanceTelemetry.prototype.onViewSelected = function (_, viewName) {
+  if (this._previousView) {
+    this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView);
+  }
+  this._previousView = viewName;
+  this._telemetry.startTimer(SELECTED_VIEW_HISTOGRAM_NAME);
+};
+
+/**
+ * Utility to record histogram calls to this instance.
+ * Should only be used in testing mode; throws otherwise.
+ */
+PerformanceTelemetry.prototype.recordLogs = function () {
+  if (!DevToolsUtils.testing) {
+    throw new Error("Can only record telemetry logs in tests.");
+  }
+
+  let originalLog = this._telemetry.log;
+  let originalLogKeyed = this._telemetry.logKeyed;
+  this._log = {};
+
+  this._telemetry.log = (function (histo, data) {
+    let results = this._log[histo] = this._log[histo] || [];
+    results.push(data);
+    originalLog(histo, data);
+  }).bind(this);
+
+  this._telemetry.logKeyed = (function (histo, key, data) {
+    let results = this._log[histo] = this._log[histo] || [];
+    results.push([key, data]);
+    originalLogKeyed(histo, key, data);
+  }).bind(this);
+};
+
+PerformanceTelemetry.prototype.getLogs = function () {
+  if (!DevToolsUtils.testing) {
+    throw new Error("Can only get telemetry logs in tests.");
+  }
+
+  return this._log;
+};
+
+exports.PerformanceTelemetry = PerformanceTelemetry;
--- a/browser/devtools/performance/moz.build
+++ b/browser/devtools/performance/moz.build
@@ -1,18 +1,20 @@
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_JS_MODULES.devtools.performance += [
+    'events.js',
     'modules/global.js',
     'modules/logic/frame-utils.js',
     'modules/logic/jit.js',
     'modules/logic/marker-utils.js',
+    'modules/logic/telemetry.js',
     'modules/logic/tree-model.js',
     'modules/logic/waterfall-utils.js',
     'modules/markers.js',
     'modules/widgets/graphs.js',
     'modules/widgets/marker-details.js',
     'modules/widgets/marker-view.js',
     'modules/widgets/markers-overview.js',
     'modules/widgets/tree-view.js',
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -4,29 +4,34 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 const { loader, require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 
 const { Task } = require("resource://gre/modules/Task.jsm");
 const { Heritage, ViewHelpers, WidgetMethods } = require("resource:///modules/devtools/ViewHelpers.jsm");
 
+// Events emitted by various objects in the panel.
+const EVENTS = require("devtools/performance/events");
+
 loader.lazyRequireGetter(this, "Services");
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 loader.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
 loader.lazyRequireGetter(this, "system",
   "devtools/toolkit/shared/system");
 
 // Logic modules
 
 loader.lazyRequireGetter(this, "L10N",
   "devtools/performance/global", true);
+loader.lazyRequireGetter(this, "PerformanceTelemetry",
+  "devtools/performance/telemetry", true);
 loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
   "devtools/performance/markers", true);
 loader.lazyRequireGetter(this, "RecordingUtils",
   "devtools/toolkit/performance/utils");
 loader.lazyRequireGetter(this, "GraphsController",
   "devtools/performance/graphs", true);
 loader.lazyRequireGetter(this, "WaterfallHeader",
   "devtools/performance/waterfall-ticks", true);
@@ -64,118 +69,16 @@ loader.lazyImporter(this, "setNamedTimeo
   "resource:///modules/devtools/ViewHelpers.jsm");
 loader.lazyImporter(this, "clearNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 loader.lazyImporter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
 const BRANCH_NAME = "devtools.performance.ui.";
 
-// Events emitted by various objects in the panel.
-const EVENTS = {
-  // Fired by the PerformanceController and OptionsView when a pref changes.
-  PREF_CHANGED: "Performance:PrefChanged",
-
-  // Fired by the PerformanceController when the devtools theme changes.
-  THEME_CHANGED: "Performance:ThemeChanged",
-
-  // Emitted by the PerformanceView when the state (display mode) changes,
-  // for example when switching between "empty", "recording" or "recorded".
-  // This causes certain panels to be hidden or visible.
-  UI_STATE_CHANGED: "Performance:UI:StateChanged",
-
-  // Emitted by the PerformanceView on clear button click
-  UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings",
-
-  // Emitted by the PerformanceView on record button click
-  UI_START_RECORDING: "Performance:UI:StartRecording",
-  UI_STOP_RECORDING: "Performance:UI:StopRecording",
-
-  // Emitted by the PerformanceView on import button click
-  UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
-  // Emitted by the RecordingsView on export button click
-  UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
-
-  // When a new recording is being tracked in the panel.
-  NEW_RECORDING: "Performance:NewRecording",
-
-  // When a recording is started or stopped or stopping via the PerformanceController
-  RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
-
-  // Emitted by the PerformanceController or RecordingView
-  // when a recording model is selected
-  RECORDING_SELECTED: "Performance:RecordingSelected",
-
-  // When recordings have been cleared out
-  RECORDINGS_CLEARED: "Performance:RecordingsCleared",
-
-  // When a recording is exported via the PerformanceController
-  RECORDING_EXPORTED: "Performance:RecordingExported",
-
-  // When the front has updated information on the profiler's circular buffer
-  PROFILER_STATUS_UPDATED: "Performance:BufferUpdated",
-
-  // When the PerformanceView updates the display of the buffer status
-  UI_BUFFER_STATUS_UPDATED: "Performance:UI:BufferUpdated",
-
-  // Emitted by the OptimizationsListView when it renders new optimization
-  // data and clears the optimization data
-  OPTIMIZATIONS_RESET: "Performance:UI:OptimizationsReset",
-  OPTIMIZATIONS_RENDERED: "Performance:UI:OptimizationsRendered",
-
-  // Emitted by the OverviewView when more data has been rendered
-  OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
-  FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered",
-  MARKERS_GRAPH_RENDERED: "Performance:UI:OverviewMarkersRendered",
-  MEMORY_GRAPH_RENDERED: "Performance:UI:OverviewMemoryRendered",
-
-  // Emitted by the OverviewView when a range has been selected in the graphs
-  OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected",
-
-  // Emitted by the DetailsView when a subview is selected
-  DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
-
-  // Emitted by the WaterfallView when it has been rendered
-  WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
-
-  // Emitted by the JsCallTreeView when a call tree has been rendered
-  JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered",
-
-  // Emitted by the JsFlameGraphView when it has been rendered
-  JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered",
-
-  // Emitted by the MemoryCallTreeView when a call tree has been rendered
-  MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered",
-
-  // Emitted by the MemoryFlameGraphView when it has been rendered
-  MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered",
-
-  // When a source is shown in the JavaScript Debugger at a specific location.
-  SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
-  SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
-
-  // These are short hands for the RECORDING_STATE_CHANGE event to make refactoring
-  // tests easier. UI components should use RECORDING_STATE_CHANGE, and these are
-  // deprecated for test usage only.
-  RECORDING_STARTED: "Performance:RecordingStarted",
-  RECORDING_WILL_STOP: "Performance:RecordingWillStop",
-  RECORDING_STOPPED: "Performance:RecordingStopped",
-
-  // Fired by the PerformanceController when `populateWithRecordings` is finished.
-  RECORDINGS_SEEDED: "Performance:RecordingsSeeded",
-
-  // Emitted by the PerformanceController when `PerformanceController.stopRecording()`
-  // is completed; used in tests, to know when a manual UI click is finished.
-  CONTROLLER_STOPPED_RECORDING: "Performance:Controller:StoppedRecording",
-
-  // Emitted by the PerformanceController when a recording is imported. Used
-  // only in tests. Should use the normal RECORDING_STATE_CHANGE in the UI.
-  RECORDING_IMPORTED: "Performance:ImportedRecording",
-};
-
 /**
  * The current target, toolbox and PerformanceFront, set by this tool's host.
  */
 let gToolbox, gTarget, gFront;
 
 /**
  * Initializes the profiler controller and views.
  */
@@ -200,59 +103,64 @@ let PerformanceController = {
   _recordings: [],
   _currentRecording: null,
 
   /**
    * Listen for events emitted by the current tab target and
    * main UI events.
    */
   initialize: Task.async(function* () {
+    this._telemetry = new PerformanceTelemetry(this);
     this.startRecording = this.startRecording.bind(this);
     this.stopRecording = this.stopRecording.bind(this);
     this.importRecording = this.importRecording.bind(this);
     this.exportRecording = this.exportRecording.bind(this);
     this.clearRecordings = this.clearRecordings.bind(this);
     this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
     this._onPrefChanged = this._onPrefChanged.bind(this);
     this._onThemeChanged = this._onThemeChanged.bind(this);
     this._onFrontEvent = this._onFrontEvent.bind(this);
+    this._pipe = this._pipe.bind(this);
 
     // Store data regarding if e10s is enabled.
     this._e10s = Services.appinfo.browserTabsRemoteAutostart;
     this._setMultiprocessAttributes();
 
     this._prefs = require("devtools/performance/global").PREFS;
     this._prefs.on("pref-changed", this._onPrefChanged);
 
     gFront.on("*", this._onFrontEvent);
     ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
+    DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._pipe);
 
     gDevTools.on("pref-changed", this._onThemeChanged);
   }),
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
+    this._telemetry.destroy();
     this._prefs.off("pref-changed", this._onPrefChanged);
 
     gFront.off("*", this._onFrontEvent);
     ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
+    DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._pipe);
 
     gDevTools.off("pref-changed", this._onThemeChanged);
   },
 
   /**
    * Returns the current devtools theme.
    */
   getTheme: function () {
@@ -367,22 +275,17 @@ let PerformanceController = {
    *
    * @param nsILocalFile file
    *        The file to import the data from.
    */
   importRecording: Task.async(function*(_, file) {
     let recording = yield gFront.importRecording(file);
     this._addNewRecording(recording);
 
-    // Only emit in tests for legacy purposes for shorthand --
-    // other things in UI should handle the generic NEW_RECORDING
-    // event to handle lazy recordings.
-    if (DevToolsUtils.testing) {
-      this.emit(EVENTS.RECORDING_IMPORTED, recording);
-    }
+    this.emit(EVENTS.RECORDING_IMPORTED, recording);
   }),
 
   /**
    * Sets the currently active PerformanceRecording. Should rarely be called directly,
    * as RecordingsView handles this when manually selected a recording item. Exceptions
    * are when clearing the view.
    * @param PerformanceRecording recording
    */
@@ -486,31 +389,29 @@ let PerformanceController = {
    */
   _onRecordingStateChange: function (state, model) {
     this._addNewRecording(model);
 
     this.emit(EVENTS.RECORDING_STATE_CHANGE, state, model);
 
     // Emit the state specific events for tests that I'm too
     // lazy and frusterated to change right now. These events
-    // should only be used in tests, as the rest of the UI should
-    // react to general RECORDING_STATE_CHANGE events and NEW_RECORDING
-    // events to handle lazy recordings.
-    if (DevToolsUtils.testing) {
-      switch (state) {
-        case "recording-started":
-          this.emit(EVENTS.RECORDING_STARTED, model);
-          break;
-        case "recording-stopping":
-          this.emit(EVENTS.RECORDING_WILL_STOP, model);
-          break;
-        case "recording-stopped":
-          this.emit(EVENTS.RECORDING_STOPPED, model);
-          break;
-      }
+    // should only be used in tests and specific rare cases (telemetry),
+    // as the rest of the UI should react to general RECORDING_STATE_CHANGE
+    // events and NEW_RECORDING events to handle lazy recordings.
+    switch (state) {
+      case "recording-started":
+        this.emit(EVENTS.RECORDING_STARTED, model);
+        break;
+      case "recording-stopping":
+        this.emit(EVENTS.RECORDING_WILL_STOP, model);
+        break;
+      case "recording-stopped":
+        this.emit(EVENTS.RECORDING_STOPPED, model);
+        break;
     }
   },
 
   /**
    * Takes a recording and returns a value between 0 and 1 indicating how much
    * of the buffer is used.
    */
   getBufferUsageForRecording: function (recording) {
@@ -630,16 +531,23 @@ let PerformanceController = {
       $("#performance-view").setAttribute("e10s", "disabled");
     }
     // Could be a chance where the directive goes away yet e10s is still on
     else if (!enabled && !supported) {
       $("#performance-view").setAttribute("e10s", "unsupported");
     }
   },
 
+  /**
+   * Pipes an event from some source to the PerformanceController.
+   */
+  _pipe: function (eventName, ...data) {
+    this.emit(eventName, ...data);
+  },
+
   toString: () => "[object PerformanceController]"
 };
 
 /**
  * Convenient way of emitting events from the controller.
  */
 EventEmitter.decorate(PerformanceController);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-telemetry.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ */
+
+function* spawnTest() {
+  PMM_loadFrameScripts(gBrowser);
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController, OverviewView, DetailsView, WaterfallView, JsCallTreeView, JsFlameGraphView } = panel.panelWin;
+
+  Services.prefs.setBoolPref(MEMORY_PREF, false);
+  let DURATION = "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS";
+  let COUNT = "DEVTOOLS_PERFTOOLS_RECORDING_COUNT";
+  let CONSOLE_COUNT = "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT";
+  let FEATURES = "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED";
+  let VIEWS = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
+  let EXPORTED = "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG";
+  let IMPORTED = "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG";
+
+  let telemetry = PerformanceController._telemetry;
+  let logs = telemetry.getLogs();
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  Services.prefs.setBoolPref(MEMORY_PREF, true);
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  is(logs[DURATION].length, 2, `two entry for ${DURATION}`);
+  ok(logs[DURATION].every(d => typeof d === "number"), `every ${DURATION} entry is a number`);
+  is(logs[COUNT].length, 2, `two entry for ${COUNT}`);
+  is(logs[CONSOLE_COUNT], void 0, `no entries for ${CONSOLE_COUNT}`);
+  is(logs[FEATURES].length, 10, `two recordings worth of entries for ${FEATURES}`);
+
+  ok(logs[FEATURES].find(r => r[0] === "withMemory" && r[1] === true), "one feature entry for memory enabled");
+  ok(logs[FEATURES].find(r => r[0] === "withMemory" && r[1] === false), "one feature entry for memory disabled");
+
+  let calltreeRendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
+  let flamegraphRendered = once(JsFlameGraphView, EVENTS.JS_FLAMEGRAPH_RENDERED);
+
+  // Go through some views to check later
+  DetailsView.selectView("js-calltree");
+  yield calltreeRendered;
+  DetailsView.selectView("js-flamegraph");
+  yield flamegraphRendered;
+
+  let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+  file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+  let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+  yield PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
+  yield exported;
+
+  ok(logs[EXPORTED], `a telemetry entry for ${EXPORTED} exists after exporting`);
+
+  let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+  yield PerformanceController.importRecording(null, file);
+  yield imported;
+
+  ok(logs[IMPORTED], `a telemetry entry for ${IMPORTED} exists after importing`);
+
+  yield consoleProfile(panel.panelWin, "rust");
+  yield consoleProfileEnd(panel.panelWin, "rust");
+
+  info("Performed a console recording.");
+
+  is(logs[DURATION].length, 3, `three entry for ${DURATION}`);
+  ok(logs[DURATION].every(d => typeof d === "number"), `every ${DURATION} entry is a number`);
+  is(logs[COUNT].length, 2, `two entry for ${COUNT}`);
+  is(logs[CONSOLE_COUNT].length, 1, `one entry for ${CONSOLE_COUNT}`);
+  is(logs[FEATURES].length, 15, `two recordings worth of entries for ${FEATURES}`);
+
+  yield teardown(panel);
+
+  // Check views after destruction to ensure `js-flamegraph` gets called with a time
+  // during destruction
+  ok(logs[VIEWS].find(r => r[0] === "waterfall" && typeof r[1] === "number"), `${VIEWS} for waterfall view and time.`);
+  ok(logs[VIEWS].find(r => r[0] === "js-calltree" && typeof r[1] === "number"), `${VIEWS} for js-calltree view and time.`);
+  ok(logs[VIEWS].find(r => r[0] === "js-flamegraph" && typeof r[1] === "number"), `${VIEWS} for js-flamegraph view and time.`);
+
+  finish();
+};
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -271,22 +271,28 @@ Telemetry.prototype = {
     this._timers.set(histogramId, new Date());
   },
 
   /**
    * Stop the timer and log elasped time for a timing-based histogram entry.
    *
    * @param String histogramId
    *        Histogram in which the data is to be stored.
+   * @param String key [optional]
+   *        Optional key for a keyed histogram.
    */
-  stopTimer: function(histogramId) {
+  stopTimer: function(histogramId, key) {
     let startTime = this._timers.get(histogramId);
     if (startTime) {
       let time = (new Date() - startTime) / 1000;
-      this.log(histogramId, time);
+      if (!key) {
+        this.log(histogramId, time);
+      } else {
+        this.logKeyed(histogramId, key, time);
+      }
       this._timers.delete(histogramId);
     }
   },
 
   /**
    * Log a value to a histogram.
    *
    * @param  {String} histogramId
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -7271,16 +7271,57 @@
   },
   "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT": {
     "expires_in_version": "never",
     "kind": "linear",
     "high": "10000000",
     "n_buckets": "10000",
     "description": "The number of edges serialized into a heap snapshot."
   },
+  "DEVTOOLS_PERFTOOLS_RECORDING_COUNT": {
+    "expires_in_version": "never",
+    "kind": "count",
+    "description": "Incremented whenever a performance tool recording is completed."
+  },
+  "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT": {
+    "expires_in_version": "never",
+    "kind": "count",
+    "description": "Incremented whenever a performance tool recording is completed that was initiated via console.profile."
+  },
+  "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "When a user imports a recording in the performance tool."
+  },
+  "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "When a user imports a recording in the performance tool."
+  },
+  "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "keyed": true,
+    "description": "When a user starts a recording with specific recording options, keyed by feature name (withMarkers, withAllocations, etc.)."
+  },
+  "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "600000",
+    "n_buckets": 20,
+    "description": "The length of a duration in MS of a performance tool recording."
+  },
+  "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "keyed": true,
+    "high": "600000",
+    "n_buckets": 20,
+    "description": "The amount of time spent in a specific performance tool view, keyed by view name (waterfall, js-calltree, js-flamegraph, etc)."
+  },
   "BROWSER_IS_USER_DEFAULT": {
     "expires_in_version": "never",
     "kind": "boolean",
     "releaseChannelCollection": "opt-out",
     "description": "The result of the startup default desktop browser check."
   },
   "BROWSER_IS_USER_DEFAULT_ERROR": {
     "expires_in_version": "never",