Commit 1776473d authored by Victor Porof's avatar Victor Porof
Browse files

Bug 1105014 - Part 1: Sync actor times and overview selection with all the...

Bug 1105014 - Part 1: Sync actor times and overview selection with all the detail views in the new performance tool, r=jsantell
parent 6b6e649c
Loading
Loading
Loading
Loading
+30 −59
Original line number Diff line number Diff line
@@ -31,6 +31,8 @@ let SharedPerformanceActors = new WeakMap();
 *
 * @param Target target
 *        The target owning this connection.
 * @return PerformanceActorsConnection
 *         The shared connection for the specified target.
 */
SharedPerformanceActors.forTarget = function(target) {
  if (this.has(target)) {
@@ -140,12 +142,13 @@ PerformanceActorsConnection.prototype = {
      this._timeline = new TimelineFront(this._target.client, this._target.form);
    } else {
      this._timeline = {
        start: () => {},
        stop: () => {},
        start: () => 0,
        stop: () => 0,
        isRecording: () => false,
        on: () => {},
        off: () => {},
        destroy: () => {}
        on: () => null,
        off: () => null,
        once: () => promise.reject(),
        destroy: () => null
      };
    }
  },
@@ -211,36 +214,41 @@ PerformanceFront.prototype = {
  /**
   * Manually begins a recording session.
   *
   * @param object options
   * @param object timelineOptions
   *        An options object to pass to the timeline front. Supported
   *        properties are `withTicks` and `withMemory`.
   * @return object
   *         A promise that is resolved once recording has started.
   */
  startRecording: Task.async(function*(options = {}) {
    let { isActive, currentTime } = yield this._request("profiler", "isActive");
  startRecording: Task.async(function*(timelineOptions = {}) {
    let profilerStatus = yield this._request("profiler", "isActive");
    let profilerStartTime;

    // Start the profiler only if it wasn't already active. The built-in
    // nsIPerformance module will be kept recording, because it's the same instance
    // for all targets and interacts with the whole platform, so we don't want
    // to affect other clients by stopping (or restarting) it.
    if (!isActive) {
    if (!profilerStatus.isActive) {
      // Extend the profiler options so that protocol.js doesn't modify the original.
      let profilerOptions = extend({}, this._customProfilerOptions);
      yield this._request("profiler", "startProfiler", profilerOptions);
      this._profilingStartTime = 0;
      profilerStartTime = 0;
      this.emit("profiler-activated");
    } else {
      this._profilingStartTime = currentTime;
      profilerStartTime = profilerStatus.currentTime;
      this.emit("profiler-already-active");
    }

    // The timeline actor is target-dependent, so just make sure
    // it's recording.
    let startTime = yield this._request("timeline", "start", options);
    // The timeline actor is target-dependent, so just make sure it's recording.
    // It won't, however, be available in older Geckos (FF < 35).
    let timelineStartTime = yield this._request("timeline", "start", timelineOptions);

    // Return only the start time from the timeline actor.
    return { startTime };
    // Return the start times from the two actors. They will be used to
    // synchronize the profiler and timeline data.
    return {
      profilerStartTime,
      timelineStartTime
    };
  }),

  /**
@@ -251,19 +259,15 @@ PerformanceFront.prototype = {
   *         with the profiler and timeline data.
   */
  stopRecording: Task.async(function*() {
    // We'll need to filter out all samples that fall out of current profile's
    // range. This is necessary because the profiler is continuously running.
    let timelineEndTime = yield this._request("timeline", "stop");
    let profilerData = yield this._request("profiler", "getProfile");
    filterSamples(profilerData, this._profilingStartTime);
    offsetSampleTimes(profilerData, this._profilingStartTime);

    let endTime = yield this._request("timeline", "stop");

    // Join all the acquired data and return it for outside consumers.
    // Return the end times from the two actors. They will be used to
    // synchronize the profiler and timeline data.
    return {
      recordingDuration: profilerData.currentTime - this._profilingStartTime,
      profilerData: profilerData,
      endTime: endTime
      profile: profilerData.profile,
      profilerEndTime: profilerData.currentTime,
      timelineEndTime: timelineEndTime
    };
  }),

@@ -280,39 +284,6 @@ PerformanceFront.prototype = {
  }
};

/**
 * Filters all the samples in the provided profiler data to be more recent
 * than the specified start time.
 *
 * @param object profilerData
 *        The profiler data received from the backend.
 * @param number profilingStartTime
 *        The earliest acceptable sample time (in milliseconds).
 */
function filterSamples(profilerData, profilingStartTime) {
  let firstThread = profilerData.profile.threads[0];

  firstThread.samples = firstThread.samples.filter(e => {
    return e.time >= profilingStartTime;
  });
}

/**
 * Offsets all the samples in the provided profiler data by the specified time.
 *
 * @param object profilerData
 *        The profiler data received from the backend.
 * @param number timeOffset
 *        The amount of time to offset by (in milliseconds).
 */
function offsetSampleTimes(profilerData, timeOffset) {
  let firstThreadSamples = profilerData.profile.threads[0].samples;

  for (let sample of firstThreadSamples) {
    sample.time -= timeOffset;
  }
}

/**
 * A collection of small wrappers promisifying functions invoking callbacks.
 */
+5 −10
Original line number Diff line number Diff line
@@ -130,21 +130,16 @@ function isValidSerializerVersion (version) {
function convertLegacyData (legacyData) {
  let { profilerData, ticksData, recordingDuration } = legacyData;

  // The `profilerData` stays, and the previously unrecorded fields
  // just are empty arrays.
  // The `profilerData` and `ticksData` stay, but the previously unrecorded
  // fields just are empty arrays.
  let data = {
    label: profilerData.profilerLabel,
    duration: recordingDuration,
    markers: [],
    frames: [],
    memory: [],
    ticks: ticksData,
    profilerData: profilerData,
    // Data from the original profiler won't contain `interval` fields,
    // but a recording duration, as well as the current time, which can be used
    // to infer the interval startTime and endTime.
    interval: {
      startTime: profilerData.currentTime - recordingDuration,
      endTime: profilerData.currentTime
    }
    profile: profilerData.profile
  };

  return data;
+82 −85
Original line number Diff line number Diff line
@@ -3,13 +3,17 @@
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const { PerformanceIO } = require("devtools/performance/io");
const { Cc, Ci, Cu, Cr } = require("chrome");

loader.lazyRequireGetter(this, "PerformanceIO",
  "devtools/performance/io", true);
loader.lazyRequireGetter(this, "RecordingUtils",
  "devtools/performance/recording-utils", true);

const RECORDING_IN_PROGRESS = exports.RECORDING_IN_PROGRESS = -1;
const RECORDING_UNAVAILABLE = exports.RECORDING_UNAVAILABLE = null;
/**
 * Model for a wholistic profile, containing start/stop times, profiling data, frames data,
 * timeline (marker, tick, memory) data, and methods to start/stop recording.
 * Model for a wholistic profile, containing the duration, profiling data,
 * frames data, timeline (marker, tick, memory) data, and methods to mark
 * a recording as 'in progress' or 'finished'.
 */

const RecordingModel = function (options={}) {
@@ -19,17 +23,20 @@ const RecordingModel = function (options={}) {
};

RecordingModel.prototype = {
  _localStartTime: RECORDING_UNAVAILABLE,
  _startTime: RECORDING_UNAVAILABLE,
  _endTime: RECORDING_UNAVAILABLE,
  _markers: [],
  _frames: [],
  _ticks: [],
  _memory: [],
  _profilerData: {},
  _label: "",
  // Private fields, only needed when a recording is started or stopped.
  _imported: false,
  _isRecording: false,
  _recording: false,
  _profilerStartTime: 0,
  _timelineStartTime: 0,

  // Serializable fields, necessary and sufficient for import and export.
  _label: "",
  _duration: 0,
  _markers: null,
  _frames: null,
  _ticks: null,
  _memory: null,
  _profile: null,

  /**
   * Loads a recording from a file.
@@ -41,16 +48,13 @@ RecordingModel.prototype = {
    let recordingData = yield PerformanceIO.loadRecordingFromFile(file);

    this._imported = true;
    this._label = recordingData.profilerData.profilerLabel || "";
    this._startTime = recordingData.interval.startTime;
    this._endTime = recordingData.interval.endTime;
    this._label = recordingData.label || "";
    this._duration = recordingData.duration;
    this._markers = recordingData.markers;
    this._frames = recordingData.frames;
    this._memory = recordingData.memory;
    this._ticks = recordingData.ticks;
    this._profilerData = recordingData.profilerData;

    return recordingData;
    this._profile = recordingData.profile;
  }),

  /**
@@ -65,24 +69,24 @@ RecordingModel.prototype = {
  }),

  /**
   * Starts recording with the PerformanceFront, storing the start times
   * on the model.
   * Starts recording with the PerformanceFront.
   *
   * @param object options
   *        An options object to pass to the timeline front. Supported
   *        properties are `withTicks` and `withMemory`.
   */
  startRecording: Task.async(function *() {
  startRecording: Task.async(function *(options = {}) {
    // Times must come from the actor in order to be self-consistent.
    // However, we also want to update the view with the elapsed time
    // even when the actor is not generating data. To do this we get
    // the local time and use it to compute a reasonable elapsed time.
    this._localStartTime = this._performance.now();

    let { startTime } = yield this._front.startRecording({
      withTicks: true,
      withMemory: true
    });
    this._isRecording = true;
    let info = yield this._front.startRecording(options);
    this._profilerStartTime = info.profilerStartTime;
    this._timelineStartTime = info.timelineStartTime;
    this._recording = true;

    this._startTime = startTime;
    this._endTime = RECORDING_IN_PROGRESS;
    this._markers = [];
    this._frames = [];
    this._memory = [];
@@ -90,63 +94,46 @@ RecordingModel.prototype = {
  }),

  /**
   * Stops recording with the PerformanceFront, storing the end times
   * on the model.
   * Stops recording with the PerformanceFront.
   */
  stopRecording: Task.async(function *() {
    let results = yield this._front.stopRecording();
    this._isRecording = false;

    // If `endTime` is not yielded from timeline actor (< Fx36), fake it.
    if (!results.endTime) {
      results.endTime = this._startTime + this.getLocalElapsedTime();
    }

    this._endTime = results.endTime;
    this._profilerData = results.profilerData;
    let info = yield this._front.stopRecording();
    this._profile = info.profile;
    this._duration = info.profilerEndTime - this._profilerStartTime;
    this._recording = false;

    // We'll need to filter out all samples that fall out of current profile's
    // range since the profiler is continuously running. Because of this, sample
    // times are not guaranteed to have a zero epoch, so offset the timestamps.
    RecordingUtils.filterSamples(this._profile, this._profilerStartTime);
    RecordingUtils.offsetSampleTimes(this._profile, this._profilerStartTime);

    // Markers need to be sorted ascending by time, to be properly displayed
    // in a waterfall view.
    this._markers = this._markers.sort((a, b) => (a.start > b.start));

    return results;
  }),

  /**
   * Returns the profile's label, from `console.profile(LABEL)`.
   * Gets the profile's label, from `console.profile(LABEL)`.
   * @return string
   */
  getLabel: function () {
    return this._label;
  },

  /**
   * Gets the amount of time elapsed locally after starting a recording.
   */
  getLocalElapsedTime: function () {
    return this._performance.now() - this._localStartTime;
  },

  /**
   * Returns duration of this recording, in milliseconds.
   * Gets duration of this recording, in milliseconds.
   * @return number
   */
  getDuration: function () {
    let { startTime, endTime } = this.getInterval();
    return endTime - startTime;
  },

  /**
   * Gets the time interval for the current recording.
   * @return object
   */
  getInterval: function() {
    let startTime = this._startTime;
    let endTime = this._endTime;

    // Compute an approximate ending time for the current recording. This is
    // needed to ensure that the view updates even when new data is
    // not being generated.
    if (endTime == RECORDING_IN_PROGRESS) {
      endTime = startTime + this.getLocalElapsedTime();
    // Compute an approximate ending time for the current recording if it is
    // still in progress. This is needed to ensure that the view updates even
    // when new data is not being generated.
    if (this._recording) {
      return this._performance.now() - this._localStartTime;
    } else {
      return this._duration;
    }

    return { startTime, endTime };
  },

  /**
@@ -185,21 +172,22 @@ RecordingModel.prototype = {
   * Gets the profiler data in this recording.
   * @return array
   */
  getProfilerData: function() {
    return this._profilerData;
  getProfile: function() {
    return this._profile;
  },

  /**
   * Gets all the data in this recording.
   */
  getAllData: function() {
    let interval = this.getInterval();
    let label = this.getLabel();
    let duration = this.getDuration();
    let markers = this.getMarkers();
    let frames = this.getFrames();
    let memory = this.getMemory();
    let ticks = this.getTicks();
    let profilerData = this.getProfilerData();
    return { interval, markers, frames, memory, ticks, profilerData };
    let profile = this.getProfile();
    return { label, duration, markers, frames, memory, ticks, profile };
  },

  /**
@@ -207,7 +195,7 @@ RecordingModel.prototype = {
   * is recording.
   */
  isRecording: function () {
    return this._isRecording;
    return this._recording;
  },

  /**
@@ -216,26 +204,35 @@ RecordingModel.prototype = {
  addTimelineData: function (eventName, ...data) {
    // If this model isn't currently recording,
    // ignore the timeline data.
    if (!this.isRecording()) {
    if (!this._recording) {
      return;
    }

    switch (eventName) {
      // Accumulate markers into an array.
      // Accumulate markers into an array. Furthermore, timestamps do not
      // have a zero epoch, so offset all of them by the timeline's start time.
      case "markers":
        let [markers] = data;
        RecordingUtils.offsetMarkerTimes(markers, this._timelineStartTime);
        Array.prototype.push.apply(this._markers, markers);
        break;

      // Accumulate stack frames into an array.
      case "frames":
        let [, frames] = data;
        Array.prototype.push.apply(this._frames, frames);
        break;
      // Accumulate memory measurements into an array.

      // Accumulate memory measurements into an array. Furthermore, the
      // timestamp does not have a zero epoch, so offset it.
      case "memory":
        let [delta, measurement] = data;
        this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
        let [currentTime, measurement] = data;
        this._memory.push({
          delta: currentTime - this._timelineStartTime,
          value: measurement.total / 1024 / 1024
        });
        break;

      // Save the accumulated refresh driver ticks.
      case "ticks":
        let [, timestamps] = data;
+59 −0
Original line number Diff line number Diff line
/* 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";

/**
 * Utility functions for managing recording models and their internal data,
 * such as filtering profile samples or offsetting timestamps.
 */

exports.RecordingUtils = {};

/**
 * Filters all the samples in the provided profiler data to be more recent
 * than the specified start time.
 *
 * @param object profile
 *        The profiler data received from the backend.
 * @param number profilerStartTime
 *        The earliest acceptable sample time (in milliseconds).
 */
exports.RecordingUtils.filterSamples = function(profile, profilerStartTime) {
  let firstThread = profile.threads[0];

  firstThread.samples = firstThread.samples.filter(e => {
    return e.time >= profilerStartTime;
  });
}

/**
 * Offsets all the samples in the provided profiler data by the specified time.
 *
 * @param object profile
 *        The profiler data received from the backend.
 * @param number timeOffset
 *        The amount of time to offset by (in milliseconds).
 */
exports.RecordingUtils.offsetSampleTimes = function(profile, timeOffset) {
  let firstThread = profile.threads[0];

  for (let sample of firstThread.samples) {
    sample.time -= timeOffset;
  }
}

/**
 * Offsets all the markers in the provided timeline data by the specified time.
 *
 * @param array markers
 *        The markers array received from the backend.
 * @param number timeOffset
 *        The amount of time to offset by (in milliseconds).
 */
exports.RecordingUtils.offsetMarkerTimes = function(markers, timeOffset) {
  for (let marker of markers) {
    marker.start -= timeOffset;
    marker.end -= timeOffset;
  }
}
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ EXTRA_JS_MODULES.devtools.performance += [
    'modules/front.js',
    'modules/io.js',
    'modules/recording-model.js',
    'modules/recording-utils.js',
    'panel.js'
]

Loading