Bug 1364150 - Introduce debounce middleware; r=bgrins
☠☠ backed out by f4262773c433 ☠ ☠
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 11 May 2017 12:40:20 -0700
changeset 363031 634b361b1b9db2b26d64a5944c7ebaa88fd5991a
parent 363030 09b1aee5ad9fb7c1e884036985576121b4a67766
child 363032 a08ea7f330370f90091ef1ee4b3a814bc2112755
push id91245
push userkwierso@gmail.com
push dateFri, 09 Jun 2017 01:42:31 +0000
treeherdermozilla-inbound@85f387a3a99b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1364150
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 1364150 - Introduce debounce middleware; r=bgrins MozReview-Commit-ID: Hu3rQ3iJHzP
devtools/client/shared/redux/middleware/debounce.js
devtools/client/shared/redux/middleware/moz.build
devtools/client/webconsole/new-console-output/actions/enhancers.js
devtools/client/webconsole/new-console-output/actions/index.js
devtools/client/webconsole/new-console-output/actions/messages.js
devtools/client/webconsole/new-console-output/actions/moz.build
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/store.js
devtools/client/webconsole/new-console-output/test/helpers.js
devtools/client/webconsole/webpack.config.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/debounce.js
@@ -0,0 +1,84 @@
+/* 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";
+
+/**
+ * Redux middleware for debouncing actions.
+ *
+ * Schedules actions with { meta: { debounce: true } } to be delayed
+ * by wait milliseconds. If another action is fired during this
+ * time-frame both actions are inserted into a queue and delayed.
+ * Maximum delay is defined by maxWait argument.
+ *
+ * Handling more actions at once results in better performance since
+ * components need to be re-rendered less often.
+ *
+ * @param string wait Wait for specified amount of milliseconds
+ *                    before executing an action. The time is used
+ *                    to collect more actions and handle them all
+ *                    at once.
+ * @param string maxWait Max waiting time. It's used in case of
+ *                       a long stream of actions.
+ */
+function debounceActions(wait, maxWait) {
+  let queuedActions = [];
+
+  return store => next => {
+    let debounced = debounce(() => {
+      next(batchActions(queuedActions));
+      queuedActions = [];
+    }, wait, maxWait);
+
+    return action => {
+      if (!action.meta || !action.meta.debounce) {
+        return next(action);
+      }
+
+      if (action.type == BATCH_ACTIONS) {
+        queuedActions.push(...action.actions);
+      } else {
+        queuedActions.push(action);
+      }
+
+      return debounced();
+    };
+  };
+}
+
+function debounce(cb, wait, maxWait) {
+  let timeout, maxTimeout;
+  let doFunction = () => {
+    clearTimeout(timeout);
+    clearTimeout(maxTimeout);
+    timeout = maxTimeout = null;
+    cb();
+  };
+
+  return () => {
+    clearTimeout(timeout);
+    timeout = setTimeout(doFunction, wait);
+    if (!maxTimeout) {
+      maxTimeout = setTimeout(doFunction, maxWait);
+    }
+  };
+}
+
+const BATCH_ACTIONS = Symbol("BATCH_ACTIONS");
+
+/**
+ * Action creator for action-batching.
+ */
+function batchActions(batchedActions, debounceFlag = true) {
+  return {
+    type: BATCH_ACTIONS,
+    meta: { debounce: debounceFlag },
+    actions: batchedActions,
+  };
+}
+
+module.exports = {
+  BATCH_ACTIONS,
+  batchActions,
+  debounceActions,
+};
--- a/devtools/client/shared/redux/middleware/moz.build
+++ b/devtools/client/shared/redux/middleware/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'debounce.js',
     'history.js',
     'log.js',
     'promise.js',
     'task.js',
     'thunk.js',
     'wait-service.js',
 )
 
