Skip to content
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

Merged
merged 10 commits into from
Oct 20, 2020
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- name: Lint
run: yarn lint

- name: Unit tests
run: yarn run test-unit

test-types:
runs-on: ubuntu-latest
strategy:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"test-types": "node test/typescript/index.js && tsc --esModuleInterop true --noEmit true --strictNullChecks true --noImplicitAny true --strict true test/typescript/*.ts",
"eslint": "eslint '**/*.{js,md,ts}' --max-warnings 0 --ignore-path ./.eslintignore",
"eslint-fix": "npx eslint --fix '**/*.{js,md,ts}' --max-warnings 0 --ignore-path ./.eslintignore",
"test-unit": "NODE_ENV=test mocha --exit --bail --timeout 3000 --require ./babel-register test/unit/*.js",
"test": "NODE_ENV=test mocha --exit --bail --timeout 15000 --require ./babel-register test/integration/*.js --async-stack-traces",
"test-local": "STREAM_LOCAL_TEST_RUN=true yarn test",
"testall": "NODE_ENV=test mocha --exit --timeout 3000 --require ./babel-register test/integration/*.js --async-stack-traces",
Expand Down
2 changes: 1 addition & 1 deletion src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1658,7 +1658,7 @@ export class Channel<
if (!this.state.messages) {
this.state.messages = Immutable([]);
}
this.state.addMessagesSorted(messages, true);
this.state.addMessagesSorted(messages, false, true);
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

this.state.watcher_count = state.watcher_count ? state.watcher_count : 0;
// convert the arrays into objects for easier syncing...
if (state.watchers) {
Expand Down
150 changes: 69 additions & 81 deletions src/channel_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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(
Expand All @@ -178,8 +168,9 @@ export class ChannelState<
ReactionType,
UserType
>,
timestampChanged = false,
) {
return this.addMessagesSorted([newMessage]);
return this.addMessagesSorted([newMessage], timestampChanged);
}

/**
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -483,7 +446,7 @@ export class ChannelState<
>['messageToImmutable']
>
>,
newMessage: ReturnType<
message: ReturnType<
ChannelState<
AttachmentType,
ChannelType,
Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
}

/**
Expand Down
Loading