Bug 929349 - Integrate a tracing debugger into our existing debugger; r=vporof,past
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 18 Dec 2013 14:17:27 -0800
changeset 161118 657e3059fee2807614efe5d07b60aa0bb863a1fb
parent 161117 d549a25bc0299874e724aaad866d0648a509662e
child 161119 04a70c8908deb1fbeff6d8838e782044a69b79c9
child 161198 c572890f16a29e29443e3e5605ccd7ec2d8f7e2a
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersvporof, past
bugs929349
milestone29.0a1
Bug 929349 - Integrate a tracing debugger into our existing debugger; r=vporof,past
browser/app/profile/firefox.js
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-panes.js
browser/devtools/debugger/debugger-view.js
browser/devtools/debugger/debugger.xul
browser/devtools/debugger/panel.js
browser/devtools/debugger/test/browser.ini
browser/devtools/debugger/test/browser_dbg_tracing-01.js
browser/devtools/debugger/test/browser_dbg_tracing-02.js
browser/devtools/debugger/test/browser_dbg_tracing-03.js
browser/devtools/debugger/test/browser_dbg_tracing-04.js
browser/devtools/debugger/test/code_tracing-01.js
browser/devtools/debugger/test/doc_tracing-01.html
browser/devtools/debugger/test/head.js
browser/devtools/shared/widgets/FastListWidget.js
browser/devtools/shared/widgets/VariablesView.jsm
browser/locales/en-US/chrome/browser/devtools/debugger.dtd
browser/locales/en-US/chrome/browser/devtools/debugger.properties
browser/themes/linux/devtools/debugger.css
browser/themes/linux/devtools/tracer-icon.png
browser/themes/linux/devtools/tracer-icon@2x.png
browser/themes/linux/jar.mn
browser/themes/osx/devtools/debugger.css
browser/themes/osx/devtools/tracer-icon.png
browser/themes/osx/devtools/tracer-icon@2x.png
browser/themes/osx/jar.mn
browser/themes/windows/devtools/debugger.css
browser/themes/windows/devtools/tracer-icon.png
browser/themes/windows/devtools/tracer-icon@2x.png
browser/themes/windows/jar.mn
toolkit/devtools/DevToolsUtils.js
toolkit/devtools/DevToolsUtils.jsm
toolkit/devtools/server/actors/tracer.js
toolkit/devtools/server/tests/unit/test_trace_actor-05.js
toolkit/devtools/server/tests/unit/test_trace_actor-06.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1116,16 +1116,17 @@ pref("devtools.debugger.chrome-enabled",
 pref("devtools.debugger.chrome-debugging-host", "localhost");
 pref("devtools.debugger.chrome-debugging-port", 6080);
 pref("devtools.debugger.remote-host", "localhost");
 pref("devtools.debugger.remote-timeout", 20000);
 pref("devtools.debugger.pause-on-exceptions", false);
 pref("devtools.debugger.ignore-caught-exceptions", true);
 pref("devtools.debugger.source-maps-enabled", true);
 pref("devtools.debugger.pretty-print-enabled", true);
+pref("devtools.debugger.tracer", false);
 
 // The default Debugger UI settings
 pref("devtools.debugger.ui.panes-sources-width", 200);
 pref("devtools.debugger.ui.panes-instruments-width", 300);
 pref("devtools.debugger.ui.panes-visible-on-startup", false);
 pref("devtools.debugger.ui.variables-sorting-enabled", true);
 pref("devtools.debugger.ui.variables-only-enum-visible", false);
 pref("devtools.debugger.ui.variables-searchbox-visible", false);
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -94,16 +94,17 @@ Cu.import("resource:///modules/devtools/
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 const promise = require("sdk/core/promise");
 const Editor = require("devtools/sourceeditor/editor");
 const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
+const FastListWidget = require("devtools/shared/widgets/FastListWidget");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
   "resource://gre/modules/devtools/Loader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
@@ -187,16 +188,17 @@ let DebuggerController = {
       window.removeEventListener("unload", this.shutdownDebugger, true);
     }
 
     return this._shutdown = DebuggerView.destroy().then(() => {
       DebuggerView.destroy();
       this.SourceScripts.disconnect();
       this.StackFrames.disconnect();
       this.ThreadState.disconnect();
+      this.Tracer.disconnect();
       this.disconnect();
 
       // Chrome debugging needs to close its parent process on shutdown.
       if (window._isChromeDebugger) {
         return this._quitApp();
       } else {
         return promise.resolve(null); // Done.
       }
@@ -213,49 +215,54 @@ let DebuggerController = {
    * @return object
    *         A promise that is resolved when the debugger finishes connecting.
    */
   connect: function() {
     if (this._connection) {
       return this._connection;
     }
 
-    let deferred = promise.defer();
-    this._connection = deferred.promise;
+    let startedDebugging = promise.defer();
+    this._connection = startedDebugging.promise;
 
     if (!window._isChromeDebugger) {
       let target = this._target;
-      let { client, form: { chromeDebugger }, threadActor } = target;
+      let { client, form: { chromeDebugger, traceActor }, threadActor } = target;
       target.on("close", this._onTabDetached);
       target.on("navigate", this._onTabNavigated);
       target.on("will-navigate", this._onTabNavigated);
+      this.client = client;
 
       if (target.chrome) {
-        this._startChromeDebugging(client, chromeDebugger, deferred.resolve);
+        this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
       } else {
-        this._startDebuggingTab(client, threadActor, deferred.resolve);
+        this._startDebuggingTab(threadActor, startedDebugging.resolve);
+        const startedTracing = promise.defer();
+        this._startTracingTab(traceActor, startedTracing.resolve);
+
+        return promise.all([startedDebugging.promise, startedTracing.promise]);
       }
 
-      return deferred.promise;
+      return startedDebugging.promise;
     }
 
     // Chrome debugging needs to make its own connection to the debuggee.
     let transport = debuggerSocketConnect(
       Prefs.chromeDebuggingHost, Prefs.chromeDebuggingPort);
 
-    let client = new DebuggerClient(transport);
+    let client = this.client = new DebuggerClient(transport);
     client.addListener("tabNavigated", this._onTabNavigated);
     client.addListener("tabDetached", this._onTabDetached);
     client.connect(() => {
       client.listTabs(aResponse => {
-        this._startChromeDebugging(client, aResponse.chromeDebugger, deferred.resolve);
+        this._startChromeDebugging(aResponse.chromeDebugger, startedDebugging.resolve);
       });
     });
 
-    return deferred.promise;
+    return startedDebugging.promise;
   },
 
   /**
    * Disconnects the debugger client and removes event handlers as necessary.
    */
   disconnect: function() {
     // Return early if the client didn't even have a chance to instantiate.
     if (!this.client) {
@@ -326,31 +333,23 @@ let DebuggerController = {
     if (aResponse.error == "wrongOrder") {
       DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
     }
   },
 
   /**
    * Sets up a debugging session.
    *
-   * @param DebuggerClient aClient
-   *        The debugger client.
    * @param string aThreadActor
    *        The remote protocol grip of the tab.
    * @param function aCallback
-   *        A function to invoke once the client attached to the active thread.
+   *        A function to invoke once the client attaches to the active thread.
    */
-  _startDebuggingTab: function(aClient, aThreadActor, aCallback) {
-    if (!aClient) {
-      Cu.reportError("No client found!");
-      return;
-    }
-    this.client = aClient;
-
-    aClient.attachThread(aThreadActor, (aResponse, aThreadClient) => {
+  _startDebuggingTab: function(aThreadActor, aCallback) {
+    this.client.attachThread(aThreadActor, (aResponse, aThreadClient) => {
       if (!aThreadClient) {
         Cu.reportError("Couldn't attach to thread: " + aResponse.error);
         return;
       }
       this.activeThread = aThreadClient;
 
       this.ThreadState.connect();
       this.StackFrames.connect();
@@ -361,31 +360,23 @@ let DebuggerController = {
         aCallback();
       }
     }, { useSourceMaps: Prefs.sourceMapsEnabled });
   },
 
   /**
    * Sets up a chrome debugging session.
    *
-   * @param DebuggerClient aClient
-   *        The debugger client.
    * @param object aChromeDebugger
    *        The remote protocol grip of the chrome debugger.
    * @param function aCallback
-   *        A function to invoke once the client attached to the active thread.
+   *        A function to invoke once the client attaches to the active thread.
    */
-  _startChromeDebugging: function(aClient, aChromeDebugger, aCallback) {
-    if (!aClient) {
-      Cu.reportError("No client found!");
-      return;
-    }
-    this.client = aClient;
-
-    aClient.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
+  _startChromeDebugging: function(aChromeDebugger, aCallback) {
+    this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
       if (!aThreadClient) {
         Cu.reportError("Couldn't attach to thread: " + aResponse.error);
         return;
       }
       this.activeThread = aThreadClient;
 
       this.ThreadState.connect();
       this.StackFrames.connect();
@@ -394,16 +385,40 @@ let DebuggerController = {
 
       if (aCallback) {
         aCallback();
       }
     }, { useSourceMaps: Prefs.sourceMapsEnabled });
   },
 
   /**
+   * Sets up an execution tracing session.
+   *
+   * @param object aTraceActor
+   *        The remote protocol grip of the trace actor.
+   * @param function aCallback
+   *        A function to invoke once the client attaches to the tracer.
+   */
+  _startTracingTab: function(aTraceActor, aCallback) {
+    this.client.attachTracer(aTraceActor, (response, traceClient) => {
+      if (!traceClient) {
+        DevToolsUtils.reportError(new Error("Failed to attach to tracing actor."));
+        return;
+      }
+
+      this.traceClient = traceClient;
+      this.Tracer.connect();
+
+      if (aCallback) {
+        aCallback();
+      }
+    });
+  },
+
+  /**
    * Detach and reattach to the thread actor with useSourceMaps true, blow
    * away old sources and get them again.
    */
   reconfigureThread: function(aUseSourceMaps) {
     this.client.reconfigureThread({ useSourceMaps: aUseSourceMaps }, aResponse => {
       if (aResponse.error) {
         let msg = "Couldn't reconfigure thread: " + aResponse.message;
         Cu.reportError(msg);
@@ -1407,16 +1422,228 @@ SourceScripts.prototype = {
       }
     }
 
     return deferred.promise;
   }
 };
 
 /**
+ * Tracer update the UI according to the messages exchanged with the tracer
+ * actor.
+ */
+function Tracer() {
+  this._trace = null;
+  this._idCounter = 0;
+  this.onTraces = this.onTraces.bind(this);
+}
+
+Tracer.prototype = {
+  get client() {
+    return DebuggerController.client;
+  },
+
+  get traceClient() {
+    return DebuggerController.traceClient;
+  },
+
+  get tracing() {
+    return !!this._trace;
+  },
+
+  /**
+   * Hooks up the debugger controller with the tracer client.
+   */
+  connect: function() {
+    this._stack = [];
+    this.client.addListener("traces", this.onTraces);
+  },
+
+  /**
+   * Disconnects the debugger controller from the tracer client. Any further
+   * communcation with the tracer actor will not have any effect on the UI.
+   */
+  disconnect: function() {
+    this._stack = null;
+    this.client.removeListener("traces", this.onTraces);
+  },
+
+  /**
+   * Instructs the tracer actor to start tracing.
+   */
+  startTracing: function(aCallback = () => {}) {
+    DebuggerView.Tracer.selectTab();
+    if (this.tracing) {
+      return;
+    }
+    this._trace = "dbg.trace" + Math.random();
+    this.traceClient.startTrace([
+      "name",
+      "location",
+      "parameterNames",
+      "depth",
+      "arguments",
+      "return",
+      "throw",
+      "yield"
+    ], this._trace, (aResponse) => {
+      const { error } = aResponse;
+      if (error) {
+        DevToolsUtils.reportException(error);
+        this._trace = null;
+      }
+
+      aCallback(aResponse);
+    });
+  },
+
+  /**
+   * Instructs the tracer actor to stop tracing.
+   */
+  stopTracing: function(aCallback = () => {}) {
+    if (!this.tracing) {
+      return;
+    }
+    this.traceClient.stopTrace(this._trace, aResponse => {
+      const { error } = aResponse;
+      if (error) {
+        DevToolsUtils.reportException(error);
+      }
+
+      this._trace = null;
+      aCallback(aResponse);
+    });
+  },
+
+  onTraces: function (aEvent, { traces }) {
+    const tracesLength = traces.length;
+    let tracesToShow;
+    if (tracesLength > TracerView.MAX_TRACES) {
+      tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
+                                  tracesLength);
+      DebuggerView.Tracer.empty();
+      this._stack.splice(0, this._stack.length);
+    } else {
+      tracesToShow = traces;
+    }
+
+    for (let t of tracesToShow) {
+      if (t.type == "enteredFrame") {
+        this._onCall(t);
+      } else {
+        this._onReturn(t);
+      }
+    }
+
+    DebuggerView.Tracer.commit();
+  },
+
+  /**
+   * Callback for handling a new call frame.
+   */
+  _onCall: function({ name, location, parameterNames, depth, arguments: args }) {
+    const item = {
+      name: name,
+      location: location,
+      id: this._idCounter++
+    };
+    this._stack.push(item);
+    DebuggerView.Tracer.addTrace({
+      type: "call",
+      name: name,
+      location: location,
+      depth: depth,
+      parameterNames: parameterNames,
+      arguments: args,
+      frameId: item.id
+    });
+  },
+
+  /**
+   * Callback for handling an exited frame.
+   */
+  _onReturn: function(aPacket) {
+    if (!this._stack.length) {
+      return;
+    }
+
+    const { name, id, location } = this._stack.pop();
+    DebuggerView.Tracer.addTrace({
+      type: aPacket.why,
+      name: name,
+      location: location,
+      depth: aPacket.depth,
+      frameId: id,
+      returnVal: aPacket.return || aPacket.throw || aPacket.yield
+    });
+  },
+
+  /**
+   * Create an object which has the same interface as a normal object client,
+   * but since we already have all the information for an object that we will
+   * ever get (the server doesn't create actors when tracing, just firehoses
+   * data and forgets about it) just return the data immdiately.
+   *
+   * @param Object aObject
+   *        The tracer object "grip" (more like a limited snapshot).
+   * @returns Object
+   *          The synchronous client object.
+   */
+  syncGripClient: function(aObject) {
+    return {
+      get isFrozen() { return aObject.frozen; },
+      get isSealed() { return aObject.sealed; },
+      get isExtensible() { return aObject.extensible; },
+
+      get ownProperties() { return aObject.ownProperties; },
+      get prototype() { return null; },
+
+      getParameterNames: callback => callback(aObject),
+      getPrototypeAndProperties: callback => callback(aObject),
+      getPrototype: callback => callback(aObject),
+
+      getOwnPropertyNames: (callback) => {
+        callback({
+          ownPropertyNames: aObject.ownProperties
+            ?  Object.keys(aObject.ownProperties)
+            : []
+        });
+      },
+
+      getProperty: (property, callback) => {
+        callback({
+          descriptor: aObject.ownProperties
+            ? aObject.ownProperties[property]
+            : null
+        });
+      },
+
+      getDisplayString: callback => callback("[object " + aObject.class + "]"),
+
+      getScope: callback => callback({
+        error: "scopeNotAvailable",
+        message: "Cannot get scopes for traced objects"
+      })
+    };
+  },
+
+  /**
+   * Wraps object snapshots received from the tracer server so that we can
+   * differentiate them from long living object grips from the debugger server
+   * in the variables view.
+   *
+   * @param Object aObject
+   *        The object snapshot from the tracer actor.
+   */
+  WrappedObject: function(aObject) {
+    this.object = aObject;
+  }
+};
+
+/**
  * Handles breaking on event listeners in the currently debugged target.
  */
 function EventListeners() {
   this._onEventListeners = this._onEventListeners.bind(this);
 }
 
 EventListeners.prototype = {
   /**
@@ -1950,16 +2177,17 @@ let Prefs = new ViewHelpers.Prefs("devto
   panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
   variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
   variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"],
   variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"],
   pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
   ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
   sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
   prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
+  tracerEnabled: ["Bool", "debugger.tracer"],
   editorTabSize: ["Int", "editor.tabsize"]
 });
 
 /**
  * Returns true if this is a chrome debugger instance.
  * @return boolean
  */
 XPCOMUtils.defineLazyGetter(window, "_isChromeDebugger", function() {
@@ -1977,16 +2205,17 @@ EventEmitter.decorate(this);
  */
 DebuggerController.initialize();
 DebuggerController.Parser = new Parser();
 DebuggerController.ThreadState = new ThreadState();
 DebuggerController.StackFrames = new StackFrames();
 DebuggerController.SourceScripts = new SourceScripts();
 DebuggerController.Breakpoints = new Breakpoints();
 DebuggerController.Breakpoints.DOM = new EventListeners();
+DebuggerController.Tracer = new Tracer();
 
 /**
  * Export some properties to the global scope for easier access.
  */
 Object.defineProperties(window, {
   "gTarget": {
     get: function() DebuggerController._target
   },
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1052,16 +1052,386 @@ SourcesView.prototype = Heritage.extend(
   _cmPopup: null,
   _cbPanel: null,
   _cbTextbox: null,
   _selectedBreakpointItem: null,
   _conditionalPopupVisible: false
 });
 
 /**
+ * Functions handling the traces UI.
+ */
+function TracerView() {
+  this._selectedItem = null;
+  this._matchingItems = null;
+  this.widget = null;
+
+  this._highlightItem = this._highlightItem.bind(this);
+  this._isNotSelectedItem = this._isNotSelectedItem.bind(this);
+
+  this._unhighlightMatchingItems =
+    DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this));
+  this._onToggleTracing =
+    DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this));
+  this._onStartTracing =
+    DevToolsUtils.makeInfallible(this._onStartTracing.bind(this));
+  this._onClear = DevToolsUtils.makeInfallible(this._onClear.bind(this));
+  this._onSelect = DevToolsUtils.makeInfallible(this._onSelect.bind(this));
+  this._onMouseOver =
+    DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
+  this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this));
+}
+
+TracerView.MAX_TRACES = 200;
+
+TracerView.prototype = Heritage.extend(WidgetMethods, {
+  /**
+   * Initialization function, called when the debugger is started.
+   */
+  initialize: function() {
+    dumpn("Initializing the TracerView");
+
+    this._traceButton = document.getElementById("trace");
+    this._tracerTab = document.getElementById("tracer-tab");
+
+    // Remove tracer related elements from the dom and tear everything down if
+    // the tracer isn't enabled.
+    if (!Prefs.tracerEnabled) {
+      this._traceButton.remove();
+      this._traceButton = null;
+      this._tracerTab.remove();
+      this._tracerTab = null;
+      document.getElementById("tracer-tabpanel").remove();
+      this.widget = null;
+      return;
+    }
+
+    this.widget = new FastListWidget(document.getElementById("tracer-traces"));
+
+    this._traceButton.removeAttribute("hidden");
+    this._tracerTab.removeAttribute("hidden");
+
+    this._tracerDeck = document.getElementById("tracer-deck");
+    this._search = document.getElementById("tracer-search");
+
+    this._template = document.getElementsByClassName("trace-item-template")[0];
+    this._templateItem = this._template.getElementsByClassName("trace-item")[0];
+    this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0];
+    this._templateNameNode = this._template.getElementsByClassName("trace-name")[0];
+
+    this.widget.addEventListener("select", this._onSelect, false);
+    this.widget.addEventListener("mouseover", this._onMouseOver, false);
+    this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false);
+
+    this._search.addEventListener("input", this._onSearch, false);
+
+    this._startTooltip = L10N.getStr("startTracingTooltip");
+    this._stopTooltip = L10N.getStr("stopTracingTooltip");
+    this._traceButton.setAttribute("tooltiptext", this._startTooltip);
+  },
+
+  /**
+   * Destruction function, called when the debugger is closed.
+   */
+  destroy: function() {
+    dumpn("Destroying the TracerView");
+
+    if (!this.widget) {
+      return;
+    }
+
+    this.widget.removeEventListener("select", this._onSelect, false);
+    this.widget.removeEventListener("mouseover", this._onMouseOver, false);
+    this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false);
+    this._search.removeEventListener("input", this._onSearch, false);
+  },
+
+  /**
+   * Function invoked by the "toggleTracing" command to switch the tracer state.
+   */
+  _onToggleTracing: function() {
+    if (DebuggerController.Tracer.tracing) {
+      this._onStopTracing();
+    } else {
+      this._onStartTracing();
+    }
+  },
+
+  /**
+   * Function invoked either by the "startTracing" command or by
+   * _onToggleTracing to start execution tracing in the backend.
+   */
+  _onStartTracing: function() {
+    this._tracerDeck.selectedIndex = 0;
+    this._traceButton.setAttribute("checked", true);
+    this._traceButton.setAttribute("tooltiptext", this._stopTooltip);
+    this.empty();
+    DebuggerController.Tracer.startTracing();
+  },
+
+  /**
+   * Function invoked by _onToggleTracing to stop execution tracing in the
+   * backend.
+   */
+  _onStopTracing: function() {
+    this._traceButton.removeAttribute("checked");
+    this._traceButton.setAttribute("tooltiptext", this._startTooltip);
+    DebuggerController.Tracer.stopTracing();
+  },
+
+  /**
+   * Function invoked by the "clearTraces" command to empty the traces pane.
+   */
+  _onClear: function() {
+    this.empty();
+  },
+
+  /**
+   * Populate the given parent scope with the variable with the provided name
+   * and value.
+   *
+   * @param String aName
+   *        The name of the variable.
+   * @param Object aParent
+   *        The parent scope.
+   * @param Object aValue
+   *        The value of the variable.
+   */
+  _populateVariable: function(aName, aParent, aValue) {
+    let item = aParent.addItem(aName, { value: aValue });
+    if (aValue) {
+      DebuggerView.Variables.controller.populate(
+        item, new DebuggerController.Tracer.WrappedObject(aValue));
+      item.expand();
+      item.twisty = false;
+    }
+  },
+
+  /**
+   * Handler for the widget's "select" event. Displays parameters, exception, or
+   * return value depending on whether the selected trace is a call, throw, or
+   * return respectively.
+   *
+   * @param Object traceItem
+   *        The selected trace item.
+   */
+  _onSelect: function _onSelect({ detail: traceItem }) {
+    if (!traceItem) {
+      return;
+    }
+
+    const data = traceItem.attachment.trace;
+    const { location: { url, line } } = data;
+    DebuggerView.setEditorLocation(url, line, { noDebug: true });
+
+    DebuggerView.Variables.empty();
+    const scope = DebuggerView.Variables.addScope();
+
+    if (data.type == "call") {
+      const params = DevToolsUtils.zip(data.parameterNames, data.arguments);
+      for (let [name, val] of params) {
+        if (val === undefined) {
+          scope.addItem(name, { value: "<value not available>" });
+        } else {
+          this._populateVariable(name, scope, val);
+        }
+      }
+    } else {
+      const varName = "<" +
+        (data.type == "throw" ? "exception" : data.type) +
+        ">";
+      this._populateVariable(varName, scope, data.returnVal);
+    }
+
+    scope.expand();
+    DebuggerView.showInstrumentsPane();
+  },
+
+  /**
+   * Add the hover frame enter/exit highlighting to a given item.
+   */
+  _highlightItem: function(aItem) {
+    aItem.target.querySelector(".trace-item")
+      .classList.add("selected-matching");
+  },
+
+  /**
+   * Remove the hover frame enter/exit highlighting to a given item.
+   */
+  _unhighlightItem: function(aItem) {
+    if (!aItem || !aItem.target) {
+      return;
+    }
+    const match = aItem.target.querySelector(".selected-matching");
+    if (match) {
+      match.classList.remove("selected-matching");
+    }
+  },
+
+  /**
+   * Remove the frame enter/exit pair highlighting we do when hovering.
+   */
+  _unhighlightMatchingItems: function() {
+    if (this._matchingItems) {
+      this._matchingItems.forEach(this._unhighlightItem);
+      this._matchingItems = null;
+    }
+  },
+
+  /**
+   * Returns true if the given item is not the selected item.
+   */
+  _isNotSelectedItem: function(aItem) {
+    return aItem !== this.selectedItem;
+  },
+
+  /**
+   * Highlight the frame enter/exit pair of items for the given item.
+   */
+  _highlightMatchingItems: function(aItem) {
+    this._unhighlightMatchingItems();
+    this._matchingItems = this.items.filter(t => t.value == aItem.value);
+    this._matchingItems
+      .filter(this._isNotSelectedItem)
+      .forEach(this._highlightItem);
+  },
+
+  /**
+   * Listener for the mouseover event.
+   */
+  _onMouseOver: function({ target }) {
+    const traceItem = this.getItemForElement(target);
+    if (traceItem) {
+      this._highlightMatchingItems(traceItem);
+    }
+  },
+
+  /**
+   * Listener for typing in the search box.
+   */
+  _onSearch: function() {
+    const query = this._search.value.trim().toLowerCase();
+    this.filterContents(item =>
+      item.attachment.trace.name.toLowerCase().contains(query));
+  },
+
+  /**
+   * Select the traces tab in the sidebar.
+   */
+  selectTab: function() {
+    const tabs = this._tracerTab.parentElement;
+    tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab);
+    this._tracerDeck.selectedIndex = 0;
+  },
+
+  /**
+   * Commit all staged items to the widget. Overridden so that we can call
+   * |FastListWidget.prototype.flush|.
+   */
+  commit: function() {
+    WidgetMethods.commit.call(this);
+    // TODO: Accessing non-standard widget properties. Figure out what's the
+    // best way to expose such things. Bug 895514.
+    this.widget.flush();
+  },
+
+  /**
+   * Adds the trace record provided as an argument to the view.
+   *
+   * @param object aTrace
+   *        The trace record coming from the tracer actor.
+   */
+  addTrace: function(aTrace) {
+    const { type, frameId } = aTrace;
+
+    // Create the element node for the trace item.
+    let view = this._createView(aTrace);
+
+    // Append a source item to this container.
+    this.push([view, aTrace.frameId, ""], {
+      staged: true,
+      attachment: {
+        trace: aTrace
+      }
+    });
+  },
+
+  /**
+   * Customization function for creating an item's UI.
+   *
+   * @return nsIDOMNode
+   *         The network request view.
+   */
+  _createView: function({ type, name, frameId, parameterNames, returnVal,
+                          location, depth, arguments: args }) {
+    let fragment = document.createDocumentFragment();
+
+    this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
+    this._templateItem.style.MozPaddingStart = depth + "em";
+
+    const TYPES = ["call", "yield", "return", "throw"];
+    for (let t of TYPES) {
+      this._templateTypeIcon.classList.toggle("trace-" + t, t == type);
+    }
+    this._templateTypeIcon.setAttribute("value", {
+      call: "\u2192",
+      yield: "Y",
+      return: "\u2190",
+      throw: "E",
+      terminated: "TERMINATED"
+    }[type]);
+
+    this._templateNameNode.setAttribute("value", name);
+
+    // All extra syntax and parameter nodes added.
+    const addedNodes = [];
+
+    if (parameterNames) {
+      const syntax = (p) => {
+        const el = document.createElement("label");
+        el.setAttribute("value", p);
+        el.classList.add("trace-syntax");
+        el.classList.add("plain");
+        addedNodes.push(el);
+        return el;
+      };
+
+      this._templateItem.appendChild(syntax("("));
+
+      for (let i = 0, n = parameterNames.length; i < n; i++) {
+        let param = document.createElement("label");
+        param.setAttribute("value", parameterNames[i]);
+        param.classList.add("trace-param");
+        param.classList.add("plain");
+        addedNodes.push(param);
+        this._templateItem.appendChild(param);
+
+        if (i + 1 !== n) {
+          this._templateItem.appendChild(syntax(", "));
+        }
+      }
+
+      this._templateItem.appendChild(syntax(")"));
+    }
+
+    // Flatten the DOM by removing one redundant box (the template container).
+    for (let node of this._template.childNodes) {
+      fragment.appendChild(node.cloneNode(true));
+    }
+
+    // Remove any added nodes from the template.
+    for (let node of addedNodes) {
+      this._templateItem.removeChild(node);
+    }
+
+    return fragment;
+  }
+});
+
+/**
  * Utility functions for handling sources.
  */
 let SourceUtils = {
   _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
   _groupsCache: new Map(),
 
   /**
    * Returns true if the specified url and/or content type are specific to
@@ -2784,11 +3154,12 @@ LineResults.size = function() {
   return count;
 };
 
 /**
  * Preliminary setup for the DebuggerView object.
  */
 DebuggerView.Sources = new SourcesView();
 DebuggerView.VariableBubble = new VariableBubbleView();
+DebuggerView.Tracer = new TracerView();
 DebuggerView.WatchExpressions = new WatchExpressionsView();
 DebuggerView.EventListeners = new EventListenersView();
 DebuggerView.GlobalSearch = new GlobalSearchView();
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -55,16 +55,17 @@ let DebuggerView = {
     this.Filtering.initialize();
     this.FilteredSources.initialize();
     this.FilteredFunctions.initialize();
     this.ChromeGlobals.initialize();
     this.StackFrames.initialize();
     this.StackFramesClassicList.initialize();
     this.Sources.initialize();
     this.VariableBubble.initialize();
+    this.Tracer.initialize();
     this.WatchExpressions.initialize();
     this.EventListeners.initialize();
     this.GlobalSearch.initialize();
     this._initializeVariablesView();
     this._initializeEditor(deferred.resolve);
 
     document.title = L10N.getStr("DebuggerWindowTitle");
 
@@ -90,16 +91,17 @@ let DebuggerView = {
     this.Filtering.destroy();
     this.FilteredSources.destroy();
     this.FilteredFunctions.destroy();
     this.ChromeGlobals.destroy();
     this.StackFrames.destroy();
     this.StackFramesClassicList.destroy();
     this.Sources.destroy();
     this.VariableBubble.destroy();
+    this.Tracer.destroy();
     this.WatchExpressions.destroy();
     this.EventListeners.destroy();
     this.GlobalSearch.destroy();
     this._destroyPanes();
     this._destroyEditor(deferred.resolve);
 
     return deferred.promise;
   },
@@ -164,17 +166,21 @@ let DebuggerView = {
       searchEnabled: Prefs.variablesSearchboxVisible,
       eval: DebuggerController.StackFrames.evaluate,
       lazyEmpty: true
     });
 
     // Attach a controller that handles interfacing with the debugger protocol.
     VariablesViewController.attach(this.Variables, {
       getEnvironmentClient: aObject => gThreadClient.environment(aObject),
-      getObjectClient: aObject => gThreadClient.pauseGrip(aObject)
+      getObjectClient: aObject => {
+        return aObject instanceof DebuggerController.Tracer.WrappedObject
+          ? DebuggerController.Tracer.syncGripClient(aObject.object)
+          : gThreadClient.pauseGrip(aObject)
+      }
     });
 
     // Relay events from the VariablesView.
     this.Variables.on("fetched", (aEvent, aType) => {
       switch (aType) {
         case "variables":
           window.emit(EVENTS.FETCHED_VARIABLES);
           break;
@@ -632,16 +638,17 @@ let DebuggerView = {
   Options: null,
   Filtering: null,
   FilteredSources: null,
   FilteredFunctions: null,
   GlobalSearch: null,
   ChromeGlobals: null,
   StackFrames: null,
   Sources: null,
+  Tracer: null,
   Variables: null,
   VariableBubble: null,
   WatchExpressions: null,
   EventListeners: null,
   editor: null,
   _editorSource: {},
   _loadingText: "",
   _body: null,
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -81,16 +81,22 @@
     <command id="toggleShowPanesOnStartup"
              oncommand="DebuggerView.Options._toggleShowPanesOnStartup()"/>
     <command id="toggleShowOnlyEnum"
              oncommand="DebuggerView.Options._toggleShowVariablesOnlyEnum()"/>
     <command id="toggleShowVariablesFilterBox"
              oncommand="DebuggerView.Options._toggleShowVariablesFilterBox()"/>
     <command id="toggleShowOriginalSource"
              oncommand="DebuggerView.Options._toggleShowOriginalSource()"/>
+    <command id="toggleTracing"
+             oncommand="DebuggerView.Tracer._onToggleTracing()"/>
+    <command id="startTracing"
+             oncommand="DebuggerView.Tracer._onStartTracing()"/>
+    <command id="clearTraces"
+             oncommand="DebuggerView.Tracer._onClear()"/>
   </commandset>
 
   <popupset id="debuggerPopupset">
     <menupopup id="sourceEditorContextMenu"
                onpopupshowing="goUpdateGlobalEditMenuItems()">
       <menuitem id="se-dbg-cMenu-addBreakpoint"
                 label="&debuggerUI.seMenuBreak;"
                 key="addBreakpointKey"
@@ -299,16 +305,23 @@
                        tabindex="0"/>
         <toolbarbutton id="step-in"
                        class="devtools-toolbarbutton"
                        tabindex="0"/>
         <toolbarbutton id="step-out"
                        class="devtools-toolbarbutton"
                        tabindex="0"/>
       </hbox>
+      <hbox>
+        <toolbarbutton id="trace"
+                       class="devtools-toolbarbutton"
+                       command="toggleTracing"
+                       tabindex="0"
+                       hidden="true"/>
+      </hbox>
       <menulist id="chrome-globals"
                 class="devtools-menulist"
                 sizetopopup="none" hidden="true"/>
       <vbox id="stackframes" flex="1"/>
       <textbox id="searchbox"
                class="devtools-searchinput" type="search"/>
       <toolbarbutton id="instruments-pane-toggle"
                      class="devtools-toolbarbutton"
@@ -323,16 +336,17 @@
     <scrollbox id="globalsearch" orient="vertical" hidden="true"/>
     <splitter class="devtools-horizontal-splitter" hidden="true"/>
     <hbox id="debugger-widgets" flex="1">
       <tabbox id="sources-pane"
               class="devtools-sidebar-tabs">
         <tabs>
           <tab id="sources-tab" label="&debuggerUI.tabs.sources;"/>
           <tab id="callstack-tab" label="&debuggerUI.tabs.callstack;"/>
+          <tab id="tracer-tab" label="&debuggerUI.tabs.traces;" hidden="true"/>
         </tabs>
         <tabpanels flex="1">
           <tabpanel id="sources-tabpanel">
             <vbox id="sources" flex="1"/>
             <toolbar id="sources-toolbar" class="devtools-toolbar">
               <hbox id="sources-controls">
                 <toolbarbutton id="black-box"
                                class="devtools-toolbarbutton"
@@ -349,16 +363,51 @@
                              class="devtools-toolbarbutton"
                              tooltiptext="&debuggerUI.sources.toggleBreakpoints;"
                              command="toggleBreakpointsCommand"/>
             </toolbar>
           </tabpanel>
           <tabpanel id="callstack-tabpanel">
             <vbox id="callstack-list" flex="1"/>
           </tabpanel>
+          <tabpanel id="tracer-tabpanel" flex="1">
+            <deck id="tracer-deck" selectedIndex="1" flex="1">
+              <vbox flex="1">
+                <vbox id="tracer-traces" flex="1">
+                  <hbox class="trace-item-template" hidden="true">
+                    <hbox class="trace-item" align="center" flex="1" crop="end">
+                      <label class="trace-type plain"/>
+                      <label class="trace-name plain" crop="end"/>
+                    </hbox>
+                  </hbox>
+                </vbox>
+                <toolbar id="tracer-toolbar" class="devtools-toolbar">
+                  <toolbarbutton id="clear-tracer"
+                                 label="&debuggerUI.clearButton;"
+                                 tooltiptext="&debuggerUI.clearButton.tooltip;"
+                                 command="clearTraces"
+                                 class="devtools-toolbarbutton"/>
+                  <textbox id="tracer-search"
+                           class="devtools-searchinput"
+                           flex="1"
+                           type="search"/>
+                </toolbar>
+              </vbox>
+              <vbox id="tracer-message"
+                    flex="1"
+                    align="center"
+                    pack="center">
+                <description value="&debuggerUI.tracingNotStarted.label;" />
+                <button id="start-tracing"
+                        class="devtools-toolbarbutton"
+                        command="startTracing"
+                        label="&debuggerUI.startTracing;"/>
+              </vbox>
+            </deck>
+          </tabpanel>
         </tabpanels>
       </tabbox>
       <splitter id="sources-and-editor-splitter"
                 class="devtools-side-splitter"/>
       <deck id="editor-deck" flex="4">
         <vbox id="editor"/>
         <vbox id="black-boxed-message" align="center" pack="center">
           <label id="black-boxed-message-label">
--- a/browser/devtools/debugger/panel.js
+++ b/browser/devtools/debugger/panel.js
@@ -4,16 +4,18 @@
  * 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 { Cc, Ci, Cu, Cr } = require("chrome");
 const promise = require("sdk/core/promise");
 const EventEmitter = require("devtools/shared/event-emitter");
 
+const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
+
 function DebuggerPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
   this._destroyer = null;
 
   this._view = this.panelWin.DebuggerView;
   this._controller = this.panelWin.DebuggerController;
   this._view._hostType = this._toolbox.hostType;
@@ -52,18 +54,17 @@ DebuggerPanel.prototype = {
         this._toolbox.on("host-changed", this.handleHostChanged);
         this.target.on("thread-paused", this.highlightWhenPaused);
         this.target.on("thread-resumed", this.unhighlightWhenResumed);
         this.isReady = true;
         this.emit("ready");
         return this;
       })
       .then(null, function onError(aReason) {
-        Cu.reportError("DebuggerPanel open failed. " +
-                       aReason.error + ": " + aReason.message);
+        DevToolsUtils.reportException("DebuggerPane.prototype.open", aReason);
       });
   },
 
   // DevToolPanel API
 
   get target() this._toolbox.target,
 
   destroy: function() {
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -15,16 +15,17 @@ support-files =
   code_location-changes.js
   code_math.js
   code_math.map
   code_math.min.js
   code_math_bogus_map.min.js
   code_script-switching-01.js
   code_script-switching-02.js
   code_test-editor-mode
+  code_tracing-01.js
   code_ugly.js
   code_ugly-2.js
   code_ugly-3.js
   code_ugly-4.js
   doc_binary_search.html
   doc_blackboxing.html
   doc_closures.html
   doc_cmd-break.html
@@ -51,16 +52,17 @@ support-files =
   doc_random-javascript.html
   doc_recursion-stack.html
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_script-switching-01.html
   doc_script-switching-02.html
   doc_step-out.html
+  doc_tracing-01.html
   doc_watch-expressions.html
   doc_with-frame.html
   head.js
   sjs_random-javascript.sjs
   testactors.js
 
 [browser_dbg_aaa_run_first_leaktest.js]
 [browser_dbg_bfcache.js]
@@ -183,16 +185,20 @@ support-files =
 [browser_dbg_stack-03.js]
 [browser_dbg_stack-04.js]
 [browser_dbg_stack-05.js]
 [browser_dbg_stack-06.js]
 [browser_dbg_stack-07.js]
 [browser_dbg_step-out.js]
 [browser_dbg_tabactor-01.js]
 [browser_dbg_tabactor-02.js]
+[browser_dbg_tracing-01.js]
+[browser_dbg_tracing-02.js]
+[browser_dbg_tracing-03.js]
+[browser_dbg_tracing-04.js]
 [browser_dbg_variables-view-01.js]
 [browser_dbg_variables-view-02.js]
 [browser_dbg_variables-view-03.js]
 [browser_dbg_variables-view-04.js]
 [browser_dbg_variables-view-05.js]
 [browser_dbg_variables-view-accessibility.js]
 [browser_dbg_variables-view-data.js]
 [browser_dbg_variables-view-edit-getset-01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tracing-01.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we get the expected frame enter/exit logs in the tracer view.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
+
+let gTab, gDebuggee, gPanel, gDebugger;
+
+function test() {
+  SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
+    initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPanel = aPanel;
+      gDebugger = gPanel.panelWin;
+
+      waitForSourceShown(gPanel, "code_tracing-01.js")
+        .then(() => startTracing(gPanel))
+        .then(clickButton)
+        .then(() => waitForClientEvents(aPanel, "traces"))
+        .then(testTraceLogs)
+        .then(() => stopTracing(gPanel))
+        .then(() => {
+          const deferred = promise.defer();
+          SpecialPowers.popPrefEnv(deferred.resolve);
+          return deferred.promise;
+        })
+        .then(() => closeDebuggerAndFinish(gPanel))
+        .then(null, aError => {
+          ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+        });
+    });
+  });
+}
+
+function clickButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebuggee.document.querySelector("button"),
+                            gDebuggee);
+}
+
+function testTraceLogs() {
+  const onclickLogs = filterTraces(gPanel,
+                                   t => t.querySelector(".trace-name[value=onclick]"));
+  is(onclickLogs.length, 2, "Should have two logs from 'onclick'");
+  ok(onclickLogs[0].querySelector(".trace-call"),
+     "The first 'onclick' log should be a call.");
+  ok(onclickLogs[1].querySelector(".trace-return"),
+     "The second 'onclick' log should be a return.");
+  for (let t of onclickLogs) {
+    ok(t.querySelector(".trace-item").getAttribute("tooltiptext")
+        .contains("doc_tracing-01.html"));
+  }
+
+  const nonOnclickLogs = filterTraces(gPanel,
+                                      t => !t.querySelector(".trace-name[value=onclick]"));
+  for (let t of nonOnclickLogs) {
+    ok(t.querySelector(".trace-item").getAttribute("tooltiptext")
+        .contains("code_tracing-01.js"));
+  }
+
+  const mainLogs = filterTraces(gPanel,
+                                t => t.querySelector(".trace-name[value=main]"));
+  is(mainLogs.length, 2, "Should have an enter and an exit for 'main'");
+  ok(mainLogs[0].querySelector(".trace-call"),
+     "The first 'main' log should be a call.");
+  ok(mainLogs[1].querySelector(".trace-return"),
+     "The second 'main' log should be a return.");
+
+  const factorialLogs = filterTraces(gPanel,
+                                     t => t.querySelector(".trace-name[value=factorial]"));
+  is(factorialLogs.length, 10, "Should have 5 enter, and 5 exit frames for 'factorial'");
+  ok(factorialLogs.slice(0, 5).every(t => t.querySelector(".trace-call")),
+     "The first five 'factorial' logs should be calls.");
+  ok(factorialLogs.slice(5).every(t => t.querySelector(".trace-return")),
+     "The second five 'factorial' logs should be returns.")
+
+  // Test that the depth affects padding so that calls are indented properly.
+  let lastDepth = -Infinity;
+  for (let t of factorialLogs.slice(0, 5)) {
+    let depth = parseInt(t.querySelector(".trace-item").style.MozPaddingStart, 10);
+    ok(depth > lastDepth, "The depth should be increasing");
+    lastDepth = depth;
+  }
+  lastDepth = Infinity;
+  for (let t of factorialLogs.slice(5)) {
+    let depth = parseInt(t.querySelector(".trace-item").style.MozPaddingStart, 10);
+    ok(depth < lastDepth, "The depth should be decreasing");
+    lastDepth = depth;
+  }
+
+  const throwerLogs = filterTraces(gPanel,
+                                   t => t.querySelector(".trace-name[value=thrower]"));
+  is(throwerLogs.length, 2, "Should have an enter and an exit for 'thrower'");
+  ok(throwerLogs[0].querySelector(".trace-call"),
+     "The first 'thrower' log should be a call.");
+  ok(throwerLogs[1].querySelector(".trace-throw",
+     "The second 'thrower' log should be a throw."));
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tracing-02.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we highlight matching calls and returns on hover.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
+
+let gTab, gDebuggee, gPanel, gDebugger;
+
+function test() {
+  SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
+    initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPanel = aPanel;
+      gDebugger = gPanel.panelWin;
+
+      waitForSourceShown(gPanel, "code_tracing-01.js")
+        .then(() => startTracing(gPanel))
+        .then(clickButton)
+        .then(() => waitForClientEvents(aPanel, "traces"))
+        .then(highlightCall)
+        .then(testReturnHighlighted)
+        .then(unhighlightCall)
+        .then(testNoneHighlighted)
+        .then(() => stopTracing(gPanel))
+        .then(() => {
+          const deferred = promise.defer();
+          SpecialPowers.popPrefEnv(deferred.resolve);
+          return deferred.promise;
+        })
+        .then(() => closeDebuggerAndFinish(gPanel))
+        .then(null, aError => {
+          ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+        });
+    });
+  });
+}
+
+function clickButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebuggee.document.querySelector("button"),
+                            gDebuggee);
+}
+
+function highlightCall() {
+  const callTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0];
+  EventUtils.sendMouseEvent({ type: "mouseover" },
+                            callTrace,
+                            gDebugger);
+}
+
+function testReturnHighlighted() {
+  const returnTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[1];
+  ok(Array.indexOf(returnTrace.querySelector(".trace-item").classList, "selected-matching") >= 0,
+     "The corresponding return log should be highlighted.");
+}
+
+function unhighlightCall() {
+  const callTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0];
+  EventUtils.sendMouseEvent({ type: "mouseout" },
+                            callTrace,
+                            gDebugger);
+}
+
+function testNoneHighlighted() {
+  const highlightedTraces = filterTraces(gPanel, t => t.querySelector(".selected-matching"));
+  is(highlightedTraces.length, 0, "Shouldn't have any highlighted traces");
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tracing-03.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can jump to function definitions by clicking on logs.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
+
+let gTab, gDebuggee, gPanel, gDebugger;
+
+function test() {
+  SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
+    initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPanel = aPanel;
+      gDebugger = gPanel.panelWin;
+
+      waitForSourceShown(gPanel, "code_tracing-01.js")
+        .then(() => startTracing(gPanel))
+        .then(clickButton)
+        .then(() => waitForClientEvents(aPanel, "traces"))
+        .then(() => {
+          // Switch away from the JS file so we can make sure that clicking on a
+          // log will switch us back to the correct JS file.
+          aPanel.panelWin.DebuggerView.Sources.selectedValue = TAB_URL;
+          return ensureSourceIs(aPanel, TAB_URL, true);
+        })
+        .then(() => {
+          const finished = waitForSourceShown(gPanel, "code_tracing-01.js");
+          clickTraceLog();
+          return finished;
+        })
+        .then(testCorrectLine)
+        .then(() => stopTracing(gPanel))
+        .then(() => {
+          const deferred = promise.defer();
+          SpecialPowers.popPrefEnv(deferred.resolve);
+          return deferred.promise;
+        })
+        .then(() => closeDebuggerAndFinish(gPanel))
+        .then(null, aError => {
+          ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+        });
+    });
+  });
+}
+
+function clickButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebuggee.document.querySelector("button"),
+                            gDebuggee);
+}
+
+function clickTraceLog() {
+  filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0].click();
+}
+
+function testCorrectLine() {
+  is(gDebugger.DebuggerView.editor.getCursor().line, 19,
+     "The editor should have the function definition site's line selected.");
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tracing-04.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that when we click on logs, we get the parameters/return value in the variables view.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
+
+let gTab, gDebuggee, gPanel, gDebugger, gVariables;
+
+function test() {
+  SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
+    initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPanel = aPanel;
+      gDebugger = gPanel.panelWin;
+      gVariables = gDebugger.DebuggerView.Variables;
+
+      waitForSourceShown(gPanel, "code_tracing-01.js")
+        .then(() => startTracing(gPanel))
+        .then(clickButton)
+        .then(() => waitForClientEvents(aPanel, "traces"))
+        .then(clickTraceCall)
+        .then(testParams)
+        .then(clickTraceReturn)
+        .then(testReturn)
+        .then(() => stopTracing(gPanel))
+        .then(() => {
+          const deferred = promise.defer();
+          SpecialPowers.popPrefEnv(deferred.resolve);
+          return deferred.promise;
+        })
+        .then(() => closeDebuggerAndFinish(gPanel))
+        .then(null, aError => {
+          DevToolsUtils.reportException("browser_dbg_tracing-04.js", aError);
+          ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+        });
+    });
+  });
+}
+
+function clickButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebuggee.document.querySelector("button"),
+                            gDebuggee);
+}
+
+function clickTraceCall() {
+  filterTraces(gPanel, t => t.querySelector(".trace-name[value=factorial]"))[0]
+    .click();
+}
+
+function testParams() {
+  const name = gDebugger.document.querySelector(".variables-view-variable .name");
+  ok(name, "Should have a variable name");
+  is(name.getAttribute("value"), "n", "The variable name should be n");
+
+  const value = gDebugger.document.querySelector(".variables-view-variable .value.token-number");
+  ok(value, "Should have a variable value");
+  is(value.getAttribute("value"), "5", "The variable value should be 5");
+}
+
+function clickTraceReturn() {
+  filterTraces(gPanel, t => t.querySelector(".trace-name[value=factorial]"))
+    .pop().click();
+}
+
+function testReturn() {
+  const name = gDebugger.document.querySelector(".variables-view-variable .name");
+  ok(name, "Should have a variable name");
+  is(name.getAttribute("value"), "<return>", "The variable name should be <return>");
+
+  const value = gDebugger.document.querySelector(".variables-view-variable .value.token-number");
+  ok(value, "Should have a variable value");
+  is(value.getAttribute("value"), "120", "The variable value should be 120");
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+  gVariables = null;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/code_tracing-01.js
@@ -0,0 +1,29 @@
+function factorial(n) {
+  if (n <= 1) {
+    return 1;
+  } else {
+    return n * factorial(n - 1);
+  }
+}
+
+function* yielder(n) {
+  while (n-- >= 0) {
+    yield { value: n, squared: n * n };
+  }
+}
+
+function thrower() {
+  throw new Error("Curse your sudden but inevitable betrayal!");
+}
+
+function main() {
+  factorial(5);
+
+  // XXX bug 923729: Can't test yielding yet.
+  // for (let x of yielder(5)) {}
+
+  try {
+    thrower();
+  } catch (e) {
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_tracing-01.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger Tracer test page</title>
+  </head>
+
+  <body>
+    <script src="code_tracing-01.js"></script>
+    <button onclick="main()">Click me!</button>
+  </body>
+</html>
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -370,16 +370,36 @@ function waitForThreadEvents(aPanel, aEv
       thread.removeListener(aEventName, onEvent);
       deferred.resolve.apply(deferred, aArgs);
     }
   });
 
   return deferred.promise;
 }
 
