forked from mirrors/gecko-dev
		
	 eaa4969e77
			
		
	
	
		eaa4969e77
		
	
	
	
	
		
			
			MozReview-Commit-ID: LPTEx2h4F1o --HG-- rename : devtools/server/actors/tab.js => devtools/server/actors/targets/browsing-context.js rename : devtools/shared/specs/tab.js => devtools/shared/specs/targets/browsing-context.js extra : rebase_source : e62720693ad492cdfb68ab21af1f54d78e85123a
		
			
				
	
	
		
			494 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			494 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 { Cu } = require("chrome");
 | |
| 
 | |
| loader.lazyRequireGetter(this, "Services");
 | |
| loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 | |
| 
 | |
| loader.lazyRequireGetter(this, "Memory",
 | |
|   "devtools/server/performance/memory", true);
 | |
| loader.lazyRequireGetter(this, "Timeline",
 | |
|   "devtools/server/performance/timeline", true);
 | |
| loader.lazyRequireGetter(this, "Profiler",
 | |
|   "devtools/server/performance/profiler", true);
 | |
| loader.lazyRequireGetter(this, "PerformanceRecordingActor",
 | |
|   "devtools/server/actors/performance-recording", true);
 | |
| loader.lazyRequireGetter(this, "mapRecordingOptions",
 | |
|   "devtools/shared/performance/recording-utils", true);
 | |
| loader.lazyRequireGetter(this, "getSystemInfo",
 | |
|   "devtools/shared/system", true);
 | |
| 
 | |
| const PROFILER_EVENTS = [
 | |
|   "console-api-profiler",
 | |
|   "profiler-started",
 | |
|   "profiler-stopped",
 | |
|   "profiler-status"
 | |
| ];
 | |
| 
 | |
| // Max time in milliseconds for the allocations event to occur, which will
 | |
| // occur on every GC, or at least as often as DRAIN_ALLOCATIONS_TIMEOUT.
 | |
| const DRAIN_ALLOCATIONS_TIMEOUT = 2000;
 | |
| 
 | |
| /**
 | |
|  * A connection to underlying actors (profiler, memory, framerate, etc.)
 | |
|  * shared by all tools in a target.
 | |
|  *
 | |
|  * @param Target target
 | |
|  *        The target owning this connection.
 | |
|  */
 | |
| function PerformanceRecorder(conn, targetActor) {
 | |
|   EventEmitter.decorate(this);
 | |
| 
 | |
|   this.conn = conn;
 | |
|   this.targetActor = targetActor;
 | |
| 
 | |
|   this._pendingConsoleRecordings = [];
 | |
|   this._recordings = [];
 | |
| 
 | |
|   this._onTimelineData = this._onTimelineData.bind(this);
 | |
|   this._onProfilerEvent = this._onProfilerEvent.bind(this);
 | |
| }
 | |
| 
 | |
