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

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;