Bug 1371721 - Create a MESSAGES_ADD action to add batches of messages at once draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Tue, 13 Jun 2017 15:23:33 -0700
changeset 593590 a6badc7a27317eff7cbc650c90eb2759b1a92d03
parent 593475 91134c95d68cbcfe984211fa3cbd28d610361ef1
child 633159 a6e9fb15c64324ef2bdc7eda929ccd0ccbcbc054
push id63748
push userbgrinstead@mozilla.com
push dateTue, 13 Jun 2017 22:33:26 +0000
bugs1371721
milestone56.0a1
Bug 1371721 - Create a MESSAGES_ADD action to add batches of messages at once MozReview-Commit-ID: 1MxCqlmDzJV
devtools/client/webconsole/new-console-output/actions/messages.js
devtools/client/webconsole/new-console-output/constants.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/reducers/messages.js
--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -8,26 +8,51 @@
 
 const {
   prepareMessage
 } = require("devtools/client/webconsole/new-console-output/utils/messages");
 const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
 const { batchActions } = require("devtools/client/webconsole/new-console-output/actions/enhancers");
 const {
   MESSAGE_ADD,
+  MESSAGES_ADD,
   NETWORK_MESSAGE_UPDATE,
   MESSAGES_CLEAR,
   MESSAGE_OPEN,
   MESSAGE_CLOSE,
   MESSAGE_TYPE,
   MESSAGE_TABLE_RECEIVE,
 } = require("../constants");
 
 const defaultIdGenerator = new IdGenerator();
 
