diff --git a/browser/devtools/performance/modules/front.js b/browser/devtools/performance/modules/front.js index 85c2e2c74d78..c59245e4b0f2 100644 --- a/browser/devtools/performance/modules/front.js +++ b/browser/devtools/performance/modules/front.js @@ -202,6 +202,7 @@ function PerformanceFront(connection) { // Pipe events from TimelineActor to the PerformanceFront connection._timeline.on("markers", markers => this.emit("markers", markers)); + connection._timeline.on("frames", (delta, frames) => this.emit("frames", delta, frames)); connection._timeline.on("memory", (delta, measurement) => this.emit("memory", delta, measurement)); connection._timeline.on("ticks", (delta, timestamps) => this.emit("ticks", delta, timestamps)); } diff --git a/browser/devtools/performance/performance-controller.js b/browser/devtools/performance/performance-controller.js index a58ad17715fd..ef0f0cd514ee 100644 --- a/browser/devtools/performance/performance-controller.js +++ b/browser/devtools/performance/performance-controller.js @@ -147,6 +147,7 @@ let PerformanceController = { _startTime: RECORDING_UNAVAILABLE, _endTime: RECORDING_UNAVAILABLE, _markers: [], + _frames: [], _memory: [], _ticks: [], _profilerData: {}, @@ -169,6 +170,7 @@ let PerformanceController = { gFront.on("ticks", this._onTimelineData); // framerate gFront.on("markers", this._onTimelineData); // timeline markers + gFront.on("frames", this._onTimelineData); // stack frames gFront.on("memory", this._onTimelineData); // timeline memory }, @@ -183,6 +185,7 @@ let PerformanceController = { gFront.off("ticks", this._onTimelineData); gFront.off("markers", this._onTimelineData); + gFront.off("frames", this._onTimelineData); gFront.off("memory", this._onTimelineData); }, @@ -205,6 +208,7 @@ let PerformanceController = { this._startTime = startTime; this._endTime = RECORDING_IN_PROGRESS; this._markers = []; + this._frames = []; this._memory = []; this._ticks = []; @@ -256,6 +260,7 @@ let PerformanceController = { this._startTime = recordingData.interval.startTime; this._endTime = recordingData.interval.endTime; this._markers = recordingData.markers; + this._frames = recordingData.frames; this._memory = recordingData.memory; this._ticks = recordingData.ticks; this._profilerData = recordingData.profilerData; @@ -300,6 +305,14 @@ let PerformanceController = { return this._markers; }, + /** + * Gets the accumulated stack frames in the current recording. + * @return array + */ + getFrames: function() { + return this._frames; + }, + /** * Gets the accumulated memory measurements in this recording. * @return array @@ -330,10 +343,11 @@ let PerformanceController = { getAllData: function() { let interval = this.getInterval(); let markers = this.getMarkers(); + let frames = this.getFrames(); let memory = this.getMemory(); let ticks = this.getTicks(); let profilerData = this.getProfilerData(); - return { interval, markers, memory, ticks, profilerData }; + return { interval, markers, frames, memory, ticks, profilerData }; }, /** @@ -345,6 +359,11 @@ let PerformanceController = { let [markers] = data; Array.prototype.push.apply(this._markers, markers); } + // Accumulate stack frames into an array. + else if (eventName == "frames") { + let [delta, frames] = data; + Array.prototype.push.apply(this._frames, frames); + } // Accumulate memory measurements into an array. else if (eventName == "memory") { let [delta, measurement] = data; diff --git a/browser/devtools/performance/views/details-waterfall.js b/browser/devtools/performance/views/details-waterfall.js index 4d9ab9d427ee..77e0e1890d02 100644 --- a/browser/devtools/performance/views/details-waterfall.js +++ b/browser/devtools/performance/views/details-waterfall.js @@ -72,7 +72,11 @@ let WaterfallView = { */ _onMarkerSelected: function (event, marker) { if (event === "selected") { - this.markerDetails.render(marker); + this.markerDetails.render({ + toolbox: gToolbox, + marker: marker, + frames: PerformanceController.getFrames() + }); } if (event === "unselected") { this.markerDetails.empty(); diff --git a/browser/devtools/timeline/timeline.js b/browser/devtools/timeline/timeline.js index 73f67c001320..ea94dac0c445 100644 --- a/browser/devtools/timeline/timeline.js +++ b/browser/devtools/timeline/timeline.js @@ -77,6 +77,7 @@ let TimelineController = { _endTime: 0, _markers: [], _memory: [], + _frames: [], /** * Initialization function, called when the tool is started. @@ -85,8 +86,10 @@ let TimelineController = { this._onRecordingTick = this._onRecordingTick.bind(this); this._onMarkers = this._onMarkers.bind(this); this._onMemory = this._onMemory.bind(this); + this._onFrames = this._onFrames.bind(this); gFront.on("markers", this._onMarkers); gFront.on("memory", this._onMemory); + gFront.on("frames", this._onFrames); }, /** @@ -95,6 +98,7 @@ let TimelineController = { destroy: function() { gFront.off("markers", this._onMarkers); gFront.off("memory", this._onMemory); + gFront.off("frames", this._onFrames); }, /** @@ -121,6 +125,16 @@ let TimelineController = { return this._memory; }, + /** + * Gets stack frame array reported by the actor. The marker "stack" + * and "endStack" properties are indices into this array. See + * actors/utils/stack.js for more details. + * @return array + */ + getFrames: function() { + return this._frames; + }, + /** * Updates the views to show or hide the memory recording data. */ @@ -163,6 +177,7 @@ let TimelineController = { this._endTime = startTime; this._markers = []; this._memory = []; + this._frames = []; this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL); }, @@ -226,6 +241,18 @@ let TimelineController = { this._memory.push({ delta, value: measurement.total / 1024 / 1024 }); }, + /** + * Callback handling the "frames" event on the timeline front. + * + * @param number delta + * The number of milliseconds elapsed since epoch. + * @param object frames + * Newly generated frame objects. + */ + _onFrames: function(delta, frames) { + Array.prototype.push.apply(this._frames, frames); + }, + /** * Callback invoked at a fixed interval while recording. * Updates the current time and the timeline overview. @@ -318,7 +345,11 @@ let TimelineView = { */ _onMarkerSelected: function(event, marker) { if (event == "selected") { - this.markerDetails.render(marker); + this.markerDetails.render({ + toolbox: gToolbox, + marker: marker, + frames: TimelineController.getFrames() + }); } if (event == "unselected") { this.markerDetails.empty(); diff --git a/browser/devtools/timeline/widgets/marker-details.js b/browser/devtools/timeline/widgets/marker-details.js index 72a400b0e9dd..1183ed941bca 100644 --- a/browser/devtools/timeline/widgets/marker-details.js +++ b/browser/devtools/timeline/widgets/marker-details.js @@ -5,6 +5,7 @@ "use strict"; let { Ci } = require("chrome"); +let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; /** * This file contains the rendering code for the marker sidebar. @@ -103,10 +104,13 @@ MarkerDetails.prototype = { /** * Populates view with marker's details. * - * @param object marker - * The marker to display. + * @param object params + * An options object holding: + * toolbox - The toolbox. + * marker - The marker to display. + * frames - Array of stack frame information; see stack.js. */ - render: function(marker) { + render: function({toolbox: toolbox, marker: marker, frames: frames}) { this.empty(); // UI for any marker @@ -139,6 +143,84 @@ MarkerDetails.prototype = { break; default: } + + if (marker.stack) { + let property = "timeline.markerDetail.stack"; + if (marker.endStack) { + property = "timeline.markerDetail.startStack"; + } + this.renderStackTrace({toolbox: toolbox, parent: this._parent, property: property, + frameIndex: marker.stack, frames: frames}); + } + + if (marker.endStack) { + this.renderStackTrace({toolbox: toolbox, parent: this._parent, property: "timeline.markerDetail.endStack", + frameIndex: marker.endStack, frames: frames}); + } + }, + + /** + * Render a stack trace. + * + * @param object params + * An options object with the following members: + * object toolbox - The toolbox. + * nsIDOMNode parent - The parent node holding the view. + * string property - String identifier for label's name. + * integer frameIndex - The index of the topmost stack frame. + * array frames - Array of stack frames. + */ + renderStackTrace: function({toolbox: toolbox, parent: parent, + property: property, frameIndex: frameIndex, + frames: frames}) { + let labelName = this._document.createElement("label"); + labelName.className = "plain marker-details-labelname"; + labelName.setAttribute("value", L10N.getStr(property)); + parent.appendChild(labelName); + + while (frameIndex > 0) { + let frame = frames[frameIndex]; + let url = frame.source; + let displayName = frame.functionDisplayName; + let line = frame.line; + + let hbox = this._document.createElement("hbox"); + + if (displayName) { + let functionLabel = this._document.createElement("label"); + functionLabel.setAttribute("value", displayName); + hbox.appendChild(functionLabel); + } + + if (url) { + let aNode = this._document.createElement("a"); + aNode.className = "waterfall-marker-location theme-link devtools-monospace"; + aNode.href = url; + aNode.draggable = false; + aNode.setAttribute("title", url); + + let text = WebConsoleUtils.abbreviateSourceURL(url) + ":" + line; + let label = this._document.createElement("label"); + label.setAttribute("value", text); + aNode.appendChild(label); + hbox.appendChild(aNode); + + aNode.addEventListener("click", (event) => { + event.preventDefault(); + viewSourceInDebugger(toolbox, url, line); + }); + } + + if (!displayName && !url) { + let label = this._document.createElement("label"); + label.setAttribute("value", L10N.getStr("timeline.markerDetail.unknownFrame")); + hbox.appendChild(label); + } + + parent.appendChild(hbox); + + frameIndex = frame.parent; + } }, /** @@ -185,6 +267,36 @@ MarkerDetails.prototype = { } }, -} +}; + +/** + * Opens/selects the debugger in this toolbox and jumps to the specified + * file name and line number. + * @param object toolbox + * The toolbox. + * @param string url + * @param number line + */ +let viewSourceInDebugger = Task.async(function *(toolbox, url, line) { + // If the Debugger was already open, switch to it and try to show the + // source immediately. Otherwise, initialize it and wait for the sources + // to be added first. + let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger"); + let { panelWin: dbg } = yield toolbox.selectTool("jsdebugger"); + + if (!debuggerAlreadyOpen) { + yield dbg.once(dbg.EVENTS.SOURCES_ADDED); + } + + let { DebuggerView } = dbg; + let { Sources } = DebuggerView; + + let item = Sources.getItemForAttachment(a => a.source.url === url); + if (item) { + return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true }); + } + + return Promise.reject("Couldn't find the specified source in the debugger."); +}); exports.MarkerDetails = MarkerDetails; diff --git a/browser/locales/en-US/chrome/browser/devtools/timeline.properties b/browser/locales/en-US/chrome/browser/devtools/timeline.properties index 0ca936130250..19b825f18b24 100644 --- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties +++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties @@ -65,3 +65,7 @@ timeline.markerDetail.DOMEventPhase=Phase: timeline.markerDetail.DOMEventTargetPhase=Target timeline.markerDetail.DOMEventCapturingPhase=Capture timeline.markerDetail.DOMEventBubblingPhase=Bubbling +timeline.markerDetail.stack=Stack: +timeline.markerDetail.startStack=Stack at start: +timeline.markerDetail.endStack=Stack at end: +timeline.markerDetail.unknownFrame= diff --git a/browser/themes/shared/devtools/performance.inc.css b/browser/themes/shared/devtools/performance.inc.css index 5a9d2adef114..1dc6c46d4949 100644 --- a/browser/themes/shared/devtools/performance.inc.css +++ b/browser/themes/shared/devtools/performance.inc.css @@ -319,6 +319,15 @@ border-color: initial !important; } +.waterfall-marker-location { + color: -moz-nativehyperlinktext; +} + +.waterfall-marker-location:hover, +.waterfall-marker-location:focus { + text-decoration: underline; +} + #waterfall-details { -moz-padding-start: 8px; -moz-padding-end: 8px; diff --git a/browser/themes/shared/devtools/timeline.inc.css b/browser/themes/shared/devtools/timeline.inc.css index 46249335dd14..09532cd022e9 100644 --- a/browser/themes/shared/devtools/timeline.inc.css +++ b/browser/themes/shared/devtools/timeline.inc.css @@ -175,6 +175,15 @@ border-color: initial!important; } +.waterfall-marker-location { + color: -moz-nativehyperlinktext; +} + +.waterfall-marker-location:hover, +.waterfall-marker-location:focus { + text-decoration: underline; +} + #timeline-waterfall-details { -moz-padding-start: 8px; -moz-padding-end: 8px; diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 9cb9163f3673..aead1f614a1e 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -2887,6 +2887,7 @@ nsDocShell::PopProfileTimelineMarkers(JSContext* aCx, // docShell if an Layer marker type was recorded too. nsTArray profileTimelineMarkers; + SequenceRooter rooter(aCx, &profileTimelineMarkers); // If we see an unpaired START, we keep it around for the next call // to PopProfileTimelineMarkers. We store the kept START objects in @@ -2938,17 +2939,18 @@ nsDocShell::PopProfileTimelineMarkers(JSContext* aCx, } else { // But ignore paint start/end if no layer has been painted. if (!isPaint || (isPaint && hasSeenPaintedLayer)) { - mozilla::dom::ProfileTimelineMarker marker; + mozilla::dom::ProfileTimelineMarker* marker = + profileTimelineMarkers.AppendElement(); - marker.mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); - marker.mStart = startPayload->GetTime(); - marker.mEnd = endPayload->GetTime(); + marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); + marker->mStart = startPayload->GetTime(); + marker->mEnd = endPayload->GetTime(); + marker->mStack = startPayload->GetStack(); if (isPaint) { - marker.mRectangles.Construct(layerRectangles); - } else { - startPayload->AddDetails(marker); + marker->mRectangles.Construct(layerRectangles); } - profileTimelineMarkers.AppendElement(marker); + startPayload->AddDetails(*marker); + endPayload->AddDetails(*marker); } // We want the start to be dropped either way. diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index f4e1f77e0f55..41cb8516fda6 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -23,6 +23,7 @@ #include "mozilla/TimeStamp.h" #include "GeckoProfiler.h" #include "mozilla/dom/ProfileTimelineMarkerBinding.h" +#include "jsapi.h" // Helper Classes #include "nsCOMPtr.h" @@ -30,6 +31,7 @@ #include "nsString.h" #include "nsAutoPtr.h" #include "nsThreadUtils.h" +#include "nsContentUtils.h" // Threshold value in ms for META refresh based redirects #define REFRESH_REDIRECT_TIMER 15000 @@ -272,6 +274,9 @@ public: MOZ_COUNT_CTOR(TimelineMarker); MOZ_ASSERT(aName); aDocShell->Now(&mTime); + if (aMetaData == TRACING_INTERVAL_START) { + CaptureStack(); + } } TimelineMarker(nsDocShell* aDocShell, const char* aName, @@ -284,6 +289,9 @@ public: MOZ_COUNT_CTOR(TimelineMarker); MOZ_ASSERT(aName); aDocShell->Now(&mTime); + if (aMetaData == TRACING_INTERVAL_START) { + CaptureStack(); + } } virtual ~TimelineMarker() @@ -300,7 +308,10 @@ public: } // Add details specific to this marker type to aMarker. The - // standard elements have already been set. + // standard elements have already been set. This method is + // called on both the starting and ending markers of a pair. + // Ordinarily the ending marker doesn't need to do anything + // here. virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) { } @@ -330,11 +341,42 @@ public: return mCause; } + JSObject* GetStack() + { + if (mStackTrace) { + return mStackTrace->get(); + } + return nullptr; + } + + protected: + + void CaptureStack() + { + JSContext* ctx = nsContentUtils::GetCurrentJSContext(); + if (ctx) { + JS::RootedObject stack(ctx); + if (JS::CaptureCurrentStack(ctx, &stack)) { + mStackTrace.emplace(ctx, stack.get()); + } else { + JS_ClearPendingException(ctx); + } + } + } + private: + const char* mName; TracingMetadata mMetaData; DOMHighResTimeStamp mTime; nsString mCause; + + // While normally it is not a good idea to make a persistent + // root, in this case changing nsDocShell to participate in + // cycle collection was deemed too invasive, the stack trace + // can't actually cause a cycle, and the markers are only held + // here temporarily to boot. + mozilla::Maybe> mStackTrace; }; // Add new profile timeline markers to this docShell. This will only add diff --git a/docshell/test/browser/browser.ini b/docshell/test/browser/browser.ini index b08b5d09a333..cd8422060a61 100644 --- a/docshell/test/browser/browser.ini +++ b/docshell/test/browser/browser.ini @@ -39,6 +39,7 @@ support-files = browser_timelineMarkers-frame-02.js browser_timelineMarkers-frame-03.js browser_timelineMarkers-frame-04.js + browser_timelineMarkers-frame-05.js head.js frame-head.js @@ -105,3 +106,4 @@ skip-if = e10s [browser_timelineMarkers-02.js] [browser_timelineMarkers-03.js] [browser_timelineMarkers-04.js] +[browser_timelineMarkers-05.js] diff --git a/docshell/test/browser/browser_timelineMarkers-05.js b/docshell/test/browser/browser_timelineMarkers-05.js new file mode 100644 index 000000000000..cc6eb58f2948 --- /dev/null +++ b/docshell/test/browser/browser_timelineMarkers-05.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let URL = '
'; +URL = "data:text/html;charset=utf8," + encodeURIComponent(URL); + +let test = makeTimelineTest("browser_timelineMarkers-frame-05.js", URL); diff --git a/docshell/test/browser/browser_timelineMarkers-frame-05.js b/docshell/test/browser/browser_timelineMarkers-frame-05.js new file mode 100644 index 000000000000..d43d42651fac --- /dev/null +++ b/docshell/test/browser/browser_timelineMarkers-frame-05.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function forceSyncReflow(div) { + div.setAttribute('class', 'resize-change-color'); + // Force a reflow. + return div.offsetWidth; +} + +function testSendingEvent() { + content.document.body.dispatchEvent(new content.Event("dog")); +} + +function testConsoleTime() { + content.console.time("cats"); +} + +function testConsoleTimeEnd() { + content.console.timeEnd("cats"); +} + +let TESTS = [{ + desc: "Stack trace on sync reflow", + searchFor: "Reflow", + setup: function(docShell) { + let div = content.document.querySelector("div"); + forceSyncReflow(div); + }, + check: function(markers) { + markers = markers.filter(m => m.name == "Reflow"); + ok(markers.length > 0, "Reflow marker includes stack"); + ok(markers[0].stack.functionDisplayName == "forceSyncReflow"); + } +}, { + desc: "Stack trace on DOM event", + searchFor: "DOMEvent", + setup: function(docShell) { + content.document.body.addEventListener("dog", + function(e) { console.log("hi"); }, + true); + testSendingEvent(); + }, + check: function(markers) { + markers = markers.filter(m => m.name == "DOMEvent"); + ok(markers.length > 0, "DOMEvent marker includes stack"); + ok(markers[0].stack.functionDisplayName == "testSendingEvent", + "testSendingEvent is on the stack"); + } +}, { + desc: "Stack trace on console event", + searchFor: "ConsoleTime", + setup: function(docShell) { + testConsoleTime(); + testConsoleTimeEnd(); + }, + check: function(markers) { + markers = markers.filter(m => m.name == "ConsoleTime"); + ok(markers.length > 0, "ConsoleTime marker includes stack"); + ok(markers[0].stack.functionDisplayName == "testConsoleTime", + "testConsoleTime is on the stack"); + ok(markers[0].endStack.functionDisplayName == "testConsoleTimeEnd", + "testConsoleTimeEnd is on the stack"); + } +}]; + +timelineContentTest(TESTS); diff --git a/dom/base/Console.cpp b/dom/base/Console.cpp index 5ef2259bf8b3..fc22838619a5 100644 --- a/dom/base/Console.cpp +++ b/dom/base/Console.cpp @@ -813,6 +813,9 @@ public: const nsAString& aCause) : nsDocShell::TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause) { + if (aMetaData == TRACING_INTERVAL_END) { + CaptureStack(); + } } virtual bool Equals(const nsDocShell::TimelineMarker* aOther) @@ -826,7 +829,11 @@ public: virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) { - aMarker.mCauseName.Construct(GetCause()); + if (GetMetaData() == TRACING_INTERVAL_START) { + aMarker.mCauseName.Construct(GetCause()); + } else { + aMarker.mEndStack = GetStack(); + } } }; diff --git a/dom/events/EventListenerManager.cpp b/dom/events/EventListenerManager.cpp index f4ce9817a48e..047b99700598 100644 --- a/dom/events/EventListenerManager.cpp +++ b/dom/events/EventListenerManager.cpp @@ -1036,8 +1036,10 @@ public: virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) { - aMarker.mType.Construct(GetCause()); - aMarker.mEventPhase.Construct(mPhase); + if (GetMetaData() == TRACING_INTERVAL_START) { + aMarker.mType.Construct(GetCause()); + aMarker.mEventPhase.Construct(mPhase); + } } private: diff --git a/dom/webidl/ProfileTimelineMarker.webidl b/dom/webidl/ProfileTimelineMarker.webidl index 2fc11f1a4957..bfa7f01889ee 100644 --- a/dom/webidl/ProfileTimelineMarker.webidl +++ b/dom/webidl/ProfileTimelineMarker.webidl @@ -15,8 +15,10 @@ dictionary ProfileTimelineMarker { DOMString name = ""; DOMHighResTimeStamp start = 0; DOMHighResTimeStamp end = 0; + object? stack = null; /* For ConsoleTime markers. */ DOMString causeName; + object? endStack = null; /* For DOMEvent markers. */ DOMString type; unsigned short eventPhase; diff --git a/toolkit/devtools/server/actors/memory.js b/toolkit/devtools/server/actors/memory.js index 1f5b1eb2bd8e..a4c41227c412 100644 --- a/toolkit/devtools/server/actors/memory.js +++ b/toolkit/devtools/server/actors/memory.js @@ -9,6 +9,8 @@ let protocol = require("devtools/server/protocol"); let { method, RetVal, Arg, types } = protocol; const { reportException } = require("devtools/toolkit/DevToolsUtils"); loader.lazyRequireGetter(this, "events", "sdk/event/core"); +loader.lazyRequireGetter(this, "StackFrameCache", + "devtools/server/actors/utils/stack", true); /** * A method decorator that ensures the actor is in the expected state before @@ -60,17 +62,14 @@ let MemoryActor = protocol.ActorClass({ return this._dbg; }, - initialize: function(conn, parent) { + initialize: function(conn, parent, frameCache = new StackFrameCache()) { protocol.Actor.prototype.initialize.call(this, conn); this.parent = parent; this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"] .getService(Ci.nsIMemoryReporterManager); this.state = "detached"; this._dbg = null; - this._framesToCounts = null; - this._framesToIndices = null; - this._framesToForms = null; - + this._frameCache = frameCache; this._onWindowReady = this._onWindowReady.bind(this); events.on(this.parent, "window-ready", this._onWindowReady); @@ -124,25 +123,9 @@ let MemoryActor = protocol.ActorClass({ } }, - _initFrames: function() { - if (this._framesToCounts) { - // The maps are already initialized. - return; - } - - this._framesToCounts = new Map(); - this._framesToIndices = new Map(); - this._framesToForms = new Map(); - }, - _clearFrames: function() { if (this.dbg.memory.trackingAllocationSites) { - this._framesToCounts.clear(); - this._framesToCounts = null; - this._framesToIndices.clear(); - this._framesToIndices = null; - this._framesToForms.clear(); - this._framesToForms = null; + this._frameCache.clearFrames(); } }, @@ -153,7 +136,7 @@ let MemoryActor = protocol.ActorClass({ if (this.state == "attached") { if (isTopLevel && this.dbg.memory.trackingAllocationSites) { this._clearDebuggees(); - this._initFrames(); + nthis._frameCache.initFrames(); } this.dbg.addDebuggees(); } @@ -177,7 +160,7 @@ let MemoryActor = protocol.ActorClass({ * See the protocol.js definition of AllocationsRecordingOptions above. */ startRecordingAllocations: method(expectState("attached", function(options = {}) { - this._initFrames(); + this._frameCache.initFrames(); this.dbg.memory.allocationSamplingProbability = options.probability != null ? options.probability : 1.0; @@ -278,94 +261,18 @@ let MemoryActor = protocol.ActorClass({ // because we potentially haven't seen some or all of them yet. After this // loop, we can rely on the fact that every frame we deal with already has // its metadata stored. - this._assignFrameIndices(waived); - this._createFrameForms(waived); - this._countFrame(waived); + let index = this._frameCache.addFrame(waived); - packet.allocations.push(this._framesToIndices.get(waived)); + packet.allocations.push(index); packet.allocationsTimestamps.push(timestamp); } - // Now that we are guaranteed to have a form for every frame, we know the - // size the "frames" property's array must be. We use that information to - // create dense arrays even though we populate them out of order. - const size = this._framesToForms.size; - packet.frames = Array(size).fill(null); - packet.counts = Array(size).fill(0); - - // Populate the "frames" and "counts" properties. - for (let [stack, index] of this._framesToIndices) { - packet.frames[index] = this._framesToForms.get(stack); - packet.counts[index] = this._framesToCounts.get(stack) || 0; - } - - return packet; + return this._frameCache.updateFramePacket(packet); }), { request: {}, response: RetVal("json") }), - /** - * Assigns an index to the given frame and its parents, if an index is not - * already assigned. - * - * @param SavedFrame frame - * A frame to assign an index to. - */ - _assignFrameIndices: function(frame) { - if (this._framesToIndices.has(frame)) { - return; - } - - if (frame) { - this._assignFrameIndices(frame.parent); - } - - const index = this._framesToIndices.size; - this._framesToIndices.set(frame, index); - }, - - /** - * Create the form for the given frame, if one doesn't already exist. - * - * @param SavedFrame frame - * A frame to create a form for. - */ - _createFrameForms: function(frame) { - if (this._framesToForms.has(frame)) { - return; - } - - let form = null; - if (frame) { - form = { - line: frame.line, - column: frame.column, - source: frame.source, - functionDisplayName: frame.functionDisplayName, - parent: this._framesToIndices.get(frame.parent) - }; - this._createFrameForms(frame.parent); - } - - this._framesToForms.set(frame, form); - }, - - /** - * Increment the allocation count for the provided frame. - * - * @param SavedFrame frame - * The frame whose allocation count should be incremented. - */ - _countFrame: function(frame) { - if (!this._framesToCounts.has(frame)) { - this._framesToCounts.set(frame, 1); - } else { - let count = this._framesToCounts.get(frame); - this._framesToCounts.set(frame, count + 1); - } - }, - /* * Force a browser-wide GC. */ diff --git a/toolkit/devtools/server/actors/timeline.js b/toolkit/devtools/server/actors/timeline.js index fdc8e0ff9ad1..213397fde26e 100644 --- a/toolkit/devtools/server/actors/timeline.js +++ b/toolkit/devtools/server/actors/timeline.js @@ -28,6 +28,7 @@ const events = require("sdk/event/core"); const {setTimeout, clearTimeout} = require("sdk/timers"); const {MemoryActor} = require("devtools/server/actors/memory"); const {FramerateActor} = require("devtools/server/actors/framerate"); +const {StackFrameCache} = require("devtools/server/actors/utils/stack"); // How often do we pull markers from the docShells, and therefore, how often do // we send events to the front (knowing that when there are no markers in the @@ -86,6 +87,12 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ type: "ticks", delta: Arg(0, "number"), timestamps: Arg(1, "array-of-numbers-as-strings") + }, + + "frames" : { + type: "frames", + delta: Arg(0, "number"), + frames: Arg(1, "json") } }, @@ -95,6 +102,7 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ this._isRecording = false; this._startTime = 0; + this._stackFrames = null; // Make sure to get markers from new windows as they become available this._onWindowReady = this._onWindowReady.bind(this); @@ -170,6 +178,25 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ markers = [...markers, ...docShell.popProfileTimelineMarkers()]; } + // The docshell may return markers with stack traces attached. + // Here we transform the stack traces via the stack frame cache, + // which lets us preserve tail sharing when transferring the + // frames to the client. We must waive xrays here because Firefox + // doesn't understand that the Debugger.Frame object is safe to + // use from chrome. See Tutorial-Alloc-Log-Tree.md. + for (let marker of markers) { + if (marker.stack) { + marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack)); + } + if (marker.endStack) { + marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack)); + } + } + + let frames = this._stackFrames.makeEvent(); + if (frames) { + events.emit(this, "frames", endTime, frames); + } if (markers.length > 0) { events.emit(this, "markers", markers, endTime); } @@ -206,13 +233,16 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ } this._isRecording = true; this._startTime = this.docShells[0].now(); + this._stackFrames = new StackFrameCache(); + this._stackFrames.initFrames(); for (let docShell of this.docShells) { docShell.recordProfileTimelineMarkers = true; } if (withMemory) { - this._memoryActor = new MemoryActor(this.conn, this.tabActor); + this._memoryActor = new MemoryActor(this.conn, this.tabActor, + this._stackFrames); events.emit(this, "memory", this._startTime, this._memoryActor.measure()); } if (withTicks) { @@ -240,6 +270,7 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ return; } this._isRecording = false; + this._stackFrames = null; if (this._memoryActor) { this._memoryActor = null; diff --git a/toolkit/devtools/server/actors/utils/stack.js b/toolkit/devtools/server/actors/utils/stack.js new file mode 100644 index 000000000000..3093e9768f50 --- /dev/null +++ b/toolkit/devtools/server/actors/utils/stack.js @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let {Class} = require("sdk/core/heritage"); + +/** + * A helper class that stores stack frame objects. Each frame is + * assigned an index, and if a frame is added more than once, the same + * index is used. Users of the class can get an array of all frames + * that have been added. + */ +let StackFrameCache = Class({ + /** + * Initialize this object. + */ + initialize: function() { + this._framesToCounts = null; + this._framesToIndices = null; + this._framesToForms = null; + this._lastEventSize = -1; + }, + + /** + * Prepare to accept frames. + */ + initFrames: function() { + if (this._framesToCounts) { + // The maps are already initialized. + return; + } + + this._framesToCounts = new Map(); + this._framesToIndices = new Map(); + this._framesToForms = new Map(); + this._lastEventSize = -1; + }, + + /** + * Forget all stored frames and reset to the initialized state. + */ + clearFrames: function() { + this._framesToCounts.clear(); + this._framesToCounts = null; + this._framesToIndices.clear(); + this._framesToIndices = null; + this._framesToForms.clear(); + this._framesToForms = null; + this._lastEventSize = -1; + }, + + /** + * Add a frame to this stack frame cache, and return the index of + * the frame. + */ + addFrame: function(frame) { + this._assignFrameIndices(frame); + this._createFrameForms(frame); + this._countFrame(frame); + return this._framesToIndices.get(frame); + }, + + /** + * A helper method for the memory actor. This populates the packet + * object with "frames" and "counts" properties. Each of these + * properties will be an array indexed by frame ID. "frames" will + * contain frame objects (see makeEvent) and "counts" will hold + * allocation counts for each frame. + * + * @param packet + * The packet to update. + * + * @returns packet + */ + updateFramePacket: function(packet) { + // Now that we are guaranteed to have a form for every frame, we know the + // size the "frames" property's array must be. We use that information to + // create dense arrays even though we populate them out of order. + const size = this._framesToForms.size; + packet.frames = Array(size).fill(null); + packet.counts = Array(size).fill(0); + + // Populate the "frames" and "counts" properties. + for (let [stack, index] of this._framesToIndices) { + packet.frames[index] = this._framesToForms.get(stack); + packet.counts[index] = this._framesToCounts.get(stack) || 0; + } + + return packet; + }, + + /** + * If any new stack frames have been added to this cache since the + * last call to makeEvent (clearing the cache also resets the "last + * call"), then return a new array describing the new frames. If no + * new frames are available, return null. + * + * The frame cache assumes that the user of the cache keeps track of + * all previously-returned arrays and, in theory, concatenates them + * all to form a single array holding all frames added to the cache + * since the last reset. This concatenated array can be indexed by + * the frame ID. The array returned by this function, though, is + * dense and starts at 0. + * + * Each element in the array is an object of the form: + * { + * line: , + * column: , + * source: , + * functionDisplayName: , + * parent: + * } + * + * The intent of this approach is to make it simpler to efficiently + * send frame information over the debugging protocol, by only + * sending new frames. + * + * @returns array or null + */ + makeEvent: function() { + const size = this._framesToForms.size; + if (!size || size <= this._lastEventSize) { + return null; + } + + let packet = Array(size - this._lastEventSize).fill(null); + for (let [stack, index] of this._framesToIndices) { + if (index > this._lastEventSize) { + packet[index - this._lastEventSize - 1] = this._framesToForms.get(stack); + } + } + + this._lastEventSize = size; + + return packet; + }, + + /** + * Assigns an index to the given frame and its parents, if an index is not + * already assigned. + * + * @param SavedFrame frame + * A frame to assign an index to. + */ + _assignFrameIndices: function(frame) { + if (this._framesToIndices.has(frame)) { + return; + } + + if (frame) { + this._assignFrameIndices(frame.parent); + } + + const index = this._framesToIndices.size; + this._framesToIndices.set(frame, index); + }, + + /** + * Create the form for the given frame, if one doesn't already exist. + * + * @param SavedFrame frame + * A frame to create a form for. + */ + _createFrameForms: function(frame) { + if (this._framesToForms.has(frame)) { + return; + } + + let form = null; + if (frame) { + form = { + line: frame.line, + column: frame.column, + source: frame.source, + functionDisplayName: frame.functionDisplayName, + parent: this._framesToIndices.get(frame.parent) + }; + this._createFrameForms(frame.parent); + } + + this._framesToForms.set(frame, form); + }, + + /** + * Increment the allocation count for the provided frame. + * + * @param SavedFrame frame + * The frame whose allocation count should be incremented. + */ + _countFrame: function(frame) { + if (!this._framesToCounts.has(frame)) { + this._framesToCounts.set(frame, 1); + } else { + let count = this._framesToCounts.get(frame); + this._framesToCounts.set(frame, count + 1); + } + } +}); + +exports.StackFrameCache = StackFrameCache; diff --git a/toolkit/devtools/server/moz.build b/toolkit/devtools/server/moz.build index f6c017b3fa49..144ba7aee478 100644 --- a/toolkit/devtools/server/moz.build +++ b/toolkit/devtools/server/moz.build @@ -73,7 +73,8 @@ EXTRA_JS_MODULES.devtools.server.actors.utils += [ 'actors/utils/automation-timeline.js', 'actors/utils/make-debugger.js', 'actors/utils/map-uri-to-addon-id.js', - 'actors/utils/ScriptStore.js' + 'actors/utils/ScriptStore.js', + 'actors/utils/stack.js', ] FAIL_ON_WARNINGS = True