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

Improve performance of the list of chat messages #1271

Merged
merged 15 commits into from
Nov 20, 2018

Conversation

danxuliu
Copy link
Member

@danxuliu danxuliu commented Oct 26, 2018

Requires #1302

It seemed that this day was never going to come, but finally, here it is :-D
🎆 🎆 🎆 🎆 🎆 This pull request is ready for review!!! 🎆 🎆 🎆 🎆 🎆

Each time a new element or a group of elements is added to the document the browser needs to layout it (calculate its position and height), but if the elements use a static or relative positioning (they are part of the flow of the document) it needs to reflow the document too (layout other existing elements again that could be affected or affect the new elements).

When joining a room all the chat messages in the room were loaded and added one by one to the document. Each time a new message was added to the list all the other messages already in the list had to be layout again, and thus the document was reflown again and again each time with a higher number of elements.

Instead of adding each message on its own when several of them were received an initial improvement is to add them all at once. This way, instead of one reflow for each element only one reflow for the whole set is needed. However, this only helps up to some point; once the number of elements in the list is very high even a single reflow takes a lot of time, so not only the initial load, but also regular chat is sluggish.

The only way to improve the performance is thus limit the number of elements in the list. The virtual list does that while giving the user the impression that the list contains all the messages. How? By keeping in the document only the elements that are actually visible, removing those that are out of view, and updating them as needed when the user scrolls the list. For more information please refer to the documentation in the code itself ;-)

Of course, despite its benefits, this approach also comes with its own issues :-)

  • As the elements in the virtual list are removed when they are no longer visible, the selected text in a message disappears when the message is scrolled out of view. It could be probably solved by not removing the elements during a scroll if they are selected, but for the time being it is a limitation of this new implementation; I will try to solve that later in a different pull request.
  • Firefox has subpixel accuracy for the position and size of the elements, but surprisingly the scroll position is handled as an integer. Due to this it is not possible to keep exactly the same scroll position when elements are prepended to the list, and several prepends in a row (for example, when the list is reloaded) cause a wiggly effect and a small drift from the proper scroll position. Unfortunately I do not know how to solve that, although it is only a nuisance more than a problem.
  • It happened to me that when scrolling the list by keeping a key pressed Chromium stopped sending scroll events... and then when key was released sent them all of a sudden. I would say that it was just a strange (and not reproducible) Chromium bug more than a problem with the virtual list itself.
  • Scrolling while the list is being reloaded in the responsive view of Chromium shows the wrong elements. The problem seems to be that when the scroll even is handled calling scrollTop() returns the scroll position when the event was triggered instead of the current one (as done in the normal view of Chromium or in Firefox), so when the visible elements are updated the wrong ones end being shown. I do not know how that could be fixed, but it should not be a very big problem, as once the reload ends the scrolling shows the right elements, and again it actually looks like a bug in Chromium, and only in its responsive view.

I have developed and tested the virtual list with the desktop versions of Firefox and Chromium; tests with the mobile versions are appreciated ;-) (And by the way, adding so much code without unit, integration or acceptance tests make me dizzy :-P ).

How to test (setup):
To notice the performance improvement it is necessary to use a room with a large number of messages. A naive way to get a room with a large number of messages would be to modify the ChatController to include the following code before $creationDateTime = new \DateTime('now', new \DateTimeZone('UTC')); in sendMessage:

	$numberOfMessages = 10000;
	$baseTime = new \DateTime('now', new \DateTimeZone('UTC'));
	$baseTimeTimestamp = $baseTime->getTimestamp() - $numberOfMessages;
	$currentMessage = 1;
	for ($i=0; $i<$numberOfMessages; $i++) {

			$creationDateTime = new \DateTime();
			$creationDateTime->setTimestamp($baseTimeTimestamp + $i);

			try {
					$comment = $this->chatManager->sendMessage($room, $actorType, $actorId, (string)$currentMessage, $creationDateTime);
			} catch (MessageTooLongException $e) {
					return new DataResponse([], Http::STATUS_REQUEST_ENTITY_TOO_LARGE);
			} catch (\Exception $e) {
					return new DataResponse([], Http::STATUS_BAD_REQUEST);
			}
			$currentMessage++;
	}

With that code, whenever a new message is sent to the controller (simply typing in the UI, for example) 10000 extra messages would be added too to that room.

That code should be used only during the setup of the room; for the actual test of the virtual list it should be removed, as otherwise even with a virtual list sending a new message would take time.

How to test:

  • Enter a room with a large number of messages
  • Send new messages
  • Resize the window
  • Scroll the list

Result before this pull request:
Go for a walk while the messages are loaded when entering a room and hope that the browser has not crashed when you return; make some stretches whenever a new message is added or the window is resized; wait a little when scrolling the list

Result after this pull request:
Make some stretches while the messages are loaded when entering a room*; enjoy an immediate update whenever a new message is added, the window is resized or when scrolling the list

*When entering the room the browser becomes slightly unresponsive; this is mainly caused by the continuous load of messages by the model without any throttling, but this was not fixed because it will no longer happen once the messages are loaded on request in a later pull request.

@danxuliu danxuliu force-pushed the improve-performance-of-the-list-of-chat-messages branch 6 times, most recently from 1d21ebf to 3d3a470 Compare November 20, 2018 09:52
@danxuliu danxuliu added this to the 💚 Next Major milestone Nov 20, 2018
js/views/chatview.js Outdated Show resolved Hide resolved
Currently models are only appended, so there is no need to handle an
insertion. Moreover, it is not handled anywhere else, so even if a model
was inserted the result would be broken.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
The calculations to get the scroll position were based on both the
previous newest comment and on the current newest comment just added.
However, as the current newest comment is the one that needs to be fully
shown the calculations can be solved using only that element.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This avoids iterating over all the comments each time a new one is
added.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Each time a new node is added to the document a reflow and a render is
triggered. This can cause the UI to become unresponsive when a lot of
nodes are added in a row, for example, when loading the messages for the
first time after entering a room.

To mitigate that now the DOM elements for the messages to add are
buffered using a document fragment. Instead of adding the element for
each model directly to the document now they are added to the document
fragment, which does not cause a reflow nor a render, and once all the
elements have been created then they are added at once to the document,
which causes a single reflow and render instead of one for each element.

Despite that, note that when there are thousands of messages in the list
even a single reflow and render can make the UI hang for a few seconds;
using a virtual list is probably the only way to solve this.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Despite the browser optimizations there is a limit in the number of
elements that can be added to a document before the browser becomes
sluggish when the document is further modified.

This problem can be clearly seen in the list of chat messages with long
conversations; adding several messages to the document all at once
instead of one by one can improve the performance of the initial load,
but when the number of messages grows too much any other addition causes
the browser to become unresponsive. In this case adding several messages
at once (which may not be possible either during a regular chat) does
not help, because the real culprit here is that the browser needs to
layout again all the previous messages.

To solve that problem this commit introduces an initial implementation
of a virtual list; the virtual list keeps in the document only those
elements that are currently visible and refreshes them as needed when
the list is scrolled.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Before the 3.0.0 release jQuery rounded the height to the nearest
integer, so the height returned by jQuery is not the correct value in
browsers with subpixel accuracy (positions and sizes with decimal
values), like Firefox. Due to this all the height related functions of
jQuery need to be replaced with custom versions that return the height
as a float instead of an integer.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Although similar, prepending elements is not just a "mirrored" append;
besides caching the position and size of the added elements it is also
necessary to update the position of the rest of the elements, as it will
have have changed due to the prepended elements.

However, it is not necessary to show those elements to update their
position; it is assumed that the existing elements will not be modified,
so their position can be updated based on the height of the prepended
elements (which needs to account for a possible collapse of the margins
between the first existing element and the first prepended element).

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
When the size of the container changes the position and size of all the
elements in the list could change. The virtual list relies on the cached
values for those properties, so they must be cached again when they
change.

The values need to be cached from elements in the document, but it is
not possible to add all the elements at once, even if temporary, to
cache their values. Thus the reload is an incremental process that
starts with the visible elements and progressively updates the rest of
elements; during that process the list can be scrolled only to those
elements already loaded, as those are the only ones with valid cached
values.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
The virtual list requires that its internal wrappers use an absolute
position. Due to that absolute position the padding of the container
does not affect the wrappers, so the desired padding must be applied
through its left and right position.

As the virtual list keeps only a subset of its elements in the DOM the
":first-child" pseudo-selector no longer refers to the actual first
child element, but to the first one currently in the DOM; it would be
necessary to apply the CSS rules using a specific CSS class set only in
the desired element. However, as the first comment always includes the
date separator, which already has a top margin, the top padding is not
really needed in the first comment, so it was simply removed.

Moving the message list between the main view and the sidebar changes
its size, and thus it is necessary to reload the virtual list; when the
virtual list is reloaded it is ensured that the last visible element
will still be visible after the reload, so the chat view no longer needs
to explicitly handle that.

In a similar way, the message list also needs to be reloaded when the
window is resized, or when the chat view is in the main view and the
sidebar is opened or closed, as those actions change the size of the
main view.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Firefox automatically includes scrollable elements in the sequential
keyboard navigation, but it needs to be explicitly done for Chromium.

Moreover, setting "tabindex" is also needed in Chromium to be able to
scroll by keeping a key pressed after focusing on the scrollable element
by clicking on one of its children; in Firefox the event keeps being
processed and the scrolling goes on until the key is released, but in
Chromium, when "tabindex" is not set, the scrolling stops as soon as the
child element is removed (which in the case of the virtual list happens
when the element goes out of view).

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
As the virtual list removes its child elements that are no longer
visible the index of the messages in the acceptance test is no longer an
absolute index for the whole list, but an index only for the currently
visible messages. However, as all the messages sent in each test fit in
the available space no message is hidden and the previous indexes are
still valid.

The only change needed is in the locator for chat messages; as they are
declared as direct children of their parent now they must be set as
descendants of the wrapper instead of as descendants of the message list
itself.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
@danxuliu danxuliu force-pushed the improve-performance-of-the-list-of-chat-messages branch from 3d3a470 to 31e31a6 Compare November 20, 2018 11:18
Copy link
Member

@nickvergessen nickvergessen left a comment

Choose a reason for hiding this comment

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

Works 🐘

@nickvergessen
Copy link
Member

No backport on this one 🙊

@nickvergessen nickvergessen merged commit 902ec4a into master Nov 20, 2018
@nickvergessen nickvergessen deleted the improve-performance-of-the-list-of-chat-messages branch November 20, 2018 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants