Bug 1082761 - Add Debugger.prototype.findObjects; r=jimb

This commit is contained in:
Nick Fitzgerald 2014-10-15 19:21:00 +02:00
parent a838546e0a
commit 6a91b2cf5e
14 changed files with 353 additions and 0 deletions

View File

@ -95,6 +95,15 @@ struct BreadthFirst {
// as many starting points as you like. Return false on OOM.
bool addStart(Node node) { return pending.append(node); }
// Add |node| as a starting point for the traversal (see addStart) and also
// add it to the |visited| set. Return false on OOM.
bool addStartVisited(Node node) {
typename NodeMap::AddPtr ptr = visited.lookupForAdd(node);
if (!ptr && !visited.add(ptr, node, typename Handler::NodeData()))
return false;
return addStart(node);
}
// True if the handler wants us to compute edge names; doing so can be
// expensive in time and memory. True by default.
bool wantNames;

View File

@ -378,6 +378,32 @@ other kinds of objects.
such scripts appear can be affected by the garbage collector's
behavior, so this function's behavior is not entirely deterministic.
<code>findObjects([<i>query</i>])</code>
: Return an array of [`Debugger.Object`][object] instances referring to each
live object allocated in the scope of the debuggee globals that matches
*query*. Each instance appears only once in the array. *Query* is an object
whose properties restrict which objects are returned; an object must meet
all the criteria given by *query* to be returned. If *query* is omitted, we
return the [`Debugger.Object`][object] instances for all objects allocated
in the scope of debuggee globals.
The *query* object may have the following properties:
`class`
: If present, only return objects whose internal `[[Class]]`'s name
matches the given string. Note that in some cases, the prototype object
for a given constructor has the same `[[Class]]` as the instances that
refer to it, but cannot itself be used as a valid instance of the
class. Code gathering objects by class name may need to examine them
further before trying to use them.
All properties of *query* are optional. Passing an empty object returns all
objects in debuggee globals.
Unlike `findScripts`, this function is deterministic and will never return
[`Debugger.Object`s][object] referring to previously unreachable objects
that had not been collected yet.
<code>clearBreakpoint(<i>handler</i>)</code>
: Remove all breakpoints set in this `Debugger` instance that use
<i>handler</i> as their handler. Note that, if breakpoints using other

View File

@ -0,0 +1,4 @@
// In a debugger with no debuggees, findObjects should return no objects.
var dbg = new Debugger;
assertEq(dbg.findObjects().length, 0);

View File

@ -0,0 +1,18 @@
// In a debuggee with live objects, findObjects finds those objects.
var g = newGlobal();
let defObject = v => g.eval(`this.${v} = { toString: () => "[object ${v}]" }`);
defObject("a");
defObject("b");
defObject("c");
var dbg = new Debugger();
var gw = dbg.addDebuggee(g);
var aw = gw.makeDebuggeeValue(g.a);
var bw = gw.makeDebuggeeValue(g.b);
var cw = gw.makeDebuggeeValue(g.c);
assertEq(dbg.findObjects().indexOf(aw) != -1, true);
assertEq(dbg.findObjects().indexOf(bw) != -1, true);
assertEq(dbg.findObjects().indexOf(cw) != -1, true);

View File

@ -0,0 +1,12 @@
// findObjects' result includes objects referenced by other objects.
var g = newGlobal();
var dbg = new Debugger();
var gw = dbg.addDebuggee(g);
g.eval('this.a = { b: {} };');
var bw = gw.makeDebuggeeValue(g.a.b);
var objects = dbg.findObjects();
assertEq(objects.indexOf(bw) != -1, true);

View File

@ -0,0 +1,16 @@
// findObjects' result includes objects captured by closures.
var g = newGlobal();
var dbg = new Debugger();
var gw = dbg.addDebuggee(g);
g.eval(`
this.f = (function () {
let a = { foo: () => {} };
return () => a;
}());
`);
let objects = dbg.findObjects();
let aw = gw.makeDebuggeeValue(g.f());
assertEq(objects.indexOf(aw) != -1, true);

View File

@ -0,0 +1,10 @@
// findObjects' result doesn't include any duplicates.
var g = newGlobal();
var dbg = new Debugger();
dbg.addDebuggee(g);
let objects = dbg.findObjects();
let set = new Set(objects);
assertEq(objects.length, set.size);

View File

@ -0,0 +1,14 @@
// In a debugger with multiple debuggees, findObjects finds objects from all debuggees.
var g1 = newGlobal();
var g2 = newGlobal();
var dbg = new Debugger();
var g1w = dbg.addDebuggee(g1);
var g2w = dbg.addDebuggee(g2);
g1.eval('this.a = {};');
g2.eval('this.b = {};');
var objects = dbg.findObjects();
assertEq(objects.indexOf(g1w.makeDebuggeeValue(g1.a)) != -1, true);
assertEq(objects.indexOf(g2w.makeDebuggeeValue(g2.b)) != -1, true);

View File

@ -0,0 +1,22 @@
// findObjects can filter objects by class name.
var g = newGlobal();
var dbg = new Debugger();
var gw = dbg.addDebuggee(g);
g.eval('this.re = /foo/;');
g.eval('this.d = new Date();');
var rew = gw.makeDebuggeeValue(g.re);
var dw = gw.makeDebuggeeValue(g.d);
var objects;
objects = dbg.findObjects({ class: "RegExp" });
assertEq(objects.indexOf(rew) != -1, true);
assertEq(objects.indexOf(dw) == -1, true);
objects = dbg.findObjects({ class: "Date" });
assertEq(objects.indexOf(dw) != -1, true);
assertEq(objects.indexOf(rew) == -1, true);

View File

@ -0,0 +1,12 @@
// Passing bad query properties to Debugger.prototype.findScripts throws.
load(libdir + 'asserts.js');
var dbg = new Debugger();
var g = newGlobal();
assertThrowsInstanceOf(() => dbg.findObjects({ class: null }), TypeError);
assertThrowsInstanceOf(() => dbg.findObjects({ class: true }), TypeError);
assertThrowsInstanceOf(() => dbg.findObjects({ class: 1337 }), TypeError);
assertThrowsInstanceOf(() => dbg.findObjects({ class: /re/ }), TypeError);
assertThrowsInstanceOf(() => dbg.findObjects({ class: {} }), TypeError);

View File

@ -0,0 +1,9 @@
// We don't return objects where our query's class name is the prefix of the
// object's class name and vice versa.
var dbg = new Debugger();
var g = newGlobal();
var gw = dbg.addDebuggee(g);
assertEq(dbg.findObjects({ class: "Objec" }).length, 0);
assertEq(dbg.findObjects({ class: "Objectttttt" }).length, 0);

View File

@ -32,6 +32,7 @@
macro(caller, caller, "caller") \
macro(callFunction, callFunction, "callFunction") \
macro(caseFirst, caseFirst, "caseFirst") \
macro(class_, class_, "class") \
macro(Collator, Collator, "Collator") \
macro(CollatorCompareGet, CollatorCompareGet, "Intl_Collator_compare_get") \
macro(columnNumber, columnNumber, "columnNumber") \

View File

@ -20,6 +20,7 @@
#include "jit/BaselineJIT.h"
#include "js/Debug.h"
#include "js/GCAPI.h"
#include "js/UbiNodeTraverse.h"
#include "js/Vector.h"
#include "vm/ArgumentsObject.h"
#include "vm/DebuggerMemory.h"
@ -2982,6 +2983,202 @@ Debugger::findScripts(JSContext *cx, unsigned argc, Value *vp)
return true;
}
/*
* A class for parsing 'findObjects' query arguments and searching for objects
* that match the criteria they represent.
*/
class MOZ_STACK_CLASS Debugger::ObjectQuery
{
public:
/* Construct an ObjectQuery to use matching scripts for |dbg|. */
ObjectQuery(JSContext *cx, Debugger *dbg) :
cx(cx), dbg(dbg), className(cx)
{}
/*
* Parse the query object |query|, and prepare to match only the objects it
* specifies.
*/
bool parseQuery(HandleObject query) {
/* Check for the 'class' property */
RootedValue cls(cx);
if (!JSObject::getProperty(cx, query, query, cx->names().class_, &cls))
return false;
if (!cls.isUndefined()) {
if (!cls.isString()) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"query object's 'class' property",
"neither undefined nor a string");
return false;
}
className = cls;
}
return true;
}
/* Set up this ObjectQuery appropriately for a missing query argument. */
void omittedQuery() {
className.setUndefined();
}
/*
* Traverse the heap to find all relevant objects and add them to the
* provided vector.
*/
bool findObjects(AutoObjectVector &objs) {
if (!prepareQuery())
return false;
{
/*
* We can't tolerate the GC moving things around while we're
* searching the heap. Check that nothing we do causes a GC.
*/
JS::AutoCheckCannotGC autoCannotGC;
Traversal traversal(cx, *this, autoCannotGC);
if (!traversal.init())
return false;
/* Add each debuggee global as a start point of our traversal. */
for (GlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) {
if (!traversal.addStartVisited(JS::ubi::Node(static_cast<JSObject *>(r.front()))))
return false;
}
/*
* Iterate over all compartments and add traversal start points at
* objects that have CCWs in other compartments keeping them alive.
*/
for (CompartmentsIter c(cx->runtime(), SkipAtoms); !c.done(); c.next()) {
JSCompartment *comp = c.get();
if (!comp)
continue;
for (JSCompartment::WrapperEnum e(comp); !e.empty(); e.popFront()) {
const CrossCompartmentKey &key = e.front().key();
if (key.kind != CrossCompartmentKey::ObjectWrapper)
continue;
JSObject *obj = static_cast<JSObject *>(key.wrapped);
if (!traversal.addStartVisited(JS::ubi::Node(obj)))
return false;
}
}
if (!traversal.traverse())
return false;
/*
* Iterate over the visited set of nodes and accumulate all
* |JSObject|s matching our criteria in the given vector.
*/
for (Traversal::NodeMap::Range r = traversal.visited.all(); !r.empty(); r.popFront()) {
JS::ubi::Node node = r.front().key();
if (!node.is<JSObject>())
continue;
JSObject *obj = node.as<JSObject>();
if (!className.isUndefined()) {
const char *objClassName = obj->getClass()->name;
if (strcmp(objClassName, classNameCString.ptr()) != 0)
continue;
}
if (!objs.append(obj))
return false;
}
return true;
}
}
/*
* |ubi::Node::BreadthFirst| interface.
*
* We use an empty traversal function and just iterate over the traversal's
* visited set post-facto in |findObjects|.
*/
class NodeData {};
typedef JS::ubi::BreadthFirst<ObjectQuery> Traversal;
bool operator() (Traversal &, JS::ubi::Node, const JS::ubi::Edge &, NodeData *, bool)
{
return true;
}
private:
/* The context in which we should do our work. */
JSContext *cx;
/* The debugger for which we conduct queries. */
Debugger *dbg;
/*
* If this is non-null, matching objects will have a class whose name is
* this property.
*/
RootedValue className;
/* The className member, as a C string. */
JSAutoByteString classNameCString;
/*
* Given that either omittedQuery or parseQuery has been called, prepare the
* query for matching objects.
*/
bool prepareQuery() {
if (className.isString()) {
if (!classNameCString.encodeLatin1(cx, className.toString()))
return false;
}
return true;
}
};
bool
Debugger::findObjects(JSContext *cx, unsigned argc, Value *vp)
{
THIS_DEBUGGER(cx, argc, vp, "findObjects", args, dbg);
ObjectQuery query(cx, dbg);
if (args.length() >= 1) {
RootedObject queryObject(cx, NonNullObject(cx, args[0]));
if (!queryObject || !query.parseQuery(queryObject))
return false;
} else {
query.omittedQuery();
}
/*
* Accumulate the objects in an AutoObjectVector, instead of creating the JS
* array as we go, because we mustn't allocate JS objects or GC while we
* traverse the heap graph.
*/
AutoObjectVector objects(cx);
if (!query.findObjects(objects))
return false;
size_t length = objects.length();
RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, length));
if (!result)
return false;
result->ensureDenseInitializedLength(cx, 0, length);
for (size_t i = 0; i < length; i++) {
RootedValue debuggeeVal(cx, ObjectValue(*objects[i]));
if (!dbg->wrapDebuggeeValue(cx, &debuggeeVal))
return false;
result->setDenseElement(i, debuggeeVal);
}
args.rval().setObject(*result);
return true;
}
bool
Debugger::findAllGlobals(JSContext *cx, unsigned argc, Value *vp)
{
@ -3061,6 +3258,7 @@ const JSFunctionSpec Debugger::methods[] = {
JS_FN("getNewestFrame", Debugger::getNewestFrame, 0, 0),
JS_FN("clearAllBreakpoints", Debugger::clearAllBreakpoints, 0, 0),
JS_FN("findScripts", Debugger::findScripts, 1, 0),
JS_FN("findObjects", Debugger::findObjects, 1, 0),
JS_FN("findAllGlobals", Debugger::findAllGlobals, 0, 0),
JS_FN("makeGlobalObjectReference", Debugger::makeGlobalObjectReference, 1, 0),
JS_FS_END

View File

@ -271,6 +271,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
class FrameRange;
class ScriptQuery;
class ObjectQuery;
bool addDebuggeeGlobal(JSContext *cx, Handle<GlobalObject*> obj);
bool addDebuggeeGlobal(JSContext *cx, Handle<GlobalObject*> obj,
@ -369,6 +370,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
static bool getNewestFrame(JSContext *cx, unsigned argc, Value *vp);
static bool clearAllBreakpoints(JSContext *cx, unsigned argc, Value *vp);
static bool findScripts(JSContext *cx, unsigned argc, Value *vp);
static bool findObjects(JSContext *cx, unsigned argc, Value *vp);
static bool findAllGlobals(JSContext *cx, unsigned argc, Value *vp);
static bool makeGlobalObjectReference(JSContext *cx, unsigned argc, Value *vp);
static bool construct(JSContext *cx, unsigned argc, Value *vp);