-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
perf: faster addMessagesSorted #470
Changes from all commits
6ad3976
10562af
4b7103a
6b8a7aa
dec599a
8a9fe2d
2620d57
9b0b34a
9d8aa9a
e7b8522
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,17 +11,6 @@ import { | |
UserResponse, | ||
} from './types'; | ||
|
||
const byDate = ( | ||
a: { created_at: Date | Immutable.ImmutableDate }, | ||
b: { created_at: Date | Immutable.ImmutableDate }, | ||
) => { | ||
if (!a.created_at) return -1; | ||
|
||
if (!b.created_at) return 1; | ||
|
||
return a.created_at.getTime() - b.created_at.getTime(); | ||
}; | ||
|
||
/** | ||
* ChannelState - A container class for the channel state. | ||
*/ | ||
|
@@ -167,6 +156,7 @@ export class ChannelState< | |
* addMessageSorted - Add a message to the state | ||
* | ||
* @param {MessageResponse<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType>} newMessage A new message | ||
* @param {boolean} timestampChanged Whether updating a message with changed created_at value. | ||
* | ||
*/ | ||
addMessageSorted( | ||
|
@@ -178,8 +168,9 @@ export class ChannelState< | |
ReactionType, | ||
UserType | ||
>, | ||
timestampChanged = false, | ||
) { | ||
return this.addMessagesSorted([newMessage]); | ||
return this.addMessagesSorted([newMessage], timestampChanged); | ||
} | ||
|
||
/** | ||
|
@@ -213,7 +204,8 @@ export class ChannelState< | |
* addMessagesSorted - Add the list of messages to state and resorts the messages | ||
* | ||
* @param {Array<MessageResponse<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType>>} newMessages A list of messages | ||
* @param {boolean} initializing Weather channel is being initialized. | ||
* @param {boolean} timestampChanged Whether updating messages with changed created_at value. | ||
* @param {boolean} initializing Whether channel is being initialized. | ||
* | ||
*/ | ||
addMessagesSorted( | ||
|
@@ -225,73 +217,43 @@ export class ChannelState< | |
ReactionType, | ||
UserType | ||
>[], | ||
timestampChanged = false, | ||
initializing = false, | ||
) { | ||
// parse all the new message dates and add __html for react | ||
const parsedMessages: ReturnType< | ||
ChannelState< | ||
AttachmentType, | ||
ChannelType, | ||
CommandType, | ||
EventType, | ||
MessageType, | ||
ReactionType, | ||
UserType | ||
>['messageToImmutable'] | ||
>[] = []; | ||
for (const message of newMessages) { | ||
for (let i = 0; i < newMessages.length; i += 1) { | ||
const message = this.messageToImmutable(newMessages[i]); | ||
|
||
if (initializing && message.id && this.threads[message.id]) { | ||
// If we are initializing the state of channel (e.g., in case of connection recovery), | ||
// then in that case we remove thread related to this message from threads object. | ||
// This way we can ensure that we don't have any stale data in thread object | ||
// and consumer can refetch the replies. | ||
this.threads = this.threads.without(message.id); | ||
} | ||
const parsedMsg = this.messageToImmutable(message); | ||
|
||
parsedMessages.push(parsedMsg); | ||
|
||
if (!this.last_message_at) { | ||
this.last_message_at = new Date(parsedMsg.created_at.getTime()); | ||
this.last_message_at = new Date(message.created_at.getTime()); | ||
} | ||
|
||
if ( | ||
this.last_message_at && | ||
parsedMsg.created_at.getTime() > this.last_message_at.getTime() | ||
) { | ||
this.last_message_at = new Date(parsedMsg.created_at.getTime()); | ||
if (message.created_at.getTime() > this.last_message_at.getTime()) { | ||
this.last_message_at = new Date(message.created_at.getTime()); | ||
} | ||
} | ||
|
||
// update or append the messages... | ||
const updatedThreads: string[] = []; | ||
for (const message of parsedMessages) { | ||
const isThreadReply = !!(message.parent_id && !message.show_in_channel); | ||
// update or append the messages... | ||
const parentID = message.parent_id; | ||
|
||
// add to the main message list | ||
if (!isThreadReply) { | ||
this.messages = this._addToMessageList(this.messages, message); | ||
if (!parentID || message.show_in_channel) { | ||
this.messages = this._addToMessageList(this.messages, message, timestampChanged); | ||
} | ||
|
||
// add to the thread if applicable.. | ||
const parentID: string | undefined = message.parent_id; | ||
if (parentID) { | ||
const thread = this.threads[parentID] || Immutable([]); | ||
const threadMessages = this._addToMessageList(thread, message); | ||
const threadMessages = this._addToMessageList(thread, message, timestampChanged); | ||
this.threads = this.threads.set(parentID, threadMessages); | ||
updatedThreads.push(parentID); | ||
} | ||
} | ||
|
||
// Resort the main messages and the threads that changed... | ||
const messages = Immutable.asMutable(this.messages); | ||
messages.sort(byDate); | ||
this.messages = Immutable(messages); | ||
for (const parentID of updatedThreads) { | ||
const threadMessages = this.threads[parentID] | ||
? Immutable.asMutable(this.threads[parentID]) | ||
: []; | ||
threadMessages.sort(byDate); | ||
this.threads = this.threads.set(parentID, threadMessages); | ||
} | ||
} | ||
|
||
addReaction( | ||
|
@@ -467,6 +429,7 @@ export class ChannelState< | |
* | ||
* @param {Immutable.ImmutableArray<ReturnType<ChannelState<AttachmentType, ChannelType, CommandType, EventType, MessageType, ReactionType, UserType>['messageToImmutable']>>} messages A list of messages | ||
* @param {ReturnType<ChannelState<AttachmentType, ChannelType, CommandType, EventType, MessageType, ReactionType, UserType>['messageToImmutable']>} newMessage The new message | ||
* @param {boolean} timestampChanged Whether updating a message with changed created_at value. | ||
* | ||
*/ | ||
_addToMessageList( | ||
|
@@ -483,7 +446,7 @@ export class ChannelState< | |
>['messageToImmutable'] | ||
> | ||
>, | ||
newMessage: ReturnType< | ||
message: ReturnType< | ||
ChannelState< | ||
AttachmentType, | ||
ChannelType, | ||
|
@@ -494,36 +457,61 @@ export class ChannelState< | |
UserType | ||
>['messageToImmutable'] | ||
>, | ||
timestampChanged = false, | ||
) { | ||
let updated = false; | ||
let newMessages: Immutable.ImmutableArray<ReturnType< | ||
ChannelState< | ||
AttachmentType, | ||
ChannelType, | ||
CommandType, | ||
EventType, | ||
MessageType, | ||
ReactionType, | ||
UserType | ||
>['messageToImmutable'] | ||
>> = Immutable([]); | ||
let messageArr = messages; | ||
|
||
for (let i = 0; i < messages.length; i++) { | ||
const message = messages[i]; | ||
const idMatch = !!message.id && !!newMessage.id && message.id === newMessage.id; | ||
// if created_at has changed, message should be filtered and re-inserted in correct order | ||
// slow op but usually this only happens for a message inserted to state before actual response with correct timestamp | ||
if (timestampChanged) { | ||
messageArr = messageArr.filter((msg) => !(msg.id && message.id === msg.id)); | ||
} | ||
|
||
if (idMatch) { | ||
// @ts-expect-error - ImmutableArray.set exists in the documentation but not in the DefinitelyTyped types | ||
newMessages = messages.set(i, newMessage); | ||
updated = true; | ||
} | ||
// for empty list just concat and return | ||
if (messageArr.length === 0) return messageArr.concat(message); | ||
|
||
const messageTime = message.created_at.getTime(); | ||
|
||
// if message is newer than last item in the list concat and return | ||
if (messageArr[messageArr.length - 1].created_at.getTime() < messageTime) | ||
return messageArr.concat(message); | ||
|
||
// find the closest index to push the new message | ||
let left = 0; | ||
let middle = 0; | ||
let right = messageArr.length - 1; | ||
while (left <= right) { | ||
middle = Math.floor((right + left) / 2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In most cases wouldn't a bottom up approach be faster than a binary search given the likelihood of a message being further up is less? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. L462 takes care of that. cases like pagination could add some 100 more messages to the other end of the array which will be O(n), right now this part is O(log(n)) which is quite fast already |
||
if (messageArr[middle].created_at.getTime() <= messageTime) left = middle + 1; | ||
else right = middle - 1; | ||
} | ||
|
||
if (!updated) { | ||
newMessages = messages.concat([newMessage]); | ||
// message already exists and not filtered due to timestampChanged, update and return | ||
if (!timestampChanged && message.id) { | ||
if (messageArr[left] && message.id === messageArr[left].id) | ||
// @ts-expect-error - ImmutableArray.set exists in the documentation but not in the DefinitelyTyped types | ||
return messageArr.set(left, message); | ||
|
||
if (messageArr[left - 1] && message.id === messageArr[left - 1].id) | ||
// @ts-expect-error - ImmutableArray.set exists in the documentation but not in the DefinitelyTyped types | ||
return messageArr.set(left - 1, message); | ||
} | ||
|
||
return newMessages; | ||
const mutable = messageArr.asMutable() as Array< | ||
ReturnType< | ||
ChannelState< | ||
AttachmentType, | ||
ChannelType, | ||
CommandType, | ||
EventType, | ||
MessageType, | ||
ReactionType, | ||
UserType | ||
>['messageToImmutable'] | ||
> | ||
>; | ||
mutable.splice(left, 0, message); | ||
return Immutable(mutable); | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having options an object with keys would help for readability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 params are fine, in case it grows we can change it to object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree in general but when they are booleans, it's trickier.