Bug 1371721 - Create a MESSAGES_ADD action to add batches of messages at once
MozReview-Commit-ID: 1MxCqlmDzJV
--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -8,26 +8,51 @@
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 {
MESSAGE_ADD,
+ MESSAGES_ADD,
NETWORK_MESSAGE_UPDATE,
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
@@ -101,15 +126,16 @@ function networkMessageUpdate(packet, id
return {
type: NETWORK_MESSAGE_UPDATE,
message,
};
}
module.exports = {
messageAdd,
+ messagesAdd,
messagesClear,
messageOpen,
messageClose,
messageTableDataGet,
networkMessageUpdate,
};
--- 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",
MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
REMOVED_MESSAGES_CLEAR: "REMOVED_MESSAGES_CLEAR",
TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
FILTER_TOGGLE: "FILTER_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
@@ -12,17 +12,18 @@ const actions = require("devtools/client
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/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 throttledDispatchTimeout = false;
function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) {
EventEmitter.decorate(this);
this.parentNode = parentNode;
this.jsterm = jsterm;
this.toolbox = toolbox;
@@ -139,75 +140,86 @@ NewConsoleOutputWrapper.prototype = {
filterBar,
childComponent
));
this.body = ReactDOM.render(provider, this.parentNode);
},
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(actions.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 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));
+ batchedMessageUpdates(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);
+function setTimeoutIfNeeded() {
if (!throttledDispatchTimeout) {
throttledDispatchTimeout = setTimeout(() => {
- store.dispatch(actions.batchActions(queuedActions));
- queuedActions = [];
+ store.dispatch(actions.messagesAdd(queuedMessageAdds));
+ queuedMessageUpdates.forEach(message => {
+ actions.networkMessageUpdate(message);
+ });
+ queuedMessageAdds = [];
+ queuedMessageUpdates = [];
throttledDispatchTimeout = null;
}, 50);
}
}
+function batchedMessageUpdates(message) {
+ queuedMessageUpdates.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
@@ -37,94 +37,128 @@ const MessageState = Immutable.Record({
removedMessages: [],
// Map of the form {messageId : numberOfRepeat}
repeatById: {},
// Map of the form {messageId : networkInformation}
// `networkInformation` holds request, response, totalTime, ...
networkMessagesUpdateById: {},
});
+function addMessage(state, filtersState, prefsState, newMessage) {
+ const {
+ messagesById,
+ messagesUiById,
+ messagesTableDataById,
+ networkMessagesUpdateById,
+ groupsById,
+ currentGroup,
+ repeatById,
+ visibleMessages,
+ } = state;
+ const {logLimit} = prefsState;
+ 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));
+ }
+ }
+
+ if (shouldMessageBeVisible(addedMessage, record, filtersState)) {
+ record.set("visibleMessages", [...visibleMessages, newMessage.id]);
+ }
+ });
+}
+
function messages(state = new MessageState(), action, filtersState, prefsState) {
const {
messagesById,
messagesUiById,
messagesTableDataById,
networkMessagesUpdateById,
groupsById,
currentGroup,
repeatById,
visibleMessages,
} = state;
const {logLimit} = prefsState;
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;
- }
-
- if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) {
- // Compute the new current group.
- return state.set("currentGroup", getNewCurrentGroup(currentGroup, groupsById));
- }
+ case constants.MESSAGES_ADD:
+ var newState = state;
- 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 messages = [];
+ let prunableCount = 0;
+ for (var 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) {
+ messages.unshift(action.messages[i]);
+ }
+ } else {
+ messages.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)
- );
+ messages.forEach(message => {
+ newState = addMessage(newState, filtersState, prefsState, message);
+ });
- 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));
+ return limitTopLevelMessageCount(newState, logLimit);
- if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
- // We want the group to be open by default.
- record.set("messagesUiById", messagesUiById.push(newMessage.id));
- }
- }
-
- if (shouldMessageBeVisible(addedMessage, record, filtersState)) {
- record.set("visibleMessages", [...visibleMessages, newMessage.id]);
- }
-
- // 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:
+ var newState = addMessage(state, filtersState, prefsState, action.message);
+ return limitTopLevelMessageCount(newState, logLimit);
case constants.MESSAGES_CLEAR:
return new MessageState({
// Store all removed messages associated with some arguments.
// This array is used by `releaseActorsEnhancer` to release
// all related backend actors.
"removedMessages": [...state.messagesById].reduce((res, [id, msg]) => {
if (msg.parameters) {
@@ -248,111 +282,111 @@ 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 removedMessages = [];
- 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);
-
- // Filter out messages with no arguments. Only actual arguments
- // can be associated with backend actors.
- if (message && message.parameters) {
- removedMessages.push(message);
+ if (topLevelCount <= logLimit) {
+ return;
}
- const index = visibleMessages.indexOf(id);
- if (index > -1) {
- visibleMessages.splice(index, 1);
+ const removedMessagesId = [];
+ const removedMessages = [];
+ 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;
+ }
+
+ // 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);
+
+ // Filter out messages with no arguments. Only actual arguments
+ // can be associated with backend actors.
+ if (message && message.parameters) {
+ removedMessages.push(message);
+ }
+
+ const index = visibleMessages.indexOf(id);
+ if (index > -1) {
+ visibleMessages.splice(index, 1);
+ }
+
+ return true;
+ });
+
+ if (removedMessages.length > 0) {
+ record.set("removedMessages", record.removedMessages.concat(removedMessages));
}
- return true;
- });
-
- if (removedMessages.length > 0) {
- record.set("removedMessages", record.removedMessages.concat(removedMessages));
- }
-
- if (record.visibleMessages.length > visibleMessages.length) {
- record.set("visibleMessages", visibleMessages);
- }
+ if (record.visibleMessages.length > visibleMessages.length) {
+ record.set("visibleMessages", visibleMessages);
+ }
- const isInRemovedId = id => removedMessagesId.includes(id);
- const mapHasRemovedIdKey = map => map.findKey((value, id) => isInRemovedId(id));
- const cleanUpCollection = map => removedMessagesId.forEach(id => map.remove(id));
- const cleanUpObject = object => [...Object.entries(object)]
- .reduce((res, [id, value]) => {
- if (!isInRemovedId(id)) {
- res[id] = value;
- }
- return res;
- }, {});
+ const isInRemovedId = id => removedMessagesId.includes(id);
+ const mapHasRemovedIdKey = map => map.findKey((value, id) => isInRemovedId(id));
+ const cleanUpCollection = map => removedMessagesId.forEach(id => map.remove(id));
+ 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));
- record.set("messagesById", record.messagesById.withMutations(cleanUpCollection));
+ if (record.messagesUiById.find(isInRemovedId)) {
+ record.set("messagesUiById", record.messagesUiById.withMutations(cleanUpCollection));
+ }
+ if (mapHasRemovedIdKey(record.messagesTableDataById)) {
+ record.set("messagesTableDataById",
+ record.messagesTableDataById.withMutations(cleanUpCollection));
+ }
+ if (mapHasRemovedIdKey(record.groupsById)) {
+ record.set("groupsById", record.groupsById.withMutations(cleanUpCollection));
+ }
- if (record.messagesUiById.find(isInRemovedId)) {
- record.set("messagesUiById", record.messagesUiById.withMutations(cleanUpCollection));
- }
- if (mapHasRemovedIdKey(record.messagesTableDataById)) {
- record.set("messagesTableDataById",
- record.messagesTableDataById.withMutations(cleanUpCollection));
- }
- if (mapHasRemovedIdKey(record.groupsById)) {
- record.set("groupsById", record.groupsById.withMutations(cleanUpCollection));
- }
+ if (Object.keys(record.repeatById).includes(removedMessagesId)) {
+ record.set("repeatById", cleanUpObject(record.repeatById));
+ }
- if (Object.keys(record.repeatById).includes(removedMessagesId)) {
- record.set("repeatById", cleanUpObject(record.repeatById));
- }
-
- if (Object.keys(record.networkMessagesUpdateById).includes(removedMessagesId)) {
- record.set("networkMessagesUpdateById",
- cleanUpObject(record.networkMessagesUpdateById));
- }
-
- return record;
+ if (Object.keys(record.networkMessagesUpdateById).includes(removedMessagesId)) {
+ record.set("networkMessagesUpdateById",
+ cleanUpObject(record.networkMessagesUpdateById));
+ }
+ });
}
/**
* Returns total count of top level messages (those which are not
* within a group).
*/
function getToplevelMessageCount(record) {
return record.messagesById.count(message => !message.groupId);