Bug 1391688 - Show only messages that fit in the viewport on first console render; r=bgrins.
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Tue, 24 Oct 2017 15:54:00 +0200
changeset 389404 48ad9a270454f772394bdcb19d666d879f69ac1f
parent 389403 9f4f27cd706931ecde1b97b2fe468189e7b8230f
child 389405 5614bd22556cd3d1bcfb1dda7b19b725147a94f0
push id96855
push userarchaeopteryx@coole-files.de
push dateTue, 31 Oct 2017 23:40:37 +0000
treeherdermozilla-inbound@285362745f60 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1391688
milestone58.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 1391688 - Show only messages that fit in the viewport on first console render; r=bgrins. This allow us to have a faster first meaningful render for the user. The other messages get rendered after ConsoleOutput mounting. MozReview-Commit-ID: KIptXsLmTiA
devtools/client/webconsole/new-console-output/actions/ui.js
devtools/client/webconsole/new-console-output/components/ConsoleOutput.js
devtools/client/webconsole/new-console-output/constants.js
devtools/client/webconsole/new-console-output/reducers/ui.js
devtools/client/webconsole/new-console-output/test/components/console-output.test.js
devtools/client/webconsole/new-console-output/utils/messages.js
--- a/devtools/client/webconsole/new-console-output/actions/ui.js
+++ b/devtools/client/webconsole/new-console-output/actions/ui.js
@@ -6,20 +6,21 @@
 
 "use strict";
 
 const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
 const Services = require("Services");
 
 const {
   FILTER_BAR_TOGGLE,
+  INITIALIZE,
   PERSIST_TOGGLE,
   PREFS,
+  SELECT_NETWORK_MESSAGE_TAB,
   TIMESTAMPS_TOGGLE,
-  SELECT_NETWORK_MESSAGE_TAB,
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 function filterBarToggle(show) {
   return (dispatch, getState) => {
     dispatch({
       type: FILTER_BAR_TOGGLE,
     });
     const uiState = getAllUi(getState());
@@ -46,14 +47,21 @@ function timestampsToggle(visible) {
 
 function selectNetworkMessageTab(id) {
   return {
     type: SELECT_NETWORK_MESSAGE_TAB,
     id,
   };
 }
 
+function initialize() {
+  return {
+    type: INITIALIZE
+  };
+}
+
 module.exports = {
   filterBarToggle,
+  initialize,
   persistToggle,
+  selectNetworkMessageTab,
   timestampsToggle,
-  selectNetworkMessageTab,
 };
--- a/devtools/client/webconsole/new-console-output/components/ConsoleOutput.js
+++ b/devtools/client/webconsole/new-console-output/components/ConsoleOutput.js
@@ -5,33 +5,38 @@
 
 const {
   Component,
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
+const {initialize} = require("devtools/client/webconsole/new-console-output/actions/ui");
 
 const {
   getAllMessagesById,
   getAllMessagesUiById,
   getAllMessagesTableDataById,
   getAllNetworkMessagesUpdateById,
   getVisibleMessages,
   getAllRepeatById,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/MessageContainer").MessageContainer);
 const {
   MESSAGE_TYPE,
 } = require("devtools/client/webconsole/new-console-output/constants");
+const {
+  getInitialMessageCountForViewport
+} = require("devtools/client/webconsole/new-console-output/utils/messages.js");
 
 class ConsoleOutput extends Component {
   static get propTypes() {
     return {
+      initialized: PropTypes.bool.isRequired,
       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,
@@ -55,16 +60,21 @@ class ConsoleOutput extends Component {
     this.props.serviceContainer.attachRefToHud("outputScroller", this.outputNode);
 
     // Waiting for the next paint.
     new Promise(res => requestAnimationFrame(res))
       .then(() => {
         if (this.props.onFirstMeaningfulPaint) {
           this.props.onFirstMeaningfulPaint();
         }
+
+        // Dispatching on next tick so we don't block on action execution.
+        setTimeout(() => {
+          this.props.dispatch(initialize());
+        }, 0);
       });
   }
 
   componentWillUpdate(nextProps, nextState) {
     const outputNode = this.outputNode;
     if (!outputNode || !outputNode.lastChild) {
       // Force a scroll to bottom when messages are added to an empty console.
       // This makes the console stay pinned to the bottom if a batch of messages
@@ -75,21 +85,28 @@ class ConsoleOutput extends Component {
 
     const lastChild = outputNode.lastChild;
     const visibleMessagesDelta =
       nextProps.visibleMessages.length - this.props.visibleMessages.length;
     const messagesDelta =
       nextProps.messages.size - this.props.messages.size;
 
     // We need to scroll to the bottom if:
+    // - we are reacting to the "initialize" action,
+    //   and we are already scrolled to the bottom
     // - the number of messages displayed changed
     //   and we are already scrolled to the bottom
     // - the number of messages in the store changed
     //   and the new message is an evaluation result.
     this.shouldScrollBottom =
+      (
+        !this.props.initialized &&
+        nextProps.initialized &&
+        isScrolledToBottom(lastChild, outputNode)
+      ) ||
       (messagesDelta > 0 && nextProps.messages.last().type === MESSAGE_TYPE.RESULT) ||
       (visibleMessagesDelta > 0 && isScrolledToBottom(lastChild, outputNode));
   }
 
   componentDidUpdate() {
     if (this.shouldScrollBottom) {
       scrollToBottom(this.outputNode);
     }
@@ -108,18 +125,27 @@ class ConsoleOutput extends Component {
       messages,
       messagesUi,
       messagesTableData,
       messagesRepeat,
       networkMessagesUpdate,
       networkMessageActiveTabId,
       serviceContainer,
       timestampsVisible,
+      initialized,
     } = this.props;
 
+    if (!initialized) {
+      const numberMessagesFitViewport = getInitialMessageCountForViewport(window);
+      if (numberMessagesFitViewport < visibleMessages.length) {
+        visibleMessages = visibleMessages.slice(
+          visibleMessages.length - numberMessagesFitViewport);
+      }
+    }
+
     let messageNodes = visibleMessages.map((messageId) => MessageContainer({
       dispatch,
       key: messageId,
       messageId,
       serviceContainer,
       open: messagesUi.includes(messageId),
       tableData: messagesTableData.get(messageId),
       timestampsVisible,
@@ -150,16 +176,17 @@ 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 {
+    initialized: state.ui.initialized,
     messages: getAllMessagesById(state),
     visibleMessages: getVisibleMessages(state),
     messagesUi: getAllMessagesUiById(state),
     messagesTableData: getAllMessagesTableDataById(state),
     messagesRepeat: getAllRepeatById(state),
     networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
     timestampsVisible: state.ui.timestampsVisible,
     networkMessageActiveTabId: state.ui.networkMessageActiveTabId,
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -2,33 +2,34 @@
 /* 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 actionTypes = {
   BATCH_ACTIONS: "BATCH_ACTIONS",
+  DEFAULT_FILTERS_RESET: "DEFAULT_FILTERS_RESET",
+  FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE",
+  FILTER_TEXT_SET: "FILTER_TEXT_SET",
+  FILTER_TOGGLE: "FILTER_TOGGLE",
+  FILTERS_CLEAR: "FILTERS_CLEAR",
+  INITIALIZE: "INITIALIZE",
   MESSAGE_ADD: "MESSAGE_ADD",
+  MESSAGE_CLOSE: "MESSAGE_CLOSE",
+  MESSAGE_OPEN: "MESSAGE_OPEN",
+  MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
   MESSAGES_ADD: "MESSAGES_ADD",
   MESSAGES_CLEAR: "MESSAGES_CLEAR",
-  MESSAGE_OPEN: "MESSAGE_OPEN",
-  MESSAGE_CLOSE: "MESSAGE_CLOSE",
   NETWORK_MESSAGE_UPDATE: "NETWORK_MESSAGE_UPDATE",
   NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST",
-  MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
+  PERSIST_TOGGLE: "PERSIST_TOGGLE",
   REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
+  SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
-  FILTER_TOGGLE: "FILTER_TOGGLE",
-  FILTER_TEXT_SET: "FILTER_TEXT_SET",
-  FILTERS_CLEAR: "FILTERS_CLEAR",
-  DEFAULT_FILTERS_RESET: "DEFAULT_FILTERS_RESET",
-  FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE",
-  SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
-  PERSIST_TOGGLE: "PERSIST_TOGGLE",
 };
 
 const prefs = {
   PREFS: {
     FILTER: {
       ERROR: "devtools.webconsole.filter.error",
       WARN: "devtools.webconsole.filter.warn",
       INFO: "devtools.webconsole.filter.info",
--- a/devtools/client/webconsole/new-console-output/reducers/ui.js
+++ b/devtools/client/webconsole/new-console-output/reducers/ui.js
@@ -2,43 +2,47 @@
 /* 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 {
   FILTER_BAR_TOGGLE,
+  INITIALIZE,
   PERSIST_TOGGLE,
+  SELECT_NETWORK_MESSAGE_TAB,
   TIMESTAMPS_TOGGLE,
-  SELECT_NETWORK_MESSAGE_TAB,
 } = require("devtools/client/webconsole/new-console-output/constants");
 const Immutable = require("devtools/client/shared/vendor/immutable");
 
 const {
   PANELS,
 } = require("devtools/client/netmonitor/src/constants");
 
 const UiState = Immutable.Record({
   filterBarVisible: false,
+  initialized: false,
+  networkMessageActiveTabId: PANELS.HEADERS,
   persistLogs: false,
   timestampsVisible: true,
-  networkMessageActiveTabId: PANELS.HEADERS,
 });
 
 function ui(state = new UiState(), action) {
   switch (action.type) {
     case FILTER_BAR_TOGGLE:
       return state.set("filterBarVisible", !state.filterBarVisible);
     case PERSIST_TOGGLE:
       return state.set("persistLogs", !state.persistLogs);
     case TIMESTAMPS_TOGGLE:
       return state.set("timestampsVisible", action.visible);
     case SELECT_NETWORK_MESSAGE_TAB:
       return state.set("networkMessageActiveTabId", action.id);
+    case INITIALIZE:
+      return state.set("initialized", true);
   }
 
   return state;
 }
 
 module.exports = {
   UiState,
   ui,
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/console-output.test.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+  createFactory,
+} = require("devtools/client/shared/vendor/react");
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const ConsoleOutput = createFactory(require("devtools/client/webconsole/new-console-output/components/ConsoleOutput"));
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+const {initialize} = require("devtools/client/webconsole/new-console-output/actions/ui");
+const {
+  getInitialMessageCountForViewport
+} = require("devtools/client/webconsole/new-console-output/utils/messages.js");
+
+const MESSAGES_NUMBER = 100;
+function getDefaultProps(initialized) {
+  const store = setupStore(
+    Array.from({length: MESSAGES_NUMBER})
+      // Alternate message so we don't trigger the repeat mechanism.
+      .map((_, i) => i % 2 ? "new Date(0)" : "console.log(NaN)")
+  );
+
+  if (initialized) {
+    store.dispatch(initialize());
+  }
+
+  return {
+    store,
+    serviceContainer,
+  };
+}
+
+describe("ConsoleOutput component:", () => {
+  it("Render only the last messages that fits the viewport when non-initialized", () => {
+    const rendered = render(ConsoleOutput(getDefaultProps(false)));
+    const messagesNumber = rendered.find(".message").length;
+    expect(messagesNumber).toBe(getInitialMessageCountForViewport(window));
+  });
+
+  it("Render every message when initialized", () => {
+    const rendered = render(ConsoleOutput(getDefaultProps(true)));
+    expect(rendered.find(".message").length).toBe(MESSAGES_NUMBER);
+  });
+});
--- a/devtools/client/webconsole/new-console-output/utils/messages.js
+++ b/devtools/client/webconsole/new-console-output/utils/messages.js
@@ -350,14 +350,21 @@ function getLevelFromType(type) {
 
 function isGroupType(type) {
   return [
     MESSAGE_TYPE.START_GROUP,
     MESSAGE_TYPE.START_GROUP_COLLAPSED
   ].includes(type);
 }
 
-exports.prepareMessage = prepareMessage;
-// Export for use in testing.
-exports.getRepeatId = getRepeatId;
+function getInitialMessageCountForViewport(win) {
+  const minMessageHeight = 20;
+  return Math.ceil(win.innerHeight / minMessageHeight);
+}
 
-exports.l10n = l10n;
-exports.isGroupType = isGroupType;
+module.exports = {
+  getInitialMessageCountForViewport,
+  isGroupType,
+  l10n,
+  prepareMessage,
+  // Export for use in testing.
+  getRepeatId,
+};