Bug 956376 - Part 1: Add Debugger.findSources. r=jimb
☠☠ backed out by ddd5a54f6a52 ☠ ☠
authorTooru Fujisawa <arai_a@mac.com>
Thu, 23 Aug 2018 09:27:30 +0900
changeset 488060 46dfdfe140670b0ebcaad372df05065782fc9f56
parent 488059 0e1eaa51fc0afcadb87c7230e2c01cab96ee65de
child 488061 8169bed1fafcecd6f78077a71af10b37df1af23b
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs956376
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 956376 - Part 1: Add Debugger.findSources. r=jimb
js/src/doc/Debugger/Debugger.md
js/src/jit-test/tests/debug/Debugger-findSources-01.js
js/src/jit-test/tests/debug/Debugger-findSources-02.js
js/src/jit-test/tests/debug/Debugger-findSources-03.js
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
--- a/js/src/doc/Debugger/Debugger.md
+++ b/js/src/doc/Debugger/Debugger.md
@@ -352,36 +352,20 @@ other kinds of objects.
     debuggee globals, if a debuggee global is otherwise unreachable, it may
     be dropped at any moment from the array this method returns.
 
 `getNewestFrame()`
 :   Return a [`Debugger.Frame`][frame] instance referring to the youngest
     [visible frame][vf] currently on the calling thread's stack, or `null`
     if there are no visible frames on the stack.
 
-<code>findSources([<i>query</i>]) <i>(not yet implemented)</i></code>
-:   Return an array of all [`Debugger.Source`][source] instances matching
-    <i>query</i>. Each source appears only once in the array. <i>Query</i>
-    is an object whose properties restrict which sources are returned; a
-    source must meet all the criteria given by <i>query</i> to be returned.
-    If <i>query</i> is omitted, we return all sources of all debuggee
+<code>findSources()</code>
+:   Return an array of all [`Debugger.Source`][source] instances of all debuggee
     scripts.
 
-    <i>Query</i> may have the following properties:
-
-    `url`
-    :   The source's `url` property must be equal to this value.
-
-    `global`
-    :   The source must have been evaluated in the scope of the given global
-        object. If this property's value is a [`Debugger.Object`][object] instance
-        belonging to this `Debugger` instance, then its referent is used. If the
-        object is not a global object, then the global in whose scope it was
-        allocated is used.
-
     Note that the result may include sources that can no longer ever be
     used by the debuggee: say, eval code that has finished running, or
     source for unreachable functions. Whether such sources appear can be
     affected by the garbage collector's behavior, so this function's result
     is not entirely deterministic.
 
 <code>findScripts([<i>query</i>])</code>
 :   Return an array of [`Debugger.Script`][script] instances for all debuggee scripts
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findSources-01.js
@@ -0,0 +1,4 @@
+// In a debugger with no debuggees, findSources should return no scripts.
+
+const dbg = new Debugger;
+assertEq(dbg.findSources().length, 0);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findSources-02.js
@@ -0,0 +1,15 @@
+// In a debugger with scripts, findSources finds the script source.
+
+const g = newGlobal();
+// Declare a function in order to keep the script source alive across GC.
+g.evaluate(`function fa() {}`, { fileName: "a.js" });
+g.evaluate(`function fb() {}`, { fileName: "b.js" });
+g.evaluate(`function fc() {}`, { fileName: "c.js" });
+
+const dbg = new Debugger();
+const gw = dbg.addDebuggee(g);
+
+const sources = dbg.findSources();
+assertEq(sources.filter(s => s.url == "a.js").length, 1);
+assertEq(sources.filter(s => s.url == "b.js").length, 1);
+assertEq(sources.filter(s => s.url == "c.js").length, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-findSources-03.js
@@ -0,0 +1,19 @@
+// In a debugger with multiple debuggees, findSources finds script sources across all debuggees.
+
+const g1 = newGlobal();
+const g2 = newGlobal();
+// Declare a function in order to keep the script source alive across GC.
+g1.evaluate(`function fa() {}`, { fileName: "a.js" });
+g1.evaluate(`function fb() {}`, { fileName: "b.js" });
+g2.evaluate(`function fc() {}`, { fileName: "c.js" });
+g2.evaluate(`function fd() {}`, { fileName: "d.js" });
+
+const dbg = new Debugger();
+const g1w = dbg.addDebuggee(g1);
+const g2w = dbg.addDebuggee(g2);
+
+const sources = dbg.findSources();
+assertEq(dbg.findSources().filter(s => s.url == "a.js").length, 1);
+assertEq(dbg.findSources().filter(s => s.url == "b.js").length, 1);
+assertEq(dbg.findSources().filter(s => s.url == "c.js").length, 1);
+assertEq(dbg.findSources().filter(s => s.url == "d.js").length, 1);
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -4183,41 +4183,94 @@ Debugger::removeDebuggeeGlobal(FreeOp* f
         global->realm()->updateDebuggerObservesBinarySource();
         global->realm()->updateDebuggerObservesCoverage();
     }
 }
 
 
 static inline DebuggerSourceReferent GetSourceReferent(JSObject* obj);
 
+class MOZ_STACK_CLASS Debugger::QueryBase
+{
+  protected:
+    QueryBase(JSContext* cx, Debugger* dbg)
+      : cx(cx),
+        debugger(dbg),
+        iterMarker(&cx->runtime()->gc),
+        realms(cx->zone()),
+        oom(false)
+    {}
+
+    // The context in which we should do our work.
+    JSContext* cx;
+
+    // The debugger for which we conduct queries.
+    Debugger* debugger;
+
+    // Require the set of realms to stay fixed while this query is alive.
+    gc::AutoEnterIteration iterMarker;
+
+    using RealmSet = HashSet<Realm*, DefaultHasher<Realm*>, ZoneAllocPolicy>;
+
+    // A script must be in one of these realms to match the query.
+    RealmSet realms;
+
+    // Indicates whether OOM has occurred while matching.
+    bool oom;
+
+    bool addRealm(Realm* realm) {
+        return realms.put(realm);
+    }
+
+    // Arrange for this query to match only scripts that run in |global|.
+    bool matchSingleGlobal(GlobalObject* global) {
+        MOZ_ASSERT(realms.count() == 0);
+        if (!addRealm(global->realm())) {
+            ReportOutOfMemory(cx);
+            return false;
+        }
+        return true;
+    }
+
+    // Arrange for this ScriptQuery to match all scripts running in debuggee
+    // globals.
+    bool matchAllDebuggeeGlobals() {
+        MOZ_ASSERT(realms.count() == 0);
+        // Build our realm set from the debugger's set of debuggee globals.
+        for (WeakGlobalObjectSet::Range r = debugger->debuggees.all(); !r.empty(); r.popFront()) {
+            if (!addRealm(r.front()->realm())) {
+                ReportOutOfMemory(cx);
+                return false;
+            }
+        }
+        return true;
+    }
+};
+
 /*
  * A class for parsing 'findScripts' query arguments and searching for
  * scripts that match the criteria they represent.
  */
-class MOZ_STACK_CLASS Debugger::ScriptQuery
+class MOZ_STACK_CLASS Debugger::ScriptQuery : public Debugger::QueryBase
 {
   public:
     /* Construct a ScriptQuery to use matching scripts for |dbg|. */
-    ScriptQuery(JSContext* cx, Debugger* dbg):
-        cx(cx),
-        debugger(dbg),
-        iterMarker(&cx->runtime()->gc),
-        realms(cx->zone()),
+    ScriptQuery(JSContext* cx, Debugger* dbg)
+      : QueryBase(cx, dbg),
         url(cx),
         displayURLString(cx),
         hasSource(false),
         source(cx, AsVariant(static_cast<ScriptSourceObject*>(nullptr))),
         hasLine(false),
         line(0),
         innermost(false),
         innermostForRealm(cx->zone()),
         scriptVector(cx, ScriptVector(cx)),
         lazyScriptVector(cx, LazyScriptVector(cx)),
-        wasmInstanceVector(cx, WasmInstanceObjectVector(cx)),
-        oom(false)
+        wasmInstanceVector(cx, WasmInstanceObjectVector(cx))
     {}
 
     /*
      * Parse the query object |query|, and prepare to match only the scripts
      * it specifies.
      */
     bool parseQuery(HandleObject query) {
         // Check for a 'global' property, which limits the results to those
@@ -4431,30 +4484,16 @@ class MOZ_STACK_CLASS Debugger::ScriptQu
         return lazyScriptVector;
     }
 
     Handle<WasmInstanceObjectVector> foundWasmInstances() const {
         return wasmInstanceVector;
     }
 
   private:
-    /* The context in which we should do our work. */
-    JSContext* cx;
-
-    /* The debugger for which we conduct queries. */
-    Debugger* debugger;
-
-    /* Require the set of realms to stay fixed while the ScriptQuery is alive. */
-    gc::AutoEnterIteration iterMarker;
-
-    using RealmSet = HashSet<Realm*, DefaultHasher<Realm*>, ZoneAllocPolicy>;
-
-    /* A script must be in one of these realms to match the query. */
-    RealmSet realms;
-
     /* If this is a string, matching scripts have urls equal to it. */
     RootedValue url;
 
     /* url as a C string. */
     JSAutoByteString urlCString;
 
     /* If this is a string, matching scripts' sources have displayURLs equal to
      * it. */
@@ -4494,49 +4533,16 @@ class MOZ_STACK_CLASS Debugger::ScriptQu
     Rooted<ScriptVector> scriptVector;
     Rooted<LazyScriptVector> lazyScriptVector;
 
     /*
      * Like above, but for wasm modules.
      */
     Rooted<WasmInstanceObjectVector> wasmInstanceVector;
 
-    /* Indicates whether OOM has occurred while matching. */
-    bool oom;
-
-    bool addRealm(Realm* realm) {
-        return realms.put(realm);
-    }
-
-    /* Arrange for this ScriptQuery to match only scripts that run in |global|. */
-    bool matchSingleGlobal(GlobalObject* global) {
-        MOZ_ASSERT(realms.count() == 0);
-        if (!addRealm(global->realm())) {
-            ReportOutOfMemory(cx);
-            return false;
-        }
-        return true;
-    }
-
-    /*
-     * Arrange for this ScriptQuery to match all scripts running in debuggee
-     * globals.
-     */
-    bool matchAllDebuggeeGlobals() {
-        MOZ_ASSERT(realms.count() == 0);
-        // Build our realm set from the debugger's set of debuggee globals.
-        for (WeakGlobalObjectSet::Range r = debugger->debuggees.all(); !r.empty(); r.popFront()) {
-            if (!addRealm(r.front()->realm())) {
-                ReportOutOfMemory(cx);
-                return false;
-            }
-        }
-        return true;
-    }
-
     /*
      * Given that parseQuery or omittedQuery has been called, prepare to match
      * scripts. Set urlCString and displayURLChars as appropriate.
      */
     bool prepareQuery() {
         // Compute urlCString and displayURLChars, if a url or displayURL was
         // given respectively.
         if (url.isString()) {
@@ -4761,16 +4767,170 @@ Debugger::findScripts(JSContext* cx, uns
         result->setDenseElement(wasmStart + i, ObjectValue(*scriptObject));
     }
 
     args.rval().setObject(*result);
     return true;
 }
 
 /*
+ * A class for searching sources for 'findSources'.
+ */
+class MOZ_STACK_CLASS Debugger::SourceQuery : public Debugger::QueryBase
+{
+  public:
+    using SourceSet = JS::GCHashSet<JSObject*,
+                                    js::MovableCellHasher<JSObject*>,
+                                    ZoneAllocPolicy>;
+
+    SourceQuery(JSContext* cx, Debugger* dbg)
+      : QueryBase(cx, dbg),
+        sources(cx, SourceSet(cx->zone()))
+    {}
+
+    bool findSources() {
+        if (!matchAllDebuggeeGlobals())
+            return false;
+
+        Realm* singletonRealm = nullptr;
+        if (realms.count() == 1)
+            singletonRealm = realms.all().front();
+
+        // Search each realm for debuggee scripts.
+        MOZ_ASSERT(sources.empty());
+        oom = false;
+        IterateScripts(cx, singletonRealm, this, considerScript);
+        IterateLazyScripts(cx, singletonRealm, this, considerLazyScript);
+        if (oom) {
+            ReportOutOfMemory(cx);
+            return false;
+        }
+
+        // TODO: Until such time that wasm modules are real ES6 modules,
+        // unconditionally consider all wasm toplevel instance scripts.
+        for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty(); r.popFront()) {
+            for (wasm::Instance* instance : r.front()->realm()->wasm.instances()) {
+                consider(instance->object());
+                if (oom) {
+                    ReportOutOfMemory(cx);
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    Handle<SourceSet> foundSources() const {
+        return sources;
+    }
+
+  private:
+    Rooted<SourceSet> sources;
+
+    static void considerScript(JSRuntime* rt, void* data, JSScript* script,
+                               const JS::AutoRequireNoGC& nogc) {
+        SourceQuery* self = static_cast<SourceQuery*>(data);
+        self->consider(script, nogc);
+    }
+
+    static void considerLazyScript(JSRuntime* rt, void* data, LazyScript* lazyScript,
+                                   const JS::AutoRequireNoGC& nogc) {
+        SourceQuery* self = static_cast<SourceQuery*>(data);
+        self->consider(lazyScript, nogc);
+    }
+
+    void consider(JSScript* script, const JS::AutoRequireNoGC& nogc) {
+        if (oom || script->selfHosted())
+            return;
+        Realm* realm = script->realm();
+        if (!realms.has(realm))
+            return;
+
+        if (!script->sourceObject())
+            return;
+
+        ScriptSourceObject* source =
+            &UncheckedUnwrap(script->sourceObject())->as<ScriptSourceObject>();
+        if (!sources.put(source))
+            oom = true;
+    }
+
+    void consider(LazyScript* lazyScript, const JS::AutoRequireNoGC& nogc) {
+        if (oom)
+            return;
+        Realm* realm = lazyScript->realm();
+        if (!realms.has(realm))
+            return;
+
+        // If the script is already delazified, it should already be handled.
+        if (lazyScript->maybeScript())
+            return;
+
+        ScriptSourceObject* source = &lazyScript->sourceObject();
+        if (!sources.put(source))
+            oom = true;
+    }
+
+    void consider(WasmInstanceObject* instanceObject) {
+        if (oom)
+            return;
+
+        if (!sources.put(instanceObject))
+            oom = true;
+    }
+};
+
+static inline DebuggerSourceReferent
+AsSourceReferent(JSObject* obj)
+{
+    if (obj->is<ScriptSourceObject>()) {
+        return AsVariant(&obj->as<ScriptSourceObject>());
+    }
+    return AsVariant(&obj->as<WasmInstanceObject>());
+}
+
+/* static */ bool
+Debugger::findSources(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER(cx, argc, vp, "findSources", args, dbg);
+
+    if (gc::GCRuntime::temporaryAbortIfWasmGc(cx)) {
+        JS_ReportErrorASCII(cx, "API temporarily unavailable under wasm gc");
+        return false;
+    }
+
+    SourceQuery query(cx, dbg);
+    if (!query.findSources())
+        return false;
+
+    Handle<SourceQuery::SourceSet> sources(query.foundSources());
+
+    size_t resultLength = sources.count();
+    RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, resultLength));
+    if (!result)
+        return false;
+
+    result->ensureDenseInitializedLength(cx, 0, resultLength);
+
+    size_t i = 0;
+    for (auto iter = sources.get().iter(); !iter.done(); iter.next()) {
+        Rooted<DebuggerSourceReferent> sourceReferent(cx, AsSourceReferent(iter.get()));
+        RootedObject sourceObject(cx, dbg->wrapVariantReferent(cx, sourceReferent));
+        if (!sourceObject)
+            return false;
+        result->setDenseElement(i, ObjectValue(*sourceObject));
+        i++;
+    }
+
+    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) :
@@ -5176,16 +5336,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("findSources", Debugger::findSources, 1, 0),
     JS_FN("findObjects", Debugger::findObjects, 1, 0),
     JS_FN("findAllGlobals", Debugger::findAllGlobals, 0, 0),
     JS_FN("makeGlobalObjectReference", Debugger::makeGlobalObjectReference, 1, 0),
     JS_FN("adoptDebuggeeValue", Debugger::adoptDebuggeeValue, 1, 0),
     JS_FS_END
 };
 
 const JSFunctionSpec Debugger::static_methods[] {
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -561,17 +561,19 @@ class Debugger : private mozilla::Linked
      */
 #ifdef NIGHTLY_BUILD
     uint32_t traceLoggerLastDrainedSize;
     uint32_t traceLoggerLastDrainedIteration;
 #endif
     uint32_t traceLoggerScriptedCallsLastDrainedSize;
     uint32_t traceLoggerScriptedCallsLastDrainedIteration;
 
+    class QueryBase;
     class ScriptQuery;
+    class SourceQuery;
     class ObjectQuery;
 
     MOZ_MUST_USE bool addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> obj);
     void removeDebuggeeGlobal(FreeOp* fop, GlobalObject* global,
                               WeakGlobalObjectSet::Enum* debugEnum);
 
     enum class CallUncaughtExceptionHook {
         No,
@@ -713,16 +715,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 findSources(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 setupTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp);
     static bool drainTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp);
     static bool startTraceLogger(JSContext* cx, unsigned argc, Value* vp);
     static bool endTraceLogger(JSContext* cx, unsigned argc, Value* vp);
     static bool isCompilableUnit(JSContext* cx, unsigned argc, Value* vp);