Bug 887027 - Implement a tracing profiler actor; r=past,robcee
authorJake Bailey <rbailey@mozilla.com>
Tue, 30 Jul 2013 19:09:29 -0700
changeset 153012 d8715e2c8b5570c9b601ceb336a9e73fad010383
parent 153011 5e5bb1036572c60073039330a3b82f5ea78157dd
child 153013 0ded78d3e616b649f5103cb5f61fa0f8f9db0e2f
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast, robcee
bugs887027
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 887027 - Implement a tracing profiler actor; r=past,robcee
toolkit/components/telemetry/Histograms.json
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/tracer.js
toolkit/devtools/server/main.js
toolkit/devtools/server/tests/unit/head_dbg.js
toolkit/devtools/server/tests/unit/test_trace_actor-01.js
toolkit/devtools/server/tests/unit/test_trace_actor-02.js
toolkit/devtools/server/tests/unit/test_trace_actor-03.js
toolkit/devtools/server/tests/unit/test_trace_actor-04.js
toolkit/devtools/server/tests/unit/test_trace_actor-05.js
toolkit/devtools/server/tests/unit/test_trace_actor-06.js
toolkit/devtools/server/tests/unit/test_trace_actor-07.js
toolkit/devtools/server/tests/unit/testactors.js
toolkit/devtools/server/tests/unit/xpcshell.ini
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3426,16 +3426,52 @@
     "description": "The time (in milliseconds) that it took a 'detach' request to go round trip."
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_TABDETACH_MS": {
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'detach' request to go round trip."
   },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_TRACERDETACH_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'detach' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_TRACERDETACH_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'detach' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_STARTTRACE_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'startTrace' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_STARTTRACE_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'startTrace' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_STOPTRACE_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'stopTrace' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_STOPTRACE_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'stopTrace' request to go round trip."