+function messagesAdd(packets, idGenerator = null) {
+  if (idGenerator == null) {
+    idGenerator = defaultIdGenerator;
+  }
+  let messages = packets.map(packet => prepareMessage(packet, idGenerator));
+  for (let i = messages.length - 1; i >= 0; i--) {
+    if (messages[i].type === MESSAGE_TYPE.CLEAR) {
+      return batchActions([
+        messagesClear(),
+        {
+          type: MESSAGES_ADD,
+          messages: messages.slice(i),
+        }
+      ]);
+    }
+  }
+
+  // When this is used for non-cached messages then handle clear message and split up into batches
+  return {
+    type: MESSAGES_ADD,
+    messages
+  };
+}
+
 function messageAdd(packet, idGenerator = null) {
   if (idGenerator == null) {
     idGenerator = defaultIdGenerator;
   }
   let message = prepareMessage(packet, idGenerator);
   const addMessageAction = {
     type: MESSAGE_ADD,
     message
@@ -101,15 +126,16 @@ function networkMessageUpdate(packet, id
   return {
     type: NETWORK_MESSAGE_UPDATE,
     message,
   };
 }
 
 module.exports = {
   messageAdd,
+  messagesAdd,
   messagesClear,
   messageOpen,
   messageClose,
   messageTableDataGet,
   networkMessageUpdate,
 };
 
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const actionTypes = {
   BATCH_ACTIONS: "BATCH_ACTIONS",
   MESSAGE_ADD: "MESSAGE_ADD",
+  MESSAGES_ADD: "MESSAGES_ADD",
   MESSAGES_CLEAR: "MESSAGES_CLEAR",
   MESSAGE_OPEN: "MESSAGE_OPEN",
   MESSAGE_CLOSE: "MESSAGE_CLOSE",
   NETWORK_MESSAGE_UPDATE: "NETWORK_MESSAGE_UPDATE",
   MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
   REMOVED_MESSAGES_CLEAR: "REMOVED_MESSAGES_CLEAR",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
   FILTER_TOGGLE: "FILTER_TOGGLE",
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -12,17 +12,18 @@ const actions = require("devtools/client
 const { createContextMenu } = require("devtools/client/webconsole/new-console-output/utils/context-menu");
 const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
 const FilterBar = React.createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
 
 let store = null;
-let queuedActions = [];
+let queuedMessageAdds = [];
+let queuedMessageUpdates = [];
 let throttledDispatchTimeout = false;
 
 function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) {
   EventEmitter.decorate(this);
 
   this.parentNode = parentNode;
   this.jsterm = jsterm;
   this.toolbox = toolbox;
@@ -139,75 +140,86 @@ NewConsoleOutputWrapper.prototype = {
         filterBar,
         childComponent
     ));
 
     this.body = ReactDOM.render(provider, this.parentNode);
   },
 
   dispatchMessageAdd: function (message, waitForResponse) {
-    let action = actions.messageAdd(message);
-    batchedMessageAdd(action);
+    batchedMessagesAdd(message);
 
     // Wait for the message to render to resolve with the DOM node.
     // This is just for backwards compatibility with old tests, and should
     // be removed once it's not needed anymore.
     // Can only wait for response if the action contains a valid message.
-    if (waitForResponse && action.message) {
-      let messageId = action.message.id;
+    if (waitForResponse) {
+      let {timeStamp} = message;
       return new Promise(resolve => {
         let jsterm = this.jsterm;
         jsterm.hud.on("new-messages", function onThisMessage(e, messages) {
           for (let m of messages) {
-            if (m.messageId === messageId) {
+            if (m.timeStamp === timeStamp) {
               resolve(m.node);
               jsterm.hud.off("new-messages", onThisMessage);
               return;
             }
           }
         });
       });
     }
 
     return Promise.resolve();
   },
 
   dispatchMessagesAdd: function (messages) {
-    const batchedActions = messages.map(message => actions.messageAdd(message));
-    store.dispatch(actions.batchActions(batchedActions));
+    store.dispatch(actions.messagesAdd(messages));
   },
 
   dispatchMessagesClear: function () {
     store.dispatch(actions.messagesClear());
   },
 
   dispatchTimestampsToggle: function (enabled) {
     store.dispatch(actions.timestampsToggle(enabled));
   },
 
   dispatchMessageUpdate: function (message, res) {
     // network-message-updated will emit when eventTimings message arrives
     // which is the last one of 8 updates happening on network message update.
     if (res.packet.updateType === "eventTimings") {
-      batchedMessageAdd(actions.networkMessageUpdate(message));
+      batchedMessageUpdates(message);
       this.jsterm.hud.emit("network-message-updated", res);
     }
   },
 
   // Should be used for test purpose only.
   getStore: function () {
     return store;
   }
 };
 
-function batchedMessageAdd(action) {
-  queuedActions.push(action);
+function setTimeoutIfNeeded() {
   if (!throttledDispatchTimeout) {
     throttledDispatchTimeout = setTimeout(() => {
-      store.dispatch(actions.batchActions(queuedActions));
-      queuedActions = [];
+      store.dispatch(actions.messagesAdd(queuedMessageAdds));
+      queuedMessageUpdates.forEach(message => {
+        actions.networkMessageUpdate(message);
+      });
+      queuedMessageAdds = [];
+      queuedMessageUpdates = [];
       throttledDispatchTimeout = null;
     }, 50);
   }
 }
 
+function batchedMessageUpdates(message) {
+  queuedMessageUpdates.push(message);
+  setTimeoutIfNeeded();
+}
+
+function batchedMessagesAdd(message) {
+  queuedMessageAdds.push(message);
+  setTimeoutIfNeeded();
+}
+
 // Exports from this module
 module.exports = NewConsoleOutputWrapper;
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -37,94 +37,128 @@ const MessageState = Immutable.Record({
   removedMessages: [],
   // Map of the form {messageId : numberOfRepeat}
   repeatById: {},
   // Map of the form {messageId : networkInformation}
   // `networkInformation` holds request, response, totalTime, ...
   networkMessagesUpdateById: {},
 });
 
+function addMessage(state, filtersState, prefsState, newMessage) {
+  const {
+    messagesById,
+    messagesUiById,
+    messagesTableDataById,
+    networkMessagesUpdateById,
+    groupsById,
+    currentGroup,
+    repeatById,
+    visibleMessages,
+  } = state;
+  const {logLimit} = prefsState;
+  if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
+    // When the message has a NULL type, we don't add it.
+    return state;
+  }
+
+  if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) {
+    // Compute the new current group.
+    return state.set("currentGroup", getNewCurrentGroup(currentGroup, groupsById));
+  }
+
+  if (newMessage.allowRepeating && messagesById.size > 0) {
+    let lastMessage = messagesById.last();
+    if (
+      lastMessage.repeatId === newMessage.repeatId
+      && lastMessage.groupId === currentGroup
+    ) {
+      return state.set(
+        "repeatById",
+        Object.assign({}, repeatById, {
+          [lastMessage.id]: (repeatById[lastMessage.id] || 1) + 1
+        })
+      );
+    }
+  }
+
+  return state.withMutations(function (record) {
+    // Add the new message with a reference to the parent group.
+    let parentGroups = getParentGroups(currentGroup, groupsById);
+    newMessage.groupId = currentGroup;
+    newMessage.indent = parentGroups.length;
+
+    const addedMessage = Object.freeze(newMessage);
+    record.set(
+      "messagesById",
+      messagesById.set(newMessage.id, addedMessage)
+    );
+
+    if (newMessage.type === "trace") {
+      // We want the stacktrace to be open by default.
+      record.set("messagesUiById", messagesUiById.push(newMessage.id));
+    } else if (isGroupType(newMessage.type)) {
+      record.set("currentGroup", newMessage.id);
+      record.set("groupsById", groupsById.set(newMessage.id, parentGroups));
+
+      if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
+        // We want the group to be open by default.
+        record.set("messagesUiById", messagesUiById.push(newMessage.id));
+      }
+    }
+
+    if (shouldMessageBeVisible(addedMessage, record, filtersState)) {
+      record.set("visibleMessages", [...visibleMessages, newMessage.id]);
+    }
+  });
+}
+
 function messages(state = new MessageState(), action, filtersState, prefsState) {
   const {
     messagesById,
     messagesUiById,
     messagesTableDataById,
     networkMessagesUpdateById,
     groupsById,
     currentGroup,
     repeatById,
     visibleMessages,
   } = state;
 
   const {logLimit} = prefsState;
 
   switch (action.type) {
-    case constants.MESSAGE_ADD:
-      let newMessage = action.message;
-
-      if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
-        // When the message has a NULL type, we don't add it.
-        return state;
-      }
-
-      if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) {
-        // Compute the new current group.
-        return state.set("currentGroup", getNewCurrentGroup(currentGroup, groupsById));
-      }
+    case constants.MESSAGES_ADD:
+      var newState = state;
 
-      if (newMessage.allowRepeating && messagesById.size > 0) {
-        let lastMessage = messagesById.last();
-        if (
-          lastMessage.repeatId === newMessage.repeatId
-          && lastMessage.groupId === currentGroup
-        ) {
-          return state.set(
-            "repeatById",
-            Object.assign({}, repeatById, {
-              [lastMessage.id]: (repeatById[lastMessage.id] || 1) + 1
-            })
-          );
+      // Preemptively remove messages that will never be rendered
+      let messages = [];
+      let prunableCount = 0;
+      for (var i = action.messages.length - 1; i >= 0; i--) {
+        if (!action.messages[i].groupId && !isGroupType(action.messages[i].type) &&
+            action.messages[i].type !== MESSAGE_TYPE.END_GROUP) {
+          prunableCount++;
+          // Once we've added the max number of messages that can be added stop.
+          // TODO: what about repeats?
+          if (prunableCount <= logLimit) {
+            messages.unshift(action.messages[i]);
+          }
+        } else {
+          messages.unshift(action.messages[i]);
         }
       }
 
-      return state.withMutations(function (record) {
-        // Add the new message with a reference to the parent group.
-        let parentGroups = getParentGroups(currentGroup, groupsById);
-        newMessage.groupId = currentGroup;
-        newMessage.indent = parentGroups.length;
-
-        const addedMessage = Object.freeze(newMessage);
-        record.set(
-          "messagesById",
-          messagesById.set(newMessage.id, addedMessage)
-        );
+      messages.forEach(message => {
+        newState = addMessage(newState, filtersState, prefsState, message);
+      });
 
-        if (newMessage.type === "trace") {
-          // We want the stacktrace to be open by default.
-          record.set("messagesUiById", messagesUiById.push(newMessage.id));
-        } else if (isGroupType(newMessage.type)) {
-          record.set("currentGroup", newMessage.id);
-          record.set("groupsById", groupsById.set(newMessage.id, parentGroups));
+      return limitTopLevelMessageCount(newState, logLimit);
 
-          if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
-            // We want the group to be open by default.
-            record.set("messagesUiById", messagesUiById.push(newMessage.id));
-          }
-        }
-
-        if (shouldMessageBeVisible(addedMessage, record, filtersState)) {
-          record.set("visibleMessages", [...visibleMessages, newMessage.id]);
-        }
-
-        // Remove top level message if the total count of top level messages
-        // exceeds the current limit.
-        if (record.messagesById.size > logLimit) {
-          limitTopLevelMessageCount(state, record, logLimit);
-        }
-      });
+    case constants.MESSAGE_ADD:
+      var newState = addMessage(state, filtersState, prefsState, action.message);
+      return limitTopLevelMessageCount(newState, logLimit);
 
     case constants.MESSAGES_CLEAR:
       return new MessageState({
         // Store all removed messages associated with some arguments.
         // This array is used by `releaseActorsEnhancer` to release
         // all related backend actors.
         "removedMessages": [...state.messagesById].reduce((res, [id, msg]) => {
           if (msg.parameters) {
@@ -248,111 +282,111 @@ function getParentGroups(currentGroup, g
   return groups;
 }
 
 /**
  * Remove all top level messages that exceeds message limit.
  * Also populate an array of all backend actors associated with these
  * messages so they can be released.
  */
-function limitTopLevelMessageCount(state, record, logLimit) {
-  let topLevelCount = record.groupsById.size === 0
-    ? record.messagesById.size
-    : getToplevelMessageCount(record);
-
-  if (topLevelCount <= logLimit) {
-    return record;
-  }
-
-  const removedMessagesId = [];
-  const removedMessages = [];
-  let visibleMessages = [...record.visibleMessages];
-
-  let cleaningGroup = false;
-  record.messagesById.forEach((message, id) => {
-    // If we were cleaning a group and the current message does not have
-    // a groupId, we're done cleaning.
-    if (cleaningGroup === true && !message.groupId) {
-      cleaningGroup = false;
-    }
+function limitTopLevelMessageCount(state, logLimit) {
+  return state.withMutations(function (record) {
+    let topLevelCount = record.groupsById.size === 0
+      ? record.messagesById.size
+      : getToplevelMessageCount(record);
 
-    // If we're not cleaning a group and the message count is below the logLimit,
-    // we exit the forEach iteration.
-    if (cleaningGroup === false && topLevelCount <= logLimit) {
-      return false;
-    }
-
-    // If we're not currently cleaning a group, and the current message is identified
-    // as a group, set the cleaning flag to true.
-    if (cleaningGroup === false && record.groupsById.has(id)) {
-      cleaningGroup = true;
-    }
-
-    if (!message.groupId) {
-      topLevelCount--;
-    }
-
-    removedMessagesId.push(id);
-
-    // Filter out messages with no arguments. Only actual arguments
-    // can be associated with backend actors.
-    if (message && message.parameters) {
-      removedMessages.push(message);
+    if (topLevelCount <= logLimit) {
+      return;
     }
 
-    const index = visibleMessages.indexOf(id);
-    if (index > -1) {
-      visibleMessages.splice(index, 1);
+    const removedMessagesId = [];
+    const removedMessages = [];
+    let visibleMessages = [...record.visibleMessages];
+
+    let cleaningGroup = false;
+    record.messagesById.forEach((message, id) => {
+      // If we were cleaning a group and the current message does not have
+      // a groupId, we're done cleaning.
+      if (cleaningGroup === true && !message.groupId) {
+        cleaningGroup = false;
+      }
+
+      // If we're not cleaning a group and the message count is below the logLimit,
+      // we exit the forEach iteration.
+      if (cleaningGroup === false && topLevelCount <= logLimit) {
+        return false;
+      }
+
+      // If we're not currently cleaning a group, and the current message is identified
+      // as a group, set the cleaning flag to true.
+      if (cleaningGroup === false && record.groupsById.has(id)) {
+        cleaningGroup = true;
+      }
+
+      if (!message.groupId) {
+        topLevelCount--;
+      }
+
+      removedMessagesId.push(id);
+
+      // Filter out messages with no arguments. Only actual arguments
+      // can be associated with backend actors.
+      if (message && message.parameters) {
+        removedMessages.push(message);
+      }
+
+      const index = visibleMessages.indexOf(id);
+      if (index > -1) {
+        visibleMessages.splice(index, 1);
+      }
+
+      return true;
+    });
+
+    if (removedMessages.length > 0) {
+      record.set("removedMessages", record.removedMessages.concat(removedMessages));
     }
 
-    return true;
-  });
-
-  if (removedMessages.length > 0) {
-    record.set("removedMessages", record.removedMessages.concat(removedMessages));
-  }
-
-  if (record.visibleMessages.length > visibleMessages.length) {
-    record.set("visibleMessages", visibleMessages);
-  }
+    if (record.visibleMessages.length > visibleMessages.length) {
+      record.set("visibleMessages", visibleMessages);
+    }
 
-  const isInRemovedId = id => removedMessagesId.includes(id);
-  const mapHasRemovedIdKey = map => map.findKey((value, id) => isInRemovedId(id));
-  const cleanUpCollection = map => removedMessagesId.forEach(id => map.remove(id));
-  const cleanUpObject = object => [...Object.entries(object)]
-    .reduce((res, [id, value]) => {
-      if (!isInRemovedId(id)) {
-        res[id] = value;
-      }
-      return res;
-    }, {});
+    const isInRemovedId = id => removedMessagesId.includes(id);
+    const mapHasRemovedIdKey = map => map.findKey((value, id) => isInRemovedId(id));
+    const cleanUpCollection = map => removedMessagesId.forEach(id => map.remove(id));
+    const cleanUpObject = object => [...Object.entries(object)]
+      .reduce((res, [id, value]) => {
+        if (!isInRemovedId(id)) {
+          res[id] = value;
+        }
+        return res;
+      }, {});
+
+    record.set("messagesById", record.messagesById.withMutations(cleanUpCollection));
 
-  record.set("messagesById", record.messagesById.withMutations(cleanUpCollection));
+    if (record.messagesUiById.find(isInRemovedId)) {
+      record.set("messagesUiById", record.messagesUiById.withMutations(cleanUpCollection));
+    }
+    if (mapHasRemovedIdKey(record.messagesTableDataById)) {
+      record.set("messagesTableDataById",
+        record.messagesTableDataById.withMutations(cleanUpCollection));
+    }
+    if (mapHasRemovedIdKey(record.groupsById)) {
+      record.set("groupsById", record.groupsById.withMutations(cleanUpCollection));
+    }
 
-  if (record.messagesUiById.find(isInRemovedId)) {
-    record.set("messagesUiById", record.messagesUiById.withMutations(cleanUpCollection));
-  }
-  if (mapHasRemovedIdKey(record.messagesTableDataById)) {
-    record.set("messagesTableDataById",
-      record.messagesTableDataById.withMutations(cleanUpCollection));
-  }
-  if (mapHasRemovedIdKey(record.groupsById)) {
-    record.set("groupsById", record.groupsById.withMutations(cleanUpCollection));
-  }
+    if (Object.keys(record.repeatById).includes(removedMessagesId)) {
+      record.set("repeatById", cleanUpObject(record.repeatById));
+    }
 
-  if (Object.keys(record.repeatById).includes(removedMessagesId)) {
-    record.set("repeatById", cleanUpObject(record.repeatById));
-  }
-
-  if (Object.keys(record.networkMessagesUpdateById).includes(removedMessagesId)) {
-    record.set("networkMessagesUpdateById",
-      cleanUpObject(record.networkMessagesUpdateById));
-  }
-
-  return record;
+    if (Object.keys(record.networkMessagesUpdateById).includes(removedMessagesId)) {
+      record.set("networkMessagesUpdateById",
+        cleanUpObject(record.networkMessagesUpdateById));
+    }
+  });
 }
 
 /**
  * Returns total count of top level messages (those which are not
  * within a group).
  */
 function getToplevelMessageCount(record) {
   return record.messagesById.count(message => !message.groupId);