Bug 1363678 - Move the filtering logic to the reducer. r=bgrins
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Wed, 31 May 2017 09:27:43 +0200
changeset 409945 617099302c92ebfe8fc2a34dffb0dd8d9755fc9f
parent 409944 c6b6bc96e99c2b78a97a812cdac687802decb4c3
child 409946 e9f6b74293361a45b8ac2342e7a727e93e9bf7de
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1363678
milestone55.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 1363678 - Move the filtering logic to the reducer. r=bgrins We used to do the filtering on the selector, which can be costly because we're looping through all the messages of the store on each new message. Moving the logic to the reducer allow us to be more thoughtful about which messages to evaluate and when. In order to make this change, we need to pass the filter state to the message reducer. This is done by ditching the combineReducers helper function and do the plumbing by ourselves, which isn't complex. MozReview-Commit-ID: Lw37XgEFf7e
devtools/client/webconsole/new-console-output/components/console-output.js
devtools/client/webconsole/new-console-output/reducers/messages.js
devtools/client/webconsole/new-console-output/selectors/messages.js
devtools/client/webconsole/new-console-output/store.js
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -7,37 +7,37 @@ const {
   createClass,
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
-  getAllMessages,
   getAllMessagesUiById,
   getAllMessagesTableDataById,
+  getVisibleMessages,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
 
 const ConsoleOutput = createClass({
 
   displayName: "ConsoleOutput",
 
   propTypes: {
-    messages: PropTypes.object.isRequired,
     messagesUi: PropTypes.object.isRequired,
     serviceContainer: PropTypes.shape({
       attachRefToHud: PropTypes.func.isRequired,
       openContextMenu: PropTypes.func.isRequired,
       sourceMapService: PropTypes.object,
     }),
     dispatch: PropTypes.func.isRequired,
     timestampsVisible: PropTypes.bool,
     messagesTableData: PropTypes.object.isRequired,
+    visibleMessages: PropTypes.array.isRequired,
   },
 
   componentDidMount() {
     // Do the scrolling in the nextTick since this could hit console startup performances.
     // See https://bugzilla.mozilla.org/show_bug.cgi?id=1355869
     setTimeout(() => {
       scrollToBottom(this.outputNode);
     }, 0);
@@ -48,17 +48,17 @@ const ConsoleOutput = createClass({
     const outputNode = this.outputNode;
     if (!outputNode || !outputNode.lastChild) {
       return;
     }
 
     // Figure out if we are at the bottom. If so, then any new message should be scrolled
     // into view.
     const lastChild = outputNode.lastChild;
-    const delta = nextProps.messages.size - this.props.messages.size;
+    const delta = nextProps.visibleMessages.length - this.props.visibleMessages.length;
     this.shouldScrollBottom = delta > 0 && isScrolledToBottom(lastChild, outputNode);
   },
 
   componentDidUpdate() {
     if (this.shouldScrollBottom) {
       scrollToBottom(this.outputNode);
     }
   },
@@ -67,24 +67,24 @@ const ConsoleOutput = createClass({
     this.props.serviceContainer.openContextMenu(e);
     e.stopPropagation();
     e.preventDefault();
   },
 
   render() {
     let {
       dispatch,
-      messages,
+      visibleMessages,
       messagesUi,
       messagesTableData,
       serviceContainer,
       timestampsVisible,
     } = this.props;
 
-    let messageNodes = messages.map((message) => {
+    let messageNodes = visibleMessages.map((message) => {
       return (
         MessageContainer({
           dispatch,
           message,
           key: message.id,
           serviceContainer,
           open: messagesUi.includes(message.id),
           tableData: messagesTableData.get(message.id),
@@ -115,16 +115,16 @@ function isScrolledToBottom(outputNode, 
   let lastNodeHeight = outputNode.lastChild ?
                        outputNode.lastChild.clientHeight : 0;
   return scrollNode.scrollTop + scrollNode.clientHeight >=
          scrollNode.scrollHeight - lastNodeHeight / 2;
 }
 
 function mapStateToProps(state, props) {
   return {
-    messages: getAllMessages(state),
+    visibleMessages: getVisibleMessages(state),
     messagesUi: getAllMessagesUiById(state),
     messagesTableData: getAllMessagesTableDataById(state),
     timestampsVisible: state.ui.timestampsVisible,
   };
 }
 
 module.exports = connect(mapStateToProps)(ConsoleOutput);
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -1,47 +1,58 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 Immutable = require("devtools/client/shared/vendor/immutable");
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
 const constants = require("devtools/client/webconsole/new-console-output/constants");
 const {isGroupType} = require("devtools/client/webconsole/new-console-output/utils/messages");
+const {
+  MESSAGE_TYPE,
+  MESSAGE_SOURCE
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { getGripPreviewItems } = require("devtools/client/shared/components/reps/reps");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+
 const Services = require("Services");
-
 const logLimit = Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1);
 
 const MessageState = Immutable.Record({
   // List of all the messages added to the console.
-  messagesById: Immutable.List(),
+  messagesById: Immutable.OrderedMap(),
+  // Array of the visible messages.
+  visibleMessages: [],
   // List of the message ids which are opened.
   messagesUiById: Immutable.List(),
   // Map of the form {messageId : tableData}, which represent the data passed
   // as an argument in console.table calls.
   messagesTableDataById: Immutable.Map(),
   // Map of the form {groupMessageId : groupArray},
   // where groupArray is the list of of all the parent groups' ids of the groupMessageId.
   groupsById: Immutable.Map(),
   // Message id of the current group (no corresponding console.groupEnd yet).
   currentGroup: null,
   // List of removed messages is used to release related (parameters) actors.
   // This array is not supposed to be consumed by any UI component.
   removedMessages: [],
 });
 
-function messages(state = new MessageState(), action) {
+function messages(state = new MessageState(), action, filtersState) {
   const {
     messagesById,
     messagesUiById,
     messagesTableDataById,
     groupsById,
     currentGroup,
+    visibleMessages,
   } = state;
 
   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.
@@ -51,80 +62,154 @@ function messages(state = new MessageSta
       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) {
-          return state.withMutations(function (record) {
-            record.set("messagesById", messagesById.pop().push(
-              newMessage.set("repeat", lastMessage.repeat + 1)
-            ));
-          });
+          return state.set(
+            "messagesById",
+            messagesById.set(
+              lastMessage.id,
+              lastMessage.set("repeat", lastMessage.repeat + 1)
+            )
+          );
         }
       }
 
-      let parentGroups = getParentGroups(currentGroup, groupsById);
-      newMessage = newMessage.withMutations(function (message) {
-        message.set("groupId", currentGroup);
-        message.set("indent", parentGroups.length);
-      });
-
       return state.withMutations(function (record) {
         // Add the new message with a reference to the parent group.
-        record.set("messagesById", messagesById.push(newMessage));
+        let parentGroups = getParentGroups(currentGroup, groupsById);
+        const addedMessage = newMessage.withMutations(function (message) {
+          message.set("groupId", currentGroup);
+          message.set("indent", parentGroups.length);
+        });
+        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]);
+        }
+
         // Remove top level message if the total count of top level messages
         // exceeds the current limit.
-        limitTopLevelMessageCount(state, record);
+        if (record.messagesById.size > logLimit) {
+          limitTopLevelMessageCount(state, record);
+        }
       });
+
     case constants.MESSAGES_CLEAR:
-      return state.withMutations(function (record) {
+      return new MessageState({
         // Store all removed messages associated with some arguments.
         // This array is used by `releaseActorsEnhancer` to release
         // all related backend actors.
-        record.set("removedMessages",
-          record.messagesById.filter(msg => msg.parameters).toArray());
+        "removedMessages": [...state.messagesById].reduce((res, [id, msg]) => {
+          if (msg.parameters) {
+            res.push(msg);
+          }
+          return res;
+        }, [])
+      });
+
+    case constants.MESSAGE_OPEN:
+      return state.withMutations(function (record) {
+        record.set("messagesUiById", messagesUiById.push(action.id));
 
-        // Clear immutable state.
-        record.set("messagesById", Immutable.List());
-        record.set("messagesUiById", Immutable.List());
-        record.set("groupsById", Immutable.Map());
-        record.set("currentGroup", null);
+        // If the message is a group
+        if (isGroupType(messagesById.get(action.id).type)) {
+          // We want to make its children visible
+          const messagesToShow = [...messagesById].reduce((res, [id, message]) => {
+            if (
+              !visibleMessages.includes(message.id)
+              && getParentGroups(message.groupId, groupsById).includes(action.id)
+              && shouldMessageBeVisible(
+                message,
+                record,
+                filtersState,
+                // We want to check if the message is in an open group
+                // only if it is not a direct child of the group we're opening.
+                message.groupId !== action.id
+              )
+            ) {
+              res.push(id);
+            }
+            return res;
+          }, []);
+
+          // We can then insert the messages ids right after the one of the group.
+          const insertIndex = visibleMessages.indexOf(action.id) + 1;
+          record.set("visibleMessages", [
+            ...visibleMessages.slice(0, insertIndex),
+            ...messagesToShow,
+            ...visibleMessages.slice(insertIndex),
+          ]);
+        }
       });
-    case constants.MESSAGE_OPEN:
-      return state.set("messagesUiById", messagesUiById.push(action.id));
+
     case constants.MESSAGE_CLOSE:
-      let index = state.messagesUiById.indexOf(action.id);
-      return state.deleteIn(["messagesUiById", index]);
+      return state.withMutations(function (record) {
+        let messageId = action.id;
+        let index = record.messagesUiById.indexOf(messageId);
+        record.deleteIn(["messagesUiById", index]);
+
+        // If the message is a group
+        if (isGroupType(messagesById.get(messageId).type)) {
+          // Hide all its children
+          record.set(
+            "visibleMessages",
+            [...visibleMessages].filter(id => getParentGroups(
+                messagesById.get(id).groupId,
+                groupsById
+              ).includes(messageId) === false
+            )
+          );
+        }
+      });
+
     case constants.MESSAGE_TABLE_RECEIVE:
       const {id, data} = action;
       return state.set("messagesTableDataById", messagesTableDataById.set(id, data));
     case constants.NETWORK_MESSAGE_UPDATE:
       let updateMessage = action.message;
-      return state.set("messagesById", messagesById.map((message) =>
-        (message.id === updateMessage.id) ? updateMessage : message
+      return state.set("messagesById", messagesById.set(
+        updateMessage.id,
+        updateMessage
       ));
+
     case constants.REMOVED_MESSAGES_CLEAR:
       return state.set("removedMessages", []);
+
+    case constants.FILTER_TOGGLE:
+    case constants.FILTER_TEXT_SET:
+      return state.set(
+        "visibleMessages",
+        [...messagesById].reduce((res, [messageId, message]) => {
+          if (shouldMessageBeVisible(message, state, filtersState)) {
+            res.push(messageId);
+          }
+          return res;
+        }, [])
+      );
   }
 
   return state;
 }
 
 function getNewCurrentGroup(currentGoup, groupsById) {
   let newCurrentGroup = null;
   if (currentGoup) {
@@ -232,9 +317,211 @@ function removeFirstMessage(record) {
     removedMessages.push(...removeFirstMessage(record));
     message = record.messagesById.first();
   }
 
   // Return array with all removed messages.
   return removedMessages;
 }
 
+function shouldMessageBeVisible(message, messagesState, filtersState, checkGroup = true) {
+  return (
+    (
+      checkGroup === false
+      || isInOpenedGroup(message, messagesState.groupsById, messagesState.messagesUiById)
+    )
+    && (
+      isUnfilterable(message)
+      || (
+        matchLevelFilters(message, filtersState)
+        && matchCssFilters(message, filtersState)
+        && matchNetworkFilters(message, filtersState)
+        && matchSearchFilters(message, filtersState)
+      )
+    )
+  );
+}
+
+function isUnfilterable(message) {
+  return [
+    MESSAGE_TYPE.COMMAND,
+    MESSAGE_TYPE.RESULT,
+    MESSAGE_TYPE.START_GROUP,
+    MESSAGE_TYPE.START_GROUP_COLLAPSED,
+  ].includes(message.type);
+}
+
+function isInOpenedGroup(message, groupsById, messagesUI) {
+  return !message.groupId
+    || (
+      !isGroupClosed(message.groupId, messagesUI)
+      && !hasClosedParentGroup(groupsById.get(message.groupId), messagesUI)
+    );
+}
+
+function hasClosedParentGroup(group, messagesUI) {
+  return group.some(groupId => isGroupClosed(groupId, messagesUI));
+}
+
+function isGroupClosed(groupId, messagesUI) {
+  return messagesUI.includes(groupId) === false;
+}
+
+function matchLevelFilters(message, filters) {
+  return filters.get(message.level) === true;
+}
+
+function matchNetworkFilters(message, filters) {
+  return (
+    message.source !== MESSAGE_SOURCE.NETWORK
+    || (filters.get("net") === true && message.isXHR === false)
+    || (filters.get("netxhr") === true && message.isXHR === true)
+  );
+}
+
+function matchCssFilters(message, filters) {
+  return (
+    message.source != MESSAGE_SOURCE.CSS
+    || filters.get("css") === true
+  );
+}
+
+function matchSearchFilters(message, filters) {
+  let text = filters.text || "";
+  return (
+    text === ""
+    // Look for a match in parameters.
+    || isTextInParameters(text, message.parameters)
+    // Look for a match in location.
+    || isTextInFrame(text, message.frame)
+    // Look for a match in net events.
+    || isTextInNetEvent(text, message.request)
+    // Look for a match in stack-trace.
+    || isTextInStackTrace(text, message.stacktrace)
+    // Look for a match in messageText.
+    || isTextInMessageText(text, message.messageText)
+    // Look for a match in notes.
+    || isTextInNotes(text, message.notes)
+  );
+}
+
+/**
+* Returns true if given text is included in provided stack frame.
+*/
+function isTextInFrame(text, frame) {
+  if (!frame) {
+    return false;
+  }
+
+  const {
+    functionName,
+    line,
+    column,
+    source
+  } = frame;
+  const { short } = getSourceNames(source);
+
+  return `${functionName ? functionName + " " : ""}${short}:${line}:${column}`
+    .toLocaleLowerCase()
+    .includes(text.toLocaleLowerCase());
+}
+
+/**
+* Returns true if given text is included in provided parameters.
+*/
+function isTextInParameters(text, parameters) {
+  if (!parameters) {
+    return false;
+  }
+
+  text = text.toLocaleLowerCase();
+  return getAllProps(parameters).some(prop =>
+    (prop + "").toLocaleLowerCase().includes(text)
+  );
+}
+
+/**
+* Returns true if given text is included in provided net event grip.
+*/
+function isTextInNetEvent(text, request) {
+  if (!request) {
+    return false;
+  }
+
+  text = text.toLocaleLowerCase();
+
+  let method = request.method.toLocaleLowerCase();
+  let url = request.url.toLocaleLowerCase();
+  return method.includes(text) || url.includes(text);
+}
+
+/**
+* Returns true if given text is included in provided stack trace.
+*/
+function isTextInStackTrace(text, stacktrace) {
+  if (!Array.isArray(stacktrace)) {
+    return false;
+  }
+
+  // isTextInFrame expect the properties of the frame object to be in the same
+  // order they are rendered in the Frame component.
+  return stacktrace.some(frame => isTextInFrame(text, {
+    functionName: frame.functionName || l10n.getStr("stacktrace.anonymousFunction"),
+    source: frame.filename,
+    lineNumber: frame.lineNumber,
+    columnNumber: frame.columnNumber
+  }));
+}
+
+/**
+* Returns true if given text is included in `messageText` field.
+*/
+function isTextInMessageText(text, messageText) {
+  if (!messageText) {
+    return false;
+  }
+
+  return messageText.toLocaleLowerCase().includes(text.toLocaleLowerCase());
+}
+
+/**
+* Returns true if given text is included in notes.
+*/
+function isTextInNotes(text, notes) {
+  if (!Array.isArray(notes)) {
+    return false;
+  }
+
+  return notes.some(note =>
+    // Look for a match in location.
+    isTextInFrame(text, note.frame) ||
+    // Look for a match in messageBody.
+    (
+      note.messageBody &&
+      note.messageBody.toLocaleLowerCase().includes(text.toLocaleLowerCase())
+    )
+  );
+}
+
+/**
+ * Get a flat array of all the grips and their properties.
+ *
+ * @param {Array} Grips
+ * @return {Array} Flat array of the grips and their properties.
+ */
+function getAllProps(grips) {
+  let result = grips.reduce((res, grip) => {
+    let previewItems = getGripPreviewItems(grip);
+    let allProps = previewItems.length > 0 ? getAllProps(previewItems) : [];
+    return [...res, grip, grip.class, ...allProps];
+  }, []);
+
+  // We are interested only in primitive props (to search for)
+  // not in objects and undefined previews.
+  result = result.filter(grip =>
+    typeof grip != "object" &&
+    typeof grip != "undefined"
+  );
+
+  return [...new Set(result)];
+}
+
 exports.messages = messages;
--- a/devtools/client/webconsole/new-console-output/selectors/messages.js
+++ b/devtools/client/webconsole/new-console-output/selectors/messages.js
@@ -1,49 +1,21 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
-const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
-const { getGripPreviewItems } = require("devtools/client/shared/components/reps/reps");
-const { getSourceNames } = require("devtools/client/shared/source-utils");
-const {
-  MESSAGE_TYPE,
-  MESSAGE_SOURCE
-} = require("devtools/client/webconsole/new-console-output/constants");
-
-function getAllMessages(state) {
-  let messages = getAllMessagesById(state);
-  let filters = getAllFilters(state);
-
-  let groups = getAllGroupsById(state);
-  let messagesUI = getAllMessagesUiById(state);
-
-  return messages.filter(message => {
-    return (
-      isInOpenedGroup(message, groups, messagesUI)
-      && (
-        isUnfilterable(message)
-        || (
-          matchLevelFilters(message, filters)
-          && matchCssFilters(message, filters)
-          && matchNetworkFilters(message, filters)
-          && matchSearchFilters(message, filters)
-        )
-      )
-    );
-  });
+function getAllMessagesById(state) {
+  return state.messages.messagesById;
 }
 
-function getAllMessagesById(state) {
-  return state.messages.messagesById;
+function getMessage(state, id) {
+  return getAllMessagesById(state).get(id);
 }
 
 function getAllMessagesUiById(state) {
   return state.messages.messagesUiById;
 }
 
 function getAllMessagesTableDataById(state) {
   return state.messages.messagesTableDataById;
@@ -52,189 +24,21 @@ function getAllMessagesTableDataById(sta
 function getAllGroupsById(state) {
   return state.messages.groupsById;
 }
 
 function getCurrentGroup(state) {
   return state.messages.currentGroup;
 }
 
-function isUnfilterable(message) {
-  return [
-    MESSAGE_TYPE.COMMAND,
-    MESSAGE_TYPE.RESULT,
-    MESSAGE_TYPE.START_GROUP,
-    MESSAGE_TYPE.START_GROUP_COLLAPSED,
-  ].includes(message.type);
-}
-
-function isInOpenedGroup(message, groups, messagesUI) {
-  return !message.groupId
-    || (
-      !isGroupClosed(message.groupId, messagesUI)
-      && !hasClosedParentGroup(groups.get(message.groupId), messagesUI)
-    );
-}
-
-function hasClosedParentGroup(group, messagesUI) {
-  return group.some(groupId => isGroupClosed(groupId, messagesUI));
-}
-
-function isGroupClosed(groupId, messagesUI) {
-  return messagesUI.includes(groupId) === false;
-}
-
-function matchLevelFilters(message, filters) {
-  return filters.get(message.level) === true;
-}
-
-function matchNetworkFilters(message, filters) {
-  return (
-    message.source !== MESSAGE_SOURCE.NETWORK
-    || (filters.get("net") === true && message.isXHR === false)
-    || (filters.get("netxhr") === true && message.isXHR === true)
-  );
-}
-
-function matchCssFilters(message, filters) {
-  return (
-    message.source != MESSAGE_SOURCE.CSS
-    || filters.get("css") === true
-  );
-}
-
-function matchSearchFilters(message, filters) {
-  let text = filters.text || "";
-  return (
-    text === ""
-    // Look for a match in parameters.
-    || isTextInParameters(text, message.parameters)
-    // Look for a match in location.
-    || isTextInFrame(text, message.frame)
-    // Look for a match in net events.
-    || isTextInNetEvent(text, message.request)
-    // Look for a match in stack-trace.
-    || isTextInStackTrace(text, message.stacktrace)
-    // Look for a match in messageText.
-    || isTextInMessageText(text, message.messageText)
-    // Look for a match in notes.
-    || isTextInNotes(text, message.notes)
-  );
-}
-
-/**
- * Returns true if given text is included in provided stack frame.
- */
-function isTextInFrame(text, frame) {
-  if (!frame) {
-    return false;
-  }
-
-  const { short } = getSourceNames(frame.source);
-  return `${short}:${frame.line}:${frame.column}`
-    .toLocaleLowerCase()
-    .includes(text.toLocaleLowerCase());
-}
-
-/**
- * Returns true if given text is included in provided parameters.
- */
-function isTextInParameters(text, parameters) {
-  if (!parameters) {
-    return false;
-  }
-
-  text = text.toLocaleLowerCase();
-  return getAllProps(parameters).find(prop =>
-    (prop + "").toLocaleLowerCase().includes(text)
-  );
+function getVisibleMessages(state) {
+  return state.messages.visibleMessages.map(id => getMessage(state, id));
 }
 
-/**
- * Returns true if given text is included in provided net event grip.
- */
-function isTextInNetEvent(text, request) {
-  if (!request) {
-    return false;
-  }
-
-  text = text.toLocaleLowerCase();
-
-  let method = request.method.toLocaleLowerCase();
-  let url = request.url.toLocaleLowerCase();
-  return method.includes(text) || url.includes(text);
-}
-
-/**
- * Returns true if given text is included in provided stack trace.
- */
-function isTextInStackTrace(text, stacktrace) {
-  if (!Array.isArray(stacktrace)) {
-    return false;
-  }
-
-  // isTextInFrame expect the properties of the frame object to be in the same
-  // order they are rendered in the Frame component.
-  return stacktrace.some(frame => isTextInFrame(text, {
-    functionName: frame.functionName || l10n.getStr("stacktrace.anonymousFunction"),
-    filename: frame.filename,
-    lineNumber: frame.lineNumber,
-    columnNumber: frame.columnNumber
-  }));
-}
-
-/**
- * Returns true if given text is included in `messageText` field.
- */
-function isTextInMessageText(text, messageText) {
-  if (!messageText) {
-    return false;
-  }
-
-  return messageText.toLocaleLowerCase().includes(text.toLocaleLowerCase());
-}
-
-/**
- * Returns true if given text is included in notes.
- */
-function isTextInNotes(text, notes) {
-  if (!Array.isArray(notes)) {
-    return false;
-  }
-
-  return notes.some(note =>
-    // Look for a match in location.
-    isTextInFrame(text, note.frame) ||
-    // Look for a match in messageBody.
-    (note.messageBody &&
-        note.messageBody.toLocaleLowerCase()
-          .includes(text.toLocaleLowerCase()))
-  );
-}
-
-/**
- * Get a flat array of all the grips and their properties.
- *
- * @param {Array} Grips
- * @return {Array} Flat array of the grips and their properties.
- */
-function getAllProps(grips) {
-  let result = grips.reduce((res, grip) => {
-    let previewItems = getGripPreviewItems(grip);
-    let allProps = previewItems.length > 0 ? getAllProps(previewItems) : [];
-    return [...res, grip, grip.class, ...allProps];
-  }, []);
-
-  // We are interested only in primitive props (to search for)
-  // not in objects and undefined previews.
-  result = result.filter(grip =>
-    typeof grip != "object" &&
-    typeof grip != "undefined"
-  );
-
-  return [...new Set(result)];
-}
-
-exports.getAllMessages = getAllMessages;
-exports.getAllMessagesUiById = getAllMessagesUiById;
-exports.getAllMessagesTableDataById = getAllMessagesTableDataById;
-exports.getAllGroupsById = getAllGroupsById;
-exports.getCurrentGroup = getCurrentGroup;
+module.exports = {
+  getMessage,
+  getAllMessagesById,
+  getAllMessagesUiById,
+  getAllMessagesTableDataById,
+  getAllGroupsById,
+  getCurrentGroup,
+  getVisibleMessages,
+};
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -3,17 +3,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {FilterState} = require("devtools/client/webconsole/new-console-output/reducers/filters");
 const {PrefState} = require("devtools/client/webconsole/new-console-output/reducers/prefs");
 const {UiState} = require("devtools/client/webconsole/new-console-output/reducers/ui");
 const {
   applyMiddleware,
-  combineReducers,
   compose,
   createStore
 } = require("devtools/client/shared/vendor/redux");
 const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
 const {
   MESSAGE_ADD,
   MESSAGES_CLEAR,
   REMOVED_MESSAGES_CLEAR,
@@ -39,22 +38,39 @@ function configureStore(hud) {
       netxhr: Services.prefs.getBoolPref(PREFS.FILTER.NETXHR),
     }),
     ui: new UiState({
       filterBarVisible: Services.prefs.getBoolPref(PREFS.UI.FILTER_BAR),
     })
   };
 
   return createStore(
-    combineReducers(reducers),
+    createRootReducer(),
     initialState,
     compose(applyMiddleware(thunk), enableActorReleaser(hud), enableBatching())
   );
 }
 
+function createRootReducer() {
+  return function rootReducer(state, action) {
+    const newFiltersState = reducers.filters(state.filters, action);
+    return Object.assign({}, {
+      filters: newFiltersState,
+      prefs: reducers.prefs(state.prefs, action),
+      ui: reducers.ui(state.ui, action),
+      // specifically pass the updated filters state as an additional argument.
+      messages: reducers.messages(
+        state.messages,
+        action,
+        newFiltersState
+      ),
+    });
+  };
+}
+
 /**
  * A enhancer for the store to handle batched actions.
  */
 function enableBatching() {
   return next => (reducer, initialState, enhancer) => {
     function batchingReducer(state, action) {
       switch (action.type) {
         case BATCH_ACTIONS: