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

message_list: Don't always cache "All messages" view. #29740

Merged
merged 1 commit into from Apr 25, 2024

Conversation

amanagr
Copy link
Member

@amanagr amanagr commented Apr 15, 2024

Improved version of #29259

Discussion: https://chat.zulip.org/#narrow/stream/6-frontend/topic/fetching.20message.20history

TODO: Fix node tests.

Important changes in this commit:

  • We only cache message list for "All messages" if it is the default view.

  • We modify existing handling of home message list code so that it can be used to for any message list that we want to cache using a new preserve_rendered_state variable.

  • narrow_state.filter() is return the filter for all messages list instead of undefined.

  • We start fetching messages from the latest message on app load.

  • Messages in all messages view and Recent view are always synced.

  • If all messages view list is not cached, we don't track it's last pointer, effectively sending user to the latest unread message always .

web/src/message_edit.js Outdated Show resolved Hide resolved
web/src/message_fetch.js Outdated Show resolved Hide resolved
@@ -72,6 +69,10 @@ export class MessageList {
// the user. Possibly this can be unified in some nice way.
this.reading_prevented = false;

// Used to determine whether to preserve the rendered state
// when this message list is not the current message list.
this.preserve_rendered_state = user_settings.web_home_view === "all_messages" && !this.narrowed;
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth extending the comment to mention that this doesn't get live-updated when the setting changes, but that's probably OK, as it's just an optimization?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I forgot to add a comment here. I was thinking if user had all_messages as the default view in the current session, then it doesn't make sense for us to no longer have it's rendered state be present if they changed the default view. Since, user likely still uses the combined feed view and the faster rendering of combined feed view could be a better experience if we have already done the work of keeping it updated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can update preserve_rendered_state in event handlers code if you think otherwise.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think the current logic is good, we should just make sure the subtlety is documented.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tweaked the comment a bit:

diff --git a/web/src/message_list.js b/web/src/message_list.js
index f8eac445dc..2f5fd55b8e 100644
--- a/web/src/message_list.js
+++ b/web/src/message_list.js
@@ -68,11 +68,14 @@ export class MessageList {
         // the user. Possibly this can be unified in some nice way.
         this.reading_prevented = false;
 
-        // Used to determine whether to preserve the rendered state
-        // when this message list is not the current message list.
-        // This is intentionally not live updated right now since user
-        // likely is a frequent user of the combined feed view if they
-        // had combined feed set as their default view in the current session.
+        // Whether this message list's is preserved in the DOM even
+        // when viewing other views -- a valuable optimization for
+        // fast toggling between the combined feed and other views,
+        // which we enable only when that is the user's home view.
+        //
+        // This is intentionally not live-updated when web_home_view
+        // changes, since it's easier to reason about if this
+        // optimization is active or not for an entire session.
         this.preserve_rendered_state =
             user_settings.web_home_view === "all_messages" && !this.narrowed;
 

web/src/message_fetch.js Outdated Show resolved Hide resolved
web/src/message_fetch.js Outdated Show resolved Hide resolved
@@ -112,7 +104,7 @@ export function update_recipient_bar_background_color(): void {
}

export function save_pre_narrow_offset_for_reload(): void {
if (current === undefined) {
if (!current?.preserve_rendered_state) {
return;
}
Copy link
Sponsor Member

@timabbott timabbott Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't obviously equivalent...

I guess the old logic would return if the current view was inbox/recent, or if was "all messages".

The logic returns if the current view is inbox/recent (current == undefined), or if preserve_rendered_state is false, i.e. not "all messages"? I suspect this is misconverted.

But it does look correct for the caller to no longer try to check narrow_state.has_shown_message_list_view, since it appears that this will noop in the event that no such view has been shown. (I guess that wasn't true in a previous code base state -- we'd have current = home when loading the app to inbox before current === undefined was made a possible state, right?). Could potentially do the narrow_state.has_shown_message_list_view removal in a prep commit, but this feels readable enough as written.

Copy link
Member Author

@amanagr amanagr Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this comment there:

    // Only save the pre_narrow_offset if the message list will be cached if user
    // switches to a different narrow, otherwise the pre_narrow_offset would just be lost when
    // user switches to a different narrow. In case of a reload, offset for the current
    // message is captured and restored by `reload` library.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm still a bit confused. Reading c4bb181 and 93ce1fa, I feel like either this function is badly named or it's not doing what I'd thinkg. Is for_reload about "for when we load this view again", not for reloading the browser?

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to leave further cleanup on this for a follow-up; we should definitely try to clean it up to make sense, but it's probably a very subtle detail, so I don't think it's a blocker for merging.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #29871

web/src/narrow.js Outdated Show resolved Hide resolved
web/src/server_events.js Outdated Show resolved Hide resolved
// Used to determine whether to preserve the rendered state
// when this message list is not the current message list.
this.preserve_rendered_state = user_settings.web_home_view === "all_messages" && !this.narrowed;

return this;
}

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there's one block of logic that I'm not whether it needs attention:

    update_muting_and_rerender() {                                                                      
        // For the home message list, we need to re-initialize                                          
        // _all_items for stream muting/topic unmuting from                                             
        // all_messages_data, since otherwise unmuting a previously                                     
        // muted stream won't work.                                                                     
        //                                                                                              
        // "in-home" filter doesn't included muted stream messages, so we                               
        // need to repopulate the message list with all messages to include                             
        // the previous messages in muted streams so that update_items_for_muting works.                
        if (this.data.filter.is_in_home()) {                                                            
            this.data.clear();                                                                          
            this.data.add_messages(all_messages_data.all_messages());                                   
        }                                                                                               

I'm not sure that hack still works correctly with the message_lists.home logic no longer being deeply tied to all_messages_data in the message_fetch logic -- it seems like that could very easily result in your not having the currently selected message in view, if that's not present in all_messages_data.

It's a messy problem -- we had this performance-favorable model previously that we maintain the full set of messages in all_messages_data, and thus always have the ability the rework the data if muting state changes -- but now I think if you scroll up in the view, we'll ask the server for data that we don't end up storing in all_messages_data. Hrm.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some version of this problem in theory applies to any search view with -is:muted among the filters; but those other views are going to be rare search views where people don't totally expect live-update. (And there's plenty of precedent: The has:image search view doesn't live update when someone edits a message to now have an image, and that seems totally OK).

I think there's basically two possible solutions:

  • In an unmuting or message-moving event causes some messages to (possibly) now match the combined feed view, and it's cached, we ask the server for full copies of any messages we didn't have copies of locally, a la maybe_add_narrowed_messages, as a deferred live-update step.
  • Another theory, which I'm not at all sure is a good idea, would be that we arrange that the cacheable filter.is_in_home view actually uses the real all_messages_data as its .message_list_data`? Since they're both singletons, I think it'd be workable ... but seems messy.

But it would require some preparatory refactoring; this function at least accesses .items, not .all_items on the all_messages_data object:

function load_local_messages(msg_data) {                                                                
    // This little helper loads messages into our narrow message                                        
    // data and returns true unless it's visibly empty.  We use this for                                
    // cases when our local cache (all_messages_data) has at least                                      
    // one message the user will expect to see in the new narrow.                                       
                                                                                                        
    const in_msgs = all_messages_data.all_messages();                                                   
    msg_data.add_messages(in_msgs);                                                                     
                                                                                                        
    return !msg_data.visibly_empty();                                                                   
}                                                                                                       

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do fetch the muted data for the combined feed view as we have always done. See load_messages. Modified this comment:

        // Otherwise, we don't pass narrow for the combined feed view; this is
        // required to display messages if their muted status changes without a new
        // network request, and so we need the server to send us message history from muted
        // streams and topics even though the combined feed view's in:home
        // operators will filter those.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, removed this:

        // For the home message list, we need to re-initialize                                          
        // _all_items for stream muting/topic unmuting from                                             
        // all_messages_data, since otherwise unmuting a previously                                     
        // muted stream won't work.                                                                     
        //                                                                                              
        // "in-home" filter doesn't included muted stream messages, so we                               
        // need to repopulate the message list with all messages to include                             
        // the previous messages in muted streams so that update_items_for_muting works.                
        if (this.data.filter.is_in_home()) {                                                            
            this.data.clear();                                                                          
            this.data.add_messages(all_messages_data.all_messages());                                   
        }                                                                                               

Since could actually lead to a bug where the combined feed view has discontinued messages since all messages data is anchored to latest message and the combined feed view is anchored to the first unread message.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so to be concrete, I think the thing that's worse/regressed is that if you have no unreads and go to the "All messages" view, and just scroll up, you'll do all these fetches from the server that in theory could extend the set of cached messages in all_messages_data (as we do with the "load more" button in the Recent Conversations view), but we will no longer do so.

I think that's probably OK to take no action on for this PR. Opened #29841 to track this.

@timabbott
Copy link
Sponsor Member

This looks like a great direction! I posted a few dozen comments, including identifying a few small prep PRs that could shrink this a bit. But I think #29740 (comment) is the main complex issue that might require significant reworking that I've noted in reading this.

@amanagr
Copy link
Member Author

amanagr commented Apr 18, 2024

This reduces rendering of all messages view from 7s to 3s when it is the default view on 6x CPU slowdown.

@amanagr
Copy link
Member Author

amanagr commented Apr 21, 2024

Looks like we will have to rewrite the tests for the message_fetch code path.

// If adding some new messages to the message tables caused
// our current narrow to no longer be empty, hide the empty
// feed placeholder text.
narrow_banner.hide_empty_narrow_message();
}

if (this.narrowed && !this.visibly_empty() && this.selected_id() === -1) {
if (!this.visibly_empty() && this.selected_id() === -1) {
// The message list was previously empty, but now isn't
// due to adding these messages, and we need to select a
// message. Regardless of whether the messages are new or
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these checks might still need a conditional on whether the view is currently rendered instead of this.narrowed; if it isn't, then selecting a message or hiding the banner will be applied based on the properties of an invisible background view, and that seems buggy? So maybe we need this === message_lists.current, effectively?

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to just have an explicit this.is_current_message_list boolean on this object that we can check here; it'd likely be easy for narrow.ts logic to maintain that, and would avoid needing a messy import of some sort of access message_lists.current.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a commit for this in #29871.

Important changes in this commit:
* We only cache message list for "Combined feed" if it is the
  default view.

* We modify existing handling of home message list code so that
  it can be used to for any message list that we want to cache
  using a new `preserve_rendered_state` variable.

* narrow_state.filter() returns the filter of combined feed view  list
  instead of `undefined`.

* We start fetching messages from the latest message on app load.

* Messages in all messages data and Recent view are always synced.

* If combined feed view list is not cached, we don't track it's
  last pointer, effectively sending user to the latest unread
  message always .
@timabbott
Copy link
Sponsor Member

timabbott commented Apr 25, 2024

I think I'm good to merge this. Follow-ups would be:

Merged, huge thanks for all the work on this! I expect we'll be able to see the impact in our graphs for the next deployment.

@timabbott timabbott merged commit 103c37f into zulip:main Apr 25, 2024
4 of 6 checks passed
@amanagr amanagr deleted the narrow_work branch April 27, 2024 04:38
amanagr added a commit to amanagr/zulip that referenced this pull request May 17, 2024
If the initial fetch pre zulip#29740 fetches all muted messages we don't
have any messages in all message list and hence query for
oldest message is undefined.

This results in us trying to render oldest_message_timestamp
with its value as infinity.

So, to fix it, we just wait for our other initial fetch of recent
view to return us the messages we need.
amanagr added a commit to amanagr/zulip that referenced this pull request May 17, 2024
If the initial fetch pre zulip#29740 fetches all muted messages we don't
have any messages in all message list and hence query for
oldest message is undefined.

This results in us trying to render oldest_message_timestamp
with its value as infinity.

So, to fix it, we just wait for our other initial fetch of recent
view to return us the messages we need.
amanagr added a commit to amanagr/zulip that referenced this pull request May 19, 2024
If the initial fetch pre zulip#29740 fetches all muted messages we don't
have any messages in all message list and hence query for
oldest message is undefined.

This results in us trying to render oldest_message_timestamp
with its value as infinity.

To fix it, we include muted messages in our search for oldest
message in the list and if we still cannot find one, we wait
for the next fetch.
amanagr added a commit to amanagr/zulip that referenced this pull request May 19, 2024
If the initial fetch pre zulip#29740 fetches all muted messages we don't
have any messages in all message list and hence query for
oldest message is undefined.

This results in us trying to render oldest_message_timestamp
with its value as infinity.

To fix it, we include muted messages in our search for oldest
message in the list and if we still cannot find one, we wait
for the next fetch.
timabbott pushed a commit to amanagr/zulip that referenced this pull request May 20, 2024
If the initial fetch pre zulip#29740 fetches all muted messages we don't
have any messages in all message list and hence query for
oldest message is undefined.

This results in us trying to render oldest_message_timestamp
with its value as infinity.

To fix it, we include muted messages in our search for oldest
message in the list and if we still cannot find one, we wait
for the next fetch.
timabbott pushed a commit that referenced this pull request May 20, 2024
If the initial fetch pre #29740 fetches all muted messages we don't
have any messages in all message list and hence query for
oldest message is undefined.

This results in us trying to render oldest_message_timestamp
with its value as infinity.

To fix it, we include muted messages in our search for oldest
message in the list and if we still cannot find one, we wait
for the next fetch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants