Bug 1488808 Part 11 - JS changes to trigger repaints after diverging from the recording, and handle the response, r=lsmyth.
authorBrian Hackett <bhackett1024@gmail.com>
Wed, 17 Oct 2018 10:05:36 -0600
changeset 500500 597cf7ce986a50e436398ac99491611dbeb0b30a
parent 500499 baaaaa544bb0da29752973de7ce72a7d8d5ee84a
child 500501 2ba7db69225b588c14915c8609cf82cb5f433a01
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsmyth
bugs1488808
milestone64.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 1488808 Part 11 - JS changes to trigger repaints after diverging from the recording, and handle the response, r=lsmyth.
devtools/server/actors/replay/debugger.js
devtools/server/actors/replay/graphics.js
devtools/server/actors/replay/replay.js
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -57,17 +57,21 @@ ReplayDebugger.prototype = {
   /////////////////////////////////////////////////////////
 
   replaying: true,
 
   canRewind: RecordReplayControl.canRewind,
   replayResumeBackward() { RecordReplayControl.resume(/* forward = */ false); },
   replayResumeForward() { RecordReplayControl.resume(/* forward = */ true); },
   replayTimeWarp: RecordReplayControl.timeWarp,
-  replayPause: RecordReplayControl.pause,
+
+  replayPause() {
+    RecordReplayControl.pause();
+    this._repaint();
+  },
 
   addDebuggee() {},
   removeAllDebuggees() {},
 
   replayingContent(url) {
     return this._sendRequest({ type: "getContent", url });
   },
 
@@ -85,16 +89,28 @@ ReplayDebugger.prototype = {
   // diverge from the recording. In such cases we want to be interacting with a
   // replaying process (if there is one), as recording child processes won't
   // provide useful responses to such requests.
   _sendRequestAllowDiverge(request) {
     RecordReplayControl.maybeSwitchToReplayingChild();
     return this._sendRequest(request);
   },
 
+  // Update graphics according to the current state of the child process. This
+  // should be done anytime we pause and allow the user to interact with the
+  // debugger.
+  _repaint() {
+    const rv = this._sendRequestAllowDiverge({ type: "repaint" });
+    if ("width" in rv && "height" in rv) {
+      RecordReplayControl.hadRepaint(rv.width, rv.height);
+    } else {
+      RecordReplayControl.hadRepaintFailure();
+    }
+  },
+
   _setBreakpoint(handler, position, data) {
     const id = RecordReplayControl.setBreakpoint(handler, position);
     this._breakpoints.push({id, position, data});
   },
 
   _clearMatchingBreakpoints(callback) {
     this._breakpoints = this._breakpoints.filter(breakpoint => {
       if (callback(breakpoint)) {
@@ -324,42 +340,45 @@ ReplayDebugger.prototype = {
   set onNewScript(handler) {
     this._breakpointKindSetter("NewScript", handler,
                                () => handler.call(this, this._getNewScript()));
   },
 
   get onEnterFrame() { return this._breakpointKindGetter("EnterFrame"); },
   set onEnterFrame(handler) {
     this._breakpointKindSetter("EnterFrame", handler,
-                               () => handler.call(this, this.getNewestFrame()));
+                               () => { this._repaint();
+                                       handler.call(this, this.getNewestFrame()); });
   },
 
   get replayingOnPopFrame() {
     return this._searchBreakpoints(({position, data}) => {
       return (position.kind == "OnPop" && !position.script) ? data : null;
     });
   },
 
   set replayingOnPopFrame(handler) {
     if (handler) {
-      this._setBreakpoint(() => handler.call(this, this.getNewestFrame()),
+      this._setBreakpoint(() => { this._repaint();
+                                  handler.call(this, this.getNewestFrame()); },
                           { kind: "OnPop" }, handler);
     } else {
       this._clearMatchingBreakpoints(({position}) => {
         return position.kind == "OnPop" && !position.script;
       });
     }
   },
 
   get replayingOnForcedPause() {
     return this._breakpointKindGetter("ForcedPause");
   },
   set replayingOnForcedPause(handler) {
     this._breakpointKindSetter("ForcedPause", handler,
-                               () => handler.call(this, this.getNewestFrame()));
+                               () => { this._repaint();
+                                       handler.call(this, this.getNewestFrame()); });
   },
 
   getNewConsoleMessage() {
     const message = this._sendRequest({ type: "getNewConsoleMessage" });
     return this._convertConsoleMessage(message);
   },
 
   get onConsoleMessage() {
@@ -397,17 +416,18 @@ ReplayDebuggerScript.prototype = {
   },
 
   getLineOffsets(line) { return this._forward("getLineOffsets", line); },
   getOffsetLocation(pc) { return this._forward("getOffsetLocation", pc); },
   getSuccessorOffsets(pc) { return this._forward("getSuccessorOffsets", pc); },
   getPredecessorOffsets(pc) { return this._forward("getPredecessorOffsets", pc); },
 
   setBreakpoint(offset, handler) {
-    this._dbg._setBreakpoint(() => { handler.hit(this._dbg.getNewestFrame()); },
+    this._dbg._setBreakpoint(() => { this._dbg._repaint();
+                                     handler.hit(this._dbg.getNewestFrame()); },
                              { kind: "Break", script: this._data.id, offset },
                              handler);
   },
 
   clearBreakpoint(handler) {
     this._dbg._clearMatchingBreakpoints(({position, data}) => {
       return position.script == this._data.id && handler == data;
     });
@@ -514,17 +534,18 @@ ReplayDebuggerFrame.prototype = {
       ({position}) => this._positionMatches(position, "OnStep")
     );
   },
 
   setReplayingOnStep(handler, offsets) {
     this._clearOnStepBreakpoints();
     offsets.forEach(offset => {
       this._dbg._setBreakpoint(
-        () => handler.call(this._dbg.getNewestFrame()),
+        () => { this._dbg._repaint();
+                handler.call(this._dbg.getNewestFrame()); },
         { kind: "OnStep",
           script: this._data.script,
           offset,
           frameIndex: this._data.index },
         handler);
     });
   },
 
@@ -532,16 +553,17 @@ ReplayDebuggerFrame.prototype = {
     return this._dbg._searchBreakpoints(({position, data}) => {
       return this._positionMatches(position, "OnPop") ? data : null;
     });
   },
 
   set onPop(handler) {
     if (handler) {
       this._dbg._setBreakpoint(() => {
+          this._dbg._repaint();
           const result = this._dbg._sendRequest({ type: "popFrameResult" });
           handler.call(this._dbg.getNewestFrame(),
                        this._dbg._convertCompletionValue(result));
         },
         { kind: "OnPop", script: this._data.script, frameIndex: this._data.index },
         handler);
     } else {
       this._dbg._clearMatchingBreakpoints(
--- a/devtools/server/actors/replay/graphics.js
+++ b/devtools/server/actors/replay/graphics.js
@@ -9,17 +9,17 @@
 // which are connected to the compositor in the UI process in the usual way.
 // We need to update the contents of the document to draw the raw graphics data
 // provided by the child process.
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-function updateWindow(window, buffer, width, height) {
+function updateWindow(window, buffer, width, height, hadFailure) {
   // Make sure the window has a canvas filling the screen.
   let canvas = window.middlemanCanvas;
   if (!canvas) {
     canvas = window.document.createElement("canvas");
     window.document.body.style.margin = "0px";
     window.document.body.insertBefore(canvas, window.document.body.firstChild);
     window.middlemanCanvas = canvas;
   }
@@ -31,33 +31,43 @@ function updateWindow(window, buffer, wi
   // been scaled in the child process. To avoid scaling the graphics twice,
   // transform the canvas to undo the scaling.
   const scale = window.devicePixelRatio;
   if (scale != 1) {
     canvas.style.transform =
       `scale(${ 1 / scale }) translate(-${ width / scale }px, -${ height / scale }px)`;
   }
 
+  const cx = canvas.getContext("2d");
+
   const graphicsData = new Uint8Array(buffer);
-  const imageData = canvas.getContext("2d").getImageData(0, 0, width, height);
+  const imageData = cx.getImageData(0, 0, width, height);
   imageData.data.set(graphicsData);
-  canvas.getContext("2d").putImageData(imageData, 0, 0);
+  cx.putImageData(imageData, 0, 0);
+
+  // Indicate to the user when repainting failed and we are showing old painted
+  // graphics instead of the most up-to-date graphics.
+  if (hadFailure) {
+    cx.fillStyle = "red";
+    cx.font = "48px serif";
+    cx.fillText("PAINT FAILURE", 10, 50);
+  }
 
   // Make recording/replaying tabs easier to differentiate from other tabs.
   window.document.title = "RECORD/REPLAY";
 }
 
 // Entry point for when we have some new graphics data from the child process
 // to draw.
 // eslint-disable-next-line no-unused-vars
-function Update(buffer, width, height) {
+function Update(buffer, width, height, hadFailure) {
   try {
     // Paint to all windows we can find. Hopefully there is only one.
     for (const window of Services.ww.getWindowEnumerator()) {
-      updateWindow(window, buffer, width, height);
+      updateWindow(window, buffer, width, height, hadFailure);
     }
   } catch (e) {
     dump("Middleman Graphics Update Exception: " + e + "\n");
   }
 }
 
 // eslint-disable-next-line no-unused-vars
 var EXPORTED_SYMBOLS = [
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -518,16 +518,23 @@ function forwardToScript(name) {
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Handlers
 ///////////////////////////////////////////////////////////////////////////////
 
 const gRequestHandlers = {
 
+  repaint() {
+    if (!RecordReplayControl.maybeDivergeFromRecording()) {
+      return {};
+    }
+    return RecordReplayControl.repaint();
+  },
+
   findScripts(request) {
     const query = Object.assign({}, request.query);
     if ("global" in query) {
       query.global = gPausedObjects.getObject(query.global);
     }
     if ("source" in query) {
       query.source = gScriptSources.getObject(query.source);
       if (!query.source) {