/* 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://gre/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://gre/modules/devtools/Graphs.jsm"); devtools.lazyImporter(this, "BarGraphWidget", "resource://gre/modules/devtools/Graphs.jsm"); devtools.lazyImporter(this, "CanvasGraphUtils", "resource://gre/modules/devtools/Graphs.jsm"); devtools.lazyImporter(this, "SideMenuWidget", "resource://gre/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); }