Bug 1470795 Part 1 - Debugger changes for recording/replaying, r=jorendorff.
authorBrian Hackett <bhackett1024@gmail.com>
Mon, 23 Jul 2018 21:44:04 +0000
changeset 822114 cfb449f0184cb01ebd640c14abd7995845626509
parent 822113 7d63e1235defe4c6a80d246658140470fc25c98a
child 822115 818c1a79b41dc6d18c9dbe2c600fe02905f3e7d0
push id117296
push userbmo:gl@mozilla.com
push dateTue, 24 Jul 2018 20:28:07 +0000
reviewersjorendorff
bugs1470795
milestone63.0a1
Bug 1470795 Part 1 - Debugger changes for recording/replaying, r=jorendorff.
js/src/doc/Debugger/Debugger.Script.md
js/src/doc/Debugger/Debugger.md
js/src/jit-test/tests/debug/Script-getSuccessorOrPredecessorOffsets-01.js
js/src/jit-test/tests/debug/Script-mainOffset-01.js
js/src/vm/BytecodeUtil.cpp
js/src/vm/BytecodeUtil.h
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
--- a/js/src/doc/Debugger/Debugger.Script.md
+++ b/js/src/doc/Debugger/Debugger.Script.md
@@ -171,16 +171,22 @@ from its prototype:
 
 `sourceLength`
 :   **If the instance refers to a `JSScript`**, the length, in characters, of
     this script's code within the [`Debugger.Source`][source] instance given
     by `source`.
 
     **If the instance refers to WebAssembly code**, throw a `TypeError`.
 
+`mainOffset`
+:   **If the instance refers to a `JSScript`**, the offset of the main
+    entry point of the script, excluding any prologue.
+
+    **If the instance refers to WebAssembly code**, throw a `TypeError`.
+
 `global`
 :   **If the instance refers to a `JSScript`**, a [`Debugger.Object`][object]
     instance referring to the global object in whose scope this script
     runs. The result refers to the global directly, not via a wrapper or a
     `WindowProxy` ("outer window", in Firefox).
 
     **If the instance refers to WebAssembly code**, throw a `TypeError`.
 
@@ -282,16 +288,28 @@ methods of other kinds of objects.
 
     * lineNumber: the line number for which offset is an entry point
 
     * columnNumber: the column number for which offset is an entry point
 
     * isEntryPoint: true if the offset is a column entry point, as
       would be reported by getAllColumnOffsets(); otherwise false.
 
+<code>getSuccessorOffsets(<i>offset</i>)</code>
+:   **If the instance refers to a `JSScript`**, return an array
+    containing the offsets of all bytecodes in the script which are
+    immediate successors of <i>offset</i> via non-exceptional control
+    flow paths.
+
+<code>getPredecessorOffsets(<i>offset</i>)</code>
+:   **If the instance refers to a `JSScript`**, return an array
+    containing the offsets of all bytecodes in the script for which
+    <i>offset</i> is an immediate successor via non-exceptional
+    control flow paths.
+
 `getOffsetsCoverage()`:
 :   **If the instance refers to a `JSScript`**, return `null` or an array which
     contains informations about the coverage of all opcodes. The elements of
     the array are objects, each of which describes a single opcode, and
     contains the following properties:
 
     * lineNumber: the line number of the current opcode.
 
--- a/js/src/doc/Debugger/Debugger.md
+++ b/js/src/doc/Debugger/Debugger.md
@@ -501,8 +501,14 @@ other kinds of objects.
 The functions described below are not called with a `this` value.
 
 <code id="isCompilableUnit">isCompilableUnit(<i>source</i>)</code>
 :   Given a string of source code, designated by <i>source</i>, return false if
     the string might become a valid JavaScript statement with the addition of
     more lines. Otherwise return true. The intent is to support interactive
     compilation - accumulate lines in a buffer until isCompilableUnit is true,
     then pass it to the compiler.
