From 77e22fc34d55fb54f8d38e52e4b55b41d5025faa Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Wed, 18 Dec 2013 14:17:27 -0800 Subject: [PATCH] Bug 929349 - Integrate a tracing debugger into our existing debugger; r=vporof,past --- browser/app/profile/firefox.js | 1 + .../devtools/debugger/debugger-controller.js | 291 ++++++++++++-- browser/devtools/debugger/debugger-panes.js | 371 ++++++++++++++++++ browser/devtools/debugger/debugger-view.js | 9 +- browser/devtools/debugger/debugger.xul | 49 +++ browser/devtools/debugger/panel.js | 5 +- browser/devtools/debugger/test/browser.ini | 6 + .../debugger/test/browser_dbg_tracing-01.js | 109 +++++ .../debugger/test/browser_dbg_tracing-02.js | 78 ++++ .../debugger/test/browser_dbg_tracing-03.js | 70 ++++ .../debugger/test/browser_dbg_tracing-04.js | 86 ++++ .../devtools/debugger/test/code_tracing-01.js | 29 ++ .../debugger/test/doc_tracing-01.html | 12 + browser/devtools/debugger/test/head.js | 55 +++ .../devtools/shared/widgets/FastListWidget.js | 210 ++++++++++ .../devtools/shared/widgets/VariablesView.jsm | 2 +- .../chrome/browser/devtools/debugger.dtd | 17 + .../browser/devtools/debugger.properties | 8 + browser/themes/linux/devtools/debugger.css | 118 +++++- browser/themes/linux/devtools/tracer-icon.png | Bin 0 -> 709 bytes .../themes/linux/devtools/tracer-icon@2x.png | Bin 0 -> 1323 bytes browser/themes/linux/jar.mn | 2 + browser/themes/osx/devtools/debugger.css | 118 +++++- browser/themes/osx/devtools/tracer-icon.png | Bin 0 -> 709 bytes .../themes/osx/devtools/tracer-icon@2x.png | Bin 0 -> 1323 bytes browser/themes/osx/jar.mn | 2 + browser/themes/windows/devtools/debugger.css | 118 +++++- .../themes/windows/devtools/tracer-icon.png | Bin 0 -> 709 bytes .../windows/devtools/tracer-icon@2x.png | Bin 0 -> 1323 bytes browser/themes/windows/jar.mn | 4 + toolkit/devtools/DevToolsUtils.js | 28 +- toolkit/devtools/DevToolsUtils.jsm | 1 + toolkit/devtools/server/actors/tracer.js | 152 +++---- .../server/tests/unit/test_trace_actor-05.js | 13 +- .../server/tests/unit/test_trace_actor-06.js | 36 +- 35 files changed, 1822 insertions(+), 178 deletions(-) create mode 100644 browser/devtools/debugger/test/browser_dbg_tracing-01.js create mode 100644 browser/devtools/debugger/test/browser_dbg_tracing-02.js create mode 100644 browser/devtools/debugger/test/browser_dbg_tracing-03.js create mode 100644 browser/devtools/debugger/test/browser_dbg_tracing-04.js create mode 100644 browser/devtools/debugger/test/code_tracing-01.js create mode 100644 browser/devtools/debugger/test/doc_tracing-01.html create mode 100644 browser/devtools/shared/widgets/FastListWidget.js create mode 100644 browser/themes/linux/devtools/tracer-icon.png create mode 100644 browser/themes/linux/devtools/tracer-icon@2x.png create mode 100644 browser/themes/osx/devtools/tracer-icon.png create mode 100644 browser/themes/osx/devtools/tracer-icon@2x.png create mode 100644 browser/themes/windows/devtools/tracer-icon.png create mode 100644 browser/themes/windows/devtools/tracer-icon@2x.png diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index de31be3d936..b176fbeafed 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1121,6 +1121,7 @@ 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); diff --git a/browser/devtools/debugger/debugger-controller.js b/browser/devtools/debugger/debugger-controller.js index ea42fb5ecd6..f03d25c453a 100644 --- a/browser/devtools/debugger/debugger-controller.js +++ b/browser/devtools/debugger/debugger-controller.js @@ -99,6 +99,7 @@ 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"); @@ -192,6 +193,7 @@ let DebuggerController = { this.SourceScripts.disconnect(); this.StackFrames.disconnect(); this.ThreadState.disconnect(); + this.Tracer.disconnect(); this.disconnect(); // Chrome debugging needs to close its parent process on shutdown. @@ -218,39 +220,44 @@ let DebuggerController = { 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; }, /** @@ -331,21 +338,13 @@ let DebuggerController = { /** * 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; @@ -366,21 +365,13 @@ let DebuggerController = { /** * 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; @@ -398,6 +389,30 @@ let DebuggerController = { }, { 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. @@ -1411,6 +1426,218 @@ SourceScripts.prototype = { } }; +/** + * 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. */ @@ -1955,6 +2182,7 @@ let Prefs = new ViewHelpers.Prefs("devtools", { 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"] }); @@ -1982,6 +2210,7 @@ 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. diff --git a/browser/devtools/debugger/debugger-panes.js b/browser/devtools/debugger/debugger-panes.js index 352f6fdfd33..c8e6e084c16 100644 --- a/browser/devtools/debugger/debugger-panes.js +++ b/browser/devtools/debugger/debugger-panes.js @@ -1056,6 +1056,376 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, { _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: "" }); + } 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. */ @@ -2789,6 +3159,7 @@ LineResults.size = function() { */ DebuggerView.Sources = new SourcesView(); DebuggerView.VariableBubble = new VariableBubbleView(); +DebuggerView.Tracer = new TracerView(); DebuggerView.WatchExpressions = new WatchExpressionsView(); DebuggerView.EventListeners = new EventListenersView(); DebuggerView.GlobalSearch = new GlobalSearchView(); diff --git a/browser/devtools/debugger/debugger-view.js b/browser/devtools/debugger/debugger-view.js index 51ac8e0c4da..f6f87f4aaa3 100644 --- a/browser/devtools/debugger/debugger-view.js +++ b/browser/devtools/debugger/debugger-view.js @@ -60,6 +60,7 @@ let DebuggerView = { this.StackFramesClassicList.initialize(); this.Sources.initialize(); this.VariableBubble.initialize(); + this.Tracer.initialize(); this.WatchExpressions.initialize(); this.EventListeners.initialize(); this.GlobalSearch.initialize(); @@ -95,6 +96,7 @@ let DebuggerView = { this.StackFramesClassicList.destroy(); this.Sources.destroy(); this.VariableBubble.destroy(); + this.Tracer.destroy(); this.WatchExpressions.destroy(); this.EventListeners.destroy(); this.GlobalSearch.destroy(); @@ -169,7 +171,11 @@ let DebuggerView = { // 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. @@ -637,6 +643,7 @@ let DebuggerView = { ChromeGlobals: null, StackFrames: null, Sources: null, + Tracer: null, Variables: null, VariableBubble: null, WatchExpressions: null, diff --git a/browser/devtools/debugger/debugger.xul b/browser/devtools/debugger/debugger.xul index 3e2a3287349..edaf9cb2b55 100644 --- a/browser/devtools/debugger/debugger.xul +++ b/browser/devtools/debugger/debugger.xul @@ -86,6 +86,12 @@ oncommand="DebuggerView.Options._toggleShowVariablesFilterBox()"/> + + + @@ -304,6 +310,13 @@ class="devtools-toolbarbutton" tabindex="0"/> + +