forked from mirrors/gecko-dev
		
	If the profiler unexpectedly stops, this is fine, as the new recording infrastructure is most likely in control of it. This was leading to lots of spurious errors when working on the new about:profiling infrastructure. Differential Revision: https://phabricator.services.mozilla.com/D62911 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			529 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			529 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") {
 | 
						|
      // Some other API stopped the profiler. Ignore it.
 | 
						|
    } 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);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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;
 |