+function waitForClientEvents(aPanel, aEventName, aEventRepeat = 1) {
+  info("Waiting for client event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+  let deferred = promise.defer();
+  let client = aPanel.panelWin.gClient;
+  let count = 0;
+
+  client.addListener(aEventName, function onEvent(aEventName, ...aArgs) {
+    info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+    if (count == aEventRepeat) {
+      ok(true, "Enough '" + aEventName + "' thread events have been fired.");
+      client.removeListener(aEventName, onEvent);
+      deferred.resolve.apply(deferred, aArgs);
+    }
+  });
+
+  return deferred.promise;
+}
+
 function ensureThreadClientState(aPanel, aState) {
   let thread = aPanel.panelWin.gThreadClient;
   let state = thread.state;
 
   info("Thread is: '" + state + "'.");
 
   if (state == aState) {
     return promise.resolve(null);
@@ -593,8 +613,43 @@ function hideVarPopupByScrollingEditor(a
   let popupHiding = once(tooltip, "popuphiding");
   editor.setFirstVisibleLine(0);
   return popupHiding.then(waitForTick);
 }
 
 function reopenVarPopup(...aArgs) {
   return hideVarPopup.apply(this, aArgs).then(() => openVarPopup.apply(this, aArgs));
 }
+
+// Tracing helpers
+
+function startTracing(aPanel) {
+  const deferred = promise.defer();
+  aPanel.panelWin.DebuggerController.Tracer.startTracing(aResponse => {
+    if (aResponse.error) {
+      deferred.reject(aResponse);
+    } else {
+      deferred.resolve(aResponse);
+    }
+  });
+  return deferred.promise;
+}
+
+function stopTracing(aPanel) {
+  const deferred = promise.defer();
+  aPanel.panelWin.DebuggerController.Tracer.stopTracing(aResponse => {
+    if (aResponse.error) {
+      deferred.reject(aResponse);
+    } else {
+      deferred.resolve(aResponse);
+    }
+  });
+  return deferred.promise;
+}
+
+function filterTraces(aPanel, f) {
+  const traces = aPanel.panelWin.document
+    .getElementById("tracer-traces")
+    .querySelector("scrollbox")
+    .children;
+  return Array.filter(traces, f);
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/FastListWidget.js
@@ -0,0 +1,210 @@
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Cu, Ci } = require("chrome");
+const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+
+/**
+ * A list menu widget that attempts to be very fast.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * ViewHelpers.jsm.
+ *
+ * Note: this widget also reuses SideMenuWidget CSS class names.
+ *
+ * @param nsIDOMNode aNode
+ *        The element associated with the widget.
+ */
+const FastListWidget = module.exports = function FastListWidget(aNode) {
+  this.document = aNode.ownerDocument;
+  this.window = this.document.defaultView;
+  this._parent = aNode;
+  this._fragment = this.document.createDocumentFragment();
+
+  // This is a prototype element that each item added to the list clones.
+  this._templateElement = this.document.createElement("hbox");
+  this._templateElement.className = "side-menu-widget-item side-menu-widget-item-contents";
+
+  // Create an internal scrollbox container.
+  this._list = this.document.createElement("scrollbox");
+  this._list.className = "side-menu-widget-container";
+  this._list.setAttribute("flex", "1");
+  this._list.setAttribute("orient", "vertical");
+  this._list.setAttribute("theme", "dark");
+  this._list.setAttribute("tabindex", "0");
+  this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+  this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
+  this._parent.appendChild(this._list);
+
+  this._orderedMenuElementsArray = [];
+  this._itemsByElement = new Map();
+
+  // This widget emits events that can be handled in a MenuContainer.
+  EventEmitter.decorate(this);
+
+  // Delegate some of the associated node's methods to satisfy the interface
+  // required by MenuContainer instances.
+  ViewHelpers.delegateWidgetEventMethods(this, aNode);
+}
+
+FastListWidget.prototype = {
+  /**
+   * Inserts an item in this container at the specified index, optionally
+   * grouping by name.
+   *
+   * @param number aIndex
+   *        The position in the container intended for this item.
+   * @param nsIDOMNode aContents
+   *        The node to be displayed in the container.
+   * @param Object aAttachment [optional]
+   *        Extra data for the user.
+   * @return nsIDOMNode
+   *         The element associated with the displayed item.
+   */
+  insertItemAt: function(aIndex, aContents, aAttachment={}) {
+    let element = this._templateElement.cloneNode();
+    element.appendChild(aContents);
+
+    if (aIndex >= 0) {
+      throw new Error("FastListWidget only supports appending items.");
+    }
+
+    this._fragment.appendChild(element);
+    this._orderedMenuElementsArray.push(element);
+    this._itemsByElement.set(element, this);
+
+    return element;
+  },
+
+  /**
+   * This is a non-standard widget implementation method. When appending items,
+   * they are queued in a document fragment. This method appends the document
+   * fragment to the dom.
+   */
+  flush: function() {
+    this._list.appendChild(this._fragment);
+  },
+
+  /**
+   * Removes all of the child nodes from this container.
+   */
+  removeAllItems: function() {
+    let parent = this._parent;
+    let list = this._list;
+
+    while (list.hasChildNodes()) {
+      list.firstChild.remove();
+    }
+
+    this._selectedItem = null;
+
+    this._orderedMenuElementsArray.length = 0;
+    this._itemsByElement.clear();
+  },
+
+  /**
+   * Remove the given item.
+   */
+  removeChild: function(child) {
+    throw new Error("Not yet implemented");
+  },
+
+  /**
+   * Gets the currently selected child node in this container.
+   * @return nsIDOMNode
+   */
+  get selectedItem() this._selectedItem,
+
+  /**
+   * Sets the currently selected child node in this container.
+   * @param nsIDOMNode child
+   */
+  set selectedItem(child) {
+    let menuArray = this._orderedMenuElementsArray;
+
+    if (!child) {
+      this._selectedItem = null;
+    }
+    for (let node of menuArray) {
+      if (node == child) {
+        node.classList.add("selected");
+        node.parentNode.classList.add("selected");
+        this._selectedItem = node;
+      } else {
+        node.classList.remove("selected");
+        node.parentNode.classList.remove("selected");
+      }
+    }
+
+    this.ensureElementIsVisible(this.selectedItem);
+  },
+
+  /**
+   * Returns the child node in this container situated at the specified index.
+   *
+   * @param number index
+   *        The position in the container intended for this item.
+   * @return nsIDOMNode
+   *         The element associated with the displayed item.
+   */
+  getItemAtIndex: function(index) {
+    return this._orderedMenuElementsArray[index];
+  },
+
+  /**
+   * Returns the value of the named attribute on this container.
+   *
+   * @param string name
+   *        The name of the attribute.
+   * @return string
+   *         The current attribute value.
+   */
+  getAttribute: function(name) {
+    return this._parent.getAttribute(name);
+  },
+
+  /**
+   * Adds a new attribute or changes an existing attribute on this container.
+   *
+   * @param string name
+   *        The name of the attribute.
+   * @param string value
+   *        The desired attribute value.
+   */
+  setAttribute: function(name, value) {
+    this._parent.setAttribute(name, value);
+  },
+
+  /**
+   * Removes an attribute on this container.
+   *
+   * @param string name
+   *        The name of the attribute.
+   */
+  removeAttribute: function(name) {
+    this._parent.removeAttribute(name);
+  },
+
+  /**
+   * Ensures the specified element is visible.
+   *
+   * @param nsIDOMNode element
+   *        The element to make visible.
+   */
+  ensureElementIsVisible: function(element) {
+    if (!element) {
+      return;
+    }
+
+    // Ensure the element is visible but not scrolled horizontally.
+    let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+    boxObject.ensureElementIsVisible(element);
+    boxObject.scrollBy(-element.clientWidth, 0);
+  },
+
+  window: null,
+  document: null,
+  _parent: null,
+  _list: null,
+  _selectedItem: null,
+  _orderedMenuElementsArray: null,
+  _itemsByElement: null
+};
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -3428,17 +3428,17 @@ EditableValue.prototype = Heritage.exten
 function EditableNameAndValue(aVariable, aOptions) {
   EditableName.call(this, aVariable, aOptions);
 }
 
 EditableNameAndValue.create = Editable.create;
 
 EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
   _reset: function(e) {
-    // Hide the Varible or Property if the user presses escape.
+    // Hide the Variable or Property if the user presses escape.
     this._variable.remove();
     this.deactivate();
   },
 
   _next: function(e) {
     // Override _next so as to set both key and value at the same time.
     let key = this._input.value;
     this.label.setAttribute("value", key);
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
@@ -40,16 +40,32 @@
 <!-- LOCALIZATION NOTE (debuggerUI.sources.prettyPrint): This is the tooltip for the
   -  button that pretty prints the selected source. -->
 <!ENTITY debuggerUI.sources.prettyPrint "Prettify Source">
 
 <!-- LOCALIZATION NOTE (debuggerUI.sources.toggleBreakpoints): This is the tooltip for the
   -  button that toggles all breakpoints for all sources. -->
 <!ENTITY debuggerUI.sources.toggleBreakpoints "Enable/disable all breakpoints">
 
+<!-- LOCALIZATION NOTE (debuggerUI.tracingNotStarted.label): This is the text
+  -  displayed when tracing hasn't started in the debugger UI. -->
+<!ENTITY debuggerUI.tracingNotStarted.label "Tracing has not started.">
+
+<!-- LOCALIZATION NOTE (debuggerUI.startTracing): This is the text displayed in
+  - the button to start execution tracing. -->
+<!ENTITY debuggerUI.startTracing "Start Tracing">
+
+<!-- LOCALIZATION NOTE (debuggerUI.clearButton): This is the label for
+  -  the button that clears the collected tracing data in the tracing tab. -->
+<!ENTITY debuggerUI.clearButton "Clear">
+
+<!-- LOCALIZATION NOTE (debuggerUI.clearButton.tooltip): This is the tooltip for
+  -  the button that clears the collected tracing data in the tracing tab. -->
+<!ENTITY debuggerUI.clearButton.tooltip "Clear the collected traces">
+
 <!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the
   -  checkbox that toggles pausing on exceptions. -->
 <!ENTITY debuggerUI.pauseExceptions     "Pause on exceptions">
 <!ENTITY debuggerUI.pauseExceptions.key "E">
 
 <!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the
   -  checkbox that toggles ignoring caught exceptions. -->
 <!ENTITY debuggerUI.ignoreCaughtExceptions     "Ignore caught exceptions">
@@ -131,16 +147,17 @@
   -  appears in the source editor context menu for adding a conditional
   -  breakpoint. -->
 <!ENTITY debuggerUI.seMenuCondBreak     "Add conditional breakpoint">
 <!ENTITY debuggerUI.seMenuCondBreak.key "B">
 
 <!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that
   -  appears in the debugger's side pane tabs. -->
 <!ENTITY debuggerUI.tabs.sources        "Sources">
+<!ENTITY debuggerUI.tabs.traces         "Traces">
 <!ENTITY debuggerUI.tabs.callstack      "Call Stack">
 <!ENTITY debuggerUI.tabs.variables      "Variables">
 <!ENTITY debuggerUI.tabs.events         "Events">
 
 <!-- LOCALIZATION NOTE (debuggerUI.seMenuAddWatch): This is the text that
   -  appears in the source editor context menu for adding an expression. -->
 <!ENTITY debuggerUI.seMenuAddWatch      "Selection to watch expression">
 <!ENTITY debuggerUI.seMenuAddWatch.key  "E">
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -44,16 +44,24 @@ expandPanes=Expand panes
 # LOCALIZATION NOTE (pauseLabel): The label that is displayed on the pause
 # button when the debugger is in a running state.
 pauseButtonTooltip=Click to pause (%S)
 
 # LOCALIZATION NOTE (resumeLabel): The label that is displayed on the pause
 # button when the debugger is in a paused state.
 resumeButtonTooltip=Click to resume (%S)
 
+# LOCALIZATION NOTE (startTracingTooltip): The label that is displayed on the trace
+# button when execution tracing is stopped.
+startTracingTooltip=Click to start tracing
+
+# LOCALIZATION NOTE (stopTracingTooltip): The label that is displayed on the trace
+# button when execution tracing is started.
+stopTracingTooltip=Click to stop tracing
+
 # LOCALIZATION NOTE (stepOverTooltip): The label that is displayed on the
 # button that steps over a function call.
 stepOverTooltip=Step Over (%S)
 
 # LOCALIZATION NOTE (stepInTooltip): The label that is displayed on the
 # button that steps into a function call.
 stepInTooltip=Step In (%S)
 
--- a/browser/themes/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -88,16 +88,129 @@
   font-size: 120%;
 }
 
 #black-boxed-message-button {
   margin-top: 1em;
   padding: .25em;
 }
 
+/* Tracer */
+
+#trace {
+  list-style-image: url(tracer-icon.png);
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#trace[checked] {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+#start-tracing {
+  padding: 4px;
+  margin: 4px;
+}
+
+#clear-tracer {
+  min-width: 22px !important;
+}
+
+#tracer-search {
+  min-width: 72px !important;
+}
+
+#tracer-message {
+  /* Prevent the container deck from aquiring the height from this message. */
+  min-height: 1px;
+}
+
+.trace-name {
+  -moz-padding-start: 4px !important;
+}
+
+/* Tracer dark theme */
+
+.theme-dark #tracer-message {
+  color: #f5f7fa; /* Light foreground text */
+  background: url(background-noise-toolbar.png) #181d20; /* Content background sidebar */
+}
+
+.theme-dark #tracer-traces > scrollbox {
+  background-color: #181d20 !important; /* Content background sidebar */
+}
+
+.theme-dark .trace-item {
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.trace-item.selected-matching {
+  background-color: #1d4f73; /* Select highlight blue */
+}
+
+.theme-dark .trace-call {
+  color: #46afe3; /* highlight blue */
+}
+
+.theme-dark .trace-return,
+.theme-dark .trace-yield {
+  color: #70bf53; /* highlight green */
+}
+
+.theme-dark .trace-throw {
+  color: #eb5368; /* highlight red */
+}
+
+.theme-dark .trace-param {
+  color: #8fa1b2; /* Content text grey  */
+}
+
+.theme-dark .trace-syntax {
+  color: #5e88b0; /* highlight blue-grey */
+}
+
+/* Tracer light theme */
+
+.theme-light #tracer-message {
+  color: #292e33; /* Dark foreground text */
+  background: url(background-noise-toolbar.png) #f7f7f7; /* Content background sidebar */
+}
+
+.theme-light #tracer-traces > scrollbox {
+  background-color: #f7f7f7 !important; /* Content background sidebar */
+}
+
+.theme-light .trace-item {
+  color: #292e33; /* Dark foreground text */
+}
+
+.trace-item.selected-matching {
+  background-color: #4c9ed9; /* Select highlight blue */
+}
+
+.theme-light .trace-call {
+  color: #0088cc; /* highlight blue */
+}
+
+.theme-light .trace-return,
+.theme-light .trace-yield {
+  color: #2cbb0f; /* highlight green */
+}
+
+.theme-light .trace-throw {
+  color: #ed2655; /* highlight red */
+}
+
+.theme-light .trace-param {
+  color: #8fa1b2; /* Content text grey  */
+}
+
+.theme-light .trace-syntax {
+  color: #5f88b0; /* highlight blue-grey */
+}
+
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .theme-light .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
@@ -221,21 +334,16 @@
 
 .devtools-tooltip-simple-text.token-other {
   text-align: center;
   color: #333 !important;
 }
 
 /* Instruments pane (watch expressions, variables, event listeners...) */
 
