Bug 1408182 - Replace ImmutableJS by plain JS code; r=rickychien
authorAlexandre Poirot <poirot.alex@gmail.com>
Wed, 11 Oct 2017 17:35:09 +0200
changeset 392904 3c7a4c69bb8cc50c080c66e49ca36a0f3a2528a9
parent 392903 6de571030d5d998dcadbd3dac602fa006395165c
child 392905 a77dc52424ef72a2c419b8497d1e24af50d8aa26
push id32945
push userccoroiu@mozilla.com
push dateTue, 21 Nov 2017 23:30:10 +0000
treeherdermozilla-central@0ee6ca490391 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrickychien
bugs1408182
milestone59.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 1408182 - Replace ImmutableJS by plain JS code; r=rickychien MozReview-Commit-ID: FFUNk97n4zS
devtools/client/netmonitor/.eslintrc.js
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/StatisticsPanel.js
devtools/client/netmonitor/src/components/Toolbar.js
devtools/client/netmonitor/src/reducers/requests.js
devtools/client/netmonitor/src/selectors/requests.js
devtools/client/netmonitor/src/selectors/timing-markers.js
devtools/client/netmonitor/src/selectors/ui.js
devtools/client/netmonitor/test/browser_net_copy_params.js
--- a/devtools/client/netmonitor/.eslintrc.js
+++ b/devtools/client/netmonitor/.eslintrc.js
@@ -15,9 +15,15 @@ module.exports = {
   "rules": {
     // The netmonitor is being migrated to HTML and cleaned of
     // chrome-privileged code, so this rule disallows requiring chrome
     // code. Some files in the netmonitor disable this rule still. The
     // goal is to enable the rule globally on all files.
     /* eslint-disable max-len */
     "mozilla/reject-some-requires": ["error", "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm|devtools/shared/platform/(chome|content)/.*)$"],
   },
+
+  "parserOptions": {
+    "ecmaFeatures": {
+      experimentalObjectRestSpread: true,
+    },
+  },
 };
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -32,17 +32,17 @@ const MAX_SCROLL_HEIGHT = 2147483647;
  * Renders the actual contents of the request list.
  */
 class RequestListContent extends Component {
   static get propTypes() {
     return {
       connector: PropTypes.object.isRequired,
       columns: PropTypes.object.isRequired,
       dispatch: PropTypes.func.isRequired,
-      displayedRequests: PropTypes.object.isRequired,
+      displayedRequests: PropTypes.array.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
       onItemMouseDown: PropTypes.func.isRequired,
       onSecurityIconMouseDown: PropTypes.func.isRequired,
       onSelectDelta: PropTypes.func.isRequired,
       onWaterfallMouseDown: PropTypes.func.isRequired,
       scale: PropTypes.number,
--- a/devtools/client/netmonitor/src/components/StatisticsPanel.js
+++ b/devtools/client/netmonitor/src/components/StatisticsPanel.js
@@ -35,17 +35,17 @@ const CHARTS_CACHE_DISABLED = L10N.getSt
  * download the different parts of your site.
  */
 class StatisticsPanel extends Component {
   static get propTypes() {
     return {
       connector: PropTypes.object.isRequired,
       closeStatistics: PropTypes.func.isRequired,
       enableRequestFilterTypeOnly: PropTypes.func.isRequired,
-      requests: PropTypes.object,
+      requests: PropTypes.array,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       isVerticalSpliter: MediaQueryList.matches,
@@ -62,17 +62,17 @@ class StatisticsPanel extends Component 
   componentWillMount() {
     this.mdnLinkContainerNodes = new Map();
   }
 
   componentDidUpdate(prevProps) {
     MediaQueryList.addListener(this.onLayoutChange);
 
     const { requests } = this.props;
-    let ready = requests && !requests.isEmpty() && requests.every((req) =>
+    let ready = requests && requests.length && requests.every((req) =>
       req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
       req.status !== undefined && req.totalTime !== undefined
     );
 
     this.createChart({
       id: "primedCacheChart",
       title: CHARTS_CACHE_ENABLED,
       data: ready ? this.sanitizeChartDataSource(requests, false) : null,
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -66,17 +66,17 @@ class Toolbar extends Component {
       toggleNetworkDetails: PropTypes.func.isRequired,
       enablePersistentLogs: PropTypes.func.isRequired,
       togglePersistentLogs: PropTypes.func.isRequired,
       persistentLogsEnabled: PropTypes.bool.isRequired,
       disableBrowserCache: PropTypes.func.isRequired,
       toggleBrowserCache: PropTypes.func.isRequired,
       browserCacheDisabled: PropTypes.bool.isRequired,
       toggleRequestFilterType: PropTypes.func.isRequired,
-      filteredRequests: PropTypes.object.isRequired,
+      filteredRequests: PropTypes.array.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.autocompleteProvider = this.autocompleteProvider.bind(this);
     this.toggleRequestFilterType = this.toggleRequestFilterType.bind(this);
     this.updatePersistentLogsEnabled = this.updatePersistentLogsEnabled.bind(this);
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -1,84 +1,197 @@
 /* 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 I = require("devtools/client/shared/vendor/immutable");
 const {
   getUrlDetails,
   processNetworkUpdates,
 } = require("../utils/request-utils");
 const {
   ADD_REQUEST,
   CLEAR_REQUESTS,
   CLONE_SELECTED_REQUEST,
   OPEN_NETWORK_DETAILS,
   REMOVE_SELECTED_CUSTOM_REQUEST,
   SELECT_REQUEST,
   SEND_CUSTOM_REQUEST,
   TOGGLE_RECORDING,
   UPDATE_REQUEST,
 } = require("../constants");
 
-const Request = I.Record({
-  id: null,
-  // Set to true in case of a request that's being edited as part of "edit and resend"
-  isCustom: false,
-  // Request properties - at the beginning, they are unknown and are gradually filled in
-  startedMillis: undefined,
-  endedMillis: undefined,
-  method: undefined,
-  url: undefined,
-  urlDetails: undefined,
-  remotePort: undefined,
-  remoteAddress: undefined,
-  isXHR: undefined,
-  cause: undefined,
-  fromCache: undefined,
-  fromServiceWorker: undefined,
-  status: undefined,
-  statusText: undefined,
-  httpVersion: undefined,
-  securityState: undefined,
-  securityInfo: undefined,
-  mimeType: "text/plain",
-  contentSize: undefined,
-  transferredSize: undefined,
-  totalTime: undefined,
-  eventTimings: undefined,
-  headersSize: undefined,
-  // Text value is used for storing custom request query
-  // which only appears when user edit the custom requst form
-  customQueryValue: undefined,
-  requestHeaders: undefined,
-  requestHeadersFromUploadStream: undefined,
-  requestCookies: undefined,
-  requestPostData: undefined,
-  responseHeaders: undefined,
-  responseCookies: undefined,
-  responseContent: undefined,
-  responseContentAvailable: false,
-  formDataSections: undefined,
-});
+/**
+ * This structure stores list of all HTTP requests received
+ * from the backend. It's using plain JS structures to store
+ * data instead of ImmutableJS, which is performance expensive.
+ */
+function Requests() {
+  return {
+    // Map with all requests (key = actor ID, value = request object)
+    requests: mapNew(),
+    // Selected request ID
+    selectedId: null,
+    preselectedId: null,
+    // True if the monitor is recording HTTP traffic
+    recording: true,
+    // Auxiliary fields to hold requests stats
+    firstStartedMillis: +Infinity,
+    lastEndedMillis: -Infinity,
+  };
+}
+
+/**
+ * This reducer is responsible for maintaining list of request
+ * within the Network panel.
+ */
+function requestsReducer(state = Requests(), action) {
+  switch (action.type) {
+    // Appending new request into the list/map.
+    case ADD_REQUEST: {
+      let nextState = { ...state };
+
+      let newRequest = {
+        id: action.id,
+        ...action.data,
+        urlDetails: getUrlDetails(action.data.url),
+      };
+
+      nextState.requests = mapSet(state.requests, newRequest.id, newRequest);
+
+      // Update the started/ended timestamps.
+      let { startedMillis } = action.data;
+      if (startedMillis < state.firstStartedMillis) {
+        nextState.firstStartedMillis = startedMillis;
+      }
+      if (startedMillis > state.lastEndedMillis) {
+        nextState.lastEndedMillis = startedMillis;
+      }
+
+      // Select the request if it was preselected and there is no other selection.
+      if (state.preselectedId && state.preselectedId === action.id) {
+        nextState.selectedId = state.selectedId || state.preselectedId;
+        nextState.preselectedId = null;
+      }
+
+      return nextState;
+    }
+
+    // Update an existing request (with received data).
+    case UPDATE_REQUEST: {
+      let { requests, lastEndedMillis } = state;
+
+      let request = requests.get(action.id);
+      if (!request) {
+        return state;
+      }
+
+      request = {
+        ...request,
+        ...processNetworkUpdates(action.data),
+      };
+
+      return {
+        ...state,
+        requests: mapSet(state.requests, action.id, request),
+        lastEndedMillis: lastEndedMillis,
+      };
+    }
 
-const Requests = I.Record({
-  // The collection of requests (keyed by id)
-  requests: I.Map(),
-  // Selection state
-  selectedId: null,
-  preselectedId: null,
-  // Auxiliary fields to hold requests stats
-  firstStartedMillis: +Infinity,
-  lastEndedMillis: -Infinity,
-  // Recording state
-  recording: true,
-});
+    // Remove all requests in the list. Create fresh new state
+    // object, but keep value of the `recording` field.
+    case CLEAR_REQUESTS: {
+      return {
+        ...Requests(),
+        recording: state.recording,
+      };
+    }
+
+    // Select specific request.
+    case SELECT_REQUEST: {
+      return {
+        ...state,
+        selectedId: action.id,
+      };
+    }
+
+    // Clone selected request for re-send.
+    case CLONE_SELECTED_REQUEST: {
+      let { requests, selectedId } = state;
+
+      if (!selectedId) {
+        return state;
+      }
+
+      let clonedRequest = requests.get(selectedId);
+      if (!clonedRequest) {
+        return state;
+      }
+
+      let newRequest = {
+        id: clonedRequest.id + "-clone",
+        method: clonedRequest.method,
+        url: clonedRequest.url,
+        urlDetails: clonedRequest.urlDetails,
+        requestHeaders: clonedRequest.requestHeaders,
+        requestPostData: clonedRequest.requestPostData,
+        isCustom: true
+      };
+
+      return {
+        ...state,
+        requests: mapSet(requests, newRequest.id, newRequest),
+        selectedId: newRequest.id,
+      };
+    }
+
+    // Removing temporary cloned request (created for re-send, but canceled).
+    case REMOVE_SELECTED_CUSTOM_REQUEST: {
+      return closeCustomRequest(state);
+    }
+
+    // Re-sending an existing request.
+    case SEND_CUSTOM_REQUEST: {
+      // When a new request with a given id is added in future, select it immediately.
+      // where we know in advance the ID of the request, at a time when it
+      // wasn't sent yet.
+      return closeCustomRequest(state.set("preselectedId", action.id));
+    }
+
+    // Pause/resume button clicked.
+    case TOGGLE_RECORDING: {
+      return {
+        ...state,
+        recording: !state.recording,
+      };
+    }
+
+    // Side bar with request details opened.
+    case OPEN_NETWORK_DETAILS: {
+      let nextState = { ...state };
+      if (!action.open) {
+        nextState.selectedId = null;
+        return nextState;
+      }
+
+      if (!state.selectedId && !state.requests.isEmpty()) {
+        nextState.selectedId = [...state.requests.values()][0].id;
+        return nextState;
+      }
+
+      return state;
+    }
+
+    default:
+      return state;
+  }
+}
+
+// Helpers
 
 /**
  * Remove the currently selected custom request.
  */
 function closeCustomRequest(state) {
   let { requests, selectedId } = state;
 
   if (!selectedId) {
@@ -87,127 +200,49 @@ function closeCustomRequest(state) {
 
   let removedRequest = requests.get(selectedId);
 
   // Only custom requests can be removed
   if (!removedRequest || !removedRequest.isCustom) {
     return state;
   }
 
-  return state.withMutations(st => {
-    st.requests = st.requests.delete(selectedId);
-    st.selectedId = null;
-  });
+  return {
+    ...state,
+    requests: mapDelete(state.requests, selectedId),
+    selectedId: null,
+  };
 }
 
-function requestsReducer(state = new Requests(), action) {
-  switch (action.type) {
-    case ADD_REQUEST: {
-      return state.withMutations(st => {
-        let newRequest = new Request(Object.assign(
-          { id: action.id },
-          action.data,
-          { urlDetails: getUrlDetails(action.data.url) }
-        ));
-        st.requests = st.requests.set(newRequest.id, newRequest);
-
-        // Update the started/ended timestamps
-        let { startedMillis } = action.data;
-        if (startedMillis < st.firstStartedMillis) {
-          st.firstStartedMillis = startedMillis;
-        }
-        if (startedMillis > st.lastEndedMillis) {
-          st.lastEndedMillis = startedMillis;
-        }
+// Immutability helpers
+// FIXME The following helper API need refactoring, see bug 1418969.
 
-        // Select the request if it was preselected and there is no other selection
-        if (st.preselectedId && st.preselectedId === action.id) {
-          st.selectedId = st.selectedId || st.preselectedId;
-          st.preselectedId = null;
-        }
-      });
-    }
-    case CLEAR_REQUESTS: {
-      return new Requests({
-        recording: state.recording
-      });
-    }
-    case CLONE_SELECTED_REQUEST: {
-      let { requests, selectedId } = state;
-
-      if (!selectedId) {
-        return state;
-      }
-
-      let clonedRequest = requests.get(selectedId);
-      if (!clonedRequest) {
-        return state;
-      }
-
-      let newRequest = new Request({
-        id: clonedRequest.id + "-clone",
-        method: clonedRequest.method,
-        url: clonedRequest.url,
-        urlDetails: clonedRequest.urlDetails,
-        requestHeaders: clonedRequest.requestHeaders,
-        requestPostData: clonedRequest.requestPostData,
-        isCustom: true
-      });
+/**
+ * Clone an existing map.
+ */
+function mapNew(map) {
+  let newMap = new Map(map);
+  newMap.isEmpty = () => newMap.size == 0;
+  newMap.valueSeq = () => [...newMap.values()];
+  return newMap;
+}
 
-      return state.withMutations(st => {
-        st.requests = requests.set(newRequest.id, newRequest);
-        st.selectedId = newRequest.id;
-      });
-    }
-    case OPEN_NETWORK_DETAILS: {
-      if (!action.open) {
-        return state.set("selectedId", null);
-      }
-
-      if (!state.selectedId && !state.requests.isEmpty()) {
-        return state.set("selectedId", state.requests.first().id);
-      }
+/**
+ * Append new item into existing map and return new map.
+ */
+function mapSet(map, key, value) {
+  let newMap = mapNew(map);
+  return newMap.set(key, value);
+}
 
-      return state;
-    }
-    case REMOVE_SELECTED_CUSTOM_REQUEST: {
-      return closeCustomRequest(state);
-    }
-    case SELECT_REQUEST: {
-      return state.set("selectedId", action.id);
-    }
-    case SEND_CUSTOM_REQUEST: {
-      // When a new request with a given id is added in future, select it immediately.
-      // where we know in advance the ID of the request, at a time when it
-      // wasn't sent yet.
-      return closeCustomRequest(state.set("preselectedId", action.id));
-    }
-    case TOGGLE_RECORDING: {
-      return state.set("recording", !state.recording);
-    }
-    case UPDATE_REQUEST: {
-      let { requests, lastEndedMillis } = state;
-
-      let updatedRequest = requests.get(action.id);
-      if (!updatedRequest) {
-        return state;
-      }
-
-      updatedRequest = updatedRequest.withMutations(request => {
-        let values = processNetworkUpdates(action.data);
-        request = Object.assign(request, values);
-      });
-
-      return state.withMutations(st => {
-        st.requests = requests.set(updatedRequest.id, updatedRequest);
-        st.lastEndedMillis = lastEndedMillis;
-      });
-    }
-
-    default:
-      return state;
-  }
+/**
+ * Remove an item from existing map and return new map.
+ */
+function mapDelete(map, key) {
+  let newMap = mapNew(map);
+  newMap.requests.delete(key);
+  return newMap;
 }
 
 module.exports = {
   Requests,
   requestsReducer,
 };
--- a/devtools/client/netmonitor/src/selectors/requests.js
+++ b/devtools/client/netmonitor/src/selectors/requests.js
@@ -51,43 +51,54 @@ const getTypeFilterFn = createSelector(
     const matchesType = filters.requestFilterTypes.some((enabled, filter) => {
       return enabled && Filters[filter] && Filters[filter](r);
     });
     return matchesType;
   }
 );
 
 const getSortFn = createSelector(
-  state => state.requests.requests,
+  state => state.requests,
   state => state.sort,
-  (requests, sort) => {
+  ({ requests }, sort) => {
     const sorter = Sorters[sort.type || "waterfall"];
     const ascending = sort.ascending ? +1 : -1;
     return (a, b) => ascending * sortWithClones(requests, sorter, a, b);
   }
 );
 
 const getSortedRequests = createSelector(
-  state => state.requests.requests,
+  state => state.requests,
   getSortFn,
-  (requests, sortFn) => requests.valueSeq().sort(sortFn).toList()
+  ({ requests }, sortFn) => {
+    let arr = requests.valueSeq().sort(sortFn);
+    arr.get = index => arr[index];
+    arr.isEmpty = () => this.length == 0;
+    arr.size = arr.length;
+    return arr;
+  }
 );
 
 const getDisplayedRequests = createSelector(
-  state => state.requests.requests,
+  state => state.requests,
   getFilterFn,
   getSortFn,
-  (requests, filterFn, sortFn) => requests.valueSeq()
-    .filter(filterFn).sort(sortFn).toList()
+  ({ requests }, filterFn, sortFn) => {
+    let arr = requests.valueSeq().filter(filterFn).sort(sortFn);
+    arr.get = index => arr[index];
+    arr.isEmpty = () => this.length == 0;
+    arr.size = arr.length;
+    return arr;
+  }
 );
 
 const getTypeFilteredRequests = createSelector(
-  state => state.requests.requests,
+  state => state.requests,
   getTypeFilterFn,
-  (requests, filterFn) => requests.valueSeq().filter(filterFn).toList()
+  ({ requests }, filterFn) => requests.valueSeq().filter(filterFn)
 );
 
 const getDisplayedRequestsSummary = createSelector(
   getDisplayedRequests,
   state => state.requests.lastEndedMillis - state.requests.firstStartedMillis,
   (requests, totalMillis) => {
     if (requests.size == 0) {
       return { count: 0, bytes: 0, millis: 0 };
--- a/devtools/client/netmonitor/src/selectors/timing-markers.js
+++ b/devtools/client/netmonitor/src/selectors/timing-markers.js
@@ -1,13 +1,13 @@
 /* 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";
 
 function getDisplayedTimingMarker(state, marker) {
-  return state.timingMarkers.get(marker) - state.requests.get("firstStartedMillis");
+  return state.timingMarkers.get(marker) - state.requests.firstStartedMillis;
 }
 
 module.exports = {
   getDisplayedTimingMarker,
 };
--- a/devtools/client/netmonitor/src/selectors/ui.js
+++ b/devtools/client/netmonitor/src/selectors/ui.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { REQUESTS_WATERFALL } = require("../constants");
 const { getDisplayedRequests } = require("./requests");
 
 function isNetworkDetailsToggleButtonDisabled(state) {
-  return getDisplayedRequests(state).isEmpty();
+  return getDisplayedRequests(state).length == 0;
 }
 
 const EPSILON = 0.001;
 
 function getWaterfallScale(state) {
   const { requests, timingMarkers, ui } = state;
 
   if (requests.firstStartedMillis === +Infinity || ui.waterfallWidth === null) {
--- a/devtools/client/netmonitor/test/browser_net_copy_params.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_params.js
@@ -93,18 +93,18 @@ add_task(function* () {
         "be hidden.");
   }
 
   function* testCopyPostData(index, postData) {
     // Wait for formDataSections and requestPostData state are ready in redux store
     // since copyPostData API needs to read these state.
     yield waitUntil(() => {
       let { requests } = store.getState().requests;
-      let actIDs = Object.keys(requests.toJS());
-      let { formDataSections, requestPostData } = requests.get(actIDs[index]).toJS();
+      let actIDs = [...requests.keys()];
+      let { formDataSections, requestPostData } = requests.get(actIDs[index]);
       return formDataSections && requestPostData;
     });
     EventUtils.sendMouseEvent({ type: "mousedown" },
       document.querySelectorAll(".request-list-item")[index]);
     EventUtils.sendMouseEvent({ type: "contextmenu" },
       document.querySelectorAll(".request-list-item")[index]);
     yield waitForClipboardPromise(function setup() {
       monitor.panelWin.parent.document