mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 17:16:12 +00:00
710 lines
19 KiB
JavaScript
710 lines
19 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 { DebuggerServer } = require("devtools/server/main");
|
|
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
|
|
const Debugger = require("Debugger");
|
|
const { GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common");
|
|
const promise = require("promise");
|
|
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
|
|
// TODO bug 943125: remove this polyfill and use Debugger.Frame.prototype.depth
|
|
// once it is implemented.
|
|
function getFrameDepth(frame) {
|
|
if (typeof(frame.depth) != "number") {
|
|
if (!frame.older) {
|
|
frame.depth = 0;
|
|
} else {
|
|
frame.depth = getFrameDepth(frame.older) + 1;
|
|
}
|
|
}
|
|
|
|
return frame.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 = 3;
|
|
|
|
/**
|
|
* The maximum number of an object's properties we will serialize.
|
|
*/
|
|
const MAX_PROPERTIES = 3;
|
|
|
|
/**
|
|
* The complete set of trace types supported.
|
|
*/
|
|
const TRACE_TYPES = new Set([
|
|
"time",
|
|
"return",
|
|
"throw",
|
|
"yield",
|
|
"name",
|
|
"location",
|
|
"hitCount",
|
|
"callsite",
|
|
"parameterNames",
|
|
"arguments",
|
|
"depth"
|
|
]);
|
|
|
|
/**
|
|
* Creates a TracerActor. TracerActor provides a stream of function
|
|
* call/return packets to a remote client gathering a full trace.
|
|
*/
|
|
function TracerActor(aConn, aParent)
|
|
{
|
|
this._dbg = null;
|
|
this._parent = aParent;
|
|
this._attached = false;
|
|
this._activeTraces = new MapStack();
|
|
this._totalTraces = 0;
|
|
this._startTime = 0;
|
|
this._sequence = 0;
|
|
this._bufferSendTimer = null;
|
|
this._buffer = [];
|
|
this._hitCounts = new WeakMap();
|
|
this._packetScheduler = new JobScheduler();
|
|
|
|
// 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.onEnterFrame = this.onEnterFrame.bind(this);
|
|
this.onExitFrame = this.onExitFrame.bind(this);
|
|
}
|
|
|
|
TracerActor.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; },
|
|
|
|
get dbg() {
|
|
if (!this._dbg) {
|
|
this._dbg = this._parent.makeDebugger();
|
|
}
|
|
return this._dbg;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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"
|
|
};
|
|
}
|
|
|
|
this.dbg.addDebuggees();
|
|
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.onEnterFrame = this.onEnterFrame;
|
|
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]--;
|
|
}
|
|
|
|
// Clear hit counts if no trace is requesting them.
|
|
if (!this._requestsForTraceType.hitCount) {
|
|
this._hitCounts.clear();
|
|
}
|
|
|
|
if (this.idle) {
|
|
this._dbg.onEnterFrame = undefined;
|
|
this.dbg.enabled = false;
|
|
}
|
|
|
|
return {
|
|
type: "stoppedTrace",
|
|
why: "requested",
|
|
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) {
|
|
Task.spawn(function*() {
|
|
// This function might request original (i.e. source-mapped) location,
|
|
// which is asynchronous. We need to ensure that packets are sent out
|
|
// in the correct order.
|
|
let runInOrder = this._packetScheduler.schedule();
|
|
|
|
let packet = {
|
|
type: "enteredFrame",
|
|
sequence: this._sequence++
|
|
};
|
|
|
|
if (this._requestsForTraceType.hitCount) {
|
|
if (aFrame.script) {
|
|
// Increment hit count.
|
|
let previousHitCount = this._hitCounts.get(aFrame.script) || 0;
|
|
this._hitCounts.set(aFrame.script, previousHitCount + 1);
|
|
|
|
packet.hitCount = this._hitCounts.get(aFrame.script);
|
|
}
|
|
}
|
|
|
|
if (this._parent.threadActor && aFrame.script) {
|
|
packet.blackBoxed = this._parent.threadActor.sources.isBlackBoxed(aFrame.script.url);
|
|
} else {
|
|
packet.blackBoxed = false;
|
|
}
|
|
|
|
if (this._requestsForTraceType.callsite) {
|
|
if (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(createValueSnapshot(arg, true));
|
|
}
|
|
}
|
|
|
|
if (this._requestsForTraceType.depth) {
|
|
packet.depth = getFrameDepth(aFrame);
|
|
}
|
|
|
|
const onExitFrame = this.onExitFrame;
|
|
aFrame.onPop = function (aCompletion) {
|
|
onExitFrame(this, aCompletion);
|
|
};
|
|
|
|
// Async work is done below that doesn't depend on the frame
|
|
// being live
|
|
|
|
let name = aFrame.callee
|
|
? aFrame.callee.displayName || "(anonymous function)"
|
|
: "(" + aFrame.type + ")";
|
|
let sourceMappedLocation;
|
|
|
|
if (this._requestsForTraceType.name || this._requestsForTraceType.location) {
|
|
if (aFrame.script) {
|
|
let sources = this._parent.threadActor.sources;
|
|
|
|
sourceMappedLocation = yield sources.getOriginalLocation(new GeneratedLocation(
|
|
sources.createNonSourceMappedActor(aFrame.script.source),
|
|
aFrame.script.startLine,
|
|
// 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).
|
|
getOffsetColumn(aFrame.offset, aFrame.script)
|
|
));
|
|
}
|
|
}
|
|
|
|
if (this._requestsForTraceType.name) {
|
|
if (sourceMappedLocation && sourceMappedLocation.originalName) {
|
|
packet.name = sourceMappedLocation.originalName;
|
|
} else {
|
|
packet.name = name;
|
|
}
|
|
packet.name = name;
|
|
}
|
|
|
|
if (this._requestsForTraceType.location) {
|
|
if (sourceMappedLocation) {
|
|
// Don't copy sourceMappedLocation directly because it
|
|
// contains a reference to the source actor
|
|
packet.location = {
|
|
url: sourceMappedLocation.originalUrl,
|
|
line: sourceMappedLocation.originalLine,
|
|
column: sourceMappedLocation.originalColumn
|
|
};
|
|
}
|
|
}
|
|
|
|
runInOrder(() => this._send(packet));
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* 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 runInOrder = this._packetScheduler.schedule();
|
|
|
|
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 = getFrameDepth(aFrame);
|
|
}
|
|
|
|
if (aCompletion) {
|
|
if (this._requestsForTraceType.return && "return" in aCompletion) {
|
|
packet.return = createValueSnapshot(aCompletion.return, true);
|
|
}
|
|
|
|
else if (this._requestsForTraceType.throw && "throw" in aCompletion) {
|
|
packet.throw = createValueSnapshot(aCompletion.throw, true);
|
|
}
|
|
|
|
else if (this._requestsForTraceType.yield && "yield" in aCompletion) {
|
|
packet.yield = createValueSnapshot(aCompletion.yield, true);
|
|
}
|
|
}
|
|
|
|
runInOrder(() => this._send(packet));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The request types this actor can handle.
|
|
*/
|
|
TracerActor.prototype.requestTypes = {
|
|
"attach": TracerActor.prototype.onAttach,
|
|
"detach": TracerActor.prototype.onDetach,
|
|
"startTrace": TracerActor.prototype.onStartTrace,
|
|
"stopTrace": TracerActor.prototype.onStopTrace
|
|
};
|
|
|
|
exports.TracerActor = TracerActor;
|
|
|
|
/**
|
|
* 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] || undefined;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
};
|
|
|
|
// 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 aDetailed boolean
|
|
* If true, capture slightly more detailed information, like some
|
|
* properties on an object.
|
|
*
|
|
* @return Object
|
|
* A primitive value or a snapshot of an object.
|
|
*/
|
|
function createValueSnapshot(aValue, aDetailed=false) {
|
|
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 aDetailed
|
|
? detailedObjectSnapshot(aValue)
|
|
: objectSnapshot(aValue);
|
|
default:
|
|
DevToolsUtils.reportException("TracerActor",
|
|
new Error("Failed to provide a grip for: " + aValue));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a very minimal snapshot of the given debuggee object.
|
|
*
|
|
* @param aObject Debugger.Object
|
|
* The object to describe with the created grip.
|
|
*/
|
|
function objectSnapshot(aObject) {
|
|
return {
|
|
"type": "object",
|
|
"class": aObject.class,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a (slightly more) detailed snapshot of the given debuggee object.
|
|
*
|
|
* @param aObject Debugger.Object
|
|
* The object to describe with the created descriptor.
|
|
*/
|
|
function detailedObjectSnapshot(aObject) {
|
|
let desc = objectSnapshot(aObject);
|
|
let ownProperties = desc.ownProperties = Object.create(null);
|
|
|
|
if (aObject.class == "DeadObject") {
|
|
return desc;
|
|
}
|
|
|
|
let i = 0;
|
|
for (let name of aObject.getOwnPropertyNames()) {
|
|
if (i++ > MAX_PROPERTIES) {
|
|
break;
|
|
}
|
|
let desc = propertySnapshot(name, aObject);
|
|
if (desc) {
|
|
ownProperties[name] = desc;
|
|
}
|
|
}
|
|
|
|
return desc;
|
|
}
|
|
|
|
/**
|
|
* A helper method that creates a snapshot of the object's |aName| property.
|
|
*
|
|
* @param aName string
|
|
* The property of which the snapshot is taken.
|
|
*
|
|
* @param aObject Debugger.Object
|
|
* The object whose property the snapshot is taken of.
|
|
*
|
|
* @return Object
|
|
* The snapshot of the property.
|
|
*/
|
|
function propertySnapshot(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
|
|
};
|
|
}
|
|
|
|
// Only create descriptors for simple values. We skip objects and properties
|
|
// that have getters and setters; ain't nobody got time for that!
|
|
if (!desc
|
|
|| typeof desc.value == "object" && desc.value !== null
|
|
|| !("value" in desc)) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
configurable: desc.configurable,
|
|
enumerable: desc.enumerable,
|
|
writable: desc.writable,
|
|
value: createValueSnapshot(desc.value)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Scheduler for jobs to be run in the same order as in which they were
|
|
* scheduled.
|
|
*/
|
|
function JobScheduler()
|
|
{
|
|
this._lastScheduledJob = promise.resolve();
|
|
}
|
|
|
|
JobScheduler.prototype = {
|
|
/**
|
|
* Schedule a new job.
|
|
*
|
|
* @return A function that can be called anytime with a job as a parameter.
|
|
* Job won't be run until all previously scheduled jobs were run.
|
|
*/
|
|
schedule: function() {
|
|
let deferred = promise.defer();
|
|
let previousJob = this._lastScheduledJob;
|
|
this._lastScheduledJob = deferred.promise;
|
|
return function runInOrder(aJob) {
|
|
previousJob.then(() => {
|
|
aJob();
|
|
deferred.resolve();
|
|
});
|
|
};
|
|
}
|
|
};
|
|
|
|
exports.JobScheduler = JobScheduler;
|