gecko/toolkit/devtools/server/actors/tracer.js

758 lines
20 KiB
JavaScript

/* 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";
const { Cu } = require("chrome");
const { reportException } =
Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}).DevToolsUtils;
const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
Cu.import("resource://gre/modules/jsdebugger.jsm");
addDebuggerToGlobal(this);
// TODO bug 943125: remove this polyfill and use Debugger.Frame.prototype.depth
// once it is implemented.
if (!Object.getOwnPropertyDescriptor(Debugger.Frame.prototype, "depth")) {
Debugger.Frame.prototype._depth = null;
Object.defineProperty(Debugger.Frame.prototype, "depth", {
get: function () {
if (this._depth === null) {
if (!this.older) {
this._depth = 0;
} else {
this._depth = 1 + this.older.depth;
}
}
return this._depth;
}
});
}
const { setTimeout } = require("sdk/timers");
/**
* The number of milliseconds we should buffer frame enter/exit packets before
* sending.
*/
const BUFFER_SEND_DELAY = 50;
/**
* The maximum number of arguments we will send for any single function call.
*/
const MAX_ARGUMENTS = 5;
/**
* The maximum number of an object's properties we will serialize.
*/
const MAX_PROPERTIES = 5;
/**
* The complete set of trace types supported.
*/
const TRACE_TYPES = new Set([
"time",
"return",
"throw",
"yield",
"name",
"location",
"callsite",
"parameterNames",
"arguments",
"depth"
]);
/**
* Creates a TraceActor. TraceActor provides a stream of function
* call/return packets to a remote client gathering a full trace.
*/
function TraceActor(aConn, aParentActor)
{
this._attached = false;
this._activeTraces = new MapStack();
this._totalTraces = 0;
this._startTime = 0;
// Keep track of how many different trace requests have requested what kind of
// tracing info. This way we can minimize the amount of data we are collecting
// at any given time.
this._requestsForTraceType = Object.create(null);
for (let type of TRACE_TYPES) {
this._requestsForTraceType[type] = 0;
}
this._sequence = 0;
this._bufferSendTimer = null;
this._buffer = [];
this.onExitFrame = this.onExitFrame.bind(this);
this.global = aParentActor.window.wrappedJSObject;
}
TraceActor.prototype = {
actorPrefix: "trace",
get attached() { return this._attached; },
get idle() { return this._attached && this._activeTraces.size === 0; },
get tracing() { return this._attached && this._activeTraces.size > 0; },
/**
* Buffer traces and only send them every BUFFER_SEND_DELAY milliseconds.
*/
_send: function(aPacket) {
this._buffer.push(aPacket);
if (this._bufferSendTimer === null) {
this._bufferSendTimer = setTimeout(() => {
this.conn.send({
from: this.actorID,
type: "traces",
traces: this._buffer.splice(0, this._buffer.length)
});
this._bufferSendTimer = null;
}, BUFFER_SEND_DELAY);
}
},
/**
* Initializes a Debugger instance and adds listeners to it.
*/
_initDebugger: function() {
this.dbg = new Debugger();
this.dbg.onEnterFrame = this.onEnterFrame.bind(this);
this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this);
this.dbg.enabled = false;
},
/**
* Add a debuggee global to the Debugger object.
*/
_addDebuggee: function(aGlobal) {
try {
this.dbg.addDebuggee(aGlobal);
} catch (e) {
// Ignore attempts to add the debugger's compartment as a debuggee.
reportException("TraceActor",
new Error("Ignoring request to add the debugger's "
+ "compartment as a debuggee"));
}
},
/**
* Add the provided window and all windows in its frame tree as debuggees.
*/
_addDebuggees: function(aWindow) {
this._addDebuggee(aWindow);
let frames = aWindow.frames;
if (frames) {
for (let i = 0; i < frames.length; i++) {
this._addDebuggees(frames[i]);
}
}
},
/**
* An object used by TraceActors to tailor their behavior depending
* on the debugging context required (chrome or content).
*/
globalManager: {
/**
* Adds all globals in the global object as debuggees.
*/
findGlobals: function() {
this._addDebuggees(this.global);
},
/**
* A function that the engine calls when a new global object has been
* created. Adds the global object as a debuggee if it is in the content
* window.
*
* @param aGlobal Debugger.Object
* The new global object that was created.
*/
onNewGlobal: function(aGlobal) {
// Content debugging only cares about new globals in the content
// window, like iframe children.
if (aGlobal.hostAnnotations &&
aGlobal.hostAnnotations.type == "document" &&
aGlobal.hostAnnotations.element === this.global) {
this._addDebuggee(aGlobal);
}
},
},
/**
* Handle a protocol request to attach to the trace actor.
*
* @param aRequest object
* The protocol request object.
*/
onAttach: function(aRequest) {
if (this.attached) {
return {
error: "wrongState",
message: "Already attached to a client"
};
}
if (!this.dbg) {
this._initDebugger();
this.globalManager.findGlobals.call(this);
}
this._attached = true;
return {
type: "attached",
traceTypes: Object.keys(this._requestsForTraceType)
.filter(k => !!this._requestsForTraceType[k])
};
},
/**
* Handle a protocol request to detach from the trace actor.
*
* @param aRequest object
* The protocol request object.
*/
onDetach: function() {
while (this.tracing) {
this.onStopTrace();
}
this.dbg = null;
this._attached = false;
return { type: "detached" };
},
/**
* Handle a protocol request to start a new trace.
*
* @param aRequest object
* The protocol request object.
*/
onStartTrace: function(aRequest) {
for (let traceType of aRequest.trace) {
if (!TRACE_TYPES.has(traceType)) {
return {
error: "badParameterType",
message: "No such trace type: " + traceType
};
}
}
if (this.idle) {
this.dbg.enabled = true;
this._sequence = 0;
this._startTime = Date.now();
}
// Start recording all requested trace types.
for (let traceType of aRequest.trace) {
this._requestsForTraceType[traceType]++;
}
this._totalTraces++;
let name = aRequest.name || "Trace " + this._totalTraces;
this._activeTraces.push(name, aRequest.trace);
return { type: "startedTrace", why: "requested", name: name };
},
/**
* Handle a protocol request to end a trace.
*
* @param aRequest object
* The protocol request object.
*/
onStopTrace: function(aRequest) {
if (!this.tracing) {
return {
error: "wrongState",
message: "No active traces"
};
}
let stoppedTraceTypes, name;
if (aRequest && aRequest.name) {
name = aRequest.name;
if (!this._activeTraces.has(name)) {
return {
error: "noSuchTrace",
message: "No active trace with name: " + name
};
}
stoppedTraceTypes = this._activeTraces.delete(name);
} else {
name = this._activeTraces.peekKey();
stoppedTraceTypes = this._activeTraces.pop();
}
for (let traceType of stoppedTraceTypes) {
this._requestsForTraceType[traceType]--;
}
if (this.idle) {
this.dbg.enabled = false;
}
return { type: "stoppedTrace", why: "requested", name: name };
},
// JS Debugger API hooks.
/**
* Called by the engine when a frame is entered. Sends an unsolicited packet
* to the client carrying requested trace information.
*
* @param aFrame Debugger.frame
* The stack frame that was entered.
*/
onEnterFrame: function(aFrame) {
let callee = aFrame.callee;
let packet = {
type: "enteredFrame",
sequence: this._sequence++
};
if (this._requestsForTraceType.name) {
packet.name = aFrame.callee
? aFrame.callee.displayName || "(anonymous function)"
: "(" + aFrame.type + ")";
}
if (this._requestsForTraceType.location && aFrame.script) {
// We should return the location of the start of the script, but
// Debugger.Script does not provide complete start locations (bug
// 901138). Instead, return the current offset (the location of the first
// statement in the function).
packet.location = {
url: aFrame.script.url,
line: aFrame.script.getOffsetLine(aFrame.offset),
column: getOffsetColumn(aFrame.offset, aFrame.script)
};
}
if (this._requestsForTraceType.callsite
&& aFrame.older
&& aFrame.older.script) {
let older = aFrame.older;
packet.callsite = {
url: older.script.url,
line: older.script.getOffsetLine(older.offset),
column: getOffsetColumn(older.offset, older.script)
};
}
if (this._requestsForTraceType.time) {
packet.time = Date.now() - this._startTime;
}
if (this._requestsForTraceType.parameterNames && aFrame.callee) {
packet.parameterNames = aFrame.callee.parameterNames;
}
if (this._requestsForTraceType.arguments && aFrame.arguments) {
packet.arguments = [];
let i = 0;
for (let arg of aFrame.arguments) {
if (i++ > MAX_ARGUMENTS) {
break;
}
packet.arguments.push(createValueGrip(arg, true));
}
}
if (this._requestsForTraceType.depth) {
packet.depth = aFrame.depth;
}
const onExitFrame = this.onExitFrame;
aFrame.onPop = function (aCompletion) {
onExitFrame(this, aCompletion);
};
this._send(packet);
},
/**
* Called by the engine when a frame is exited. Sends an unsolicited packet to
* the client carrying requested trace information.
*
* @param Debugger.Frame aFrame
* The Debugger.Frame that was just exited.
* @param aCompletion object
* The debugger completion value for the frame.
*/
onExitFrame: function(aFrame, aCompletion) {
let packet = {
type: "exitedFrame",
sequence: this._sequence++,
};
if (!aCompletion) {
packet.why = "terminated";
} else if (aCompletion.hasOwnProperty("return")) {
packet.why = "return";
} else if (aCompletion.hasOwnProperty("yield")) {
packet.why = "yield";
} else {
packet.why = "throw";
}
if (this._requestsForTraceType.time) {
packet.time = Date.now() - this._startTime;
}
if (this._requestsForTraceType.depth) {
packet.depth = aFrame.depth;
}
if (aCompletion) {
if (this._requestsForTraceType.return) {
packet.return = createValueGrip(aCompletion.return, true);
}
if (this._requestsForTraceType.throw) {
packet.throw = createValueGrip(aCompletion.throw, true);
}
if (this._requestsForTraceType.yield) {
packet.yield = createValueGrip(aCompletion.yield, true);
}
}
this._send(packet);
}
};
/**
* The request types this actor can handle.
*/
TraceActor.prototype.requestTypes = {
"attach": TraceActor.prototype.onAttach,
"detach": TraceActor.prototype.onDetach,
"startTrace": TraceActor.prototype.onStartTrace,
"stopTrace": TraceActor.prototype.onStopTrace
};
exports.register = function(handle) {
handle.addTabActor(TraceActor, "traceActor");
};
exports.unregister = function(handle) {
handle.removeTabActor(TraceActor, "traceActor");
};
/**
* MapStack is a collection of key/value pairs with stack ordering,
* where keys are strings and values are any JS value. In addition to
* the push and pop stack operations, supports a "delete" operation,
* which removes the value associated with a given key from any
* location in the stack.
*/
function MapStack()
{
// Essentially a MapStack is just sugar-coating around a standard JS
// object, plus the _stack array to track ordering.
this._stack = [];
this._map = Object.create(null);
}
MapStack.prototype = {
get size() { return this._stack.length; },
/**
* Return the key for the value on the top of the stack, or
* undefined if the stack is empty.
*/
peekKey: function() {
return this._stack[this.size - 1];
},
/**
* Return true iff a value has been associated with the given key.
*
* @param aKey string
* The key whose presence is to be tested.
*/
has: function(aKey) {
return Object.prototype.hasOwnProperty.call(this._map, aKey);
},
/**
* Return the value associated with the given key, or undefined if
* no value is associated with the key.
*
* @param aKey string
* The key whose associated value is to be returned.
*/
get: function(aKey) {
return this._map[aKey];
},
/**
* Push a new value onto the stack. If another value with the same
* key is already on the stack, it will be removed before the new
* value is pushed onto the top of the stack.
*
* @param aKey string
* The key of the object to push onto the stack.
*
* @param aValue
* The value to push onto the stack.
*/
push: function(aKey, aValue) {
this.delete(aKey);
this._stack.push(aKey);
this._map[aKey] = aValue;
},
/**
* Remove the value from the top of the stack and return it.
* Returns undefined if the stack is empty.
*/
pop: function() {
let key = this.peekKey();
let value = this.get(key);
this._stack.pop();
delete this._map[key];
return value;
},
/**
* Remove the value associated with the given key from the stack and
* return it. Returns undefined if no value is associated with the
* given key.
*
* @param aKey string
* The key for the value to remove from the stack.
*/
delete: function(aKey) {
let value = this.get(aKey);
if (this.has(aKey)) {
let keyIndex = this._stack.lastIndexOf(aKey);
this._stack.splice(keyIndex, 1);
delete this._map[aKey];
}
return value;
}
};
// TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when
// it is implemented.
function getOffsetColumn(aOffset, aScript) {
let bestOffsetMapping = null;
for (let offsetMapping of aScript.getAllColumnOffsets()) {
if (!bestOffsetMapping ||
(offsetMapping.offset <= aOffset &&
offsetMapping.offset > bestOffsetMapping.offset)) {
bestOffsetMapping = offsetMapping;
}
}
if (!bestOffsetMapping) {
// XXX: Try not to completely break the experience of using the
// tracer for the user by assuming column 0. Simultaneously,
// report the error so that there is a paper trail if the
// assumption is bad and the tracing experience becomes wonky.
reportException("TraceActor",
new Error("Could not find a column for offset " + aOffset +
" in the script " + aScript));
return 0;
}
return bestOffsetMapping.columnNumber;
}
// Serialization helper functions. Largely copied from script.js and modified
// for use in serialization rather than object actor requests.
/**
* Create a grip for the given debuggee value.
*
* @param aValue Debugger.Object|primitive
* The value to describe with the created grip.
*
* @param aUseDescriptor boolean
* If true, creates descriptors for objects rather than grips.
*
* @return ValueGrip
* A primitive value or a grip object.
*/
function createValueGrip(aValue, aUseDescriptor) {
switch (typeof aValue) {
case "boolean":
return aValue;
case "string":
if (aValue.length >= DebuggerServer.LONG_STRING_LENGTH) {
return {
type: "longString",
initial: aValue.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH),
length: aValue.length
};
}
return aValue;
case "number":
if (aValue === Infinity) {
return { type: "Infinity" };
} else if (aValue === -Infinity) {
return { type: "-Infinity" };
} else if (Number.isNaN(aValue)) {
return { type: "NaN" };
} else if (!aValue && 1 / aValue === -Infinity) {
return { type: "-0" };
}
return aValue;
case "undefined":
return { type: "undefined" };
case "object":
if (aValue === null) {
return { type: "null" };
}
return aUseDescriptor ? objectDescriptor(aValue) : objectGrip(aValue);
default:
reportException("TraceActor",
new Error("Failed to provide a grip for: " + aValue));
return null;
}
}
/**
* Create a grip for the given debuggee object.
*
* @param aObject Debugger.Object
* The object to describe with the created grip.
*/
function objectGrip(aObject) {
let g = {
"type": "object",
"class": aObject.class,
"extensible": aObject.isExtensible(),
"frozen": aObject.isFrozen(),
"sealed": aObject.isSealed()
};
// Add additional properties for functions.
if (aObject.class === "Function") {
if (aObject.name) {
g.name = aObject.name;
}
if (aObject.displayName) {
g.displayName = aObject.displayName;
}
// Check if the developer has added a de-facto standard displayName
// property for us to use.
let name = aObject.getOwnPropertyDescriptor("displayName");
if (name && name.value && typeof name.value == "string") {
g.userDisplayName = createValueGrip(name.value, aObject);
}
// Add source location information.
if (aObject.script) {
g.url = aObject.script.url;
g.line = aObject.script.startLine;
}
}
return g;
}
/**
* Create a descriptor for the given debuggee object. Descriptors are
* identical to grips, with the addition of the prototype,
* ownProperties, and safeGetterValues properties.
*
* @param aObject Debugger.Object
* The object to describe with the created descriptor.
*/
function objectDescriptor(aObject) {
let desc = objectGrip(aObject);
let ownProperties = Object.create(null);
if (Cu.isDeadWrapper(aObject)) {
desc.prototype = createValueGrip(null);
desc.ownProperties = ownProperties;
desc.safeGetterValues = Object.create(null);
return desc;
}
const names = aObject.getOwnPropertyNames();
let i = 0;
for (let name of names) {
if (i++ > MAX_PROPERTIES) {
break;
}
let desc = propertyDescriptor(name, aObject);
if (desc) {
ownProperties[name] = desc;
}
}
desc.ownProperties = ownProperties;
return desc;
}
/**
* A helper method that creates a property descriptor for the provided object,
* properly formatted for sending in a protocol response.
*
* @param aName string
* The property that the descriptor is generated for.
*
* @param aObject Debugger.Object
* The object whose property the descriptor is generated for.
*
* @return object
* The property descriptor for the property |aName| in |aObject|.
*/
function propertyDescriptor(aName, aObject) {
let desc;
try {
desc = aObject.getOwnPropertyDescriptor(aName);
} catch (e) {
// Calling getOwnPropertyDescriptor on wrapped native prototypes is not
// allowed (bug 560072). Inform the user with a bogus, but hopefully
// explanatory, descriptor.
return {
configurable: false,
writable: false,
enumerable: false,
value: e.name
};
}
// Skip objects since we only support shallow objects anyways.
if (!desc || typeof desc.value == "object" && desc.value !== null) {
return undefined;
}
let retval = {
configurable: desc.configurable,
enumerable: desc.enumerable
};
if ("value" in desc) {
retval.writable = desc.writable;
retval.value = createValueGrip(desc.value);
} else {
if ("get" in desc) {
retval.get = createValueGrip(desc.get);
}
if ("set" in desc) {
retval.set = createValueGrip(desc.set);
}
}
return retval;
}