deleted file mode 100644
--- a/devtools/client/webconsole/new-console-output/actions/enhancers.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* -*- 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 { BATCH_ACTIONS } = require("../constants");
-
-function batchActions(batchedActions) {
-  return {
-    type: BATCH_ACTIONS,
-    actions: batchedActions,
-  };
-}
-
-module.exports = {
-  batchActions
-};
--- a/devtools/client/webconsole/new-console-output/actions/index.js
+++ b/devtools/client/webconsole/new-console-output/actions/index.js
@@ -2,17 +2,16 @@
 /* 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 actionModules = [
-  require("./enhancers"),
   require("./filters"),
   require("./messages"),
   require("./ui"),
 ];
 
 const actions = Object.assign({}, ...actionModules);
 
 module.exports = actions;
--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -5,17 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 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 { batchActions } = require("devtools/client/shared/redux/middleware/debounce");
 const {
   MESSAGE_ADD,
   NETWORK_MESSAGE_UPDATE,
   MESSAGES_CLEAR,
   MESSAGE_OPEN,
   MESSAGE_CLOSE,
   MESSAGE_TYPE,
   MESSAGE_TABLE_RECEIVE,
@@ -25,16 +25,17 @@ const defaultIdGenerator = new IdGenerat
 
 function messageAdd(packet, idGenerator = null) {
   if (idGenerator == null) {
     idGenerator = defaultIdGenerator;
   }
   let message = prepareMessage(packet, idGenerator);
   const addMessageAction = {
     type: MESSAGE_ADD,
+    meta: { debounce: true },
     message
   };
 
   if (message.type === MESSAGE_TYPE.CLEAR) {
     return batchActions([
       messagesClear(),
       addMessageAction,
     ]);
@@ -95,16 +96,17 @@ function networkMessageUpdate(packet, id
   if (idGenerator == null) {
     idGenerator = defaultIdGenerator;
   }
 
   let message = prepareMessage(packet, idGenerator);
 
   return {
     type: NETWORK_MESSAGE_UPDATE,
+    meta: { debounce: true },
     message,
   };
 }
 
 module.exports = {
   messageAdd,
   messagesClear,
   messageOpen,
--- a/devtools/client/webconsole/new-console-output/actions/moz.build
+++ b/devtools/client/webconsole/new-console-output/actions/moz.build
@@ -1,12 +1,11 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
-    'enhancers.js',
     'filters.js',
     'index.js',
     'messages.js',
     'ui.js',
 )
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -6,24 +6,23 @@
 // React & Redux
 const React = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const { createContextMenu } = require("devtools/client/webconsole/new-console-output/utils/context-menu");
 const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
+const { batchActions } = require("devtools/client/shared/redux/middleware/debounce");
 
 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 throttledDispatchTimeout = false;
 
 function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) {
   EventEmitter.decorate(this);
 
   this.parentNode = parentNode;
   this.jsterm = jsterm;
   this.toolbox = toolbox;
   this.owner = owner;
@@ -140,17 +139,17 @@ NewConsoleOutputWrapper.prototype = {
         childComponent
     ));
 
     this.body = ReactDOM.render(provider, this.parentNode);
   },
 
   dispatchMessageAdd: function (message, waitForResponse) {
     let action = actions.messageAdd(message);
-    batchedMessageAdd(action);
+    store.dispatch(action);
 
     // 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.get("id");
       return new Promise(resolve => {
@@ -167,47 +166,36 @@ NewConsoleOutputWrapper.prototype = {
       });
     }
 
     return Promise.resolve();
   },
 
   dispatchMessagesAdd: function (messages) {
     const batchedActions = messages.map(message => actions.messageAdd(message));
-    store.dispatch(actions.batchActions(batchedActions));
+    store.dispatch(batchActions(batchedActions));
   },
 
   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));
+      store.dispatch(actions.networkMessageUpdate(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);
-  if (!throttledDispatchTimeout) {
-    throttledDispatchTimeout = setTimeout(() => {
-      store.dispatch(actions.batchActions(queuedActions));
-      queuedActions = [];
-      throttledDispatchTimeout = null;
-    }, 50);
-  }
-}
-
 // Exports from this module
 module.exports = NewConsoleOutputWrapper;
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -8,20 +8,23 @@ const {PrefState} = require("devtools/cl
 const {UiState} = require("devtools/client/webconsole/new-console-output/reducers/ui");
 const {
   applyMiddleware,
   compose,
   createStore
 } = require("devtools/client/shared/vendor/redux");
 const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
 const {
+  debounceActions,
+  BATCH_ACTIONS
+} = require("devtools/client/shared/redux/middleware/debounce");
+const {
   MESSAGE_ADD,
   MESSAGES_CLEAR,
   REMOVED_MESSAGES_CLEAR,
-  BATCH_ACTIONS,
   PREFS,
 } = require("devtools/client/webconsole/new-console-output/constants");
 const { reducers } = require("./reducers/index");
 const Services = require("Services");
 
 function configureStore(hud, options = {}) {
   const logLimit = options.logLimit
     || Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1);
@@ -38,20 +41,26 @@ function configureStore(hud, options = {
       net: Services.prefs.getBoolPref(PREFS.FILTER.NET),
       netxhr: Services.prefs.getBoolPref(PREFS.FILTER.NETXHR),
     }),
     ui: new UiState({
       filterBarVisible: Services.prefs.getBoolPref(PREFS.UI.FILTER_BAR),
     })
   };
 
+  let args = [thunk];
+  if (!options.noDebounce) {
+    args.push(debounceActions(16, 500));
+  }
+
+  let middleware = applyMiddleware(...args);
   return createStore(
     createRootReducer(),
     initialState,
-    compose(applyMiddleware(thunk), enableActorReleaser(hud), enableBatching())
+    compose(middleware, enableActorReleaser(hud), enableBatching())
   );
 }
 
 function createRootReducer() {
   return function rootReducer(state, action) {
     // We want to compute the new state for all properties except "messages".
     const newState = [...Object.entries(reducers)].reduce((res, [key, reducer]) => {
       if (key !== "messages") {
--- a/devtools/client/webconsole/new-console-output/test/helpers.js
+++ b/devtools/client/webconsole/new-console-output/test/helpers.js
@@ -29,17 +29,21 @@ function setupActions() {
   };
 
   return wrappedActions;
 }
 
 /**
  * Prepare the store for use in testing.
  */