+
+<code id="recordReplayProcessKind">recordReplayProcessKind()</code>
+:   Return the kind of record/replay firefox process that is currently
+    running: the string "RecordingReplaying" if this is a recording or
+    replaying process, the string "Middleman" if this is a middleman
+    process, or undefined for normal firefox content or UI processes.
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getSuccessorOrPredecessorOffsets-01.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+
+// An offset A of a script should have successor B iff B has predecessor A.
+// Offsets reached while stepping through a script should be successors of
+// each other.
+
+var scripts = [
+  "n = 1",
+  "if (n == 0) return; else n = 2",
+  "while (n < 5) n++",
+  "switch (n) { case 4: break; case 5: return 0; }",
+];
+
+var g = newGlobal();
+for (var n in scripts) {
+  g.eval("function f" + n + "() { " + scripts[n] + " }");
+}
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+  for (var n in scripts) {
+    var compares = 0;
+    var script = frame.eval("f" + n).return.script;
+    var worklist = [script.mainOffset];
+    var seen = [];
+    while (worklist.length) {
+      var offset = worklist.pop();
+      if (seen.includes(offset))
+        continue;
+      seen.push(offset);
+      var succs = script.getSuccessorOffsets(offset);
+      for (var succ of succs) {
+        compares++;
+        var preds = script.getPredecessorOffsets(succ);
+        assertEq(preds.includes(offset), true);
+        worklist.push(succ);
+      }
+    }
+    assertEq(compares != 0, true);
+    compares = 0;
+    for (var offset of seen) {
+      var preds = script.getPredecessorOffsets(offset);
+      for (var pred of preds) {
+        var succs = script.getSuccessorOffsets(pred);
+        compares++;
+        assertEq(succs.includes(offset), true);
+      }
+    }
+    assertEq(compares != 0, true);
+    script.setBreakpoint(script.mainOffset, { hit: mainBreakpointHandler });
+  }
+};
+g.eval("debugger");
+
+function mainBreakpointHandler(frame) {
+  frame.lastOffset = frame.script.mainOffset;
+  frame.onStep = onStepHandler;
+}
+
+function onStepHandler() {
+  steps++;
+  var succs = this.script.getSuccessorOffsets(this.lastOffset);
+  if (!succs.includes(this.offset)) {
+    // The onStep handler might skip over opcodes, even when running in the
+    // interpreter. Check transitive successors of the last offset.
+    var found = false;
+    for (var succ of succs) {
+      if (this.script.getSuccessorOffsets(succ).includes(this.offset))
+        found = true;
+    }
+    assertEq(found, true);
+  }
+  this.lastOffset = this.offset;
+}
+
+for (var n in scripts) {
+  var steps = 0;
+  g.eval("f" + n + "()");
+  assertEq(steps != 0, true);
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-mainOffset-01.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+// The main offset of a script should be hit before it performs any actions.
+
+var g = newGlobal();
+g.eval("var n = 0; function foo() { n = 1; }");
+var dbg = Debugger(g);
+
+var hits = 0;
+function breakpointHit(frame) {
+  hits++;
+  assertEq(frame.eval("n").return, 0);
+}
+
+dbg.onDebuggerStatement = function (frame) {
+  var script = frame.eval("foo").return.script;
+  script.setBreakpoint(script.mainOffset, { hit: breakpointHit });
+};
+g.eval("debugger; foo()");
+assertEq(g.eval("n"), 1);
+assertEq(hits, 1);
--- a/js/src/vm/BytecodeUtil.cpp
+++ b/js/src/vm/BytecodeUtil.cpp
@@ -2984,8 +2984,60 @@ js::GetCodeCoverageSummary(JSContext* cx
         return nullptr;
 
     js_memcpy(res, out.string(), len);
     res[len] = 0;
     if (length)
         *length = len;
     return res;
 }
+
+bool
+js::GetSuccessorBytecodes(jsbytecode* pc, PcVector& successors)
+{
+    JSOp op = (JSOp)*pc;
+    if (FlowsIntoNext(op)) {
+        if (!successors.append(GetNextPc(pc)))
+            return false;
+    }
+
+    if (CodeSpec[op].type() == JOF_JUMP) {
+        if (!successors.append(pc + GET_JUMP_OFFSET(pc)))
+            return false;
+    } else if (op == JSOP_TABLESWITCH) {
+        if (!successors.append(pc + GET_JUMP_OFFSET(pc)))
+            return false;
+        jsbytecode* npc = pc + JUMP_OFFSET_LEN;
+
+        int32_t low = GET_JUMP_OFFSET(npc);
+        npc += JUMP_OFFSET_LEN;
+        int ncases = GET_JUMP_OFFSET(npc) - low + 1;
+        npc += JUMP_OFFSET_LEN;
+
+        for (int i = 0; i < ncases; i++) {
+            if (!successors.append(pc + GET_JUMP_OFFSET(npc)))
+                return false;
+            npc += JUMP_OFFSET_LEN;
+        }
+    }
+
+    return true;
+}
+
+bool
+js::GetPredecessorBytecodes(JSScript* script, jsbytecode* pc, PcVector& predecessors)
+{
+    jsbytecode* end = script->code() + script->length();
+    MOZ_ASSERT(pc >= script->code() && pc < end);
+    for (jsbytecode* npc = script->code(); npc < end; npc = GetNextPc(npc)) {
+        PcVector successors;
+        if (!GetSuccessorBytecodes(npc, successors))
+            return false;
+        for (size_t i = 0; i < successors.length(); i++) {
+            if (successors[i] == pc) {
+                if (!predecessors.append(npc))
+                    return false;
+                break;
+            }
+        }
+    }
+    return true;
+}
--- a/js/src/vm/BytecodeUtil.h
+++ b/js/src/vm/BytecodeUtil.h
@@ -831,16 +831,21 @@ class PCCounts
 };
 
 static inline jsbytecode*
 GetNextPc(jsbytecode* pc)
 {
     return pc + GetBytecodeLength(pc);
 }
 
+typedef Vector<jsbytecode*, 4, SystemAllocPolicy> PcVector;
+
+bool GetSuccessorBytecodes(jsbytecode* pc, PcVector& successors);
+bool GetPredecessorBytecodes(JSScript* script, jsbytecode* pc, PcVector& predecessors);
+
 #if defined(DEBUG) || defined(JS_JITSPEW)
 /*
  * Disassemblers, for debugging only.
  */
 extern MOZ_MUST_USE bool
 Disassemble(JSContext* cx, JS::Handle<JSScript*> script, bool lines, Sprinter* sp);
 
 unsigned
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -5063,16 +5063,37 @@ Debugger::isCompilableUnit(JSContext* cx
 
         cx->clearPendingException();
     }
     JS::SetWarningReporter(cx, older);
     args.rval().setBoolean(result);
     return true;
 }
 
+/* static */ bool
+Debugger::recordReplayProcessKind(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (mozilla::recordreplay::IsMiddleman()) {
+        JSString* str = JS_NewStringCopyZ(cx, "Middleman");
+        if (!str)
+            return false;
+        args.rval().setString(str);
+    } else if (mozilla::recordreplay::IsRecordingOrReplaying()) {
+        JSString* str = JS_NewStringCopyZ(cx, "RecordingReplaying");
+        if (!str)
+            return false;
+        args.rval().setString(str);
+    } else {
+        args.rval().setUndefined();
+    }
+    return true;
+}
+
 bool
 Debugger::adoptDebuggeeValue(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGGER(cx, argc, vp, "adoptDebuggeeValue", args, dbg);
     if (!args.requireAtLeast(cx, "Debugger.adoptDebuggeeValue", 1))
         return false;
 
     RootedValue v(cx, args[0]);
@@ -5132,16 +5153,17 @@ const JSFunctionSpec Debugger::methods[]
     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[] {
     JS_FN("isCompilableUnit", Debugger::isCompilableUnit, 1, 0),
+    JS_FN("recordReplayProcessKind", Debugger::recordReplayProcessKind, 0, 0),
     JS_FS_END
 };
 
 /*** Debugger.Script *****************************************************************************/
 
 // Get the Debugger.Script referent as bare Cell. This should only be used for
 // GC operations like tracing. Please use GetScriptReferent below.
 static inline gc::Cell*
@@ -5498,16 +5520,24 @@ static bool
 DebuggerScript_getSourceLength(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get sourceEnd)", args, obj, script);
     args.rval().setNumber(uint32_t(script->sourceEnd() - script->sourceStart()));
     return true;
 }
 
 static bool
+DebuggerScript_getMainOffset(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get mainOffset)", args, obj, script);
+    args.rval().setNumber(uint32_t(script->mainOffset()));
+    return true;
+}
+
+static bool
 DebuggerScript_getGlobal(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get global)", args, obj, script);
     Debugger* dbg = Debugger::fromChildJSObject(obj);
 
     RootedValue v(cx, ObjectValue(script->global()));
     if (!dbg->wrapDebuggeeValue(cx, &v))
         return false;
@@ -5888,16 +5918,94 @@ DebuggerScript_getOffsetLocation(JSConte
     DebuggerScriptGetOffsetLocationMatcher matcher(cx, offset, &result);
     if (!referent.match(matcher))
         return false;
 
     args.rval().setObject(*result);
     return true;
 }
 
+class DebuggerScriptGetSuccessorOrPredecessorOffsetsMatcher
+{
+    JSContext* cx_;
+    size_t offset_;
+    bool successor_;
+    MutableHandleObject result_;
+
+  public:
+    DebuggerScriptGetSuccessorOrPredecessorOffsetsMatcher(JSContext* cx, size_t offset,
+                                                          bool successor,
+                                                          MutableHandleObject result)
+      : cx_(cx), offset_(offset), successor_(successor), result_(result) { }
+    using ReturnType = bool;
+    ReturnType match(HandleScript script) {
+        PcVector adjacent;
+        if (successor_) {
+            if (!GetSuccessorBytecodes(script->code() + offset_, adjacent)) {
+                ReportOutOfMemory(cx_);
+                return false;
+            }
+        } else {
+            if (!GetPredecessorBytecodes(script, script->code() + offset_, adjacent)) {
+                ReportOutOfMemory(cx_);
+                return false;
+            }
+        }
+
+        result_.set(NewDenseEmptyArray(cx_));
+        if (!result_)
+            return false;
+
+        for (jsbytecode* pc : adjacent) {
+            if (!NewbornArrayPush(cx_, result_, NumberValue(pc - script->code())))
+                return false;
+        }
+        return true;
+    }
+
+    ReturnType match(Handle<WasmInstanceObject*> instance) {
+        JS_ReportErrorASCII(cx_, "getSuccessorOrPredecessorOffsets NYI on wasm instances");
+        return false;
+    }
+};
+
+static bool
+DebuggerScript_getSuccessorOrPredecessorOffsets(JSContext* cx, unsigned argc, Value* vp,
+                                                const char* name, bool successor)
+{
+    THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, name, args, obj, referent);
+    if (!args.requireAtLeast(cx, name, 1))
+        return false;
+    size_t offset;
+    if (!ScriptOffset(cx, args[0], &offset))
+        return false;
+
+    RootedObject result(cx);
+    DebuggerScriptGetSuccessorOrPredecessorOffsetsMatcher matcher(cx, offset, successor, &result);
+    if (!referent.match(matcher))
+        return false;
+
+    args.rval().setObject(*result);
+    return true;
+}
+
+static bool
+DebuggerScript_getSuccessorOffsets(JSContext* cx, unsigned argc, Value* vp)
+{
+    return DebuggerScript_getSuccessorOrPredecessorOffsets(cx, argc, vp,
+                                                           "getSuccessorOffsets", true);
+}
+
+static bool
+DebuggerScript_getPredecessorOffsets(JSContext* cx, unsigned argc, Value* vp)
+{
+    return DebuggerScript_getSuccessorOrPredecessorOffsets(cx, argc, vp,
+                                                           "getPredecessorOffsets", false);
+}
+
 static bool
 DebuggerScript_getAllOffsets(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getAllOffsets", args, obj, script);
 
     // First pass: determine which offsets in this script are jump targets and
     // which line numbers jump to them.
     FlowGraphSummary flowData(cx);
@@ -6688,27 +6796,30 @@ static const JSPropertySpec DebuggerScri
     JS_PSG("isAsyncFunction", DebuggerScript_getIsAsyncFunction, 0),
     JS_PSG("displayName", DebuggerScript_getDisplayName, 0),
     JS_PSG("url", DebuggerScript_getUrl, 0),
     JS_PSG("startLine", DebuggerScript_getStartLine, 0),
     JS_PSG("lineCount", DebuggerScript_getLineCount, 0),
     JS_PSG("source", DebuggerScript_getSource, 0),
     JS_PSG("sourceStart", DebuggerScript_getSourceStart, 0),
     JS_PSG("sourceLength", DebuggerScript_getSourceLength, 0),
+    JS_PSG("mainOffset", DebuggerScript_getMainOffset, 0),
     JS_PSG("global", DebuggerScript_getGlobal, 0),
     JS_PSG("format", DebuggerScript_getFormat, 0),
     JS_PS_END
 };
 
 static const JSFunctionSpec DebuggerScript_methods[] = {
     JS_FN("getChildScripts", DebuggerScript_getChildScripts, 0, 0),
     JS_FN("getAllOffsets", DebuggerScript_getAllOffsets, 0, 0),
     JS_FN("getAllColumnOffsets", DebuggerScript_getAllColumnOffsets, 0, 0),
     JS_FN("getLineOffsets", DebuggerScript_getLineOffsets, 1, 0),
     JS_FN("getOffsetLocation", DebuggerScript_getOffsetLocation, 0, 0),
+    JS_FN("getSuccessorOffsets", DebuggerScript_getSuccessorOffsets, 1, 0),
+    JS_FN("getPredecessorOffsets", DebuggerScript_getPredecessorOffsets, 1, 0),
     JS_FN("setBreakpoint", DebuggerScript_setBreakpoint, 2, 0),
     JS_FN("getBreakpoints", DebuggerScript_getBreakpoints, 1, 0),
     JS_FN("clearBreakpoint", DebuggerScript_clearBreakpoint, 1, 0),
     JS_FN("clearAllBreakpoints", DebuggerScript_clearAllBreakpoints, 0, 0),
     JS_FN("isInCatchScope", DebuggerScript_isInCatchScope, 1, 0),
     JS_FN("getOffsetsCoverage", DebuggerScript_getOffsetsCoverage, 0, 0),
     JS_FS_END
 };
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -716,16 +716,17 @@ class Debugger : private mozilla::Linked
     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);
+    static bool recordReplayProcessKind(JSContext* cx, unsigned argc, Value* vp);
 #ifdef NIGHTLY_BUILD
     static bool setupTraceLogger(JSContext* cx, unsigned argc, Value* vp);
     static bool drainTraceLogger(JSContext* cx, unsigned argc, Value* vp);
 #endif
     static bool adoptDebuggeeValue(JSContext* cx, unsigned argc, Value* vp);
     static bool construct(JSContext* cx, unsigned argc, Value* vp);
     static const JSPropertySpec properties[];
     static const JSFunctionSpec methods[];