Bug 1082761 - Add Debugger.prototype.findObjects; r=jimb
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 15 Oct 2014 19:21:00 +0200
changeset 210689 8f19523dc6bfdfabe28a62a5541e2338f5e7aae5
parent 210688 95ff64741969a7353b27b1ecc279e0970abc2c50
child 210690 9cdce80d5092164c17b627a4b0472e51992a6167
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersjimb
bugs1082761
milestone36.0a1
Bug 1082761 - Add Debugger.prototype.findObjects; r=jimb
js/public/UbiNodeTraverse.h
js/src/doc/Debugger/Debugger.md
js/src/jit-test/tests/debug/Debugger-findObjects-01.js
js/src/jit-test/tests/debug/Debugger-findObjects-02.js
js/src/jit-test/tests/debug/Debugger-findObjects-03.js
js/src/jit-test/tests/debug/Debugger-findObjects-04.js
js/src/jit-test/tests/debug/Debugger-findObjects-05.js
js/src/jit-test/tests/debug/Debugger-findObjects-06.js
js/src/jit-test/tests/debug/Debugger-findObjects-07.js
js/src/jit-test/tests/debug/Debugger-findObjects-08.js
js/src/jit-test/tests/debug/Debugger-findObjects-09.js
js/src/vm/CommonPropertyNames.h
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
--- a/js/public/UbiNodeTraverse.h
+++ b/js/public/UbiNodeTraverse.h
@@ -90,16 +90,25 @@ struct BreadthFirst {
 
     // Initialize this traversal object. Return false on OOM.
     bool init() { return visited.init(); }
 
     // Add |node| as a starting point for the traversal. You may add
     // 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;
 
     // Traverse the graph in breadth-first order, starting at the given
     // start nodes, applying |handler::operator()| for each edge traversed
     // as described above.
     //
--- a/js/src/doc/Debugger/Debugger.md
+++ b/js/src/doc/Debugger/Debugger.md
@@ -373,16 +373,42 @@ other kinds of objects.
     returns all debuggee code scripts.
 
     Note that the result may include [`Debugger.Script`][script] instances for
     scripts that can no longer ever be used by the debuggee, say, those for
     eval code that has finished running, or unreachable functions. Whether
     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
     handler objects are set at the same location(s) as <i>handler</i>, they
     remain in place.
 
 `clearAllBreakpoints()`
 :   Remove all breakpoints set using this `Debugger` instance.
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-01.js
@@ -0,0 +1,4 @@
+// In a debugger with no debuggees, findObjects should return no objects.
+
+var dbg = new Debugger;
+assertEq(dbg.findObjects().length, 0);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-02.js
@@ -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);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-03.js
@@ -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);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-04.js
@@ -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);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-05.js
@@ -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);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-06.js
@@ -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);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-07.js
@@ -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);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-08.js
@@ -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);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findObjects-09.js
@@ -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);
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -27,16 +27,17 @@
     macro(byteOffset, byteOffset, "byteOffset") \
     macro(bytes, bytes, "bytes") \
     macro(BYTES_PER_ELEMENT, BYTES_PER_ELEMENT, "BYTES_PER_ELEMENT") \
     macro(call, call, "call") \
     macro(callee, callee, "callee") \
     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") \
     macro(comma, comma, ",") \
     macro(compare, compare, "compare") \
     macro(configurable, configurable, "configurable") \
     macro(construct, construct, "construct") \
     macro(constructor, constructor, "constructor") \
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -15,16 +15,17 @@
 #include "jsobj.h"
 #include "jswrapper.h"
 
 #include "frontend/BytecodeCompiler.h"
 #include "gc/Marking.h"
 #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"
 #include "vm/SPSProfiler.h"
 #include "vm/WrapperObject.h"
 
 #include "jsgcinlines.h"
 #include "jsobjinlines.h"
@@ -2977,16 +2978,212 @@ Debugger::findScripts(JSContext *cx, uns
             return false;
         result->setDenseElement(i, ObjectValue(*scriptObject));
     }
 
     args.rval().setObject(*result);
     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)
 {
     THIS_DEBUGGER(cx, argc, vp, "findAllGlobals", args, dbg);
 
     RootedObject result(cx, NewDenseEmptyArray(cx));
     if (!result)
         return false;
@@ -3056,16 +3253,17 @@ const JSFunctionSpec Debugger::methods[]
     JS_FN("addAllGlobalsAsDebuggees", Debugger::addAllGlobalsAsDebuggees, 0, 0),
     JS_FN("removeDebuggee", Debugger::removeDebuggee, 1, 0),
     JS_FN("removeAllDebuggees", Debugger::removeAllDebuggees, 0, 0),
     JS_FN("hasDebuggee", Debugger::hasDebuggee, 1, 0),
     JS_FN("getDebuggees", Debugger::getDebuggees, 0, 0),
     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
 };
 
 
 /*** Debugger.Script *****************************************************************************/
 
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -266,16 +266,17 @@ class Debugger : private mozilla::Linked
     typedef DebuggerWeakMap<PreBarrieredObject, RelocatablePtrObject> ObjectWeakMap;
     ObjectWeakMap objects;
 
     /* The map from debuggee Envs to Debugger.Environment instances. */
     ObjectWeakMap environments;
 
     class FrameRange;
     class ScriptQuery;
+    class ObjectQuery;
 
     bool addDebuggeeGlobal(JSContext *cx, Handle<GlobalObject*> obj);
     bool addDebuggeeGlobal(JSContext *cx, Handle<GlobalObject*> obj,
                            AutoDebugModeInvalidation &invalidate);
     void cleanupDebuggeeGlobalBeforeRemoval(FreeOp *fop, GlobalObject *global,
                                             GlobalObjectSet::Enum *debugEnum);
     bool removeDebuggeeGlobal(JSContext *cx, Handle<GlobalObject *> global,
                               GlobalObjectSet::Enum *debugEnum);
@@ -364,16 +365,17 @@ class Debugger : private mozilla::Linked
     static bool addAllGlobalsAsDebuggees(JSContext *cx, unsigned argc, Value *vp);
     static bool removeDebuggee(JSContext *cx, unsigned argc, Value *vp);
     static bool removeAllDebuggees(JSContext *cx, unsigned argc, Value *vp);
     static bool hasDebuggee(JSContext *cx, unsigned argc, Value *vp);
     static bool getDebuggees(JSContext *cx, unsigned argc, Value *vp);
     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);
     static const JSPropertySpec properties[];
     static const JSFunctionSpec methods[];
 
     JSObject *getHook(Hook hook) const;
     bool hasAnyLiveHooks() const;