| PerformanceRecorder.prototype = {
 | |
|   /**
 | |
|    * Initializes a connection to the profiler and other miscellaneous actors.
 | |
|    * If in the process of opening, or already open, nothing happens.
 | |
|    *
 | |
|    * @param {Object} options.systemClient
 | |
|    *        Metadata about the client's system to attach to the recording models.
 | |
|    *
 | |
|    * @return object
 | |
|    *         A promise that is resolved once the connection is established.
 | |
|    */
 | |
|   connect: function(options) {
 | |
|     if (this._connected) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Sets `this._profiler`, `this._timeline` and `this._memory`.
 | |
|     // Only initialize the timeline and memory fronts if the respective actors
 | |
|     // are available. Older Gecko versions don't have existing implementations,
 | |
|     // in which case all the methods we need can be easily mocked.
 | |
|     this._connectComponents();
 | |
|     this._registerListeners();
 | |
| 
 | |
|     this._systemClient = options.systemClient;
 | |
| 
 | |
|     this._connected = true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Destroys this connection.
 | |
|    */
 | |
|   destroy: function() {
 | |
|     this._unregisterListeners();
 | |
|     this._disconnectComponents();
 | |
| 
 | |
|     this._connected = null;
 | |
|     this._profiler = null;
 | |
|     this._timeline = null;
 | |
|     this._memory = null;
 | |
|     this._target = null;
 | |
|     this._client = null;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Initializes fronts and connects to the underlying actors using the facades
 | |
|    * found in ./actors.js.
 | |
|    */
 | |
|   _connectComponents: function() {
 | |
|     this._profiler = new Profiler(this.targetActor);
 | |
|     this._memory = new Memory(this.targetActor);
 | |
|     this._timeline = new Timeline(this.targetActor);
 | |
|     this._profiler.registerEventNotifications({ events: PROFILER_EVENTS });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Registers listeners on events from the underlying
 | |
|    * actors, so the connection can handle them.
 | |
|    */
 | |
|   _registerListeners: function() {
 | |
|     this._timeline.on("*", this._onTimelineData);
 | |
|     this._memory.on("*", this._onTimelineData);
 | |
|     this._profiler.on("*", this._onProfilerEvent);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unregisters listeners on events on the underlying actors.
 | |
|    */
 | |
|   _unregisterListeners: function() {
 | |
|     this._timeline.off("*", this._onTimelineData);
 | |
|     this._memory.off("*", this._onTimelineData);
 | |
|     this._profiler.off("*", this._onProfilerEvent);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Closes the connections to non-profiler actors.
 | |
|    */
 | |
|   _disconnectComponents: function() {
 | |
|     this._profiler.unregisterEventNotifications({ events: PROFILER_EVENTS });
 | |
|     this._profiler.destroy();
 | |
|     this._timeline.destroy();
 | |
|     this._memory.destroy();
 | |
|   },
 | |
| 
 | |
|   _onProfilerEvent: function(topic, data) {
 | |
|     if (topic === "console-api-profiler") {
 | |
|       if (data.subject.action === "profile") {
 | |
|         this._onConsoleProfileStart(data.details);
 | |
|       } else if (data.subject.action === "profileEnd") {
 | |
|         this._onConsoleProfileEnd(data.details);
 | |
|       }
 | |
|     } else if (topic === "profiler-stopped") {
 | |
|       this._onProfilerUnexpectedlyStopped();
 | |
|     } else if (topic === "profiler-status") {
 | |
|       this.emit("profiler-status", data);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Invoked whenever `console.profile` is called.
 | |
|    *
 | |
|    * @param string profileLabel
 | |
|    *        The provided string argument if available; undefined otherwise.
 | |
|    * @param number currentTime
 | |
|    *        The time (in milliseconds) when the call was made, relative to when
 | |
|    *        the nsIProfiler module was started.
 | |
|    */
 | |
|   async _onConsoleProfileStart({ profileLabel, currentTime }) {
 | |
|     const recordings = this._recordings;
 | |
| 
 | |
|     // Abort if a profile with this label already exists.
 | |
|     if (recordings.find(e => e.getLabel() === profileLabel)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Immediately emit this so the client can start setting things up,
 | |
|     // expecting a recording very soon.
 | |
|     this.emit("console-profile-start");
 | |
| 
 | |
|     await this.startRecording(Object.assign({}, getPerformanceRecordingPrefs(), {
 | |
|       console: true,
 | |
|       label: profileLabel
 | |
|     }));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Invoked whenever `console.profileEnd` is called.
 | |
|    *
 | |
|    * @param string profileLabel
 | |
|    *        The provided string argument if available; undefined otherwise.
 | |
|    * @param number currentTime
 | |
|    *        The time (in milliseconds) when the call was made, relative to when
 | |
|    *        the nsIProfiler module was started.
 | |
|    */
 | |
|   async _onConsoleProfileEnd(data) {
 | |
|     // If no data, abort; can occur if profiler isn't running and we get a surprise
 | |
|     // call to console.profileEnd()
 | |
|     if (!data) {
 | |
|       return;
 | |
|     }
 | |
|     const { profileLabel } = data;
 | |
| 
 | |
|     const pending = this._recordings.filter(r => r.isConsole() && r.isRecording());
 | |
|     if (pending.length === 0) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let model;
 | |
|     // Try to find the corresponding `console.profile` call if
 | |
|     // a label was used in profileEnd(). If no matches, abort.
 | |
|     if (profileLabel) {
 | |
|       model = pending.find(e => e.getLabel() === profileLabel);
 | |
|     } else {
 | |
|       // If no label supplied, pop off the most recent pending console recording
 | |
|       model = pending[pending.length - 1];
 | |
|     }
 | |
| 
 | |
|     // If `profileEnd()` was called with a label, and there are no matching
 | |
|     // sessions, abort.
 | |
|     if (!model) {
 | |
|       Cu.reportError(
 | |
|         "console.profileEnd() called with label that does not match a recording.");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     await this.stopRecording(model);
 | |
|   },
 | |
| 
 | |
|  /**
 | |
|   * TODO handle bug 1144438
 | |
|   */
 | |
|   _onProfilerUnexpectedlyStopped: function() {
 | |
|     Cu.reportError("Profiler unexpectedly stopped.", arguments);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called whenever there is timeline data of any of the following types:
 | |
|    * - markers
 | |
|    * - frames
 | |
|    * - memory
 | |
|    * - ticks
 | |
|    * - allocations
 | |
|    */
 | |
|   _onTimelineData: function(eventName, ...data) {
 | |
|     let eventData = Object.create(null);
 | |
| 
 | |
|     switch (eventName) {
 | |
|       case "markers": {
 | |
|         eventData = { markers: data[0], endTime: data[1] };
 | |
|         break;
 | |
|       }
 | |
|       case "ticks": {
 | |
|         eventData = { delta: data[0], timestamps: data[1] };
 | |
|         break;
 | |
|       }
 | |
|       case "memory": {
 | |
|         eventData = { delta: data[0], measurement: data[1] };
 | |
|         break;
 | |
|       }
 | |
|       case "frames": {
 | |
|         eventData = { delta: data[0], frames: data[1] };
 | |
|         break;
 | |
|       }
 | |
|       case "allocations": {
 | |
|         eventData = data[0];
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Filter by only recordings that are currently recording;
 | |
|     // TODO should filter by recordings that have realtimeMarkers enabled.
 | |
|     const activeRecordings = this._recordings.filter(r => r.isRecording());
 | |
| 
 | |
|     if (activeRecordings.length) {
 | |
|       this.emit("timeline-data", eventName, eventData, activeRecordings);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks whether or not recording is currently supported. At the moment,
 | |
|    * this is only influenced by private browsing mode and the profiler.
 | |
|    */
 | |
|   canCurrentlyRecord: function() {
 | |
|     let success = true;
 | |
|     const reasons = [];
 | |
| 
 | |
|     if (!Profiler.canProfile()) {
 | |
|       success = false;
 | |
|       reasons.push("profiler-unavailable");
 | |
|     }
 | |
| 
 | |
|     // Check other factors that will affect the possibility of successfully
 | |
|     // starting a recording here.
 | |
| 
 | |
|     return { success, reasons };
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Begins a recording session
 | |
|    *
 | |
|    * @param boolean options.withMarkers
 | |
|    * @param boolean options.withTicks
 | |
|    * @param boolean options.withMemory
 | |
|    * @param boolean options.withAllocations
 | |
|    * @param boolean options.allocationsSampleProbability
 | |
|    * @param boolean options.allocationsMaxLogLength
 | |
|    * @param boolean options.bufferSize
 | |
|    * @param boolean options.sampleFrequency
 | |
|    * @param boolean options.console
 | |
|    * @param string options.label
 | |
|    * @param boolean options.realtimeMarkers
 | |
|    * @return object
 | |
|    *         A promise that is resolved once recording has started.
 | |
|    */
 | |
|   async startRecording(options) {
 | |
|     let timelineStart, memoryStart;
 | |
| 
 | |
|     const profilerStart = (async function() {
 | |
|       const data = await this._profiler.isActive();
 | |
|       if (data.isActive) {
 | |
|         return data;
 | |
|       }
 | |
|       const startData = await this._profiler.start(
 | |
|         mapRecordingOptions("profiler", options)
 | |
|       );
 | |
| 
 | |
|       // If no current time is exposed from starting, set it to 0 -- this is an
 | |
|       // older Gecko that does not return its starting time, and uses an epoch based
 | |
|       // on the profiler's start time.
 | |
|       if (startData.currentTime == null) {
 | |
|         startData.currentTime = 0;
 | |
|       }
 | |
|       return startData;
 | |
|     }.bind(this))();
 | |
| 
 | |
|     // Timeline will almost always be on if using the DevTools, but using component
 | |
|     // independently could result in no timeline.
 | |
|     if (options.withMarkers || options.withTicks || options.withMemory) {
 | |
|       timelineStart = this._timeline.start(mapRecordingOptions("timeline", options));
 | |
|     }
 | |
| 
 | |
|     if (options.withAllocations) {
 | |
|       if (this._memory.getState() === "detached") {
 | |
|         this._memory.attach();
 | |
|       }
 | |
|       const recordingOptions = Object.assign(mapRecordingOptions("memory", options), {
 | |
|         drainAllocationsTimeout: DRAIN_ALLOCATIONS_TIMEOUT
 | |
|       });
 | |
|       memoryStart = this._memory.startRecordingAllocations(recordingOptions);
 | |
|     }
 | |
| 
 | |
|     const [profilerStartData, timelineStartData, memoryStartData] = await Promise.all([
 | |
|       profilerStart, timelineStart, memoryStart
 | |
|     ]);
 | |
| 
 | |
|     const data = Object.create(null);
 | |
|     // Filter out start times that are not actually used (0 or undefined), and
 | |
|     // find the earliest time since all sources use same epoch.
 | |
|     const startTimes = [
 | |
|       profilerStartData.currentTime,
 | |
|       memoryStartData,
 | |
|       timelineStartData
 | |
|     ].filter(Boolean);
 | |
|     data.startTime = Math.min(...startTimes);
 | |
|     data.position = profilerStartData.position;
 | |
|     data.generation = profilerStartData.generation;
 | |
|     data.totalSize = profilerStartData.totalSize;
 | |
| 
 | |
|     data.systemClient = this._systemClient;
 | |
|     data.systemHost = await getSystemInfo();
 | |
| 
 | |
|     const model = new PerformanceRecordingActor(this.conn, options, data);
 | |
|     this._recordings.push(model);
 | |
| 
 | |
|     this.emit("recording-started", model);
 | |
|     return model;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Manually ends the recording session for the corresponding PerformanceRecording.
 | |
|    *
 | |
|    * @param PerformanceRecording model
 | |
|    *        The corresponding PerformanceRecording that belongs to the recording
 | |
|    *        session wished to stop.
 | |
|    * @return PerformanceRecording
 | |
|    *         Returns the same model, populated with the profiling data.
 | |
|    */
 | |
|   async stopRecording(model) {
 | |
|     // If model isn't in the Recorder's internal store,
 | |
|     // then do nothing, like if this was a console.profileEnd
 | |
|     // from a different target.
 | |
|     if (!this._recordings.includes(model)) {
 | |
|       return model;
 | |
|     }
 | |
| 
 | |
|     // Flag the recording as no longer recording, so that `model.isRecording()`
 | |
|     // is false. Do this before we fetch all the data, and then subsequently
 | |
|     // the recording can be considered "completed".
 | |
|     this.emit("recording-stopping", model);
 | |
| 
 | |
|     // Currently there are two ways profiles stop recording. Either manually in the
 | |
|     // performance tool, or via console.profileEnd. Once a recording is done,
 | |
|     // we want to deliver the model to the performance tool (either as a return
 | |
|     // from the PerformanceFront or via `console-profile-stop` event) and then
 | |
|     // remove it from the internal store.
 | |
|     //
 | |
|     // In the case where a console.profile is generated via the console (so the tools are
 | |
|     // open), we initialize the Performance tool so it can listen to those events.
 | |
|     this._recordings.splice(this._recordings.indexOf(model), 1);
 | |
| 
 | |
|     const startTime = model._startTime;
 | |
|     const profilerData = this._profiler.getProfile({ startTime });
 | |
| 
 | |
|     // Only if there are no more sessions recording do we stop
 | |
|     // the underlying memory and timeline actors. If we're still recording,
 | |
|     // juse use Date.now() for the memory and timeline end times, as those
 | |
|     // are only used in tests.
 | |
|     if (!this.isRecording()) {
 | |
|       // Check to see if memory is recording, so we only stop recording
 | |
|       // if necessary (otherwise if the memory component is not attached, this will fail)
 | |
|       if (this._memory.isRecordingAllocations()) {
 | |
|         this._memory.stopRecordingAllocations();
 | |
|       }
 | |
|       this._timeline.stop();
 | |
|     }
 | |
| 
 | |
|     const recordingData = {
 | |
|       // Data available only at the end of a recording.
 | |
|       profile: profilerData.profile,
 | |
|       // End times for all the actors.
 | |
|       duration: profilerData.currentTime - startTime,
 | |
|     };
 | |
| 
 | |
|     this.emit("recording-stopped", model, recordingData);
 | |
|     return model;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks all currently stored recording handles and returns a boolean
 | |
|    * if there is a session currently being recorded.
 | |
|    *
 | |
|    * @return Boolean
 | |
|    */
 | |
|   isRecording: function() {
 | |
|     return this._recordings.some(h => h.isRecording());
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns all current recordings.
 | |
|    */
 | |
|   getRecordings: function() {
 | |
|     return this._recordings;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets how often the "profiler-status" event should be emitted.
 | |
|    * Used in tests.
 | |
|    */
 | |
|   setProfilerStatusInterval: function(n) {
 | |
|     this._profiler.setProfilerStatusInterval(n);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the configurations set on underlying components, used in tests.
 | |
|    * Returns an object with `probability`, `maxLogLength` for allocations, and
 | |
|    * `features`, `threadFilters`, `entries` and `interval` for profiler.
 | |
|    *
 | |
|    * @return {object}
 | |
|    */
 | |
|   getConfiguration: function() {
 | |
|     let allocationSettings = Object.create(null);
 | |
| 
 | |
|     if (this._memory.getState() === "attached") {
 | |
|       allocationSettings = this._memory.getAllocationsSettings();
 | |
|     }
 | |
| 
 | |
|     return Object.assign({}, allocationSettings, this._profiler.getStartOptions());
 | |
|   },
 | |
| 
 | |
|   toString: () => "[object PerformanceRecorder]"
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Creates an object of configurations based off of
 | |
|  * preferences for a PerformanceRecording.
 | |
|  */
 | |
| function getPerformanceRecordingPrefs() {
 | |
|   return {
 | |
|     withMarkers: true,
 | |
|     withMemory: Services.prefs.getBoolPref("devtools.performance.ui.enable-memory"),
 | |
|     withTicks: Services.prefs.getBoolPref("devtools.performance.ui.enable-framerate"),
 | |
|     withAllocations:
 | |
|       Services.prefs.getBoolPref("devtools.performance.ui.enable-allocations"),
 | |
|     allocationsSampleProbability:
 | |
|       +Services.prefs.getCharPref("devtools.performance.memory.sample-probability"),
 | |
|     allocationsMaxLogLength:
 | |
|       Services.prefs.getIntPref("devtools.performance.memory.max-log-length")
 | |
|   };
 | |
| }
 | |
| 
 | |
| exports.PerformanceRecorder = PerformanceRecorder;
 |