Bug 1104213 - add stack traces to timeline markers. r=smaug r=vporof

This commit is contained in:
Tom Tromey 2014-12-29 11:32:00 +01:00
parent a22681fd34
commit 3da37909db
20 changed files with 594 additions and 124 deletions

View File

@ -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));
}

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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

View File

@ -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]

View 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);

View 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);

View File

@ -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();
}
}
};

View File

@ -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:

View File

@ -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;

View File

@ -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.
*/

View File

@ -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;

View 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;

View File

@ -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