--- 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