/* 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";

/**
 * Many Gecko operations (painting, reflows, restyle, ...) can be tracked
 * in real time. A marker is a representation of one operation. A marker
 * has a name, start and end timestamps. Markers are stored in docShells.
 *
 * This module exposes this tracking mechanism. To use with devtools' RDP,
 * use devtools/server/actors/timeline.js directly.
 *
 * To start/stop recording markers:
 *   timeline.start()
 *   timeline.stop()
 *   timeline.isRecording()
 *
 * When markers are available, an event is emitted:
 *   timeline.on("markers", function(markers) {...})
 */

const { Ci, Cu } = require("chrome");
const { Class } = require("sdk/core/heritage");
// Be aggressive about lazy loading, as this will run on every
// toolbox startup
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "Timers", "sdk/timers");
loader.lazyRequireGetter(this, "Task", "resource://gre/modules/Task.jsm", true);
loader.lazyRequireGetter(this, "Memory", "devtools/server/performance/memory", true);
loader.lazyRequireGetter(this, "Framerate", "devtools/server/performance/framerate", true);
loader.lazyRequireGetter(this, "StackFrameCache", "devtools/server/actors/utils/stack", true);
loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);

// How often do we pull markers from the docShells, and therefore, how often do
// we send events to the front (knowing that when there are no markers in the
// docShell, no event is sent).
const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms

/**
 * The timeline actor pops and forwards timeline markers registered in docshells.
 */
var Timeline = exports.Timeline = Class({
  extends: EventTarget,

  /**
   * Initializes this actor with the provided connection and tab actor.
   */
  initialize: function (tabActor) {
    this.tabActor = tabActor;

    this._isRecording = false;
    this._stackFrames = null;
    this._memory = null;
    this._framerate = null;

    // Make sure to get markers from new windows as they become available
    this._onWindowReady = this._onWindowReady.bind(this);
    this._onGarbageCollection = this._onGarbageCollection.bind(this);
    events.on(this.tabActor, "window-ready", this._onWindowReady);
  },

  /**
   * Destroys this actor, stopping recording first.
   */
  destroy: function() {
    this.stop();

    events.off(this.tabActor, "window-ready", this._onWindowReady);
    this.tabActor = null;
  },

  /**
   * Get the list of docShells in the currently attached tabActor. Note that we
   * always list the docShells included in the real root docShell, even if the
   * tabActor was switched to a child frame. This is because for now, paint
   * markers are only recorded at parent frame level so switching the timeline
   * to a child frame would hide all paint markers.
   * See https://bugzilla.mozilla.org/show_bug.cgi?id=1050773#c14
   * @return {Array}
   */
  get docShells() {
    let originalDocShell;
    let docShells = [];

    if (this.tabActor.isRootActor) {
      originalDocShell = this.tabActor.docShell;
    } else {
      originalDocShell = this.tabActor.originalDocShell;
    }

    if (!originalDocShell) {
      return docShells;
    }

    let docShellsEnum = originalDocShell.getDocShellEnumerator(
      Ci.nsIDocShellTreeItem.typeAll,
      Ci.nsIDocShell.ENUMERATE_FORWARDS
    );

    while (docShellsEnum.hasMoreElements()) {
      let docShell = docShellsEnum.getNext();
      docShells.push(docShell.QueryInterface(Ci.nsIDocShell));
    }

    return docShells;
  },

  /**
   * At regular intervals, pop the markers from the docshell, and forward
   * markers, memory, tick and frames events, if any.
   */
  _pullTimelineData: function() {
    let docShells = this.docShells;
    if (!this._isRecording || !docShells.length) {
      return;
    }

    let endTime = docShells[0].now();
    let markers = [];

    // Gather markers if requested.
    if (this._withMarkers || this._withDocLoadingEvents) {
      for (let docShell of docShells) {
        for (let marker of docShell.popProfileTimelineMarkers()) {
          markers.push(marker);

          // The docshell may return markers with stack traces attached.
          // Here we transform the stack traces via the stack frame cache,
          // which lets us preserve tail sharing when transferring the
          // frames to the client.  We must waive xrays here because Firefox
          // doesn't understand that the Debugger.Frame object is safe to
          // use from chrome.  See Tutorial-Alloc-Log-Tree.md.
          if (this._withFrames) {
            if (marker.stack) {
              marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack));
            }
            if (marker.endStack) {
              marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack));
            }
          }

          // Emit some helper events for "DOMContentLoaded" and "Load" markers.
          if (this._withDocLoadingEvents) {
            if (marker.name == "document::DOMContentLoaded" ||
                marker.name == "document::Load") {
              events.emit(this, "doc-loading", marker, endTime);
            }
          }
        }
      }
    }

    // Emit markers if requested.
    if (this._withMarkers && markers.length > 0) {
      events.emit(this, "markers", markers, endTime);
    }

    // Emit framerate data if requested.
    if (this._withTicks) {
      events.emit(this, "ticks", endTime, this._framerate.getPendingTicks());
    }

    // Emit memory data if requested.
    if (this._withMemory) {
      events.emit(this, "memory", endTime, this._memory.measure());
    }

    // Emit stack frames data if requested.
    if (this._withFrames && this._withMarkers) {
      let frames = this._stackFrames.makeEvent();
      if (frames) {
        events.emit(this, "frames", endTime, frames);
      }
    }

    this._dataPullTimeout = Timers.setTimeout(() => {
      this._pullTimelineData();
    }, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
  },

  /**
   * Are we recording profile markers currently?
   */
  isRecording: function () {
    return this._isRecording;
  },

  /**
   * Start recording profile markers.
   *
   * @option {boolean} withMarkers
   *         Boolean indicating whether or not timeline markers are emitted
   *         once they're accumulated every `DEFAULT_TIMELINE_DATA_PULL_TIMEOUT`
   *         milliseconds.
   * @option {boolean} withTicks
   *         Boolean indicating whether a `ticks` event is fired and a
   *         FramerateActor is created.
   * @option {boolean} withMemory
   *         Boolean indiciating whether we want memory measurements sampled.
   * @option {boolean} withFrames
   *         Boolean indicating whether or not stack frames should be handled
   *         from timeline markers.
   * @option {boolean} withGCEvents
   *         Boolean indicating whether or not GC markers should be emitted.
   *         TODO: Remove these fake GC markers altogether in bug 1198127.
   * @option {boolean} withDocLoadingEvents
   *         Boolean indicating whether or not DOMContentLoaded and Load
   *         marker events are emitted.
   */
  start: Task.async(function *({
    withMarkers,
    withTicks,
    withMemory,
    withFrames,
    withGCEvents,
    withDocLoadingEvents,
  }) {
    let docShells = this.docShells;
    if (!docShells.length) {
      return -1;
    }
    let startTime = this._startTime = docShells[0].now();
    if (this._isRecording) {
      return startTime;
    }

    this._isRecording = true;
    this._withMarkers = !!withMarkers;
    this._withTicks = !!withTicks;
    this._withMemory = !!withMemory;
    this._withFrames = !!withFrames;
    this._withGCEvents = !!withGCEvents;
    this._withDocLoadingEvents = !!withDocLoadingEvents;

    if (this._withMarkers || this._withDocLoadingEvents) {
      for (let docShell of docShells) {
        docShell.recordProfileTimelineMarkers = true;
      }
    }

    if (this._withTicks) {
      this._framerate = new Framerate(this.tabActor);
      this._framerate.startRecording();
    }

    if (this._withMemory || this._withGCEvents) {
      this._memory = new Memory(this.tabActor, this._stackFrames);
      this._memory.attach();
    }

    if (this._withGCEvents) {
      events.on(this._memory, "garbage-collection", this._onGarbageCollection);
    }

    if (this._withFrames && this._withMarkers) {
      this._stackFrames = new StackFrameCache();
      this._stackFrames.initFrames();
    }

    this._pullTimelineData();
    return startTime;
  }),

  /**
   * Stop recording profile markers.
   */
  stop: Task.async(function *() {
    let docShells = this.docShells;
    if (!docShells.length) {
      return -1;
    }
    let endTime = this._startTime = docShells[0].now();
    if (!this._isRecording) {
      return endTime;
    }

    if (this._withMarkers || this._withDocLoadingEvents) {
      for (let docShell of docShells) {
        docShell.recordProfileTimelineMarkers = false;
      }
    }

    if (this._withTicks) {
      this._framerate.stopRecording();
      this._framerate.destroy();
      this._framerate = null;
    }

    if (this._withMemory || this._withGCEvents) {
      this._memory.detach();
      this._memory.destroy();
    }

    if (this._withGCEvents) {
      events.off(this._memory, "garbage-collection", this._onGarbageCollection);
    }

    if (this._withFrames && this._withMarkers) {
      this._stackFrames = null;
    }

    this._isRecording = false;
    this._withMarkers = false;
    this._withTicks = false;
    this._withMemory = false;
    this._withFrames = false;
    this._withDocLoadingEvents = false;
    this._withGCEvents = false;

    Timers.clearTimeout(this._dataPullTimeout);

    return endTime;
  }),

  /**
   * When a new window becomes available in the tabActor, start recording its
   * markers if we were recording.
   */
  _onWindowReady: function({ window }) {
    if (this._isRecording) {
      let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIWebNavigation)
                           .QueryInterface(Ci.nsIDocShell);
      docShell.recordProfileTimelineMarkers = true;
    }
  },

  /**
   * Fired when the Memory component emits a `garbage-collection` event. Used to
   * take the data and make it look like the rest of our markers.
   *
   * A GC "marker" here represents a full GC cycle, which may contain several incremental
   * events within its `collection` array. The marker contains a `reason` field, indicating
   * why there was a GC, and may contain a `nonincrementalReason` when SpiderMonkey could
   * not incrementally collect garbage.
   */
  _onGarbageCollection: function ({ collections, gcCycleNumber, reason, nonincrementalReason }) {
    let docShells = this.docShells;
    if (!this._isRecording || !docShells.length) {
      return;
    }

    let endTime = docShells[0].now();

    events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => {
      return {
        name: "GarbageCollection",
        causeName: reason,
        nonincrementalReason: nonincrementalReason,
        cycle: gcCycleNumber,
        start,
        end,
      };
    }), endTime);
  },
});
