Bug 1496468 - Add a console pause indicator. r=loganfsmyth
authorJason Laster <jlaster@mozilla.com>
Thu, 04 Oct 2018 20:14:53 +0000
changeset 439676 c3950445b8d1941c941f6e5f5de4f294c95f8da8
parent 439675 d8313ee5547ec5eb296415d8ec3e03abbc0b21d0
child 439677 b06daed16bea581b8352ef5c17bcba2554bdf750
push id70422
push userjlaster@mozilla.com
push dateThu, 04 Oct 2018 20:24:35 +0000
treeherderautoland@c3950445b8d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersloganfsmyth
bugs1496468
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 1496468 - Add a console pause indicator. r=loganfsmyth Differential Revision: https://phabricator.services.mozilla.com/D7741
devtools/client/debugger/new/test/mochitest/browser_dbg_rr_console_warp-01.js
devtools/client/themes/webconsole.css
devtools/client/webconsole/actions/messages.js
devtools/client/webconsole/components/ConsoleOutput.js
devtools/client/webconsole/components/Message.js
devtools/client/webconsole/components/MessageContainer.js
devtools/client/webconsole/components/message-types/ConsoleApiCall.js
devtools/client/webconsole/components/message-types/PageError.js
devtools/client/webconsole/constants.js
devtools/client/webconsole/reducers/messages.js
devtools/client/webconsole/selectors/messages.js
devtools/client/webconsole/webconsole-output-wrapper.js
devtools/server/actors/replay/debugger.js
devtools/server/actors/thread.js
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg_rr_console_warp-01.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg_rr_console_warp-01.js
@@ -13,16 +13,29 @@ function findMessages(hud, text, selecto
   const messages = hud.ui.outputNode.querySelectorAll(selector);
   const elements = Array.prototype.filter.call(
     messages,
     (el) => el.textContent.includes(text)
   );
   return elements;
 }
 
+function waitForThreadEvents(console, eventName) {
+  info(`Waiting for thread event '${eventName}' to fire.`);
+  const thread = console.threadClient;
+
+  return new Promise(function(resolve, reject) {
+    thread.addListener(eventName, function onEvent(eventName, ...args) {
+      info(`Thread event '${eventName}' fired.`);
+      thread.removeListener(eventName, onEvent);
+      resolve.apply(resolve, args);
+    });
+  });
+}
+
 async function openContextMenu(hud, element) {
   const onConsoleMenuOpened = hud.ui.consoleOutput.once("menu-open");
   synthesizeContextMenuEvent(element);
   await onConsoleMenuOpened;
   const doc = hud.ui.consoleOutput.owner.chromeWindow.document;
   return doc.getElementById("webconsole-menu");
 }
 