-#instruments-pane > tabs > tab {
-  min-height: 25px !important;
-  padding: 0 !important;
-}
-
 #instruments-pane .side-menu-widget-container,
 #instruments-pane .side-menu-widget-empty-notice-container {
   box-shadow: none !important;
 }
 
 /* Watch expressions view */
 
 #expressions {
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8229dd7c78f37edcf897d773a9b84bc5be707322
GIT binary patch
literal 709
zc$@*n0y_PPP)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^0007vNkl<ZNDb{&
zO=uHA6rMMeADdDV4=MJb7*GCy6$-Vfr6?#Bu@|B72k~SJ7V6D|(q8l`>dBvr9u!)d
z=)sEzK|R@ndhjL)>Os<2v8|@Nv%6{L`DSU7-TX8Oc=W-t@8|p8e6#ar8T)T7et|!R
zb8$`D=Uar=zy{LUTlCyu48dFE-e5Cd2o?flr%kJH1rdXrVe8cZSO-1R1DWjX&PQLj
zlrXNERVfoe5F?aqEJ(3^q<)7cBEwJ8$X}1g<9QwT&wcOZ0ww^UAs>+qlYqS%Pxh9n
z;^;zZ`*J`>wD<|8@VNF{F<`Je(zSJnurD-T1o$MqCbZwkXmPI%A{Qx2+;;^;!oeq|
zX@8?m!i)0ysER|>V~@@OhgTh8@v>A0&Sx0n$0kQJM)*S;oX2^!^037By`{tiHS+5v
zgkd?8hG{>~oAwzWp(^Nq_M?}?)bNv;>^(Yvl}s<rcqzU!GYo4~p>t=Iv8Wg1`oOaa
z_|2|Jc&?_lk)Q=G3swAZaCWJ?8iR~|M&#!f#(L-`RLoUMJLaZY7-xVdb*bRU3$a*i
zr2=c>&7&9;x!crdN+Z{m-SaM$$TeZPWmkY9?Q1mJ$r6F4nLYw#8khpib%sOJgmW~6
zcK(zao$|n!h^#!Z&jrLU4AoJ(1=1ZtI<svwT4fOJBvuaUIGo0i(wgfEAm*+rSa+pc
zRYivY!a*lQo^)}vU}Vj5u5x4tblm|p1-AWL`}HgujT)uh>w4e(ax@55ny%V07q9sK
zbtZq-<LX>zoUOh&lCZR27D^(7l!Z(5lNo7y2B^{D*BG#VE#9Su<L%6V7U45lC=6>c
rzGNFgGLt*o_uzfozv?!Y__xr%u-f#VuXHO+00000NkvXXu0mjfRFYBc
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..de0ded4c8d12508c0cd4846cd794517ebcf20cbf
GIT binary patch
literal 1323
zc$@(%1=RY9P)<h;3K|Lk000e1NJLTq002M$001Be1^@s6qMd$(000E?Nkl<ZXa((9
zTWB0r7(V}**`2+lNikkxHkL+giv&u&wIqeohhm{xvB5^`gD6Esyn-|@^@fOIEml!5
z6-4NZB~7Z;Uc?7|kS3|tDusv^t3tBdU`eXm?Cj3$&iVZ(b+#vy+1*JqyD#p6<(%{V
zmv7ED=bSlb8LOAN0(AxI3IwP?I*rZKS#!Ux8{Gk>^_{A-e=N~Ckh;)0klf)Xi2HGx
zkB<l&O@J6j#vajg=2M8M%qM=KzQEub%CVR+*wr<V`tY{BCqw0O1}=b1h5|S0xxy<X
zLR`jc)uxy5Xt=rc==x9pAVIuKRRx%#M6pUY@^4K~<8|JTP<3>JVQE9F`zPAHn7|F{
zIwnkRXl~qZpQDmcQ1rU}>23TY6!u!9rv2i!$^Etc$rZ>jDr56$3kEBI!+wvLzd(q4
zhkDwEWm{1Id#%!ReTkvCRdxutZ88<!h=;=`-Fer4gQ_uk?l9pS-L?SxSrTC@iEzZp
zSzH1c)9k47;BC$@v)Gdkxm=s)lk2Eo&B5DrQRKW&<}C`qEo+JlG*hcvfZ>D}13g%Z
zaN%VO6a#iv&R1LlHki%k*CDVi0<lqcnopY&stoZeuNdTT*3v?+HUZdT&nfin-Ua~A
zBeLc>M!x_+nD|>uxS!(r!sg@Kme^v?lvx5e;6m!ZbGaKs0^W7!;0ix6ky3ZJw?oct
z3$owUKfa1)(~s_)B<$zY8F=vYLoFHER$Kxy7<e1tP$aCrUhTn`dG0+4tEJmLc-vns
z6EtJWW^t*2{GBl^^f9TJujKEt3xz9vk%)TCh5PQV85mosFlBpFEYo{z(=x+%;3Y>3
znRt5SzP1la#g!ExlQHiQX!g?97|G?>SCr(>Qi<F+iKWva_)U8Ar`$Hb{muj9cQeL6
zp`E#5N{@U*ai{&pl3$v()cZ$z+P*9oQ&NCTu5b?%)^-Y>StbS;zLraqrX}g*cbJLL
z(Rdif%8{3|bsk7P$AIl7pP9RaBET2F5#?tz6W+5d)_0=k`k%{j*tR(W(1D2403M^k
z-&HBt%T90Jo~C=fY<{q<{iuxd3ED)_^Sn?g#}AuZb@F+plAmLrB>)XRN~b}Z@_4)B
zP|JGlq4SH9`3XA4;-*^h7C(dOboy#Vjl4|cOFhW}lK;J3Pjh8Zm<7Q<Ym9}Cd-<*z
z9B-q01RYUvrI($sU+I@EVXF)SWWloDrepAc>|7LJS~vI5DeWfN7<ij=i&^Ru@i?4w
z=cYd~z9+2OGp|5P`{v=E8-8>6rH!TJ>B;e3crAn?gjwQl4$Br!P%(qbfL|+n6$PM8
z_I)xGWMj~61U}Tz5ca$dqY#f1pBXf#oR6ma;P7bchzq;iH4N;?3v(wRTXY${)Utr@
z5@03WxZgf~9(CG7JTF2{j;_JfI=TlOr2d?{0R6l40B30L16Ni;;^NSjC9^I<cB0}Q
z;AGWxJ%L<(kqJyVm|f>NKD#&)`CZ1U(O!LUe7P$47j*bpF6XCv{4KPB{Ul>Sw2Mo?
zNfwL6l636d0i2IISS{=Cy4%haxacCzZ)?FjM*y3Yrs~D0roKRbEqz^!hj4R{2$N}X
zVFNwLKDRCCzNY{-35igMYN2;PQTEw_2wk5Hn!H@T(Vh--xVz<H+7ot?-xOVlu6X|`
h$5Xe}75JYk@GrW<rtuxF1FZl6002ovPDHLkV1ltumEiyY
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -216,16 +216,18 @@ browser.jar:
   skin/classic/browser/devtools/debugger-pause.png     (devtools/debugger-pause.png)
   skin/classic/browser/devtools/debugger-play.png      (devtools/debugger-play.png)
   skin/classic/browser/devtools/debugger-step-in.png   (devtools/debugger-step-in.png)
   skin/classic/browser/devtools/debugger-step-out.png  (devtools/debugger-step-out.png)
   skin/classic/browser/devtools/debugger-step-over.png (devtools/debugger-step-over.png)
   skin/classic/browser/devtools/debugger-blackbox.png  (devtools/debugger-blackbox.png)
   skin/classic/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
   skin/classic/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
+  skin/classic/browser/devtools/tracer-icon.png        (devtools/tracer-icon.png)
+  skin/classic/browser/devtools/tracer-icon@2x.png     (devtools/tracer-icon@2x.png)
   skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
   skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
   skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png)
   skin/classic/browser/devtools/toggle-tools.png          (devtools/toggle-tools.png)
   skin/classic/browser/devtools/dock-bottom.png           (devtools/dock-bottom.png)
   skin/classic/browser/devtools/dock-side.png             (devtools/dock-side.png)
   skin/classic/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -90,16 +90,129 @@
   font-size: 120%;
 }
 
 #black-boxed-message-button {
   margin-top: 1em;
   padding: .25em;
 }
 
+/* Tracer */
+
+#trace {
+  list-style-image: url(tracer-icon.png);
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#trace[checked] {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+#start-tracing {
+  padding: 4px;
+  margin: 4px;
+}
+
+#clear-tracer {
+  min-width: 22px !important;
+}
+
+#tracer-search {
+  min-width: 72px !important;
+}
+
+#tracer-message {
+  /* Prevent the container deck from aquiring the height from this message. */
+  min-height: 1px;
+}
+
+.trace-name {
+  -moz-padding-start: 4px !important;
+}
+
+/* Tracer dark theme */
+
+.theme-dark #tracer-message {
+  color: #f5f7fa; /* Light foreground text */
+  background: url(background-noise-toolbar.png) #181d20; /* Content background sidebar */
+}
+
+.theme-dark #tracer-traces > scrollbox {
+  background-color: #181d20 !important; /* Content background sidebar */
+}
+
+.theme-dark .trace-item {
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.trace-item.selected-matching {
+  background-color: #1d4f73; /* Select highlight blue */
+}
+
+.theme-dark .trace-call {
+  color: #46afe3; /* highlight blue */
+}
+
+.theme-dark .trace-return,
+.theme-dark .trace-yield {
+  color: #70bf53; /* highlight green */
+}
+
+.theme-dark .trace-throw {
+  color: #eb5368; /* highlight red */
+}
+
+.theme-dark .trace-param {
+  color: #8fa1b2; /* Content text grey  */
+}
+
+.theme-dark .trace-syntax {
+  color: #5e88b0; /* highlight blue-grey */
+}
+
+/* Tracer light theme */
+
+.theme-light #tracer-message {
+  color: #292e33; /* Dark foreground text */
+  background: url(background-noise-toolbar.png) #f7f7f7; /* Content background sidebar */
+}
+
+.theme-light #tracer-traces > scrollbox {
+  background-color: #f7f7f7 !important; /* Content background sidebar */
+}
+
+.theme-light .trace-item {
+  color: #292e33; /* Dark foreground text */
+}
+
+.trace-item.selected-matching {
+  background-color: #4c9ed9; /* Select highlight blue */
+}
+
+.theme-light .trace-call {
+  color: #0088cc; /* highlight blue */
+}
+
+.theme-light .trace-return,
+.theme-light .trace-yield {
+  color: #2cbb0f; /* highlight green */
+}
+
+.theme-light .trace-throw {
+  color: #ed2655; /* highlight red */
+}
+
+.theme-light .trace-param {
+  color: #8fa1b2; /* Content text grey  */
+}
+
+.theme-light .trace-syntax {
+  color: #5f88b0; /* highlight blue-grey */
+}
+
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .theme-light .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
@@ -223,21 +336,16 @@
 
 .devtools-tooltip-simple-text.token-other {
   text-align: center;
   color: #333 !important;
 }
 
 /* Instruments pane (watch expressions, variables, event listeners...) */
 
-#instruments-pane > tabs > tab {
-  min-height: 1em !important;
-  padding: 0 !important;
-}
-
 #instruments-pane .side-menu-widget-container,
 #instruments-pane .side-menu-widget-empty-notice-container {
   box-shadow: none !important;
 }
 
 /* Watch expressions view */
 
 #expressions {
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8229dd7c78f37edcf897d773a9b84bc5be707322
GIT binary patch
literal 709
zc$@*n0y_PPP)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^0007vNkl<ZNDb{&
zO=uHA6rMMeADdDV4=MJb7*GCy6$-Vfr6?#Bu@|B72k~SJ7V6D|(q8l`>dBvr9u!)d
z=)sEzK|R@ndhjL)>Os<2v8|@Nv%6{L`DSU7-TX8Oc=W-t@8|p8e6#ar8T)T7et|!R
zb8$`D=Uar=zy{LUTlCyu48dFE-e5Cd2o?flr%kJH1rdXrVe8cZSO-1R1DWjX&PQLj
zlrXNERVfoe5F?aqEJ(3^q<)7cBEwJ8$X}1g<9QwT&wcOZ0ww^UAs>+qlYqS%Pxh9n
z;^;zZ`*J`>wD<|8@VNF{F<`Je(zSJnurD-T1o$MqCbZwkXmPI%A{Qx2+;;^;!oeq|
zX@8?m!i)0ysER|>V~@@OhgTh8@v>A0&Sx0n$0kQJM)*S;oX2^!^037By`{tiHS+5v
zgkd?8hG{>~oAwzWp(^Nq_M?}?)bNv;>^(Yvl}s<rcqzU!GYo4~p>t=Iv8Wg1`oOaa
z_|2|Jc&?_lk)Q=G3swAZaCWJ?8iR~|M&#!f#(L-`RLoUMJLaZY7-xVdb*bRU3$a*i
zr2=c>&7&9;x!crdN+Z{m-SaM$$TeZPWmkY9?Q1mJ$r6F4nLYw#8khpib%sOJgmW~6
zcK(zao$|n!h^#!Z&jrLU4AoJ(1=1ZtI<svwT4fOJBvuaUIGo0i(wgfEAm*+rSa+pc
zRYivY!a*lQo^)}vU}Vj5u5x4tblm|p1-AWL`}HgujT)uh>w4e(ax@55ny%V07q9sK
zbtZq-<LX>zoUOh&lCZR27D^(7l!Z(5lNo7y2B^{D*BG#VE#9Su<L%6V7U45lC=6>c
rzGNFgGLt*o_uzfozv?!Y__xr%u-f#VuXHO+00000NkvXXu0mjfRFYBc
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..de0ded4c8d12508c0cd4846cd794517ebcf20cbf
GIT binary patch
literal 1323
zc$@(%1=RY9P)<h;3K|Lk000e1NJLTq002M$001Be1^@s6qMd$(000E?Nkl<ZXa((9
zTWB0r7(V}**`2+lNikkxHkL+giv&u&wIqeohhm{xvB5^`gD6Esyn-|@^@fOIEml!5
z6-4NZB~7Z;Uc?7|kS3|tDusv^t3tBdU`eXm?Cj3$&iVZ(b+#vy+1*JqyD#p6<(%{V
zmv7ED=bSlb8LOAN0(AxI3IwP?I*rZKS#!Ux8{Gk>^_{A-e=N~Ckh;)0klf)Xi2HGx
zkB<l&O@J6j#vajg=2M8M%qM=KzQEub%CVR+*wr<V`tY{BCqw0O1}=b1h5|S0xxy<X
zLR`jc)uxy5Xt=rc==x9pAVIuKRRx%#M6pUY@^4K~<8|JTP<3>JVQE9F`zPAHn7|F{
zIwnkRXl~qZpQDmcQ1rU}>23TY6!u!9rv2i!$^Etc$rZ>jDr56$3kEBI!+wvLzd(q4
zhkDwEWm{1Id#%!ReTkvCRdxutZ88<!h=;=`-Fer4gQ_uk?l9pS-L?SxSrTC@iEzZp
zSzH1c)9k47;BC$@v)Gdkxm=s)lk2Eo&B5DrQRKW&<}C`qEo+JlG*hcvfZ>D}13g%Z
zaN%VO6a#iv&R1LlHki%k*CDVi0<lqcnopY&stoZeuNdTT*3v?+HUZdT&nfin-Ua~A
zBeLc>M!x_+nD|>uxS!(r!sg@Kme^v?lvx5e;6m!ZbGaKs0^W7!;0ix6ky3ZJw?oct
z3$owUKfa1)(~s_)B<$zY8F=vYLoFHER$Kxy7<e1tP$aCrUhTn`dG0+4tEJmLc-vns
z6EtJWW^t*2{GBl^^f9TJujKEt3xz9vk%)TCh5PQV85mosFlBpFEYo{z(=x+%;3Y>3
znRt5SzP1la#g!ExlQHiQX!g?97|G?>SCr(>Qi<F+iKWva_)U8Ar`$Hb{muj9cQeL6
zp`E#5N{@U*ai{&pl3$v()cZ$z+P*9oQ&NCTu5b?%)^-Y>StbS;zLraqrX}g*cbJLL
z(Rdif%8{3|bsk7P$AIl7pP9RaBET2F5#?tz6W+5d)_0=k`k%{j*tR(W(1D2403M^k
z-&HBt%T90Jo~C=fY<{q<{iuxd3ED)_^Sn?g#}AuZb@F+plAmLrB>)XRN~b}Z@_4)B
zP|JGlq4SH9`3XA4;-*^h7C(dOboy#Vjl4|cOFhW}lK;J3Pjh8Zm<7Q<Ym9}Cd-<*z
z9B-q01RYUvrI($sU+I@EVXF)SWWloDrepAc>|7LJS~vI5DeWfN7<ij=i&^Ru@i?4w
z=cYd~z9+2OGp|5P`{v=E8-8>6rH!TJ>B;e3crAn?gjwQl4$Br!P%(qbfL|+n6$PM8
z_I)xGWMj~61U}Tz5ca$dqY#f1pBXf#oR6ma;P7bchzq;iH4N;?3v(wRTXY${)Utr@
z5@03WxZgf~9(CG7JTF2{j;_JfI=TlOr2d?{0R6l40B30L16Ni;;^NSjC9^I<cB0}Q
z;AGWxJ%L<(kqJyVm|f>NKD#&)`CZ1U(O!LUe7P$47j*bpF6XCv{4KPB{Ul>Sw2Mo?
zNfwL6l636d0i2IISS{=Cy4%haxacCzZ)?FjM*y3Yrs~D0roKRbEqz^!hj4R{2$N}X
zVFNwLKDRCCzNY{-35igMYN2;PQTEw_2wk5Hn!H@T(Vh--xVz<H+7ot?-xOVlu6X|`
h$5Xe}75JYk@GrW<rtuxF1FZl6002ovPDHLkV1ltumEiyY
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -319,16 +319,18 @@ browser.jar:
   skin/classic/browser/devtools/debugger-pause.png          (devtools/debugger-pause.png)
   skin/classic/browser/devtools/debugger-play.png           (devtools/debugger-play.png)
   skin/classic/browser/devtools/debugger-step-in.png        (devtools/debugger-step-in.png)
   skin/classic/browser/devtools/debugger-step-out.png       (devtools/debugger-step-out.png)
   skin/classic/browser/devtools/debugger-step-over.png      (devtools/debugger-step-over.png)
   skin/classic/browser/devtools/debugger-blackbox.png       (devtools/debugger-blackbox.png)
   skin/classic/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
   skin/classic/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
+  skin/classic/browser/devtools/tracer-icon.png             (devtools/tracer-icon.png)
+  skin/classic/browser/devtools/tracer-icon@2x.png          (devtools/tracer-icon@2x.png)
   skin/classic/browser/devtools/floating-scrollbars.css     (devtools/floating-scrollbars.css)
   skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
   skin/classic/browser/devtools/responsive-se-resizer.png   (devtools/responsive-se-resizer.png)
   skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
   skin/classic/browser/devtools/responsive-background.png   (devtools/responsive-background.png)
   skin/classic/browser/devtools/toggle-tools.png            (devtools/toggle-tools.png)
   skin/classic/browser/devtools/dock-bottom.png             (devtools/dock-bottom.png)
--- a/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -88,16 +88,129 @@
   font-size: 120%;
 }
 
 #black-boxed-message-button {
   margin-top: 1em;
   padding: .25em;
 }
 
+/* Tracer */
+
+#trace {
+  list-style-image: url(tracer-icon.png);
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#trace[checked] {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+#start-tracing {
+  padding: 4px;
+  margin: 4px;
+}
+
+#clear-tracer {
+  min-width: 22px !important;
+}
+
+#tracer-search {
+  min-width: 72px !important;
+}
+
+#tracer-message {
+  /* Prevent the container deck from aquiring the height from this message. */
+  min-height: 1px;
+}
+
+.trace-name {
+  -moz-padding-start: 4px !important;
+}
+
+/* Tracer dark theme */
+
+.theme-dark #tracer-message {
+  color: #f5f7fa; /* Light foreground text */
+  background: url(background-noise-toolbar.png) #181d20; /* Content background sidebar */
+}
+
+.theme-dark #tracer-traces > scrollbox {
+  background-color: #181d20 !important; /* Content background sidebar */
+}
+
+.theme-dark .trace-item {
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.trace-item.selected-matching {
+  background-color: #1d4f73; /* Select highlight blue */
+}
+
+.theme-dark .trace-call {
+  color: #46afe3; /* highlight blue */
+}
+
+.theme-dark .trace-return,
+.theme-dark .trace-yield {
+  color: #70bf53; /* highlight green */
+}
+
+.theme-dark .trace-throw {
+  color: #eb5368; /* highlight red */
+}
+
+.theme-dark .trace-param {
+  color: #8fa1b2; /* Content text grey  */
+}
+
+.theme-dark .trace-syntax {
+  color: #5e88b0; /* highlight blue-grey */
+}
+
+/* Tracer light theme */
+
+.theme-light #tracer-message {
+  color: #292e33; /* Dark foreground text */
+  background: url(background-noise-toolbar.png) #f7f7f7; /* Content background sidebar */
+}
+
+.theme-light #tracer-traces > scrollbox {
+  background-color: #f7f7f7 !important; /* Content background sidebar */
+}
+
+.theme-light .trace-item {
+  color: #292e33; /* Dark foreground text */
+}
+
+.trace-item.selected-matching {
+  background-color: #4c9ed9; /* Select highlight blue */
+}
+
+.theme-light .trace-call {
+  color: #0088cc; /* highlight blue */
+}
+
+.theme-light .trace-return,
+.theme-light .trace-yield {
+  color: #2cbb0f; /* highlight green */
+}
+
+.theme-light .trace-throw {
+  color: #ed2655; /* highlight red */
+}
+
+.theme-light .trace-param {
+  color: #8fa1b2; /* Content text grey  */
+}
+
+.theme-light .trace-syntax {
+  color: #5f88b0; /* highlight blue-grey */
+}
+
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .theme-light .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
@@ -221,21 +334,16 @@
 
 .devtools-tooltip-simple-text.token-other {
   text-align: center;
   color: #333 !important;
 }
 
 /* Instruments pane (watch expressions, variables, event listeners...) */
 
-#instruments-pane > tabs > tab {
-  min-height: 25px !important;
-  padding: 0 !important;
-}
-
 #instruments-pane .side-menu-widget-container,
 #instruments-pane .side-menu-widget-empty-notice-container {
   box-shadow: none !important;
 }
 
 /* Watch expressions view */
 
 #expressions {
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8229dd7c78f37edcf897d773a9b84bc5be707322
GIT binary patch
literal 709
zc$@*n0y_PPP)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^0007vNkl<ZNDb{&
zO=uHA6rMMeADdDV4=MJb7*GCy6$-Vfr6?#Bu@|B72k~SJ7V6D|(q8l`>dBvr9u!)d
z=)sEzK|R@ndhjL)>Os<2v8|@Nv%6{L`DSU7-TX8Oc=W-t@8|p8e6#ar8T)T7et|!R
zb8$`D=Uar=zy{LUTlCyu48dFE-e5Cd2o?flr%kJH1rdXrVe8cZSO-1R1DWjX&PQLj
zlrXNERVfoe5F?aqEJ(3^q<)7cBEwJ8$X}1g<9QwT&wcOZ0ww^UAs>+qlYqS%Pxh9n
z;^;zZ`*J`>wD<|8@VNF{F<`Je(zSJnurD-T1o$MqCbZwkXmPI%A{Qx2+;;^;!oeq|
zX@8?m!i)0ysER|>V~@@OhgTh8@v>A0&Sx0n$0kQJM)*S;oX2^!^037By`{tiHS+5v
zgkd?8hG{>~oAwzWp(^Nq_M?}?)bNv;>^(Yvl}s<rcqzU!GYo4~p>t=Iv8Wg1`oOaa
z_|2|Jc&?_lk)Q=G3swAZaCWJ?8iR~|M&#!f#(L-`RLoUMJLaZY7-xVdb*bRU3$a*i
zr2=c>&7&9;x!crdN+Z{m-SaM$$TeZPWmkY9?Q1mJ$r6F4nLYw#8khpib%sOJgmW~6
zcK(zao$|n!h^#!Z&jrLU4AoJ(1=1ZtI<svwT4fOJBvuaUIGo0i(wgfEAm*+rSa+pc
zRYivY!a*lQo^)}vU}Vj5u5x4tblm|p1-AWL`}HgujT)uh>w4e(ax@55ny%V07q9sK
zbtZq-<LX>zoUOh&lCZR27D^(7l!Z(5lNo7y2B^{D*BG#VE#9Su<L%6V7U45lC=6>c
rzGNFgGLt*o_uzfozv?!Y__xr%u-f#VuXHO+00000NkvXXu0mjfRFYBc
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..de0ded4c8d12508c0cd4846cd794517ebcf20cbf
GIT binary patch
literal 1323
zc$@(%1=RY9P)<h;3K|Lk000e1NJLTq002M$001Be1^@s6qMd$(000E?Nkl<ZXa((9
zTWB0r7(V}**`2+lNikkxHkL+giv&u&wIqeohhm{xvB5^`gD6Esyn-|@^@fOIEml!5
z6-4NZB~7Z;Uc?7|kS3|tDusv^t3tBdU`eXm?Cj3$&iVZ(b+#vy+1*JqyD#p6<(%{V
zmv7ED=bSlb8LOAN0(AxI3IwP?I*rZKS#!Ux8{Gk>^_{A-e=N~Ckh;)0klf)Xi2HGx
zkB<l&O@J6j#vajg=2M8M%qM=KzQEub%CVR+*wr<V`tY{BCqw0O1}=b1h5|S0xxy<X
zLR`jc)uxy5Xt=rc==x9pAVIuKRRx%#M6pUY@^4K~<8|JTP<3>JVQE9F`zPAHn7|F{
zIwnkRXl~qZpQDmcQ1rU}>23TY6!u!9rv2i!$^Etc$rZ>jDr56$3kEBI!+wvLzd(q4
zhkDwEWm{1Id#%!ReTkvCRdxutZ88<!h=;=`-Fer4gQ_uk?l9pS-L?SxSrTC@iEzZp
zSzH1c)9k47;BC$@v)Gdkxm=s)lk2Eo&B5DrQRKW&<}C`qEo+JlG*hcvfZ>D}13g%Z
zaN%VO6a#iv&R1LlHki%k*CDVi0<lqcnopY&stoZeuNdTT*3v?+HUZdT&nfin-Ua~A
zBeLc>M!x_+nD|>uxS!(r!sg@Kme^v?lvx5e;6m!ZbGaKs0^W7!;0ix6ky3ZJw?oct
z3$owUKfa1)(~s_)B<$zY8F=vYLoFHER$Kxy7<e1tP$aCrUhTn`dG0+4tEJmLc-vns
z6EtJWW^t*2{GBl^^f9TJujKEt3xz9vk%)TCh5PQV85mosFlBpFEYo{z(=x+%;3Y>3
znRt5SzP1la#g!ExlQHiQX!g?97|G?>SCr(>Qi<F+iKWva_)U8Ar`$Hb{muj9cQeL6
zp`E#5N{@U*ai{&pl3$v()cZ$z+P*9oQ&NCTu5b?%)^-Y>StbS;zLraqrX}g*cbJLL
z(Rdif%8{3|bsk7P$AIl7pP9RaBET2F5#?tz6W+5d)_0=k`k%{j*tR(W(1D2403M^k
z-&HBt%T90Jo~C=fY<{q<{iuxd3ED)_^Sn?g#}AuZb@F+plAmLrB>)XRN~b}Z@_4)B
zP|JGlq4SH9`3XA4;-*^h7C(dOboy#Vjl4|cOFhW}lK;J3Pjh8Zm<7Q<Ym9}Cd-<*z
z9B-q01RYUvrI($sU+I@EVXF)SWWloDrepAc>|7LJS~vI5DeWfN7<ij=i&^Ru@i?4w
z=cYd~z9+2OGp|5P`{v=E8-8>6rH!TJ>B;e3crAn?gjwQl4$Br!P%(qbfL|+n6$PM8
z_I)xGWMj~61U}Tz5ca$dqY#f1pBXf#oR6ma;P7bchzq;iH4N;?3v(wRTXY${)Utr@
z5@03WxZgf~9(CG7JTF2{j;_JfI=TlOr2d?{0R6l40B30L16Ni;;^NSjC9^I<cB0}Q
z;AGWxJ%L<(kqJyVm|f>NKD#&)`CZ1U(O!LUe7P$47j*bpF6XCv{4KPB{Ul>Sw2Mo?
zNfwL6l636d0i2IISS{=Cy4%haxacCzZ)?FjM*y3Yrs~D0roKRbEqz^!hj4R{2$N}X
zVFNwLKDRCCzNY{-35igMYN2;PQTEw_2wk5Hn!H@T(Vh--xVz<H+7ot?-xOVlu6X|`
h$5Xe}75JYk@GrW<rtuxF1FZl6002ovPDHLkV1ltumEiyY
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -244,16 +244,18 @@ browser.jar:
         skin/classic/browser/devtools/debugger-pause.png            (devtools/debugger-pause.png)
         skin/classic/browser/devtools/debugger-play.png             (devtools/debugger-play.png)
         skin/classic/browser/devtools/debugger-step-in.png          (devtools/debugger-step-in.png)
         skin/classic/browser/devtools/debugger-step-out.png         (devtools/debugger-step-out.png)
         skin/classic/browser/devtools/debugger-step-over.png        (devtools/debugger-step-over.png)
         skin/classic/browser/devtools/debugger-blackbox.png         (devtools/debugger-blackbox.png)
         skin/classic/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
         skin/classic/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
+        skin/classic/browser/devtools/tracer-icon.png               (devtools/tracer-icon.png)
+        skin/classic/browser/devtools/tracer-icon@2x.png            (devtools/tracer-icon@2x.png)
         skin/classic/browser/devtools/responsive-se-resizer.png     (devtools/responsive-se-resizer.png)
         skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
         skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
         skin/classic/browser/devtools/responsive-background.png     (devtools/responsive-background.png)
         skin/classic/browser/devtools/toggle-tools.png              (devtools/toggle-tools.png)
         skin/classic/browser/devtools/dock-bottom.png               (devtools/dock-bottom.png)
         skin/classic/browser/devtools/dock-side.png                 (devtools/dock-side.png)
         skin/classic/browser/devtools/floating-scrollbars.css       (devtools/floating-scrollbars.css)
@@ -552,16 +554,18 @@ browser.jar:
         skin/classic/aero/browser/devtools/debugger-pause.png        (devtools/debugger-pause.png)
         skin/classic/aero/browser/devtools/debugger-play.png         (devtools/debugger-play.png)
         skin/classic/aero/browser/devtools/debugger-step-in.png      (devtools/debugger-step-in.png)
         skin/classic/aero/browser/devtools/debugger-step-out.png     (devtools/debugger-step-out.png)
         skin/classic/aero/browser/devtools/debugger-step-over.png    (devtools/debugger-step-over.png)
         skin/classic/aero/browser/devtools/debugger-blackbox.png     (devtools/debugger-blackbox.png)
         skin/classic/aero/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
         skin/classic/aero/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
+        skin/classic/aero/devtools/tracer-icon.png                   (devtools/tracer-icon.png)
+        skin/classic/aero/devtools/tracer-icon@2x.png                (devtools/tracer-icon@2x.png)
         skin/classic/aero/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
         skin/classic/aero/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
         skin/classic/aero/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
         skin/classic/aero/browser/devtools/responsive-background.png (devtools/responsive-background.png)
         skin/classic/aero/browser/devtools/toggle-tools.png          (devtools/toggle-tools.png)
         skin/classic/aero/browser/devtools/dock-bottom.png           (devtools/dock-bottom.png)
         skin/classic/aero/browser/devtools/dock-side.png             (devtools/dock-side.png)
         skin/classic/aero/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -78,21 +78,47 @@ this.makeInfallible = function makeInfal
       if (aName) {
         who += " " + aName;
       }
       reportException(who, ex);
     }
   }
 }
 
+/**
+ * Interleaves two arrays element by element, returning the combined array, like
+ * a zip. In the case of arrays with different sizes, undefined values will be
+ * interleaved at the end along with the extra values of the larger array.
+ *
+ * @param Array a
+ * @param Array b
+ * @returns Array
+ *          The combined array, in the form [a1, b1, a2, b2, ...]
+ */
+this.zip = function zip(a, b) {
+  if (!b) {
+    return a;
+  }
+  if (!a) {
+    return b;
+  }
+  const pairs = [];
+  for (let i = 0, aLength = a.length, bLength = b.length;
+       i < aLength || i < bLength;
+       i++) {
+    pairs.push([a[i], b[i]]);
+  }
+  return pairs;
+};
+
 const executeSoon = aFn => {
   Services.tm.mainThread.dispatch({
     run: this.makeInfallible(aFn)
   }, Components.interfaces.nsIThread.DISPATCH_NORMAL);
-}
+};
 
 /**
  * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over
  * very large arrays by yielding to the browser and continuing execution on the
  * next tick.
  *
  * @param Array aArray
  *        The array being iterated over.
--- a/toolkit/devtools/DevToolsUtils.jsm
+++ b/toolkit/devtools/DevToolsUtils.jsm
@@ -17,14 +17,15 @@ this.EXPORTED_SYMBOLS = [ "DevToolsUtils
 Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
   .getService(Components.interfaces.mozIJSSubScriptLoader)
   .loadSubScript("resource://gre/modules/devtools/DevToolsUtils.js", this);
 
 this.DevToolsUtils = {
   safeErrorString: safeErrorString,
   reportException: reportException,
   makeInfallible: makeInfallible,
+  zip: zip,
   yieldingEach: yieldingEach,
   reportingDisabled: false , // Used by tests.
   defineLazyPrototypeGetter: defineLazyPrototypeGetter,
   getProperty: getProperty,
   hasSafeGetter: hasSafeGetter,
 };
--- a/toolkit/devtools/server/actors/tracer.js
+++ b/toolkit/devtools/server/actors/tracer.js
@@ -39,22 +39,22 @@ const { setTimeout } = require("sdk/time
  * The number of milliseconds we should buffer frame enter/exit packets before
  * sending.
  */
 const BUFFER_SEND_DELAY = 50;
 
 /**
  * The maximum number of arguments we will send for any single function call.
  */
-const MAX_ARGUMENTS = 5;
+const MAX_ARGUMENTS = 3;
 
 /**
  * The maximum number of an object's properties we will serialize.
  */
-const MAX_PROPERTIES = 5;
+const MAX_PROPERTIES = 3;
 
 /**
  * The complete set of trace types supported.
  */
 const TRACE_TYPES = new Set([
   "time",
   "return",
   "throw",
@@ -310,17 +310,16 @@ TraceActor.prototype = {
   /**
    * 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 = {
       type: "enteredFrame",
       sequence: this._sequence++
     };
 
     if (this._requestsForTraceType.name) {
       packet.name = aFrame.callee
         ? aFrame.callee.displayName || "(anonymous function)"
@@ -360,17 +359,17 @@ TraceActor.prototype = {
 
     if (this._requestsForTraceType.arguments && aFrame.arguments) {
       packet.arguments = [];
       let i = 0;
       for (let arg of aFrame.arguments) {
         if (i++ > MAX_ARGUMENTS) {
           break;
         }
-        packet.arguments.push(createValueGrip(arg, true));
+        packet.arguments.push(createValueSnapshot(arg, true));
       }
     }
 
     if (this._requestsForTraceType.depth) {
       packet.depth = aFrame.depth;
     }
 
     const onExitFrame = this.onExitFrame;
@@ -410,26 +409,26 @@ TraceActor.prototype = {
       packet.time = Date.now() - this._startTime;
     }
 
     if (this._requestsForTraceType.depth) {
       packet.depth = aFrame.depth;
     }
 
     if (aCompletion) {
-      if (this._requestsForTraceType.return) {
-        packet.return = createValueGrip(aCompletion.return, true);
+      if (this._requestsForTraceType.return && "return" in aCompletion) {
+        packet.return = createValueSnapshot(aCompletion.return, true);
       }
 
-      if (this._requestsForTraceType.throw) {
-        packet.throw = createValueGrip(aCompletion.throw, true);
+      else if (this._requestsForTraceType.throw && "throw" in aCompletion) {
+        packet.throw = createValueSnapshot(aCompletion.throw, true);
       }
 
-      if (this._requestsForTraceType.yield) {
-        packet.yield = createValueGrip(aCompletion.yield, true);
+      else if (this._requestsForTraceType.yield && "yield" in aCompletion) {
+        packet.yield = createValueSnapshot(aCompletion.yield, true);
       }
     }
 
     this._send(packet);
   }
 };
 
 /**
@@ -544,56 +543,36 @@ MapStack.prototype = {
     }
     return value;
   }
 };
 
 // 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;
+  return 0;
 }
 
-
 // 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.
  *
  * @param aValue Debugger.Object|primitive
  *        The value to describe with the created grip.
  *
- * @param aUseDescriptor boolean
- *        If true, creates descriptors for objects rather than grips.
+ * @param aDetailed boolean
+ *        If true, capture slightly more detailed information, like some
+ *        properties on an object.
  *
- * @return ValueGrip
- *        A primitive value or a grip object.
+ * @return Object
+ *         A primitive value or a snapshot of an object.
  */
-function createValueGrip(aValue, aUseDescriptor) {
+function createValueSnapshot(aValue, aDetailed=false) {
   switch (typeof aValue) {
     case "boolean":
       return aValue;
     case "string":
       if (aValue.length >= DebuggerServer.LONG_STRING_LENGTH) {
         return {
           type: "longString",
           initial: aValue.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH),
@@ -613,145 +592,102 @@ function createValueGrip(aValue, aUseDes
       }
       return aValue;
     case "undefined":
       return { type: "undefined" };
     case "object":
       if (aValue === null) {
         return { type: "null" };
       }
-      return aUseDescriptor ? objectDescriptor(aValue) : objectGrip(aValue);
+      return aDetailed
+        ? detailedObjectSnapshot(aValue)
+        : objectSnapshot(aValue);
     default:
       reportException("TraceActor",
                       new Error("Failed to provide a grip for: " + aValue));
       return null;
   }
 }
 
 /**
- * Create a grip for the given debuggee object.
+ * Create a very minimal snapshot of the given debuggee object.
  *
  * @param aObject Debugger.Object
  *        The object to describe with the created grip.
  */
-function objectGrip(aObject) {
-  let g = {
+function objectSnapshot(aObject) {
+  return {
     "type": "object",
     "class": aObject.class,
-    "extensible": aObject.isExtensible(),
-    "frozen": aObject.isFrozen(),
-    "sealed": aObject.isSealed()
   };
-
-  // Add additional properties for functions.
-  if (aObject.class === "Function") {
-    if (aObject.name) {
-      g.name = aObject.name;
-    }
-    if (aObject.displayName) {
-      g.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") {
-      g.userDisplayName = createValueGrip(name.value, aObject);
-    }
-
-    // Add source location information.
-    if (aObject.script) {
-      g.url = aObject.script.url;
-      g.line = aObject.script.startLine;
-    }
-  }
-
-  return g;
 }
 
 /**
- * Create a descriptor for the given debuggee object. Descriptors are
- * identical to grips, with the addition of the prototype,
- * ownProperties, and safeGetterValues properties.
+ * Create a (slightly more) detailed snapshot of the given debuggee object.
  *
  * @param aObject Debugger.Object
  *        The object to describe with the created descriptor.
  */
-function objectDescriptor(aObject) {
-  let desc = objectGrip(aObject);
-  let ownProperties = Object.create(null);
+function detailedObjectSnapshot(aObject) {
+  let desc = objectSnapshot(aObject);
+  let ownProperties = desc.ownProperties = Object.create(null);
 
-  if (Cu.isDeadWrapper(aObject)) {
-    desc.prototype = createValueGrip(null);
-    desc.ownProperties = ownProperties;
-    desc.safeGetterValues = Object.create(null);
+  if (aObject.class == "DeadObject") {
     return desc;
   }
 
-  const names = aObject.getOwnPropertyNames();
   let i = 0;
-  for (let name of names) {
+  for (let name of aObject.getOwnPropertyNames()) {
     if (i++ > MAX_PROPERTIES) {
       break;
     }
-    let desc = propertyDescriptor(name, aObject);
+    let desc = propertySnapshot(name, aObject);
     if (desc) {
       ownProperties[name] = desc;
     }
   }
 
-  desc.ownProperties = ownProperties;
-
   return desc;
 }
 
 /**
- * A helper method that creates a property descriptor for the provided object,
- * properly formatted for sending in a protocol response.
+ * A helper method that creates a snapshot of the object's |aName| property.
  *
  * @param aName string
- *        The property that the descriptor is generated for.
+ *        The property of which the snapshot is taken.
  *
  * @param aObject Debugger.Object
- *        The object whose property the descriptor is generated for.
+ *        The object whose property the snapshot is taken of.
  *
- * @return object
- *         The property descriptor for the property |aName| in |aObject|.
+ * @return Object
+ *         The snapshot of the property.
  */
-function propertyDescriptor(aName, aObject) {
+function propertySnapshot(aName, aObject) {
   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
     };
   }
 
-  // Skip objects since we only support shallow objects anyways.
-  if (!desc || typeof desc.value == "object" && desc.value !== null) {
+  // Only create descriptors for simple values. We skip objects and properties
+  // that have getters and setters; ain't nobody got time for that!
+  if (!desc
+      || typeof desc.value == "object" && desc.value !== null
+      || !("value" in desc)) {
     return undefined;
   }
 
-  let retval = {
+  return {
     configurable: desc.configurable,
-    enumerable: desc.enumerable
+    enumerable: desc.enumerable,
+    writable: desc.writable,
+    value: createValueSnapshot(desc.value)
   };
-
-  if ("value" in desc) {
-    retval.writable = desc.writable;
-    retval.value = createValueGrip(desc.value);
-  } else {
-    if ("get" in desc) {
-      retval.get = createValueGrip(desc.get);
-    }
-    if ("set" in desc) {
-      retval.set = createValueGrip(desc.set);
-    }
-  }
-  return retval;
 }
--- a/toolkit/devtools/server/tests/unit/test_trace_actor-05.js
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-05.js
@@ -84,21 +84,24 @@ function test_enter_exit_frame()
     .then(stop_trace)
     .then(function() {
       let url = getFileUrl("tracerlocations.js");
 
       check_location(traces[0].location, { url: url, line: 1, column: 0 });
 
       do_check_eq(traces[1].name, "foo");
 
-      // foo's definition is at tracerlocations.js:3:0, but
-      // Debugger.Script does not provide complete definition
-      // locations (bug 901138). tracerlocations.js:4:2 is the first
-      // statement in the function (used as an approximation).
-      check_location(traces[1].location, { url: url, line: 4, column: 2 });
+      // XXX: foo's definition is at tracerlocations.js:3:0, but Debugger.Script
+      // does not provide complete definition locations (bug 901138). Therefore,
+      // we use the first statement in the function (tracerlocations.js:4:2) as
+      // an approximation.
+      //
+      // However, |column| will always be 0 until we can get bug 863089
+      // fixed.
+      check_location(traces[1].location, { url: url, line: 4, column: 0 });
       check_location(traces[1].callsite, { url: url, line: 8, column: 0 });
 
       do_check_eq(typeof traces[1].parameterNames, "object");
       do_check_eq(traces[1].parameterNames.length, 1);
       do_check_eq(traces[1].parameterNames[0], "x");
 
       do_check_eq(typeof traces[1].arguments, "object");
       do_check_true(Array.isArray(traces[1].arguments));
--- a/toolkit/devtools/server/tests/unit/test_trace_actor-06.js
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-06.js
@@ -62,49 +62,52 @@ function eval_code()
   gDebuggee.eval("(" + function() {
     function identity(x) {
       return x;
     }
 
     let circular = {};
     circular.self = circular;
 
-    // Make sure there is only 5 properties per object because that is the value
+    // Make sure there is only 3 properties per object because that is the value
     // of MAX_PROPERTIES in the server.
     let obj = {
       num: 0,
       str: "foo",
       bool: false,
-      undef: undefined,
-      nil: null
     };
     let obj2 = {
-      inf: Infinity,
+      undef: undefined,
+      nil: null,
+      inf: Infinity
+    };
+    let obj3 = {
       ninf: -Infinity,
       nan: NaN,
-      nzero: -0,
-      obj: circular
+      nzero: -0
     };
-    let obj3 = {
+    let obj4 = {
+      obj: circular,
       arr: [1,2,3,4,5]
     };
 
     identity();
     identity(0);
     identity("");
     identity(false);
     identity(undefined);
     identity(null);
     identity(Infinity);
     identity(-Infinity);
     identity(NaN);
     identity(-0);
     identity(obj);
     identity(obj2);
     identity(obj3);
+    identity(obj4);
   } + ")()");
 }
 
 function stop_trace()
 {
   let deferred = defer();
   gTraceClient.stopTrace(null, function() { deferred.resolve(); });
   return deferred.promise;
@@ -165,16 +168,21 @@ function check_trace(aTrace)
     break;
   case 24:
   case 25:
     check_obj2(aTrace.type, value);
     break;
   case 26:
   case 27:
     check_obj3(aTrace.type, value);
+    break;
+  case 28:
+  case 29:
+    check_obj4(aTrace.type, value);
+    break;
   }
 }
 
 function check_value(aActual, aExpectedType, aExpectedValue)
 {
   do_check_eq(typeof aActual, aExpectedType);
   do_check_eq(aExpectedType === "object" ? aActual.type : aActual, aExpectedValue);
 }
@@ -186,43 +194,43 @@ function check_obj(aType, aObj) {
   do_check_eq(typeof aObj.ownProperties.num, "object");
   do_check_eq(aObj.ownProperties.num.value, 0);
 
   do_check_eq(typeof aObj.ownProperties.str, "object");
   do_check_eq(aObj.ownProperties.str.value, "foo");
 
   do_check_eq(typeof aObj.ownProperties.bool, "object");
   do_check_eq(aObj.ownProperties.bool.value, false);
+}
 
+function check_obj2(aType, aObj) {
   do_check_eq(typeof aObj.ownProperties.undef, "object");
   do_check_eq(typeof aObj.ownProperties.undef.value, "object");
   do_check_eq(aObj.ownProperties.undef.value.type, "undefined");
 
   do_check_eq(typeof aObj.ownProperties.nil, "object");
   do_check_eq(typeof aObj.ownProperties.nil.value, "object");
   do_check_eq(aObj.ownProperties.nil.value.type, "null");
-}
 
-function check_obj2(aType, aObj) {
   do_check_eq(typeof aObj.ownProperties.inf, "object");
   do_check_eq(typeof aObj.ownProperties.inf.value, "object");
   do_check_eq(aObj.ownProperties.inf.value.type, "Infinity");
+}
 
+function check_obj3(aType, aObj) {
   do_check_eq(typeof aObj.ownProperties.ninf, "object");
   do_check_eq(typeof aObj.ownProperties.ninf.value, "object");
   do_check_eq(aObj.ownProperties.ninf.value.type, "-Infinity");
 
   do_check_eq(typeof aObj.ownProperties.nan, "object");
   do_check_eq(typeof aObj.ownProperties.nan.value, "object");
   do_check_eq(aObj.ownProperties.nan.value.type, "NaN");
 
   do_check_eq(typeof aObj.ownProperties.nzero, "object");
   do_check_eq(typeof aObj.ownProperties.nzero.value, "object");
   do_check_eq(aObj.ownProperties.nzero.value.type, "-0");
-
-  // Sub-objects aren't added.
-  do_check_eq(typeof aObj.ownProperties.obj, "undefined");
 }
 
-function check_obj3(aType, aObj) {
+function check_obj4(aType, aObj) {
   // Sub-objects aren't added.
+  do_check_eq(typeof aObj.ownProperties.obj, "undefined");
   do_check_eq(typeof aObj.ownProperties.arr, "undefined");
 }