+  },
   "COOKIES_3RDPARTY_NUM_SITES_ACCEPTED": {
     "kind": "linear",
     "low": "5",
     "high": "145",
     "n_buckets": "30",
     "description": "The number of distinct pairs (first-party site, third-party site attempting to set cookie) for which the third-party cookie has been accepted. Sites are considered identical if they have the same eTLD + 1. Measures are normalized per 24h."
   },
   "COOKIES_3RDPARTY_NUM_SITES_BLOCKED": {
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -184,17 +184,19 @@ const UnsolicitedNotifications = {
   "newGlobal": "newGlobal",
   "newScript": "newScript",
   "newSource": "newSource",
   "tabDetached": "tabDetached",
   "tabListChanged": "tabListChanged",
   "tabNavigated": "tabNavigated",
   "pageError": "pageError",
   "webappsEvent": "webappsEvent",
-  "documentLoad": "documentLoad"
+  "documentLoad": "documentLoad",
+  "enteredFrame": "enteredFrame",
+  "exitedFrame": "exitedFrame"
 };
 
 /**
  * Set of pause types that are sent by the server and not as an immediate
  * response to a client request.
  */
 const UnsolicitedPauses = {
   "resumeLimit": "resumeLimit",
@@ -494,16 +496,38 @@ DebuggerClient.prototype = {
         this._threadClients[aThreadActor] = threadClient;
         this.activeThread = threadClient;
       }
       aOnResponse(aResponse, threadClient);
     });
   },
 
   /**
+   * Attach to a trace actor.
+   *
+   * @param string aTraceActor
+   *        The actor ID for the tracer to attach.
+   * @param function aOnResponse
+   *        Called with the response packet and a TraceClient
+   *        (which will be undefined on error).
+   */
+  attachTracer: function DC_attachTracer(aTraceActor, aOnResponse) {
+    let packet = {
+      to: aTraceActor,
+      type: "attach"
+    };
+    this.request(packet, (aResponse) => {
+      if (!aResponse.error) {
+        let traceClient = new TraceClient(this, aTraceActor);
+        aOnResponse(aResponse, traceClient);
+      }
+    });
+  },
+
+  /**
    * Reconfigure a thread actor.
    *
    * @param object aOptions
    *        A dictionary object of the new options to use in the thread actor.
    * @param function aOnResponse
    *        Called with the response packet.
    */
   reconfigureThread: function(aOptions, aOnResponse) {
@@ -1591,16 +1615,134 @@ ThreadClient.prototype = {
     return new SourceClient(this._client, aForm);
   }
 
 };
 
 eventSource(ThreadClient.prototype);
 
 /**
+ * Creates a tracing profiler client for the remote debugging protocol
+ * server. This client is a front to the trace actor created on the
+ * server side, hiding the protocol details in a traditional
+ * JavaScript API.
+ *
+ * @param aClient DebuggerClient
+ *        The debugger client parent.
+ * @param aActor string
+ *        The actor ID for this thread.
+ */
+function TraceClient(aClient, aActor) {
+  this._client = aClient;
+  this._actor = aActor;
+  this._traces = Object.create(null);
+  this._activeTraces = 0;
+
+  this._client.addListener(UnsolicitedNotifications.enteredFrame,
+                           this.onEnteredFrame.bind(this));
+  this._client.addListener(UnsolicitedNotifications.exitedFrame,
+                           this.onExitedFrame.bind(this));
+
+  this.request = this._client.request;
+}
+
+TraceClient.prototype = {
+  get actor()   { return this._actor; },
+  get tracing() { return this._activeTraces > 0; },
+
+  get _transport() { return this._client._transport; },
+
+  /**
+   * Detach from the trace actor.
+   */
+  detach: DebuggerClient.requester({ type: "detach" },
+                                   { telemetry: "TRACERDETACH" }),
+
+  /**
+   * Start a new trace.
+   *
+   * @param aTrace [string]
+   *        An array of trace types to be recorded by the new trace.
+   *
+   * @param aName string
+   *        The name of the new trace.
+   *
+   * @param aOnResponse function
+   *        Called with the request's response.
+   */
+  startTrace: DebuggerClient.requester({
+    type: "startTrace",
+    name: args(1),
+    trace: args(0)
+  }, {
+    after: function(aResponse) {
+      if (aResponse.error) {
+        return aResponse;
+      }
+
+      let name = aResponse.name;
+
+      if (!this._traces[name] || !this._traces[name].active) {
+        this._activeTraces++;
+      }
+
+      this._traces[name] = {
+        active: true
+      };
+
+      return aResponse;
+    },
+    telemetry: "STARTTRACE"
+  }),
+
+  /**
+   * End a trace. If a name is provided, stop the named
+   * trace. Otherwise, stop the most recently started trace.
+   *
+   * @param aName string
+   *        The name of the trace to stop.
+   *
+   * @param aOnResponse function
+   *        Called with the request's response.
+   */
+  stopTrace: DebuggerClient.requester({
+    type: "stopTrace",
+    name: args(0)
+  }, {
+    after: function(aResponse) {
+      if (aResponse.error) {
+        return aResponse;
+      }
+
+      this._traces[aResponse.name].active = false;
+      this._activeTraces--;
+
+      return aResponse;
+    },
+    telemetry: "STOPTRACE"
+  }),
+
+  /**
+   * Called when the trace actor notifies that a frame has been entered.
+   */
+  onEnteredFrame: function JSTC_onEnteredFrame(aEvent, aResponse) {
+    this.notify("enteredFrame", aResponse);
+  },
+
+  /**
+   * Called when the trace actor notifies that a frame has been exited.
+   */
+  onExitedFrame: function JSTC_onExitedFrame(aEvent, aResponse) {
+    this.notify("exitedFrame", aResponse);
+  }
+};
+
+eventSource(TraceClient.prototype);
+
+/**
  * Grip clients are used to retrieve information about the relevant object.
  *
  * @param aClient DebuggerClient
  *        The debugger client parent.
  * @param aGrip object
  *        A pause-lifetime object grip returned by the protocol.
  */
 function GripClient(aClient, aGrip)
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/tracer.js
@@ -0,0 +1,810 @@
+/* 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");
+
+const { reportException } =
+  Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}).DevToolsUtils;
+
+const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+
+Cu.import("resource://gre/modules/jsdebugger.jsm");
+addDebuggerToGlobal(this);
+
+/**
+ * Creates a TraceActor. TraceActor provides a stream of function
+ * call/return packets to a remote client gathering a full trace.
+ */
+function TraceActor(aConn, aBrowserTabActor)
+{
+  this._attached = false;
+  this._activeTraces = new MapStack();
+  this._totalTraces = 0;
+  this._startTime = 0;
+  this._requestsForTraceType = Object.create(null);
+  for (let type of TraceTypes.types) {
+    this._requestsForTraceType[type] = 0;
+  }
+  this._sequence = 0;
+
+  this.global = aBrowserTabActor.contentWindow.wrappedJSObject;
+}
+
+TraceActor.prototype = {
+  actorPrefix: "trace",
+
+  get attached() { return this._attached; },
+  get idle()     { return this._attached && this._activeTraces.size === 0; },
+  get tracing()  { return this._attached && this._activeTraces.size > 0; },
+
+  /**
+   * Handle a TraceTypes.Events event by calling each handler which has been
+   * requested by an active trace and adding its result to the packet.
+   *
+   * @param aEvent string
+   *        The event to dispatch.
+   *
+   * @param aPacket object
+   *        The debugger protocol packet.
+   *
+   * @param aArgs object
+   *        The arguments object for the handler.
+   */
+  _handleEvent: function(aEvent, aPacket, aArgs) {
+    let handlersForEvent = TraceTypes.handlers[aEvent];
+    for (let traceType in handlersForEvent) {
+      if (this._requestsForTraceType[traceType]) {
+        aPacket[traceType] = handlersForEvent[traceType].call(null, aArgs);
+      }
+    }
+  },
+
+  /**
+   * Initializes a Debugger instance and adds listeners to it.
+   */
+  _initDebugger: function() {
+    this.dbg = new Debugger();
+    this.dbg.onEnterFrame = this.onEnterFrame.bind(this);
+    this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this);
+    this.dbg.enabled = false;
+  },
+
+  /**
+   * Add a debuggee global to the Debugger object.
+   */
+  _addDebuggee: function(aGlobal) {
+    try {
+      this.dbg.addDebuggee(aGlobal);
+    } catch (e) {
+      // Ignore attempts to add the debugger's compartment as a debuggee.
+      reportException("TraceActor",
+                      new Error("Ignoring request to add the debugger's "
+                                + "compartment as a debuggee"));
+    }
+  },
+
+  /**
+   * Add the provided window and all windows in its frame tree as debuggees.
+   */
+  _addDebuggees: function(aWindow) {
+    this._addDebuggee(aWindow);
+    let frames = aWindow.frames;
+    if (frames) {
+      for (let i = 0; i < frames.length; i++) {
+        this._addDebuggees(frames[i]);
+      }
+    }
+  },
+
+  /**
+   * An object used by TraceActors to tailor their behavior depending
+   * on the debugging context required (chrome or content).
+   */
+  globalManager: {
+    /**
+     * Adds all globals in the global object as debuggees.
+     */
+    findGlobals: function() {
+      this._addDebuggees(this.global);
+    },
+
+    /**
+     * A function that the engine calls when a new global object has been
+     * created. Adds the global object as a debuggee if it is in the content
+     * window.
+     *
+     * @param aGlobal Debugger.Object
+     *        The new global object that was created.
+     */
+    onNewGlobal: function(aGlobal) {
+      // Content debugging only cares about new globals in the content
+      // window, like iframe children.
+      if (aGlobal.hostAnnotations &&
+          aGlobal.hostAnnotations.type == "document" &&
+          aGlobal.hostAnnotations.element === this.global) {
+        this._addDebuggee(aGlobal);
+      }
+    },
+  },
+
+  /**
+   * Handle a protocol request to attach to the trace actor.
+   *
+   * @param aRequest object
+   *        The protocol request object.
+   */
+  onAttach: function(aRequest) {
+    if (this.attached) {
+      return {
+        error: "wrongState",
+        message: "Already attached to a client"
+      };
+    }
+
+    if (!this.dbg) {
+      this._initDebugger();
+      this.globalManager.findGlobals.call(this);
+    }
+
+    this._attached = true;
+
+    return { type: "attached", traceTypes: TraceTypes.types };
+  },
+
+  /**
+   * Handle a protocol request to detach from the trace actor.
+   *
+   * @param aRequest object
+   *        The protocol request object.
+   */
+  onDetach: function() {
+    while (this.tracing) {
+      this.onStopTrace();
+    }
+
+    this.dbg = null;
+
+    this._attached = false;
+    this.conn.send({ from: this.actorID, type: "detached" });
+  },
+
+  /**
+   * Handle a protocol request to start a new trace.
+   *
+   * @param aRequest object
+   *        The protocol request object.
+   */
+  onStartTrace: function(aRequest) {
+    for (let traceType of aRequest.trace) {
+      if (TraceTypes.types.indexOf(traceType) < 0) {
+        return {
+          error: "badParameterType",
+          message: "No such trace type: " + traceType
+        };
+      }
+    }
+
+    if (this.idle) {
+      this.dbg.enabled = true;
+      this._sequence = 0;
+      this._startTime = +new Date;
+    }
+
+    // Start recording all requested trace types.
+    for (let traceType of aRequest.trace) {
+      this._requestsForTraceType[traceType]++;
+    }
+
+    this._totalTraces++;
+    let name = aRequest.name || "Trace " + this._totalTraces;
+    this._activeTraces.push(name, aRequest.trace);
+
+    return { type: "startedTrace", why: "requested", name: name };
+  },
+
+  /**
+   * Handle a protocol request to end a trace.
+   *
+   * @param aRequest object
+   *        The protocol request object.
+   */
+  onStopTrace: function(aRequest) {
+    if (!this.tracing) {
+      return {
+        error: "wrongState",
+        message: "No active traces"
+      };
+    }
+
+    let stoppedTraceTypes, name;
+    if (aRequest && aRequest.name) {
+      name = aRequest.name;
+      if (!this._activeTraces.has(name)) {
+        return {
+          error: "noSuchTrace",
+          message: "No active trace with name: " + name
+        };
+      }
+      stoppedTraceTypes = this._activeTraces.delete(name);
+    } else {
+      name = this._activeTraces.peekKey();
+      stoppedTraceTypes = this._activeTraces.pop();
+    }
+
+    for (let traceType of stoppedTraceTypes) {
+      this._requestsForTraceType[traceType]--;
+    }
+
+    if (this.idle) {
+      this.dbg.enabled = false;
+    }
+
+    return { type: "stoppedTrace", why: "requested", name: name };
+  },
+
+  // JS Debugger API hooks.
+
+  /**
+   * Called by the engine when a frame is entered. Sends an unsolicited packet
+   * to the client carrying requested trace information.
+   *
+   * @param aFrame Debugger.frame
+   *        The stack frame that was entered.
+   */
+  onEnterFrame: function(aFrame) {
+    let callee = aFrame.callee;
+    let packet = {
+      from: this.actorID,
+      type: "enteredFrame",
+      sequence: this._sequence++
+    };
+
+    this._handleEvent(TraceTypes.Events.enterFrame, packet, {
+      frame: aFrame,
+      startTime: this._startTime
+    });
+
+    aFrame.onPop = this.onExitFrame.bind(this);
+
+    this.conn.send(packet);
+  },
+
+  /**
+   * Called by the engine when a frame is exited. Sends an unsolicited packet to
+   * the client carrying requested trace information.
+   *
+   * @param aValue object
+   *        The debugger completion value for the frame.
+   */
+  onExitFrame: function(aValue) {
+    let packet = {
+      from: this.actorID,
+      type: "exitedFrame",
+      sequence: this._sequence++
+    };
+
+    this._handleEvent(TraceTypes.Events.exitFrame, packet, { value: aValue });
+
+    this.conn.send(packet);
+  }
+};
+
+/**
+ * The request types this actor can handle.
+ */
+TraceActor.prototype.requestTypes = {
+  "attach": TraceActor.prototype.onAttach,
+  "detach": TraceActor.prototype.onDetach,
+  "startTrace": TraceActor.prototype.onStartTrace,
+  "stopTrace": TraceActor.prototype.onStopTrace
+};
+
+exports.register = function(handle) {
+  handle.addTabActor(TraceActor, "traceActor");
+};
+
+exports.unregister = function(handle) {
+  handle.removeTabActor(TraceActor, "traceActor");
+};
+
+
+/**
+ * MapStack is a collection of key/value pairs with stack ordering,
+ * where keys are strings and values are any JS value. In addition to
+ * the push and pop stack operations, supports a "delete" operation,
+ * which removes the value associated with a given key from any
+ * location in the stack.
+ */
+function MapStack()
+{
+  // Essentially a MapStack is just sugar-coating around a standard JS
+  // object, plus the _stack array to track ordering.
+  this._stack = [];
+  this._map = Object.create(null);
+}
+
+MapStack.prototype = {
+  get size() { return this._stack.length; },
+
+  /**
+   * Return the key for the value on the top of the stack, or
+   * undefined if the stack is empty.
+   */
+  peekKey: function() {
+    return this._stack[this.size - 1];
+  },
+
+  /**
+   * Return true iff a value has been associated with the given key.
+   *
+   * @param aKey string
+   *        The key whose presence is to be tested.
+   */
+  has: function(aKey) {
+    return Object.prototype.hasOwnProperty.call(this._map, aKey);
+  },
+
+  /**
+   * Return the value associated with the given key, or undefined if
+   * no value is associated with the key.
+   *
+   * @param aKey string
+   *        The key whose associated value is to be returned.
+   */
+  get: function(aKey) {
+    return this._map[aKey];
+  },
+
+  /**
+   * Push a new value onto the stack. If another value with the same
+   * key is already on the stack, it will be removed before the new
+   * value is pushed onto the top of the stack.
+   *
+   * @param aKey string
+   *        The key of the object to push onto the stack.
+   *
+   * @param aValue
+   *        The value to push onto the stack.
+   */
+  push: function(aKey, aValue) {
+    this.delete(aKey);
+    this._stack.push(aKey);
+    this._map[aKey] = aValue;
+  },
+
+  /**
+   * Remove the value from the top of the stack and return it.
+   * Returns undefined if the stack is empty.
+   */
+  pop: function() {
+    let key = this.peekKey();
+    let value = this.get(key);
+    this._stack.pop();
+    delete this._map[key];
+    return value;
+  },
+
+  /**
+   * Remove the value associated with the given key from the stack and
+   * return it. Returns undefined if no value is associated with the
+   * given key.
+   *
+   * @param aKey string
+   *        The key for the value to remove from the stack.
+   */
+  delete: function(aKey) {
+    let value = this.get(aKey);
+    if (this.has(aKey)) {
+      let keyIndex = this._stack.lastIndexOf(aKey);
+      this._stack.splice(keyIndex, 1);
+      delete this._map[aKey];
+    }
+    return value;
+  }
+};
+
+
+/**
+ * TraceTypes is a collection of handlers which generate optional trace
+ * information. Handlers are associated with an event (from TraceTypes.Event)
+ * and a trace type, and return a value to be embedded in the packet associated
+ * with that event.
+ */
+let TraceTypes = {
+  handlers: {},
+  types: [],
+
+  register: function(aType, aEvent, aHandler) {
+    if (!this.handlers[aEvent]) {
+      this.handlers[aEvent] = {};
+    }
+    this.handlers[aEvent][aType] = aHandler;
+    if (this.types.indexOf(aType) < 0) {
+      this.types.push(aType);
+    }
+  }
+};
+
+TraceTypes.Events = {
+  "enterFrame": "enterFrame",
+  "exitFrame": "exitFrame"
+};
+
+TraceTypes.register("name", TraceTypes.Events.enterFrame, function({ frame }) {
+  return frame.callee
+    ? frame.callee.displayName || "(anonymous function)"
+    : "(" + frame.type + ")";
+});
+
+TraceTypes.register("callsite", TraceTypes.Events.enterFrame, function({ frame }) {
+  if (!frame.script) {
+    return undefined;
+  }
+  return {
+    url: frame.script.url,
+    line: frame.script.getOffsetLine(frame.offset),
+    column: getOffsetColumn(frame.offset, frame.script)
+  };
+});
+
+TraceTypes.register("time", TraceTypes.Events.enterFrame, timeSinceTraceStarted);
+TraceTypes.register("time", TraceTypes.Events.exitFrame, timeSinceTraceStarted);
+
+TraceTypes.register("parameterNames", TraceTypes.Events.enterFrame, function({ frame }) {
+  return frame.callee ? frame.callee.parameterNames : undefined;
+});
+
+TraceTypes.register("arguments", TraceTypes.Events.enterFrame, function({ frame }) {
+  if (!frame.arguments) {
+    return undefined;
+  }
+  let objectPool = [];
+  let objToId = new Map();
+  let args = Array.prototype.slice.call(frame.arguments);
+  let values = args.map(arg => createValueGrip(arg, objectPool, objToId));
+  return { values: values, objectPool: objectPool };
+});
+
+TraceTypes.register("return", TraceTypes.Events.exitFrame,
+                    serializeCompletionValue.bind(null, "return"));
+
+TraceTypes.register("throw", TraceTypes.Events.exitFrame,
+                    serializeCompletionValue.bind(null, "throw"));
+
+TraceTypes.register("yield", TraceTypes.Events.exitFrame,
+                    serializeCompletionValue.bind(null, "yield"));
+
+
+// TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when
+// it is implemented.
+function getOffsetColumn(aOffset, aScript) {
+  let bestOffsetMapping = null;
+  for (let offsetMapping of aScript.getAllColumnOffsets()) {
+    if (!bestOffsetMapping ||
+        (offsetMapping.offset <= aOffset &&
+         offsetMapping.offset > bestOffsetMapping.offset)) {
+      bestOffsetMapping = offsetMapping;
+    }
+  }
+
+  if (!bestOffsetMapping) {
+    // XXX: Try not to completely break the experience of using the
+    // tracer for the user by assuming column 0. Simultaneously,
+    // report the error so that there is a paper trail if the
+    // assumption is bad and the tracing experience becomes wonky.
+    reportException("TraceActor",
+                    new Error("Could not find a column for offset " + aOffset +
+                              " in the script " + aScript));
+    return 0;
+  }
+
+  return bestOffsetMapping.columnNumber;
+}
+
+/**
+ * Returns elapsed time since the given start time.
+ */
+function timeSinceTraceStarted({ startTime }) {
+  return +new Date - startTime;
+}
+
+/**
+ * Creates a pool of object descriptors and a value grip for the given
+ * completion value, to be serialized by JSON.stringify.
+ *
+ * @param aType string
+ *        The type of completion value to serialize (return, throw, or yield).
+ */
+function serializeCompletionValue(aType, { value }) {
+  if (typeof value[aType] === "undefined") {
+    return undefined;
+  }
+  let objectPool = [];
+  let objToId = new Map();
+  let valueGrip = createValueGrip(value[aType], objectPool, objToId);
+  return { value: valueGrip, objectPool: objectPool };
+}
+
+
+// Serialization helper functions. Largely copied from script.js and modified
+// for use in serialization rather than object actor requests.
+
+/**
+ * Create a grip for the given debuggee value. If the value is an object, will
+ * create an object descriptor in the given object pool.
+ *
+ * @param aValue Debugger.Object|primitive
+ *        The value to describe with the created grip.
+ *
+ * @param aPool [ObjectDescriptor]
+ *        The pool of objects that may be referenced by |aValue|.
+ *
+ * @param aObjectToId Map
+ *        A map from Debugger.Object instances to indices into the pool.
+ *
+ * @return ValueGrip
+ *        A primitive value or a grip object.
+ */
+function createValueGrip(aValue, aPool, aObjectToId) {
+  let type = typeof aValue;
+
+  if (type === "string" && aValue.length >= DebuggerServer.LONG_STRING_LENGTH) {
+    return {
+      type: "longString",
+      initial: aValue.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH),
+      length: aValue.length
+    };
+  }
+
+  if (type === "boolean" || type === "string" || type === "number") {
+    return aValue;
+  }
+
+  if (aValue === null) {
+    return { type: "null" };
+  }
+
+  if (aValue === undefined) {
+    return { type: "undefined" };
+  }
+
+  if (typeof(aValue) === "object") {
+    createObjectDescriptor(aValue, aPool, aObjectToId);
+    return { type: "object", objectId: aObjectToId.get(aValue) };
+  }
+
+  reportException("TraceActor",
+                  new Error("Failed to provide a grip for: " + aValue));
+  return null;
+}
+
+/**
+ * Create an object descriptor in the object pool for the given debuggee object,
+ * if it is not already present.
+ *
+ * @param aObject Debugger.Object
+ *        The object to describe with the created descriptor.
+ *
+ * @param aPool [ObjectDescriptor]
+ *        The pool of objects that may be referenced by |aObject|.
+ *
+ * @param aObjectToId Map
+ *        A map from Debugger.Object instances to indices into the pool.
+ */
+function createObjectDescriptor(aObject, aPool, aObjectToId) {
+  if (aObjectToId.has(aObject)) {
+    return;
+  }
+
+  aObjectToId.set(aObject, aPool.length);
+  let desc = Object.create(null);
+  aPool.push(desc);
+
+  // Add properties which object actors include in object grips.
+  desc.class = aObject.class;
+  desc.extensible = aObject.isExtensible();
+  desc.frozen = aObject.isFrozen();
+  desc.sealed = aObject.isSealed();
+
+  // Add additional properties for functions.
+  if (aObject.class === "Function") {
+    if (aObject.name) {
+      desc.name = aObject.name;
+    }
+    if (aObject.displayName) {
+      desc.displayName = aObject.displayName;
+    }
+
+    // Check if the developer has added a de-facto standard displayName
+    // property for us to use.
+    let name = aObject.getOwnPropertyDescriptor("displayName");
+    if (name && name.value && typeof name.value == "string") {
+      desc.userDisplayName = createValueGrip(name.value, aObject, aPool, aObjectToId);
+    }
+  }
+
+  let ownProperties = Object.create(null);
+  let propNames;
+  try {
+    propNames = aObject.getOwnPropertyNames();
+  } catch(ex) {
+    // The above can throw if aObject points to a dead object.
+    // TODO: we should use Cu.isDeadWrapper() - see bug 885800.
+    desc.prototype = createValueGrip(null);
+    desc.ownProperties = ownProperties;
+    desc.safeGetterValues = Object.create(null);
+    return;
+  }
+
+  for (let name of propNames) {
+    ownProperties[name] = createPropertyDescriptor(name, aObject, aPool, aObjectToId);
+  }
+
+  desc.prototype = createValueGrip(aObject.proto, aPool, aObjectToId);
+  desc.ownProperties = ownProperties;
+  desc.safeGetterValues = findSafeGetterValues(ownProperties, aObject, aPool, aObjectToId);
+}
+
+/**
+ * A helper method that creates a property descriptor for the provided object,
+ * properly formatted for sending in a protocol response.
+ *
+ * @param aName string
+ *        The property that the descriptor is generated for.
+ *
+ * @param aObject Debugger.Object
+ *        The object whose property the descriptor is generated for.
+ *
+ * @param aPool [ObjectDescriptor]
+ *        The pool of objects that may be referenced by this property.
+ *
+ * @param aObjectToId Map
+ *        A map from Debugger.Object instances to indices into the pool.
+ *
+ * @return object
+ *         The property descriptor for the property |aName| in |aObject|.
+ */
+function createPropertyDescriptor(aName, aObject, aPool, aObjectToId) {
+  let desc;
+  try {
+    desc = aObject.getOwnPropertyDescriptor(aName);
+  } catch (e) {
+    // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
+    // allowed (bug 560072). Inform the user with a bogus, but hopefully
+    // explanatory, descriptor.
+    return {
+      configurable: false,
+      writable: false,
+      enumerable: false,
+      value: e.name
+    };
+  }
+
+  let retval = {
+    configurable: desc.configurable,
+    enumerable: desc.enumerable
+  };
+
+  if ("value" in desc) {
+    retval.writable = desc.writable;
+    retval.value = createValueGrip(desc.value, aPool, aObjectToId);
+  } else {
+    if ("get" in desc) {
+      retval.get = createValueGrip(desc.get, aPool, aObjectToId);
+    }
+    if ("set" in desc) {
+      retval.set = createValueGrip(desc.set, aPool, aObjectToId);
+    }
+  }
+  return retval;
+}
+
+/**
+ * Find the safe getter values for the given Debugger.Object.
+ *
+ * @param aOwnProperties object
+ *        The object that holds the list of known ownProperties for |aObject|.
+ *
+ * @param Debugger.Object object
+ *        The object to find safe getter values for.
+ *
+ * @param aPool [ObjectDescriptor]
+ *        The pool of objects that may be referenced by |aObject| getters.
+ *
+ * @param aObjectToId Map
+ *        A map from Debugger.Object instances to indices into the pool.
+ *
+ * @return object
+ *         An object that maps property names to safe getter descriptors.
+ */
+function findSafeGetterValues(aOwnProperties, aObject, aPool, aObjectToId) {
+  let safeGetterValues = Object.create(null);
+  let obj = aObject;
+  let level = 0;
+
+  while (obj) {
+    let getters = findSafeGetters(obj);
+    for (let name of getters) {
+      // Avoid overwriting properties from prototypes closer to this.obj. Also
+      // avoid providing safeGetterValues from prototypes if property |name|
+      // is already defined as an own property.
+      if (name in safeGetterValues ||
+          (obj != aObject && name in aOwnProperties)) {
+        continue;
+      }
+
+      let desc = null, getter = null;
+      try {
+        desc = obj.getOwnPropertyDescriptor(name);
+        getter = desc.get;
+      } catch (ex) {
+        // The above can throw if the cache becomes stale.
+      }
+      if (!getter) {
+        continue;
+      }
+
+      let result = getter.call(aObject);
+      if (result && !("throw" in result)) {
+        let getterValue = undefined;
+        if ("return" in result) {
+          getterValue = result.return;
+        } else if ("yield" in result) {
+          getterValue = result.yield;
+        }
+        // WebIDL attributes specified with the LenientThis extended attribute
+        // return undefined and should be ignored.
+        if (getterValue !== undefined) {
+          safeGetterValues[name] = {
+            getterValue: createValueGrip(getterValue, aPool, aObjectToId),
+            getterPrototypeLevel: level,
+            enumerable: desc.enumerable,
+            writable: level == 0 ? desc.writable : true,
+          };
+        }
+      }
+    }
+
+    obj = obj.proto;
+    level++;
+  }
+
+  return safeGetterValues;
+}
+
+/**
+ * Find the safe getters for a given Debugger.Object. Safe getters are native
+ * getters which are safe to execute.
+ *
+ * @param Debugger.Object aObject
+ *        The Debugger.Object where you want to find safe getters.
+ *
+ * @return Set
+ *         A Set of names of safe getters.
+ */
+function findSafeGetters(aObject) {
+  let getters = new Set();
+  for (let name of aObject.getOwnPropertyNames()) {
+    let desc = null;
+    try {
+      desc = aObject.getOwnPropertyDescriptor(name);
+    } catch (e) {
+      // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
+      // allowed (bug 560072).
+    }
+    if (!desc || desc.value !== undefined || !("get" in desc)) {
+      continue;
+    }
+
+    let fn = desc.get;
+    if (fn && fn.callable && fn.class == "Function" &&
+        fn.script === undefined) {
+      getters.add(name);
+    }
+  }
+
+  return getters;
+}
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -288,16 +288,17 @@ var DebuggerServer = {
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
     this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     this.registerModule("devtools/server/actors/inspector");
+    this.registerModule("devtools/server/actors/tracer");
   },
 
   /**
    * Install tab actors in documents loaded in content childs
    */
   addChildActors: function () {
     // In case of apps being loaded in parent process, DebuggerServer is already
     // initialized and browser actors are already loaded,
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -159,22 +159,33 @@ function attachTestTabAndResume(aClient,
   });
 }
 
 /**
  * Initialize the testing debugger server.
  */
 function initTestDebuggerServer()
 {
+  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
   DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
   DebuggerServer.addActors("resource://test/testactors.js");
   // Allow incoming connections.
   DebuggerServer.init(function () { return true; });
 }
 
