mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 22:01:30 +00:00
Bug 1104213 - add stack traces to timeline markers. r=smaug r=vporof
This commit is contained in:
parent
a22681fd34
commit
3da37909db
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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=<unknown location>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -2887,6 +2887,7 @@ nsDocShell::PopProfileTimelineMarkers(JSContext* aCx,
|
||||
// docShell if an Layer marker type was recorded too.
|
||||
|
||||
nsTArray<mozilla::dom::ProfileTimelineMarker> profileTimelineMarkers;
|
||||
SequenceRooter<mozilla::dom::ProfileTimelineMarker> 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.
|
||||
|
@ -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<JS::PersistentRooted<JSObject*>> mStackTrace;
|
||||
};
|
||||
|
||||
// Add new profile timeline markers to this docShell. This will only add
|
||||
|
@ -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]
|
||||
|
15
docshell/test/browser/browser_timelineMarkers-05.js
Normal file
15
docshell/test/browser/browser_timelineMarkers-05.js
Normal file
@ -0,0 +1,15 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
let URL = '<!DOCTYPE html><style>' +
|
||||
'body {margin:0; padding: 0;} ' +
|
||||
'div {width:100px;height:100px;background:red;} ' +
|
||||
'.resize-change-color {width:50px;height:50px;background:blue;} ' +
|
||||
'.change-color {width:50px;height:50px;background:yellow;} ' +
|
||||
'.add-class {}' +
|
||||
'</style><div></div>';
|
||||
URL = "data:text/html;charset=utf8," + encodeURIComponent(URL);
|
||||
|
||||
let test = makeTimelineTest("browser_timelineMarkers-frame-05.js", URL);
|
68
docshell/test/browser/browser_timelineMarkers-frame-05.js
Normal file
68
docshell/test/browser/browser_timelineMarkers-frame-05.js
Normal file
@ -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);
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
|
202
toolkit/devtools/server/actors/utils/stack.js
Normal file
202
toolkit/devtools/server/actors/utils/stack.js
Normal file
@ -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: <line number for this frame>,
|
||||
* column: <column number for this frame>,
|
||||
* source: <filename string for this frame>,
|
||||
* functionDisplayName: <this frame's inferred function name function or null>,
|
||||
* parent: <frame ID -- an index into the concatenated array mentioned above>
|
||||
* }
|
||||
*
|
||||
* 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;
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user