-function setupStore(input, hud, options) {
+function setupStore(input, hud, options = {}) {
+  // Disable debouncing for tests. It makes tests simpler
+  // if actions are dispatched and handled synchronously.
+  options.noDebounce = true;
+
   const store = configureStore(hud, options);
 
   // Add the messages from the input commands to the store.
   input.forEach((cmd) => {
     store.dispatch(actions.messageAdd(stubPackets.get(cmd)));
   });
 
   return store;
--- a/devtools/client/webconsole/webpack.config.js
+++ b/devtools/client/webconsole/webpack.config.js
@@ -65,16 +65,17 @@ webpackConfig.resolve = {
     "devtools/shared/l10n": path.join(__dirname, "../../shared/l10n"),
 
     "devtools/client/framework/devtools": path.join(__dirname, "../../client/shims/devtools"),
     "devtools/client/framework/menu": "devtools-modules/client/framework/menu",
     "devtools/client/framework/menu-item": path.join(__dirname, "../../client/framework/menu-item"),
 
     "devtools/client/shared/components/reps/reps": path.join(__dirname, "../../client/shared/components/reps/reps"),
     "devtools/client/shared/redux/middleware/thunk": path.join(__dirname, "../../client/shared/redux/middleware/thunk"),
+    "devtools/client/shared/redux/middleware/debounce": path.join(__dirname, "../../client/shared/redux/middleware/debounce"),
     "devtools/client/shared/components/stack-trace": path.join(__dirname, "../../client/shared/components/stack-trace"),
     "devtools/client/shared/source-utils": path.join(__dirname, "../../client/shared/source-utils"),
     "devtools/client/shared/components/frame": path.join(__dirname, "../../client/shared/components/frame"),
 
     "devtools/shared/defer": path.join(__dirname, "../../shared/defer"),
     "devtools/shared/event-emitter": "devtools-modules/shared/event-emitter",
     "devtools/shared/client/main": path.join(__dirname, "new-console-output/test/fixtures/ObjectClient"),
     "devtools/shared/platform/clipboard": path.join(__dirname, "../../shared/platform/content/clipboard"),