@@ -56,16 +69,20 @@ async function test() {
   let menuPopup = await openContextMenu(hud, message);
   let timeWarpItem = menuPopup.querySelector("#console-menu-time-warp");
   ok(timeWarpItem, "Time warp menu item is available");
   timeWarpItem.click();
   await hideContextMenu(hud);
 
   await once(Services.ppmm, "TimeWarpFinished");
 
+  await waitForThreadEvents(console, 'paused')
+  messages = findMessages(hud, "", ".paused");
+  ok(messages.length == 1, "Found one paused message");
+
   let toolbox = await attachDebugger(tab), client = toolbox.threadClient;
   await client.interrupt();
 
   await checkEvaluateInTopFrame(client, "number", 5);
 
   // Initially we are paused inside the 'new Error()' call on line 19. The
   // first reverse step takes us to the start of that line.
   await reverseStepOverToLine(client, 19);
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -72,16 +72,17 @@ a {
   width: 100%;
   /* Avoid vertical padding, so that we can draw full-height items (e.g. indent guides).
    * Use vertical margins on children instead. */
   padding-inline-start: 1px;
   padding-inline-end: 8px;
   border-inline-start: solid 3px transparent;
   font-size: var(--console-output-font-size);
   line-height: var(--console-output-line-height);
+  position: relative;
 }
 
 /*
  * By default, prevent any element in message to overflow.
  * This makes console reflows faster (See Bug 1487457).
  */
 .message * {
   overflow: hidden;
@@ -101,16 +102,32 @@ a {
   background-color: var(--error-background-color);
 }
 
 .message.warn {
   color: var(--warning-color);
   background-color: var(--warning-background-color);
 }
 
+.message.paused::before {
+  background: #d8461f;
+  opacity: 0.6;
+  width: 100vw;
+  height: 1px;
+  bottom: 0px;
+  left: -3px;
+  display: block;
+  content: "";
+  position: absolute;
+}
+
+.message.paused ~ .message {
+  opacity: 0.5;
+}
+
 .message.startGroup,
 .message.startGroupCollapsed {
   --console-output-indent-border-color: transparent;
 }
 
 .message > .prefix,
 .message > .timestamp {
   flex: none;
@@ -185,17 +202,17 @@ a {
 }
 
 .message.warn > .icon {
   color: var(--console-output-icon-warning);
   background-image: var(--theme-console-alert-image);
 }
 
 
-span.icon[title="Jump"] {
+.message > span.icon[title="Jump"] {
   background-image:var(--theme-console-jump-image);
   background-size: 14px 14px;
   cursor: pointer;
   opacity: 0;
 }
 
 .message:hover span.icon[title="Jump"] {
   opacity: 1;
--- a/devtools/client/webconsole/actions/messages.js
+++ b/devtools/client/webconsole/actions/messages.js
@@ -16,16 +16,17 @@ const {
   MESSAGES_ADD,
   NETWORK_MESSAGE_UPDATE,
   NETWORK_UPDATE_REQUEST,
   MESSAGES_CLEAR,
   MESSAGE_OPEN,
   MESSAGE_CLOSE,
   MESSAGE_TYPE,
   MESSAGE_TABLE_RECEIVE,
+  PAUSED_EXCECUTION_POINT,
   PRIVATE_MESSAGES_CLEAR,
 } = require("../constants");
 
 const defaultIdGenerator = new IdGenerator();
 
 function messagesAdd(packets, idGenerator = null) {
   if (idGenerator == null) {
     idGenerator = defaultIdGenerator;
@@ -52,16 +53,23 @@ function messagesAdd(packets, idGenerato
 }
 
 function messagesClear() {
   return {
     type: MESSAGES_CLEAR
   };
 }
 
+function setPauseExecutionPoint(executionPoint) {
+  return {
+    type: PAUSED_EXCECUTION_POINT,
+    executionPoint
+  };
+}
+
 function privateMessagesClear() {
   return {
     type: PRIVATE_MESSAGES_CLEAR
   };
 }
 
 function messageOpen(id) {
   return {
@@ -134,9 +142,10 @@ module.exports = {
   messageOpen,
   messageClose,
   messageTableDataGet,
   networkMessageUpdate,
   networkUpdateRequest,
   privateMessagesClear,
   // for test purpose only.
   messageTableDataReceive,
+  setPauseExecutionPoint,
 };
--- a/devtools/client/webconsole/components/ConsoleOutput.js
+++ b/devtools/client/webconsole/components/ConsoleOutput.js
@@ -10,16 +10,17 @@ const { connect } = require("devtools/cl
 const {initialize} = require("devtools/client/webconsole/actions/ui");
 
 const {
   getAllMessagesById,
   getAllMessagesUiById,
   getAllMessagesTableDataById,
   getAllNetworkMessagesUpdateById,
   getVisibleMessages,
+  getPausedExecutionPoint,
   getAllRepeatById,
 } = require("devtools/client/webconsole/selectors/messages");
 const MessageContainer = createFactory(require("devtools/client/webconsole/components/MessageContainer").MessageContainer);
 const {
   MESSAGE_TYPE,
 } = require("devtools/client/webconsole/constants");
 const {
   getInitialMessageCountForViewport
@@ -39,16 +40,17 @@ class ConsoleOutput extends Component {
       dispatch: PropTypes.func.isRequired,
       timestampsVisible: PropTypes.bool,
       messagesTableData: PropTypes.object.isRequired,
       messagesRepeat: PropTypes.object.isRequired,
       networkMessagesUpdate: PropTypes.object.isRequired,
       visibleMessages: PropTypes.array.isRequired,
       networkMessageActiveTabId: PropTypes.string.isRequired,
       onFirstMeaningfulPaint: PropTypes.func.isRequired,
+      pausedExecutionPoint: PropTypes.any
     };
   }
 
   constructor(props) {
     super(props);
     this.onContextMenu = this.onContextMenu.bind(this);
   }
 
@@ -127,16 +129,17 @@ class ConsoleOutput extends Component {
       messagesUi,
       messagesTableData,
       messagesRepeat,
       networkMessagesUpdate,
       networkMessageActiveTabId,
       serviceContainer,
       timestampsVisible,
       initialized,
+      pausedExecutionPoint
     } = this.props;
 
     if (!initialized) {
       const numberMessagesFitViewport = getInitialMessageCountForViewport(window);
       if (numberMessagesFitViewport < visibleMessages.length) {
         visibleMessages = visibleMessages.slice(
           visibleMessages.length - numberMessagesFitViewport);
       }
@@ -148,16 +151,17 @@ class ConsoleOutput extends Component {
       messageId,
       serviceContainer,
       open: messagesUi.includes(messageId),
       tableData: messagesTableData.get(messageId),
       timestampsVisible,
       repeat: messagesRepeat[messageId],
       networkMessageUpdate: networkMessagesUpdate[messageId],
       networkMessageActiveTabId,
+      pausedExecutionPoint,
       getMessage: () => messages.get(messageId)
     }));
 
     return (
       dom.div({
         className: "webconsole-output",
         onContextMenu: this.onContextMenu,
         ref: node => {
@@ -178,16 +182,17 @@ function isScrolledToBottom(outputNode, 
                        outputNode.lastChild.clientHeight : 0;
   return scrollNode.scrollTop + scrollNode.clientHeight >=
          scrollNode.scrollHeight - lastNodeHeight / 2;
 }
 
 function mapStateToProps(state, props) {
   return {
     initialized: state.ui.initialized,
+    pausedExecutionPoint: getPausedExecutionPoint(state),
     messages: getAllMessagesById(state),
     visibleMessages: getVisibleMessages(state),
     messagesUi: getAllMessagesUiById(state),
     messagesTableData: getAllMessagesTableDataById(state),
     messagesRepeat: getAllRepeatById(state),
     networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
     timestampsVisible: state.ui.timestampsVisible,
     networkMessageActiveTabId: state.ui.networkMessageActiveTabId,
--- a/devtools/client/webconsole/components/Message.js
+++ b/devtools/client/webconsole/components/Message.js
@@ -58,17 +58,18 @@ class Message extends Component {
         onViewSourceInStyleEditor: PropTypes.func,
         openContextMenu: PropTypes.func.isRequired,
         openLink: PropTypes.func.isRequired,
         sourceMapService: PropTypes.any,
       }),
       notes: PropTypes.arrayOf(PropTypes.shape({
         messageBody: PropTypes.string.isRequired,
         frame: PropTypes.any,
-      }))
+      })),
+      isPaused: PropTypes.bool
     };
   }
 
   static get defaultProps() {
     return {
       indent: 0
     };
   }
@@ -138,30 +139,31 @@ class Message extends Component {
 
   render() {
     const {
       open,
       collapsible,
       collapseTitle,
       source,
       type,
+      isPaused,
       level,
       indent,
       topLevelClasses,
       messageBody,
       frame,
       stacktrace,
       serviceContainer,
       exceptionDocURL,
       timeStamp = Date.now(),
       timestampsVisible,
       notes
     } = this.props;
 
-    topLevelClasses.push("message", source, type, level);
+    topLevelClasses.push("message", source, type, level, isPaused ? "paused" : "");
     if (open) {
       topLevelClasses.push("open");
     }
 
     let timestampEl;
     if (timestampsVisible === true) {
       timestampEl = dom.span({
         className: "timestamp devtools-monospace"
--- a/devtools/client/webconsole/components/MessageContainer.js
+++ b/devtools/client/webconsole/components/MessageContainer.js
@@ -19,16 +19,23 @@ const componentMap = new Map([
   ["ConsoleApiCall", require("./message-types/ConsoleApiCall")],
   ["ConsoleCommand", require("./message-types/ConsoleCommand")],
   ["DefaultRenderer", require("./message-types/DefaultRenderer")],
   ["EvaluationResult", require("./message-types/EvaluationResult")],
   ["NetworkEventMessage", require("./message-types/NetworkEventMessage")],
   ["PageError", require("./message-types/PageError")]
 ]);
 
+function isPaused({ getMessage, pausedExecutionPoint }) {
+  const message = getMessage();
+  return pausedExecutionPoint
+  && message.executionPoint
+    && pausedExecutionPoint.checkpoint === message.executionPoint.checkpoint;
+}
+
 class MessageContainer extends Component {
   static get propTypes() {
     return {
       messageId: PropTypes.string.isRequired,
       open: PropTypes.bool.isRequired,
       serviceContainer: PropTypes.object.isRequired,
       tableData: PropTypes.object,
       timestampsVisible: PropTypes.bool.isRequired,
@@ -47,29 +54,33 @@ class MessageContainer extends Component
   shouldComponentUpdate(nextProps, nextState) {
     const repeatChanged = this.props.repeat !== nextProps.repeat;
     const openChanged = this.props.open !== nextProps.open;
     const tableDataChanged = this.props.tableData !== nextProps.tableData;
     const timestampVisibleChanged =
       this.props.timestampsVisible !== nextProps.timestampsVisible;
     const networkMessageUpdateChanged =
       this.props.networkMessageUpdate !== nextProps.networkMessageUpdate;
+    const pausedChanged = isPaused(this.props) !== isPaused(nextProps);
 
     return repeatChanged
       || openChanged
       || tableDataChanged
       || timestampVisibleChanged
-      || networkMessageUpdateChanged;
+      || networkMessageUpdateChanged
+      || pausedChanged;
   }
 
   render() {
     const message = this.props.getMessage();
 
     const MessageComponent = getMessageComponent(message);
-    return MessageComponent(Object.assign({message}, this.props));
+    return MessageComponent(Object.assign({message}, this.props, {
+      isPaused: isPaused(this.props)
+    }));
   }
 }
 
 function getMessageComponent(message) {
   if (!message) {
     return componentMap.get("DefaultRenderer");
   }
 
--- a/devtools/client/webconsole/components/message-types/ConsoleApiCall.js
+++ b/devtools/client/webconsole/components/message-types/ConsoleApiCall.js
@@ -34,16 +34,17 @@ function ConsoleApiCall(props) {
   const {
     dispatch,
     message,
     open,
     tableData,
     serviceContainer,
     timestampsVisible,
     repeat,
+    isPaused
   } = props;
   const {
     id: messageId,
     executionPoint,
     indent,
     source,
     type,
     level,
@@ -110,16 +111,17 @@ function ConsoleApiCall(props) {
 
   const collapsible = isGroupType(type)
     || (type === "error" && Array.isArray(stacktrace));
   const topLevelClasses = ["cm-s-mozilla"];
 
   return Message({
     messageId,
     executionPoint,
+    isPaused,
     open,
     collapsible,
     collapseTitle,
     source,
     type,
     level,
     topLevelClasses,
     messageBody,
--- a/devtools/client/webconsole/components/message-types/PageError.js
+++ b/devtools/client/webconsole/components/message-types/PageError.js
@@ -27,16 +27,17 @@ PageError.defaultProps = {
 function PageError(props) {
   const {
     dispatch,
     message,
     open,
     repeat,
     serviceContainer,
     timestampsVisible,
+    isPaused
   } = props;
   const {
     id: messageId,
     indent,
     source,
     type,
     level,
     messageText,
@@ -52,16 +53,17 @@ function PageError(props) {
     messageBody = messageText;
   } else if (typeof messageText === "object" && messageText.type === "longString") {
     messageBody = `${message.messageText.initial}…`;
   }
 
   return Message({
     dispatch,
     messageId,
+    isPaused,
     open,
     collapsible: Array.isArray(stacktrace),
     source,
     type,
     level,
     topLevelClasses: [],
     indent,
     messageBody,
--- a/devtools/client/webconsole/constants.js
+++ b/devtools/client/webconsole/constants.js
@@ -33,16 +33,17 @@ const actionTypes = {
   SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR",
   SIDEBAR_CLOSE: "SIDEBAR_CLOSE",
   SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE: "SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
   UPDATE_HISTORY_POSITION: "UPDATE_HISTORY_POSITION",
   REVERSE_SEARCH_INPUT_CHANGE: "REVERSE_SEARCH_INPUT_CHANGE",
   REVERSE_SEARCH_NEXT: "REVERSE_SEARCH_NEXT",
   REVERSE_SEARCH_BACK: "REVERSE_SEARCH_BACK",
+  PAUSED_EXCECUTION_POINT: "PAUSED_EXCECUTION_POINT"
 };
 
 const prefs = {
   PREFS: {
     // Filter preferences only have the suffix since they can be used either for the
     // webconsole or the browser console.
     FILTER: {
       ERROR: "filter.error",
--- a/devtools/client/webconsole/reducers/messages.js
+++ b/devtools/client/webconsole/reducers/messages.js
@@ -50,30 +50,32 @@ const MessageState = overrides => Object
   // in order to properly release them.
   // This array is not supposed to be consumed by any UI component.
   removedActors: [],
   // Map of the form {messageId : numberOfRepeat}
   repeatById: {},
   // Map of the form {messageId : networkInformation}
   // `networkInformation` holds request, response, totalTime, ...
   networkMessagesUpdateById: {},
+  pausedExecutionPoint: null
 }, overrides));
 
 function cloneState(state) {
   return {
     messagesById: new Map(state.messagesById),
     visibleMessages: [...state.visibleMessages],
     filteredMessagesCount: {...state.filteredMessagesCount},
     messagesUiById: [...state.messagesUiById],
     messagesTableDataById: new Map(state.messagesTableDataById),
     groupsById: new Map(state.groupsById),
     currentGroup: state.currentGroup,
     removedActors: [...state.removedActors],
     repeatById: {...state.repeatById},
     networkMessagesUpdateById: {...state.networkMessagesUpdateById},
+    pausedExecutionPoint: state.pausedExecutionPoint
   };
 }
 
 function addMessage(state, filtersState, prefsState, newMessage) {
   const {
     messagesById,
     groupsById,
     currentGroup,
@@ -154,16 +156,18 @@ function messages(state = MessageState()
     groupsById,
     visibleMessages,
   } = state;
 
   const {logLimit} = prefsState;
 
   let newState;
   switch (action.type) {
+    case constants.PAUSED_EXCECUTION_POINT:
+      return { ...state, pausedExecutionPoint: action.executionPoint };
     case constants.MESSAGES_ADD:
       // Preemptively remove messages that will never be rendered
       const list = [];
       let prunableCount = 0;
       let lastMessageRepeatId = -1;
       for (let i = action.messages.length - 1; i >= 0; i--) {
         const message = action.messages[i];
         if (
--- a/devtools/client/webconsole/selectors/messages.js
+++ b/devtools/client/webconsole/selectors/messages.js
@@ -44,21 +44,26 @@ function getAllRepeatById(state) {
 function getAllNetworkMessagesUpdateById(state) {
   return state.messages.networkMessagesUpdateById;
 }
 
 function getGroupsById(state) {
   return state.messages.groupsById;
 }
 
+function getPausedExecutionPoint(state) {
+  return state.messages.pausedExecutionPoint;
+}
+
 module.exports = {
   getAllGroupsById,
   getAllMessagesById,
   getAllMessagesTableDataById,
   getAllMessagesUiById,
   getAllNetworkMessagesUpdateById,
   getAllRepeatById,
   getCurrentGroup,
   getFilteredMessagesCount,
   getGroupsById,
   getMessage,
   getVisibleMessages,
+  getPausedExecutionPoint
 };
--- a/devtools/client/webconsole/webconsole-output-wrapper.js
+++ b/devtools/client/webconsole/webconsole-output-wrapper.js
@@ -168,16 +168,19 @@ WebConsoleOutputWrapper.prototype = {
         // Emit the "menu-open" event for testing.
         menu.once("open", () => this.emit("menu-open"));
         menu.popup(screenX, screenY, { doc: this.owner.chromeWindow.document });
 
         return menu;
       };
 
       if (this.toolbox) {
+        this.toolbox.threadClient.addListener("paused", this.dispatchPaused.bind(this));
+        this.toolbox.threadClient.addListener("resumed", this.dispatchResumed.bind(this));
+
         Object.assign(serviceContainer, {
           onViewSourceInDebugger: frame => {
             this.toolbox.viewSourceInDebugger(frame.url, frame.line).then(() => {
               this.telemetry.recordEvent("jump_to_source", "webconsole",
                                          null, { "session_id": this.toolbox.sessionId }
               );
               this.hud.emit("source-in-debugger-opened");
             });
@@ -350,16 +353,26 @@ WebConsoleOutputWrapper.prototype = {
 
     store.dispatch(actions.privateMessagesClear());
   },
 
   dispatchTimestampsToggle: function(enabled) {
     store.dispatch(actions.timestampsToggle(enabled));
   },
 
+  dispatchPaused: function(_, packet) {
+    if (packet.executionPoint) {
+      store.dispatch(actions.setPauseExecutionPoint(packet.executionPoint));
+    }
+  },
+
+  dispatchResumed: function(_, packet) {
+    store.dispatch(actions.setPauseExecutionPoint(null));
+  },
+
   dispatchMessageUpdate: function(message, res) {
     // network-message-updated will emit when all the update message arrives.
     // Since we can't ensure the order of the network update, we check
     // that networkInfo.updates has all we need.
     // Note that 'requestPostData' is sent only for POST requests, so we need
     // to count with that.
     // 'fetchCacheDescriptor' will also cause a network update and increment
     // the number of networkInfo.updates
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -338,27 +338,27 @@ ReplayDebugger.prototype = {
   get replayingOnForcedPause() {
     return this._breakpointKindGetter("ForcedPause");
   },
   set replayingOnForcedPause(handler) {
     this._breakpointKindSetter("ForcedPause", handler,
                                () => handler.call(this, this.getNewestFrame()));
   },
 
-  _getNewConsoleMessage() {
+  getNewConsoleMessage() {
     const message = this._sendRequest({ type: "getNewConsoleMessage" });
     return this._convertConsoleMessage(message);
   },
 
   get onConsoleMessage() {
     return this._breakpointKindGetter("ConsoleMessage");
   },
   set onConsoleMessage(handler) {
     this._breakpointKindSetter("ConsoleMessage", handler,
-                               () => handler.call(this, this._getNewConsoleMessage()));
+                               () => handler.call(this, this.getNewConsoleMessage()));
   },
 
   clearAllBreakpoints: NYI,
 
 }; // ReplayDebugger.prototype
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerScript
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -1474,16 +1474,23 @@ const ThreadActor = ActorClassWithSpec(t
     // Send off the paused packet and spin an event loop.
     const packet = { from: this.actorID,
                      type: "paused",
                      actor: this._pauseActor.actorID };
     if (frame) {
       packet.frame = this._createFrameActor(frame).form();
     }
 
+    if (this.dbg.replaying) {
+      const message = this.dbg.getNewConsoleMessage();
+      if (message) {
+        packet.executionPoint = message.executionPoint;
+      }
+    }
+
     if (poppedFrames) {
       packet.poppedFrames = poppedFrames;
     }
 
     return packet;
   },
 
   _resumed: function() {