PERF: cook message in background (#24227)

This commit starts from a simple observation: cooking messages on the hot path can be slow. Especially with a lot of mentions.

To move cooking from the hot path, this commit has made the following changes:

- updating cooked, inserting mentions and notifying user of new mentions has been moved inside the `process_message` job. It happens right after the `Chat::MessageProcessor` run, which is where the cooking happens.
- the similar existing code in `rebake!` has also been moved to rely on the `process_message`job only
- refactored `create_mentions` and `update_mentions` into one single `upsert_mentions` which can be called invariably
- allows services to decide if their job is ran inline or later. It avoids to need to know you have to use `Jobs.run_immediately!` in this case, in tests it will be inline per default
- made various frontend changes to make the chat-channel component lifecycle clearer. we had to handle `did-update @channel` which was super awkward and creating bugs with listeners which the changes of the PR made clear in failing specs
- adds a new `-processed` (and `-not-processed`) class on the chat message, this is made to have a good lifecyle hook in system specs
This commit is contained in:
Joffrey JAFFEUX
2023-11-06 15:45:30 +01:00
committed by GitHub
parent 4425e99bf9
commit 90efdd7f9d
46 changed files with 388 additions and 394 deletions

View File

@ -8,9 +8,7 @@
{{did-insert this.setUploadDropZone}}
{{did-insert this.setupListeners}}
{{will-destroy this.teardownListeners}}
{{did-update this.loadMessages @targetMessageId}}
{{did-insert this.didUpdateChannel}}
{{did-update this.didUpdateChannel @channel.id}}
{{did-insert this.addAutoFocusEventListener}}
{{will-destroy this.removeAutoFocusEventListener}}
data-id={{@channel.id}}

View File

@ -61,7 +61,6 @@ export default class ChatChannel extends Component {
@tracked isScrolling = false;
scrollable = null;
_loadedChannelId = null;
_mentionWarningsSeen = {};
_unreachableGroupMentions = [];
_overMembersLimitGroupMentions = [];
@ -98,7 +97,7 @@ export default class ChatChannel extends Component {
teardownListeners() {
this.#cancelHandlers();
removeOnPresenceChange(this.onPresenceChangeCallback);
this.unsubscribeToUpdates(this._loadedChannelId);
this.unsubscribeToUpdates(this.args.channel.id);
}
@action
@ -109,12 +108,6 @@ export default class ChatChannel extends Component {
@action
didUpdateChannel() {
this.#cancelHandlers();
if (!this.args.channel) {
return;
}
this.messagesManager.clear();
if (
@ -124,12 +117,6 @@ export default class ChatChannel extends Component {
this.chatChannelsManager.follow(this.args.channel);
}
if (this._loadedChannelId !== this.args.channel.id) {
this.unsubscribeToUpdates(this._loadedChannelId);
this.pane.selectingMessages = false;
this._loadedChannelId = this.args.channel.id;
}
const existingDraft = this.chatDraftsManager.get({
channelId: this.args.channel.id,
});
@ -647,7 +634,6 @@ export default class ChatChannel extends Component {
return;
}
this.unsubscribeToUpdates(channel.id);
this.messageBus.subscribe(
`/chat/${channel.id}`,
this.onMessage,

View File

@ -1,6 +1,6 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { cancel, throttle } from "@ember/runloop";
import { cancel, next, throttle } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import DiscourseURL from "discourse/lib/url";
@ -188,11 +188,13 @@ export default Component.extend({
},
@action
openInFullPage() {
async openInFullPage() {
this.chatStateManager.storeAppURL();
this.chatStateManager.prefersFullPage();
this.chat.activeChannel = null;
await new Promise((resolve) => next(resolve));
return DiscourseURL.routeTo(this.chatStateManager.lastKnownChatURL);
},

View File

@ -16,10 +16,12 @@
{{did-update this.fetchChannel @params.channelId}}
>
{{#if this.chat.activeChannel}}
<ChatChannel
@targetMessageId={{readonly @params.messageId}}
@channel={{this.chat.activeChannel}}
/>
{{#each (array this.chat.activeChannel) as |channel|}}
<ChatChannel
@targetMessageId={{readonly @params.messageId}}
@channel={{channel}}
/>
{{/each}}
{{/if}}
</div>
{{/if}}

View File

@ -509,6 +509,7 @@ export default class ChatMessage extends Component {
(if @message.highlighted "-highlighted")
(if (eq @message.user.id this.currentUser.id) "is-by-current-user")
(if @message.staged "-staged" "-persisted")
(if @message.processed "-processed" "-not-processed")
(if this.hasActiveState "-active")
(if @message.bookmark "-bookmarked")
(if @message.deletedAt "-deleted")

View File

@ -1,3 +1,3 @@
{{#if @channel.id}}
<ChatChannel @channel={{@channel}} @targetMessageId={{@targetMessageId}} />
{{/if}}
{{#each (array @channel) as |channel|}}
<ChatChannel @channel={{channel}} @targetMessageId={{@targetMessageId}} />
{{/each}}

View File

@ -192,6 +192,7 @@ export default class ChatChannel {
async stageMessage(message) {
message.id = guid();
message.staged = true;
message.processed = false;
message.draft = false;
message.createdAt = new Date();
message.channel = this;

View File

@ -26,6 +26,7 @@ export default class ChatMessage {
@tracked selected;
@tracked channel;
@tracked staged;
@tracked processed;
@tracked draftSaved;
@tracked draft;
@tracked createdAt;
@ -64,6 +65,7 @@ export default class ChatMessage {
this.draftSaved = args.draftSaved || args.draft_saved || false;
this.firstOfResults = args.firstOfResults || args.first_of_results || false;
this.staged = args.staged || false;
this.processed = args.processed || true;
this.edited = args.edited || false;
this.editing = args.editing || false;
this.availableFlags = args.availableFlags || args.available_flags;

View File

@ -61,6 +61,7 @@ export default class ChatThread {
async stageMessage(message) {
message.id = guid();
message.staged = true;
message.processed = false;
message.draft = false;
message.createdAt ??= moment.utc().format();
message.thread = this;

View File

@ -16,7 +16,6 @@ export function handleStagedMessage(channel, messagesManager, data) {
stagedMessage.excerpt = data.chat_message.excerpt;
stagedMessage.channel = channel;
stagedMessage.createdAt = new Date(data.chat_message.created_at);
stagedMessage.cooked = data.chat_message.cooked;
return stagedMessage;
}
@ -131,6 +130,7 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) {
message.cooked = data.chat_message.cooked;
message.processed = true;
}
}
@ -144,8 +144,6 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
handleEditMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) {
message.message = data.chat_message.message;
message.cooked = data.chat_message.cooked;
message.excerpt = data.chat_message.excerpt;
message.uploads = cloneJSON(data.chat_message.uploads || []);
message.edited = data.chat_message.edited;