Bug 1513118 Part 2 - Add support for virtual console logs to ReplayDebuggerScript, r=lsmyth.
☠☠ backed out by 9e3564442734 ☠ ☠
authorBrian Hackett <bhackett1024@gmail.com>
Sat, 29 Dec 2018 08:23:38 -1000
changeset 453662 5ce216ffcb1d
parent 453661 9417ce02d4f2
child 453663 4abb81088a9b
push id35365
push userdvarga@mozilla.com
push dateSun, 13 Jan 2019 10:05:55 +0000
treeherdermozilla-central@1218e374fbc7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsmyth
bugs1513118
milestone66.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 1513118 Part 2 - Add support for virtual console logs to ReplayDebuggerScript, r=lsmyth.
devtools/server/actors/replay/control.js
devtools/server/actors/replay/debugger.js
--- a/devtools/server/actors/replay/control.js
+++ b/devtools/server/actors/replay/control.js
@@ -686,16 +686,19 @@ function flushRecording() {
 
   for (const child of gChildren) {
     if (child && !child.recording) {
       child.pauseNeeded = false;
       child.role.poke();
     }
   }
 
+  // After flushing the recording there may be more search results.
+  maybeResumeSearch();
+
   gLastRecordingCheckpoint = gActiveChild.lastCheckpoint();
 
   // We now have a usable recording for replaying children.
   if (!gFirstReplayingChild) {
     spawnInitialReplayingChildren();
   }
 }
 
@@ -983,26 +986,102 @@ const gControl = {
   clearBreakpoints() { gActiveChild.sendClearBreakpoints(); },
   sendRequest(request) { return gActiveChild.sendDebuggerRequest(request); },
   markExplicitPause,
   maybeSwitchToReplayingChild,
   resume,
   timeWarp,
 };
 
+////////////////////////////////////////////////////////////////////////////////
+// Search Operations
+////////////////////////////////////////////////////////////////////////////////
+
+let gSearchChild;
+let gSearchRestartNeeded;
+
+function maybeRestartSearch() {
+  if (gSearchRestartNeeded && gSearchChild.paused) {
+    if (gSearchChild.lastPausePoint.checkpoint != FirstCheckpointId ||
+        gSearchChild.lastPausePoint.position) {
+      gSearchChild.sendRestoreCheckpoint(FirstCheckpointId);
+      gSearchChild.waitUntilPaused();
+    }
+    gSearchChild.sendClearBreakpoints();
+    gDebugger._forEachSearch(pos => gSearchChild.sendAddBreakpoint(pos));
+    gSearchRestartNeeded = false;
+    gSearchChild.sendResume({ forward: true });
+    return true;
+  }
+  return false;
+}
+
+function ChildRoleSearch() {}
+
+ChildRoleSearch.prototype = {
+  name: "Search",
+
+  initialize(child, { startup }) {
+    this.child = child;
+  },
+
+  hitExecutionPoint({ point, recordingEndpoint }) {
+    if (maybeRestartSearch()) {
+      return;
+    }
+
+    if (point.position) {
+      gDebugger._onSearchPause(point);
+    }
+
+    if (!recordingEndpoint) {
+      this.poke();
+    }
+  },
+
+  poke() {
+    if (!gSearchRestartNeeded && !this.child.pauseNeeded) {
+      this.child.sendResume({ forward: true });
+    }
+  },
+};
+
+function ensureHasSearchChild() {
+  if (!gSearchChild) {
+    gSearchChild = spawnReplayingChild(new ChildRoleSearch());
+  }
+}
+
+function maybeResumeSearch() {
+  if (gSearchChild && gSearchChild.paused) {
+    gSearchChild.sendResume({ forward: true });
+  }
+}
+
+const gSearchControl = {
+  reset() {
+    ensureHasSearchChild();
+    gSearchRestartNeeded = true;
+    maybeRestartSearch();
+  },
+
+  sendRequest(request) { return gSearchChild.sendDebuggerRequest(request); },
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Utilities
+///////////////////////////////////////////////////////////////////////////////
+
 // eslint-disable-next-line no-unused-vars
 function ConnectDebugger(dbg) {
   gDebugger = dbg;
   dbg._control = gControl;
+  dbg._searchControl = gSearchControl;
 }
 
-///////////////////////////////////////////////////////////////////////////////
-// Utilities
-///////////////////////////////////////////////////////////////////////////////
-
 function dumpv(str) {
   //dump("[ReplayControl] " + str + "\n");
 }
 
 function assert(v) {
   if (!v) {
     ThrowError("Assertion Failed!");
   }
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -37,16 +37,17 @@ function ReplayDebugger() {
   if (existing) {
     // There is already a ReplayDebugger in existence, use that. There can only
     // be one ReplayDebugger in the process.
     return existing;
   }
 
   // We should have been connected to control.js by the call above.
   assert(this._control);
+  assert(this._searchControl);
 
   // Preferred direction of travel when not explicitly resumed.
   this._direction = Direction.NONE;
 
   // All breakpoint positions and handlers installed by this debugger.
   this._breakpoints = [];
 
   // All ReplayDebuggerFramees that have been created while paused at the
@@ -70,16 +71,19 @@ function ReplayDebugger() {
 
   // Flag set if the dispatched _performPause() call can be ignored because the
   // server entered a thread-wide pause first.
   this._cancelPerformPause = false;
 
   // After we are done pausing, callback describing how to resume.
   this._resumeCallback = null;
 
+  // Information about all searches that exist.
+  this._searches = [];
+
   // Handler called when hitting the beginning/end of the recording, or when
   // a time warp target has been reached.
   this.replayingOnForcedPause = null;
 
   // Handler called when the child pauses for any reason.
   this.replayingOnPositionChange = null;
 }
 
@@ -344,16 +348,57 @@ ReplayDebugger.prototype = {
     this._frames.forEach(frame => frame._invalidate());
     this._frames.length = 0;
 
     this._objects.forEach(obj => obj._invalidate());
     this._objects.length = 0;
   },
 
   /////////////////////////////////////////////////////////
+  // Search management
+  /////////////////////////////////////////////////////////
+
+  _forEachSearch(callback) {
+    for (const { position } of this._searches) {
+      callback(position);
+    }
+  },
+
+  _virtualConsoleLog(position, text, callback) {
+    this._searches.push({ position, text, callback, results: [] });
+    this._searchControl.reset();
+  },
+
+  _onSearchPause(point) {
+    for (const { position, text, callback, results } of this._searches) {
+      if (RecordReplayControl.positionSubsumes(position, point.position)) {
+        if (!results.some(existing => point.progress == existing.progress)) {
+          let evaluateResult;
+          if (text) {
+            const frameData = this._searchControl.sendRequest({
+              type: "getFrame",
+              index: NewestFrameIndex,
+            });
+            if ("index" in frameData) {
+              const rv = this._searchControl.sendRequest({
+                type: "frameEvaluate",
+                index: frameData.index,
+                text,
+              });
+              evaluateResult = this._convertCompletionValue(rv, { forSearch: true });
+            }
+          }
+          results.push(point);
+          callback(point, evaluateResult);
+        }
+      }
+    }
+  },
+
+  /////////////////////////////////////////////////////////
   // Breakpoint management
   /////////////////////////////////////////////////////////
 
   _setBreakpoint(handler, position, data) {
     this._ensurePaused();
     dumpv("AddBreakpoint " + JSON.stringify(position));
     this._control.addBreakpoint(position);
     this._breakpoints.push({handler, position, data});
@@ -480,63 +525,69 @@ ReplayDebugger.prototype = {
     const data = this._sendRequest({ type: "findSources" });
     return data.map(source => this._addSource(source));
   },
 
   /////////////////////////////////////////////////////////
   // Object methods
   /////////////////////////////////////////////////////////
 
-  // Objects which |forConsole| is set are objects that were logged in console
-  // messages, and had their properties recorded so that they can be inspected
-  // without switching to a replaying child.
-  _getObject(id, forConsole) {
+  _getObject(id, options) {
+    if (options && options.forSearch) {
+      // Returning objects through searches is NYI.
+      return "<UnknownSearchObject>";
+    }
+    const forConsole = options && options.forConsole;
+
     if (id && !this._objects[id]) {
       const data = this._sendRequest({ type: "getObject", id });
       switch (data.kind) {
       case "Object":
+        // Objects which |forConsole| is set are objects that were logged in
+        // console messages, and had their properties recorded so that they can
+        // be inspected without switching to a replaying child.
         this._objects[id] = new ReplayDebuggerObject(this, data, forConsole);
         break;
       case "Environment":
         this._objects[id] = new ReplayDebuggerEnvironment(this, data);
         break;
       default:
         ThrowError("Unknown object kind");
       }
     }
     const rv = this._objects[id];
     if (forConsole) {
       rv._forConsole = true;
     }
     return rv;
   },
 
-  _convertValue(value, forConsole) {
+  _convertValue(value, options) {
     if (isNonNullObject(value)) {
       if (value.object) {
-        return this._getObject(value.object, forConsole);
+        return this._getObject(value.object, options);
       } else if (value.special == "undefined") {
         return undefined;
       } else if (value.special == "NaN") {
         return NaN;
       } else if (value.special == "Infinity") {
         return Infinity;
       } else if (value.special == "-Infinity") {
         return -Infinity;
       }
     }
     return value;
   },
 
-  _convertCompletionValue(value) {
+  _convertCompletionValue(value, options) {
     if ("return" in value) {
-      return { return: this._convertValue(value.return) };
+      return { return: this._convertValue(value.return, options) };
     }
     if ("throw" in value) {
-      return { throw: this._convertValue(value.throw) };
+      return { throw: this._convertValue(value.throw, options) };
     }
     ThrowError("Unexpected completion value");
     return null; // For eslint
   },
 
   /////////////////////////////////////////////////////////
   // Frame methods
   /////////////////////////////////////////////////////////
@@ -577,17 +628,17 @@ ReplayDebugger.prototype = {
   /////////////////////////////////////////////////////////
 
   _convertConsoleMessage(message) {
     // Console API message arguments need conversion to debuggee values, but
     // other contents of the message can be left alone.
     if (message.messageType == "ConsoleAPI" && message.arguments) {
       for (let i = 0; i < message.arguments.length; i++) {
         message.arguments[i] = this._convertValue(message.arguments[i],
-                                                  /* forConsole = */ true);
+                                                  { forConsole: true });
       }
     }
     return message;
   },
 
   /////////////////////////////////////////////////////////
   // Handlers
   /////////////////////////////////////////////////////////
@@ -678,16 +729,21 @@ ReplayDebuggerScript.prototype = {
   },
 
   clearBreakpoint(handler) {
     this._dbg._clearMatchingBreakpoints(({position, data}) => {
       return position.script == this._data.id && handler == data;
     });
   },
 
+  replayVirtualConsoleLog(offset, text, callback) {
+    this._dbg._virtualConsoleLog({ kind: "Break", script: this._data.id, offset },
+                                 text, callback);
+  },
+
   get isGeneratorFunction() { NYI(); },
   get isAsyncFunction() { NYI(); },
   getChildScripts: NYI,
   getAllOffsets: NYI,
   getBreakpoints: NYI,
   clearAllBreakpoints: NYI,
   isInCatchScope: NYI,
 };