--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -9,27 +9,53 @@
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/shared/redux/middleware/debounce");
const {
MESSAGE_ADD,
+ MESSAGES_ADD,
NETWORK_MESSAGE_UPDATE,
NETWORK_UPDATE_REQUEST,
MESSAGES_CLEAR,
MESSAGE_OPEN,
MESSAGE_CLOSE,
MESSAGE_TYPE,
MESSAGE_TABLE_RECEIVE,
} = require("../constants");
const defaultIdGenerator = new IdGenerator();
+function messagesAdd(packets, idGenerator = null) {
+ if (idGenerator == null) {
+ idGenerator = defaultIdGenerator;
+ }
+ let messages = packets.map(packet => prepareMessage(packet, idGenerator));
+ for (let i = messages.length - 1; i >= 0; i--) {
+ if (messages[i].type === MESSAGE_TYPE.CLEAR) {
+ return batchActions([
+ messagesClear(),
+ {
+ type: MESSAGES_ADD,
+ messages: messages.slice(i),
+ }
+ ]);
+ }
+ }
+
+ // When this is used for non-cached messages then handle clear message and
+ // split up into batches
+ return {
+ type: MESSAGES_ADD,
+ messages
+ };
+}
+
function messageAdd(packet, idGenerator = null) {
if (idGenerator == null) {
idGenerator = defaultIdGenerator;
}
let message = prepareMessage(packet, idGenerator);
const addMessageAction = {
type: MESSAGE_ADD,
message
@@ -112,16 +138,17 @@ function networkUpdateRequest(id, data)
type: NETWORK_UPDATE_REQUEST,
id,
data,
};
}
module.exports = {
messageAdd,
+ messagesAdd,
messagesClear,
messageOpen,
messageClose,
messageTableDataGet,
networkMessageUpdate,
networkUpdateRequest,
// for test purpose only.
messageTableDataReceive,
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -3,16 +3,17 @@
/* 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",
MESSAGE_ADD: "MESSAGE_ADD",
+ 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",
REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -4,26 +4,27 @@
"use strict";
// 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 { batchActions } = require("devtools/client/shared/redux/middleware/debounce");
const { createContextMenu } = require("devtools/client/webconsole/new-console-output/utils/context-menu");
const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
const EventEmitter = require("devtools/shared/old-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 queuedMessageAdds = [];
+let queuedMessageUpdates = [];
+let queuedRequestUpdates = [];
let throttledDispatchTimeout = false;
function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) {
EventEmitter.decorate(this);
this.parentNode = parentNode;
this.jsterm = jsterm;
this.toolbox = toolbox;
@@ -176,67 +177,65 @@ NewConsoleOutputWrapper.prototype = {
filterBar,
childComponent
));
this.body = ReactDOM.render(provider, this.parentNode);
this.jsterm.focus();
},
dispatchMessageAdd: function (message, waitForResponse) {
- let action = actions.messageAdd(message);
- batchedMessageAdd(action);
+ batchedMessagesAdd(message);
// 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.id;
+ if (waitForResponse) {
+ let {timeStamp} = message;
return new Promise(resolve => {
let jsterm = this.jsterm;
jsterm.hud.on("new-messages", function onThisMessage(e, messages) {
for (let m of messages) {
- if (m.messageId === messageId) {
+ if (m.timeStamp === timeStamp) {
resolve(m.node);
jsterm.hud.off("new-messages", onThisMessage);
return;
}
}
});
});
}
return Promise.resolve();
},
dispatchMessagesAdd: function (messages) {
- const batchedActions = messages.map(message => actions.messageAdd(message));
- store.dispatch(batchActions(batchedActions));
+ store.dispatch(actions.messagesAdd(messages));
},
dispatchMessagesClear: function () {
store.dispatch(actions.messagesClear());
},
dispatchTimestampsToggle: function (enabled) {
store.dispatch(actions.timestampsToggle(enabled));
},
dispatchMessageUpdate: function (message, res) {
// network-message-updated will emit when all the update message arrives.
// Since we can't ensure the order of the network update, we check
// that networkInfo.updates has all we need.
const NUMBER_OF_NETWORK_UPDATE = 8;
if (res.networkInfo.updates.length === NUMBER_OF_NETWORK_UPDATE) {
- batchedMessageAdd(actions.networkMessageUpdate(message));
+ batchedMessageUpdates(message);
this.jsterm.hud.emit("network-message-updated", res);
}
},
dispatchRequestUpdate: function (id, data) {
- batchedMessageAdd(actions.networkUpdateRequest(id, data));
+ batchedRequestUpdates({ id, data });
// Fire an event indicating that all data fetched from
// the backend has been received. This is based on
// 'FirefoxDataProvider.isQueuePayloadReady', see more
// comments in that method.
// (netmonitor/src/connector/firefox-data-provider).
// This event might be utilized in tests to find the right
// time when to finish.
@@ -244,21 +243,43 @@ NewConsoleOutputWrapper.prototype = {
},
// Should be used for test purpose only.
getStore: function () {
return store;
}
};
-function batchedMessageAdd(action) {
- queuedActions.push(action);
+function setTimeoutIfNeeded() {
if (!throttledDispatchTimeout) {
throttledDispatchTimeout = setTimeout(() => {
- store.dispatch(batchActions(queuedActions));
- queuedActions = [];
+ store.dispatch(actions.messagesAdd(queuedMessageAdds));
+ queuedMessageUpdates.forEach(message => {
+ actions.networkMessageUpdate(message);
+ });
+ queuedRequestUpdates.forEach(({ id, data}) => {
+ actions.networkUpdateRequest(id, data);
+ });
+ queuedMessageAdds = [];
+ queuedMessageUpdates = [];
+ queuedRequestUpdates = [];
throttledDispatchTimeout = null;
}, 50);
}
}
+function batchedMessageUpdates(message) {
+ queuedMessageUpdates.push(message);
+ setTimeoutIfNeeded();
+}
+
+function batchedRequestUpdates(message) {
+ queuedRequestUpdates.push(message);
+ setTimeoutIfNeeded();
+}
+
+function batchedMessagesAdd(message) {
+ queuedMessageAdds.push(message);
+ setTimeoutIfNeeded();
+}
+
// Exports from this module
module.exports = NewConsoleOutputWrapper;
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -49,105 +49,136 @@ const MessageState = Immutable.Record({
removedActors: [],
// Map of the form {messageId : numberOfRepeat}
repeatById: {},
// Map of the form {messageId : networkInformation}
// `networkInformation` holds request, response, totalTime, ...
networkMessagesUpdateById: {},
});
-function messages(state = new MessageState(), action, filtersState, prefsState) {
+function addMessage(state, filtersState, prefsState, newMessage) {
const {
messagesById,
messagesUiById,
- messagesTableDataById,
- networkMessagesUpdateById,
groupsById,
currentGroup,
repeatById,
visibleMessages,
filteredMessagesCount,
} = state;
+ if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
+ // When the message has a NULL type, we don't add it.
+ return state;
+ }
+
+ 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
+ && lastMessage.groupId === currentGroup
+ ) {
+ return state.set(
+ "repeatById",
+ Object.assign({}, repeatById, {
+ [lastMessage.id]: (repeatById[lastMessage.id] || 1) + 1
+ })
+ );
+ }
+ }
+
+ return state.withMutations(function (record) {
+ // Add the new message with a reference to the parent group.
+ let parentGroups = getParentGroups(currentGroup, groupsById);
+ newMessage.groupId = currentGroup;
+ newMessage.indent = parentGroups.length;
+
+ const addedMessage = Object.freeze(newMessage);
+ record.set(
+ "messagesById",
+ 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", groupsById.set(newMessage.id, parentGroups));
+
+ if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
+ // We want the group to be open by default.
+ record.set("messagesUiById", messagesUiById.push(newMessage.id));
+ }
+ }
+
+ const {
+ visible,
+ cause
+ } = getMessageVisibility(addedMessage, record, filtersState);
+
+ if (visible) {
+ record.set("visibleMessages", [...visibleMessages, newMessage.id]);
+ } else if (DEFAULT_FILTERS.includes(cause)) {
+ record.set("filteredMessagesCount", Object.assign({}, filteredMessagesCount, {
+ global: filteredMessagesCount.global + 1,
+ [cause]: filteredMessagesCount[cause] + 1
+ }));
+ }
+ });
+}
+
+function messages(state = new MessageState(), action, filtersState, prefsState) {
+ const {
+ messagesById,
+ messagesUiById,
+ messagesTableDataById,
+ networkMessagesUpdateById,
+ groupsById,
+ visibleMessages,
+ } = state;
+
const {logLimit} = prefsState;
+ let newState;
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.
- return state;
- }
+ case constants.MESSAGES_ADD:
+ newState = state;
- 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
- && lastMessage.groupId === currentGroup
- ) {
- return state.set(
- "repeatById",
- Object.assign({}, repeatById, {
- [lastMessage.id]: (repeatById[lastMessage.id] || 1) + 1
- })
- );
+ // Preemptively remove messages that will never be rendered
+ let list = [];
+ let prunableCount = 0;
+ for (let i = action.messages.length - 1; i >= 0; i--) {
+ if (!action.messages[i].groupId && !isGroupType(action.messages[i].type) &&
+ action.messages[i].type !== MESSAGE_TYPE.END_GROUP) {
+ prunableCount++;
+ // Once we've added the max number of messages that can be added stop.
+ // TODO: what about repeats?
+ if (prunableCount <= logLimit) {
+ list.unshift(action.messages[i]);
+ }
+ } else {
+ list.unshift(action.messages[i]);
}
}
- return state.withMutations(function (record) {
- // Add the new message with a reference to the parent group.
- let parentGroups = getParentGroups(currentGroup, groupsById);
- newMessage.groupId = currentGroup;
- newMessage.indent = parentGroups.length;
-
- const addedMessage = Object.freeze(newMessage);
- record.set(
- "messagesById",
- 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", groupsById.set(newMessage.id, parentGroups));
+ list.forEach(message => {
+ newState = addMessage(newState, filtersState, prefsState, message);
+ });
- if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
- // We want the group to be open by default.
- record.set("messagesUiById", messagesUiById.push(newMessage.id));
- }
- }
-
- const {
- visible,
- cause
- } = getMessageVisibility(addedMessage, record, filtersState);
+ return limitTopLevelMessageCount(newState, logLimit);
- if (visible) {
- record.set("visibleMessages", [...visibleMessages, newMessage.id]);
- } else if (DEFAULT_FILTERS.includes(cause)) {
- record.set("filteredMessagesCount", Object.assign({}, filteredMessagesCount, {
- global: filteredMessagesCount.global + 1,
- [cause]: filteredMessagesCount[cause] + 1
- }));
- }
-
- // Remove top level message if the total count of top level messages
- // exceeds the current limit.
- if (record.messagesById.size > logLimit) {
- limitTopLevelMessageCount(state, record, logLimit);
- }
- });
+ case constants.MESSAGE_ADD:
+ newState = addMessage(state, filtersState, prefsState, action.message);
+ return limitTopLevelMessageCount(newState, logLimit);
case constants.MESSAGES_CLEAR:
return new MessageState({
// Store all actors from removed messages. This array is used by
// `releaseActorsEnhancer` to release all of those backend actors.
"removedActors": [...state.messagesById].reduce((res, [id, msg]) => {
res.push(...getAllActorsInMessage(msg, state));
return res;
@@ -261,17 +292,17 @@ function messages(state = new MessageSta
headers: [],
headersSize: 0,
};
break;
}
}
}
- let newState = state.set(
+ newState = state.set(
"networkMessagesUpdateById",
Object.assign({}, networkMessagesUpdateById, {
[action.id]: Object.assign({}, request, values)
})
);
return newState;
}
@@ -336,109 +367,109 @@ function getParentGroups(currentGroup, g
return groups;
}
/**
* Remove all top level messages that exceeds message limit.
* Also populate an array of all backend actors associated with these
* messages so they can be released.
*/
-function limitTopLevelMessageCount(state, record, logLimit) {
- let topLevelCount = record.groupsById.size === 0
- ? record.messagesById.size
- : getToplevelMessageCount(record);
-
- if (topLevelCount <= logLimit) {
- return record;
- }
-
- const removedMessagesId = [];
- const removedActors = [];
- let visibleMessages = [...record.visibleMessages];
-
- let cleaningGroup = false;
- record.messagesById.forEach((message, id) => {
- // If we were cleaning a group and the current message does not have
- // a groupId, we're done cleaning.
- if (cleaningGroup === true && !message.groupId) {
- cleaningGroup = false;
- }
+function limitTopLevelMessageCount(state, logLimit) {
+ return state.withMutations(function (record) {
+ let topLevelCount = record.groupsById.size === 0
+ ? record.messagesById.size
+ : getToplevelMessageCount(record);
- // If we're not cleaning a group and the message count is below the logLimit,
- // we exit the forEach iteration.
- if (cleaningGroup === false && topLevelCount <= logLimit) {
- return false;
- }
-
- // If we're not currently cleaning a group, and the current message is identified
- // as a group, set the cleaning flag to true.
- if (cleaningGroup === false && record.groupsById.has(id)) {
- cleaningGroup = true;
- }
-
- if (!message.groupId) {
- topLevelCount--;
- }
-
- removedMessagesId.push(id);
- removedActors.push(...getAllActorsInMessage(message, record));
-
- const index = visibleMessages.indexOf(id);
- if (index > -1) {
- visibleMessages.splice(index, 1);
+ if (topLevelCount <= logLimit) {
+ return;
}
- return true;
- });
+ const removedMessagesId = [];
+ const removedActors = [];
+ let visibleMessages = [...record.visibleMessages];
- if (removedActors.length > 0) {
- record.set("removedActors", record.removedActors.concat(removedActors));
- }
-
- if (record.visibleMessages.length > visibleMessages.length) {
- record.set("visibleMessages", visibleMessages);
- }
+ let cleaningGroup = false;
+ record.messagesById.forEach((message, id) => {
+ // If we were cleaning a group and the current message does not have
+ // a groupId, we're done cleaning.
+ if (cleaningGroup === true && !message.groupId) {
+ cleaningGroup = false;
+ }
- const isInRemovedId = id => removedMessagesId.includes(id);
- const mapHasRemovedIdKey = map => map.findKey((value, id) => isInRemovedId(id));
- const objectHasRemovedIdKey = obj => Object.keys(obj).findIndex(isInRemovedId) !== -1;
- const cleanUpCollection = map => removedMessagesId.forEach(id => map.remove(id));
- const cleanUpList = list => list.filter(id => {
- return isInRemovedId(id) === false;
- });
- const cleanUpObject = object => [...Object.entries(object)]
- .reduce((res, [id, value]) => {
- if (!isInRemovedId(id)) {
- res[id] = value;
+ // If we're not cleaning a group and the message count is below the logLimit,
+ // we exit the forEach iteration.
+ if (cleaningGroup === false && topLevelCount <= logLimit) {
+ return false;
+ }
+
+ // If we're not currently cleaning a group, and the current message is identified
+ // as a group, set the cleaning flag to true.
+ if (cleaningGroup === false && record.groupsById.has(id)) {
+ cleaningGroup = true;
+ }
+
+ if (!message.groupId) {
+ topLevelCount--;
+ }
+
+ removedMessagesId.push(id);
+ removedActors.push(...getAllActorsInMessage(message, record));
+
+ const index = visibleMessages.indexOf(id);
+ if (index > -1) {
+ visibleMessages.splice(index, 1);
}
- return res;
- }, {});
+
+ return true;
+ });
- record.set("messagesById", record.messagesById.withMutations(cleanUpCollection));
+ if (removedActors.length > 0) {
+ record.set("removedActors", record.removedActors.concat(removedActors));
+ }
+
+ if (record.visibleMessages.length > visibleMessages.length) {
+ record.set("visibleMessages", visibleMessages);
+ }
- if (record.messagesUiById.find(isInRemovedId)) {
- record.set("messagesUiById", cleanUpList(record.messagesUiById));
- }
- if (mapHasRemovedIdKey(record.messagesTableDataById)) {
- record.set("messagesTableDataById",
- record.messagesTableDataById.withMutations(cleanUpCollection));
- }
- if (mapHasRemovedIdKey(record.groupsById)) {
- record.set("groupsById", record.groupsById.withMutations(cleanUpCollection));
- }
- if (objectHasRemovedIdKey(record.repeatById)) {
- record.set("repeatById", cleanUpObject(record.repeatById));
- }
+ const isInRemovedId = id => removedMessagesId.includes(id);
+ const mapHasRemovedIdKey = map => map.findKey((value, id) => isInRemovedId(id));
+ const objectHasRemovedIdKey = obj => Object.keys(obj).findIndex(isInRemovedId) !== -1;
+ const cleanUpCollection = map => removedMessagesId.forEach(id => map.remove(id));
+ const cleanUpList = list => list.filter(id => {
+ return isInRemovedId(id) === false;
+ });
+ const cleanUpObject = object => [...Object.entries(object)]
+ .reduce((res, [id, value]) => {
+ if (!isInRemovedId(id)) {
+ res[id] = value;
+ }
+ return res;
+ }, {});
+
+ record.set("messagesById", record.messagesById.withMutations(cleanUpCollection));
- if (objectHasRemovedIdKey(record.networkMessagesUpdateById)) {
- record.set("networkMessagesUpdateById",
- cleanUpObject(record.networkMessagesUpdateById));
- }
+ if (record.messagesUiById.find(isInRemovedId)) {
+ record.set("messagesUiById", cleanUpList(record.messagesUiById));
+ }
+ if (mapHasRemovedIdKey(record.messagesTableDataById)) {
+ record.set("messagesTableDataById",
+ record.messagesTableDataById.withMutations(cleanUpCollection));
+ }
+ if (mapHasRemovedIdKey(record.groupsById)) {
+ record.set("groupsById", record.groupsById.withMutations(cleanUpCollection));
+ }
+ if (objectHasRemovedIdKey(record.repeatById)) {
+ record.set("repeatById", cleanUpObject(record.repeatById));
+ }
- return record;
+ if (objectHasRemovedIdKey(record.networkMessagesUpdateById)) {
+ record.set("networkMessagesUpdateById",
+ cleanUpObject(record.networkMessagesUpdateById));
+ }
+ });
}
/**
* Get an array of all the actors logged in a specific message.
*
* @param {Message} message: The message to get actors from.
* @param {Record} state: The redux state.
* @return {Array} An array containing all the actors logged in a message.