Bug 1363678 - Move the filtering logic to the reducer. r=bgrins draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Fri, 26 May 2017 18:14:01 +0200
changeset 585167 ca04c84bd47c1b96931ac78e645b5da331168649
parent 585145 1bfa4578aa56f768626ba278a6929e23fc48db54
child 585168 4bc0b91abf0ef74224e47bd6cb8e47699d22872e
push id61033
push userbmo:nchevobbe@mozilla.com
push dateFri, 26 May 2017 16:26:43 +0000
reviewersbgrins
bugs1363678
milestone55.0a1
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 thoughful about which messages to evaluate and then. 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/actions/filters.js
devtools/client/webconsole/new-console-output/components/console-output.js
devtools/client/webconsole/new-console-output/components/filter-bar.js
devtools/client/webconsole/new-console-output/reducers/filters.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/actions/filters.js
+++ b/devtools/client/webconsole/new-console-output/actions/filters.js
@@ -11,20 +11,21 @@ const Services = require("Services");
 
 const {
   FILTER_TEXT_SET,
   FILTER_TOGGLE,
   FILTERS_CLEAR,
   PREFS,
 } = require("devtools/client/webconsole/new-console-output/constants");
 
-function filterTextSet(text) {
+function filterTextSet(oldValue, newValue) {
   return {
     type: FILTER_TEXT_SET,
-    text
+    oldValue,
+    newValue,
   };
 }
 
 function filterToggle(filter) {
   return (dispatch, getState) => {
     dispatch({
       type: FILTER_TOGGLE,
       filter,
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -7,41 +7,43 @@ 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,
   getAllGroupsById,
+  getMessage,
+  getVisibleMessages,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const { getScrollSetting } = require("devtools/client/webconsole/new-console-output/selectors/ui");
 const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
 
 const ConsoleOutput = createClass({
 
   displayName: "ConsoleOutput",
 
   propTypes: {
-    messages: PropTypes.object.isRequired,
+    getMessageById: PropTypes.func.isRequired,
     messagesUi: PropTypes.object.isRequired,
     serviceContainer: PropTypes.shape({
       attachRefToHud: PropTypes.func.isRequired,
       openContextMenu: PropTypes.func.isRequired,
       sourceMapService: PropTypes.object,
     }),
     autoscroll: PropTypes.bool.isRequired,
     dispatch: PropTypes.func.isRequired,
     timestampsVisible: PropTypes.bool,
     groups: PropTypes.object.isRequired,
     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);
@@ -73,25 +75,27 @@ const ConsoleOutput = createClass({
     e.stopPropagation();
     e.preventDefault();
   },
 
   render() {
     let {
       dispatch,
       autoscroll,
-      messages,
+      visibleMessages,
       messagesUi,
       messagesTableData,
       serviceContainer,
       groups,
       timestampsVisible,
+      getMessageById,
     } = this.props;
 
-    let messageNodes = messages.map((message) => {
+    let messageNodes = visibleMessages.map((visibleMessage) => {
+      const message = getMessageById(visibleMessage);
       const parentGroups = message.groupId ? (
         (groups.get(message.groupId) || [])
           .concat([message.groupId])
       ) : [];
 
       return (
         MessageContainer({
           dispatch,
@@ -128,17 +132,18 @@ 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),
+    getMessageById: id => getMessage(state, id),
     messagesUi: getAllMessagesUiById(state),
     messagesTableData: getAllMessagesTableDataById(state),
     autoscroll: getScrollSetting(state),
     groups: getAllGroupsById(state),
     timestampsVisible: state.ui.timestampsVisible,
   };
 }
 
--- a/devtools/client/webconsole/new-console-output/components/filter-bar.js
+++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js
@@ -44,17 +44,19 @@ const FilterBar = createClass({
     this.props.dispatch(messagesClear());
   },
 
   onClickFilterBarToggle: function () {
     this.props.dispatch(filterBarToggle());
   },
 
   onSearchInput: function (e) {
-    this.props.dispatch(filterTextSet(e.target.value));
+    const oldValue = this.props.filter.text;
+    const newValue = e.target.value;
+    this.props.dispatch(filterTextSet(oldValue, newValue));
   },
 
   render() {
     const {dispatch, filter, filterBarVisible} = this.props;
     let children = [];
 
     children.push(dom.div({className: "devtools-toolbar webconsole-filterbar-primary"},
       dom.button({
--- a/devtools/client/webconsole/new-console-output/reducers/filters.js
+++ b/devtools/client/webconsole/new-console-output/reducers/filters.js
@@ -24,17 +24,17 @@ function filters(state = new FilterState
   switch (action.type) {
     case constants.FILTER_TOGGLE:
       const {filter} = action;
       const active = !state.get(filter);
       return state.set(filter, active);
     case constants.FILTERS_CLEAR:
       return new FilterState();
     case constants.FILTER_TEXT_SET:
-      let {text} = action;
-      return state.set("text", text);
+      let {newValue} = action;
+      return state.set("text", newValue);
   }
 
   return state;
 }
 
 exports.FilterState = FilterState;
 exports.filters = filters;
--- 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,29 +62,32 @@ 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)
+            )
+          );
         }
       }
 
       return state.withMutations(function (record) {
+        const addedMessage = newMessage.set("groupId", currentGroup);
         // Add the new message with a reference to the parent group.
         record.set(
           "messagesById",
-          messagesById.push(newMessage.set("groupId", currentGroup))
+          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",
@@ -84,49 +98,208 @@ function messages(state = new MessageSta
           );
 
           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:
+      const active = filtersState.get(action.filter);
+      if (!active) {
+        // If we're turning off a filter, we can operate on visible messages only
+        return state.set(
+          "visibleMessages",
+          visibleMessages.filter(messageId =>
+            shouldMessageBeVisible(
+              messagesById.get(messageId),
+              state,
+              filtersState
+            )
+          )
+        );
+      }
+
+      // Here we can check if non-visible messages should now be visible
+      let messagesToShow = [...messagesById].reduce((res, [messageId, message]) => {
+        if (
+          !visibleMessages.includes(messageId)
+          && shouldMessageBeVisible(message, state, filtersState)
+        ) {
+          res.push(messageId);
+        }
+        return res;
+      }, []);
+
+      if (messagesToShow.length === 0) {
+        return state;
+      }
+
+      return state.set(
+        "visibleMessages",
+        visibleMessages.concat(messagesToShow)
+          .sort((idA, idB) => {
+            let messageA = messagesById.get(idA);
+            let messageB = messagesById.get(idB);
+            return messageA.timeStamp > messageB.timeStamp;
+          })
+      );
+
+    case constants.FILTER_TEXT_SET:
+      let {oldValue = "", newValue = ""} = action;
+
+      if (newValue.startsWith(oldValue)) {
+        // If the new filter value start with the old value,
+        // we can work on visible messages only.
+        return state.set(
+          "visibleMessages",
+          visibleMessages.filter(messageId =>
+            shouldMessageBeVisible(
+              messagesById.get(messageId),
+              state,
+              filtersState
+            )
+          )
+        );
+      }
+
+      if (oldValue.startsWith(newValue)) {
+        // If something was substracted from the old value,
+        // then we can work on hidden message only, because those which are visible
+        // will still be.
+        const hiddenMessagesToShow = [...messagesById]
+          .reduce((res, [messageId, message]) => {
+            if (
+              !visibleMessages.includes(messageId)
+              && shouldMessageBeVisible(message, state, filtersState)
+            ) {
+              res.push(messageId);
+            }
+            return res;
+          }, []);
+
+        if (hiddenMessagesToShow.length === 0) {
+          return state;
+        }
+
+        return state.set(
+          "visibleMessages",
+          visibleMessages.concat(hiddenMessagesToShow)
+            .sort((idA, idB) => {
+              let messageA = messagesById.get(idA);
+              let messageB = messagesById.get(idB);
+              return messageA.timeStamp > messageB.timeStamp;
+            })
+        );
+      }
+
+      // Here we have to check all the messages because
+      // the filter changed in an unpredictable way.
+      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) {
@@ -234,9 +407,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;
 }
 
-/**
- * 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: