diff --git a/browser/devtools/performance/events.js b/browser/devtools/performance/events.js new file mode 100644 index 000000000000..07cca4321c3a --- /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", +}; diff --git a/browser/devtools/performance/modules/logic/telemetry.js b/browser/devtools/performance/modules/logic/telemetry.js new file mode 100644 index 000000000000..7ee10c55cc54 --- /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; diff --git a/browser/devtools/performance/moz.build b/browser/devtools/performance/moz.build index a76e27682a59..8b9741de73a6 100644 --- a/browser/devtools/performance/moz.build +++ b/browser/devtools/performance/moz.build @@ -4,10 +4,12 @@ # 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', diff --git a/browser/devtools/performance/performance-controller.js b/browser/devtools/performance/performance-controller.js index 85a9fd29d223..c47932ea9cbf 100644 --- a/browser/devtools/performance/performance-controller.js +++ b/browser/devtools/performance/performance-controller.js @@ -9,6 +9,9 @@ const { loader, require } = Cu.import("resource://gre/modules/devtools/Loader.js 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", @@ -22,6 +25,8 @@ loader.lazyRequireGetter(this, "system", 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", @@ -69,108 +74,6 @@ loader.lazyImporter(this, "PluralForm", 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. */ @@ -205,6 +108,7 @@ let PerformanceController = { * 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); @@ -214,6 +118,7 @@ let PerformanceController = { 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; @@ -230,6 +135,7 @@ let PerformanceController = { 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); }), @@ -238,6 +144,7 @@ let PerformanceController = { * Remove events handled by the PerformanceController */ destroy: function() { + this._telemetry.destroy(); this._prefs.off("pref-changed", this._onPrefChanged); gFront.off("*", this._onFrontEvent); @@ -248,6 +155,7 @@ let PerformanceController = { 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); }, @@ -372,12 +280,7 @@ let PerformanceController = { 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); }), /** @@ -491,21 +394,19 @@ let PerformanceController = { // 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; } }, @@ -635,6 +536,13 @@ let PerformanceController = { } }, + /** + * Pipes an event from some source to the PerformanceController. + */ + _pipe: function (eventName, ...data) { + this.emit(eventName, ...data); + }, + toString: () => "[object PerformanceController]" }; diff --git a/browser/devtools/performance/test/browser_perf-telemetry.js b/browser/devtools/performance/test/browser_perf-telemetry.js new file mode 100644 index 000000000000..ba0a66c0fc4f --- /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(); +}; diff --git a/browser/devtools/shared/telemetry.js b/browser/devtools/shared/telemetry.js index 79bc4933b3d2..a84a6af1e29c 100644 --- a/browser/devtools/shared/telemetry.js +++ b/browser/devtools/shared/telemetry.js @@ -276,12 +276,18 @@ Telemetry.prototype = { * * @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); } }, diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 841bb2d18d7b..e281768cbacd 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -7276,6 +7276,47 @@ "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",