+function initTestTracerServer()
+{
+  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
+  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
+  DebuggerServer.addActors("resource://test/testactors.js");
+  DebuggerServer.registerModule("devtools/server/actors/tracer");
+  // Allow incoming connections.
+  DebuggerServer.init(function () { return true; });
+}
+
 function initSourcesBackwardsCompatDebuggerServer()
 {
   DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
   DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
   DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
   DebuggerServer.addActors("resource://test/testcompatactors.js");
   DebuggerServer.init(function () { return true; });
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-01.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that TraceActor is available and returns correct responses to
+ * startTrace and stopTrace requests.
+ */
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      do_check_true(!!aResponse.traceActor, "TraceActor should be visible in tab");
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_start_stop_response();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_start_stop_response()
+{
+  do_check_true(!gTraceClient.tracing, "TraceClient should start in idle state");
+  gTraceClient.startTrace([], null, function(aResponse) {
+    do_check_true(!!gTraceClient.tracing, "TraceClient should be in tracing state");
+    do_check_true(!aResponse.error,
+                  'startTrace should not respond with error: ' + aResponse.error);
+    do_check_eq(aResponse.type, "startedTrace",
+                'startTrace response should have "type":"startedTrace" property');
+    do_check_eq(aResponse.why, "requested",
+                'startTrace response should have "why":"requested" property');
+
+    gTraceClient.stopTrace(null, function(aResponse) {
+      do_check_true(!gTraceClient.tracing, "TraceClient should be in idle state");
+      do_check_true(!aResponse.error,
+                   'stopTrace should not respond with error: ' + aResponse.error);
+      do_check_eq(aResponse.type, "stoppedTrace",
+                  'stopTrace response should have "type":"stoppedTrace" property');
+      do_check_eq(aResponse.why, "requested",
+                  'stopTrace response should have "why":"requested" property');
+
+      finishClient(gClient);
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-02.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests re-entrant startTrace/stopTrace calls on TraceActor.  Tests
+ * that stopTrace ends the most recently started trace when not
+ * provided with a name. Tests that starting a trace with the same
+ * name twice results in only one trace being collected for that name.
+ */
+
+let {defer} = devtools.require("sdk/core/promise");
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_start_stop_reentrant();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_start_stop_reentrant()
+{
+  do_check_true(!gTraceClient.tracing, "TraceClient should start in idle state");
+
+  start_named_trace("foo")
+    .then(start_named_trace.bind(null, "foo"))
+    .then(start_named_trace.bind(null, "bar"))
+    .then(start_named_trace.bind(null, "baz"))
+    .then(stop_trace.bind(null, "bar", "bar"))
+    .then(stop_trace.bind(null, null, "baz"))
+    .then(stop_trace.bind(null, null, "foo"))
+    .then(function() {
+      do_check_true(!gTraceClient.tracing, "TraceClient should finish in idle state");
+      finishClient(gClient);
+    });
+}
+
+function start_named_trace(aName)
+{
+  let deferred = defer();
+  gTraceClient.startTrace([], aName, function(aResponse) {
+    do_check_true(!!gTraceClient.tracing, "TraceClient should be in tracing state");
+    do_check_true(!aResponse.error,
+                  'startTrace should not respond with error: ' + aResponse.error);
+    do_check_eq(aResponse.type, "startedTrace",
+                'startTrace response should have "type":"startedTrace" property');
+    do_check_eq(aResponse.why, "requested",
+                'startTrace response should have "why":"requested" property');
+    do_check_eq(aResponse.name, aName,
+                'startTrace response should have the given name');
+    deferred.resolve();
+  });
+  return deferred.promise;
+}
+
+function stop_trace(aName, aExpectedName)
+{
+  let deferred = defer();
+  gTraceClient.stopTrace(aName, function(aResponse) {
+    do_check_true(!aResponse.error,
+                  'stopTrace should not respond with error: ' + aResponse.error);
+    do_check_true(aResponse.type === "stoppedTrace",
+                  'stopTrace response should have "type":"stoppedTrace" property');
+    do_check_true(aResponse.why === "requested",
+                  'stopTrace response should have "why":"requested" property');
+    do_check_true(aResponse.name === aExpectedName,
+                  'stopTrace response should have name "' + aExpectedName
+                  + '", but had "' + aResponse.name + '"');
+    deferred.resolve();
+  });
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-03.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that automatically generated names for traces are unique.
+ */
+
+let {defer} = devtools.require("sdk/core/promise");
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_unique_generated_trace_names();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_unique_generated_trace_names()
+{
+  let deferred = defer();
+  deferred.resolve([]);
+
+  let promise = deferred.promise, traces = 50;
+  for (let i = 0; i < traces; i++)
+    promise = promise.then(start_trace);
+  for (let i = 0; i < traces; i++)
+    promise = promise.then(stop_trace);
+
+  promise = promise.then(function() {
+    finishClient(gClient);
+  });
+}
+
+function start_trace(aTraceNames)
+{
+  let deferred = defer();
+  gTraceClient.startTrace([], null, function(aResponse) {
+    let hasDuplicates = aTraceNames.some(name => name === aResponse.name);
+    do_check_true(!hasDuplicates, "Generated trace names should be unique");
+    aTraceNames.push(aResponse.name);
+    deferred.resolve(aTraceNames);
+  });
+  return deferred.promise;
+}
+
+function stop_trace(aTraceNames)
+{
+  let deferred = defer();
+  gTraceClient.stopTrace(null, function(aResponse) {
+    do_check_eq(aTraceNames.pop(), aResponse.name,
+                "Stopped trace should be most recently started trace");
+    let hasDuplicates = aTraceNames.some(name => name === aResponse.name);
+    do_check_true(!hasDuplicates, "Generated trace names should be unique");
+    deferred.resolve(aTraceNames);
+  });
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-04.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that enteredFrame packets are sent on frame entry and
+ * exitedFrame packets are sent on frame exit. Tests that the "name"
+ * trace type works for function declarations.
+ */
+
+let {defer} = devtools.require("sdk/core/promise");
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_enter_exit_frame();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_enter_exit_frame()
+{
+  let packetsSeen = 0;
+  let packetNames = [];
+
+  gTraceClient.addListener("enteredFrame", function(aEvent, aPacket) {
+    packetsSeen++;
+    do_check_eq(aPacket.type, "enteredFrame",
+                'enteredFrame response should have type "enteredFrame"');
+    do_check_eq(typeof aPacket.sequence, "number",
+                'enteredFrame response should have sequence number');
+    do_check_eq(typeof aPacket.name, "string",
+                'enteredFrame response should have function name');
+    packetNames[aPacket.sequence] = aPacket.name;
+  });
+
+  gTraceClient.addListener("exitedFrame", function(aEvent, aPacket) {
+    packetsSeen++;
+    do_check_eq(aPacket.type, "exitedFrame",
+                'exitedFrame response should have type "exitedFrame"');
+    do_check_eq(typeof aPacket.sequence, "number",
+                'exitedFrame response should have sequence number');
+  });
+
+  start_trace()
+    .then(eval_code)
+    .then(stop_trace)
+    .then(function() {
+      do_check_eq(packetsSeen, 10,
+                  'Should have seen two packets for each of 5 stack frames');
+      do_check_eq(packetNames[2], "baz",
+                  'Should have entered "baz" frame in third packet');
+      do_check_eq(packetNames[3], "bar",
+                  'Should have entered "bar" frame in fourth packet');
+      do_check_eq(packetNames[4], "foo",
+                  'Should have entered "foo" frame in fifth packet');
+      finishClient(gClient);
+    });
+}
+
+function start_trace()
+{
+  let deferred = defer();
+  gTraceClient.startTrace(["name"], null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function eval_code()
+{
+  gDebuggee.eval("(" + function() {
+    function foo() {
+      return;
+    }
+    function bar() {
+      return foo();
+    }
+    function baz() {
+      return bar();
+    }
+    baz();
+  } + ")()");
+}
+
+function stop_trace()
+{
+  let deferred = defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-05.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Simple tests for "callsite", "time", "parameterNames", "arguments",
+ * and "return" trace types.
+ */
+
+let {defer} = devtools.require("sdk/core/promise");
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_enter_exit_frame();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_enter_exit_frame()
+{
+  let packets = [];
+
+  gTraceClient.addListener("enteredFrame", function(aEvent, aPacket) {
+    do_check_eq(aPacket.type, "enteredFrame",
+                'enteredFrame response should have type "enteredFrame"');
+    do_check_eq(typeof aPacket.sequence, "number",
+                'enteredFrame response should have sequence number');
+    do_check_eq(typeof aPacket.name, "string",
+                'enteredFrame response should have function name');
+    do_check_eq(typeof aPacket.callsite, "object",
+                'enteredFrame response should have callsite');
+    do_check_eq(typeof aPacket.time, "number",
+                'enteredFrame response should have time');
+    packets[aPacket.sequence] = aPacket;
+  });
+
+  gTraceClient.addListener("exitedFrame", function(aEvent, aPacket) {
+    do_check_eq(aPacket.type, "exitedFrame",
+                'exitedFrame response should have type "exitedFrame"');
+    do_check_eq(typeof aPacket.sequence, "number",
+                'exitedFrame response should have sequence number');
+    do_check_eq(typeof aPacket.time, "number",
+                'exitedFrame response should have time');
+    packets[aPacket.sequence] = aPacket;
+  });
+
+  start_trace()
+    .then(eval_code)
+    .then(stop_trace)
+    .then(function() {
+      do_check_eq(packets[2].name, "foo",
+                  'Third packet in sequence should be entry to "foo" frame');
+
+      do_check_eq(typeof packets[2].parameterNames, "object",
+                  'foo entry packet should have parameterNames');
+      do_check_eq(packets[2].parameterNames.length, 1,
+                  'foo should have only one formal parameter');
+      do_check_eq(packets[2].parameterNames[0], "x",
+                  'foo should have formal parameter "x"');
+
+      do_check_eq(typeof packets[2].arguments, "object",
+                  'foo entry packet should have arguments');
+      do_check_eq(typeof packets[2].arguments.values, "object",
+                  'foo arguments object should have values array');
+      do_check_eq(packets[2].arguments.values.length, 1,
+                  'foo should have only one actual parameter');
+      do_check_eq(packets[2].arguments.values[0], 42,
+                  'foo should have actual parameter 42');
+
+      do_check_eq(typeof packets[3].return, "object",
+                  'Fourth packet in sequence should be exit from "foo" frame');
+      do_check_eq(packets[3].return.value, "bar",
+                  'foo should return "bar"');
+
+      finishClient(gClient);
+    });
+}
+
+function start_trace()
+{
+  let deferred = defer();
+  gTraceClient.startTrace(
+    ["name", "callsite", "time", "parameterNames", "arguments", "return"],
+    null,
+    function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function eval_code()
+{
+  gDebuggee.eval("(" + function() {
+    function foo(x) {
+      return "bar";
+    }
+    foo(42);
+  } + ")()");
+}
+
+function stop_trace()
+{
+  let deferred = defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-06.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that objects, nested objects, and circular references are
+ * correctly serialized and sent in exitedFrame packets.
+ */
+
+let {defer} = devtools.require("sdk/core/promise");
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_enter_exit_frame();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_enter_exit_frame()
+{
+  let packetsSeen = 0;
+
+  gTraceClient.addListener("exitedFrame", function(aEvent, aPacket) {
+    if (aPacket.sequence === 3) {
+      do_check_eq(typeof aPacket.return, "object",
+                  'exitedFrame response should have return value');
+
+      let objPool = aPacket.return.objectPool;
+      let retval = objPool[aPacket.return.value.objectId];
+      let obj = objPool[retval.ownProperties.obj.value.objectId];
+
+      do_check_eq(retval.ownProperties.num.value, 25);
+      do_check_eq(retval.ownProperties.str.value, "foo");
+      do_check_eq(retval.ownProperties.bool.value, false);
+      do_check_eq(retval.ownProperties.undef.value.type, "undefined");
+      do_check_eq(retval.ownProperties.nil.value.type, "null");
+      do_check_eq(obj.ownProperties.self.value.objectId,
+                  retval.ownProperties.obj.value.objectId);
+    }
+  });
+
+  start_trace()
+    .then(eval_code)
+    .then(stop_trace)
+    .then(function() {
+      finishClient(gClient);
+    });
+}
+
+function start_trace()
+{
+  let deferred = defer();
+  gTraceClient.startTrace(["return"], null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function eval_code()
+{
+  gDebuggee.eval("(" + function() {
+    function foo() {
+      let obj = Object.create(null);
+      obj.self = obj;
+
+      let retval = Object.create(null);
+      retval.num = 25;
+      retval.str = "foo";
+      retval.bool = false;
+      retval.undef = undefined;
+      retval.nil = null;
+      retval.obj = obj;
+
+      return retval;
+    }
+    foo();
+  } + ")()");
+}
+
+function stop_trace()
+{
+  let deferred = defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-07.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that chained object prototypes are correctly serialized and
+ * sent in exitedFrame packets.
+ */
+
+let {defer} = devtools.require("sdk/core/promise");
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_enter_exit_frame();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_enter_exit_frame()
+{
+  let packetsSeen = 0;
+
+  gTraceClient.addListener("exitedFrame", function(aEvent, aPacket) {
+    if (aPacket.sequence === 3) {
+      do_check_eq(typeof aPacket.return, "object",
+                  'exitedFrame response should have return value');
+
+      let objPool = aPacket.return.objectPool;
+      let obj = objPool[aPacket.return.value.objectId];
+      let propObj = objPool[obj.ownProperties.b.value.objectId];
+      let proto = objPool[obj.prototype.objectId];
+      let protoProto = objPool[proto.prototype.objectId];
+
+      do_check_eq(obj.ownProperties.a.value, 1);
+      do_check_eq(propObj.ownProperties.c.value, "c");
+      do_check_eq(proto.ownProperties.d.value, "foo");
+      do_check_eq(proto.ownProperties.e.value, 42);
+      do_check_eq(protoProto.ownProperties.f.value, 2);
+    }
+  });
+
+  start_trace()
+    .then(eval_code)
+    .then(stop_trace)
+    .then(function() {
+      finishClient(gClient);
+    });
+}
+
+function start_trace()
+{
+  let deferred = defer();
+  gTraceClient.startTrace(["return"], null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function eval_code()
+{
+  gDebuggee.eval("(" + function() {
+    function foo() {
+      let protoProto = Object.create(null);
+      protoProto.f = 2;
+
+      let proto = Object.create(protoProto);
+      proto.d = "foo";
+      proto.e = 42;
+
+      let obj = Object.create(proto);
+      obj.a = 1;
+
+      let propObj = Object.create(null);
+      propObj.c = "c";
+
+      obj.b = propObj;
+
+      return obj;
+    }
+    foo();
+  } + ")()");
+}
+
+function stop_trace()
+{
+  let deferred = defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -55,38 +55,63 @@ function createRootActor(aConnection)
 
 function TestTabActor(aConnection, aGlobal)
 {
   this.conn = aConnection;
   this._global = aGlobal;
   this._threadActor = new ThreadActor(this, this._global);
   this.conn.addActor(this._threadActor);
   this._attached = false;
+  this._extraActors = {};
 }
 
 TestTabActor.prototype = {
   constructor: TestTabActor,
   actorPrefix: "TestTabActor",
 
+  get contentWindow() {
+    return { wrappedJSObject: this._global };
+  },
+
   grip: function() {
-    return { actor: this.actorID, title: this._global.__name };
+    let response = { actor: this.actorID, title: this._global.__name };
+
+    // Walk over tab actors added by extensions and add them to a new ActorPool.
+    let actorPool = new ActorPool(this.conn);
+    this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
+    if (!actorPool.isEmpty()) {
+      this._tabActorPool = actorPool;
+      this.conn.addActorPool(this._tabActorPool);
+    }
+
+    this._appendExtraActors(response);
+
+    return response;
   },
 
   onAttach: function(aRequest) {
     this._attached = true;
-    return { type: "tabAttached", threadActor: this._threadActor.actorID };
+
+    let response = { type: "tabAttached", threadActor: this._threadActor.actorID };
+    this._appendExtraActors(response);
+
+    return response;
   },
 
   onDetach: function(aRequest) {
     if (!this._attached) {
       return { "error":"wrongState" };
     }
     return { type: "detached" };
   },
 
+  /* Support for DebuggerServer.addTabActor. */
+  _createExtraActors: CommonCreateExtraActors,
+  _appendExtraActors: CommonAppendExtraActors,
+
   // Hooks for use by TestTabActors.
   addToParentPool: function(aActor) {
     this.conn.addActor(aActor);
   },
 
   removeFromParentPool: function(aActor) {
     this.conn.removeActor(aActor);
   }
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -146,8 +146,15 @@ skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpointstore.js]
 [test_profiler_actor.js]
 [test_profiler_activation.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_unsafeDereference.js]
 [test_add_actors.js]
+[test_trace_actor-01.js]
+[test_trace_actor-02.js]
+[test_trace_actor-03.js]
+[test_trace_actor-04.js]
+[test_trace_actor-05.js]
+[test_trace_actor-06.js]
+[test_trace_actor-07.js]