Batch rendering in chat
Instead of re-rendering the chat every single time a message comes in,
I've made it so that messages are batche and then rendered all at once
every 200ms.
As a result, the amount of renders are cut from hundreds per second to
just 5. The same technique is used in native Twitch chat.
A side effect is that chat is now a little more readable. Instead of
every single message flying in, they're now grouped with much less
jumps.
Message limit has also been increased to 10000 to allow for greater chat
with minimal performance cost.
Also reverts 717d989.
This commit is contained in:
parent
2ab0d631b1
commit
334f353af1
3 changed files with 53 additions and 48 deletions
|
|
@ -35,17 +35,16 @@ class Chat extends StatelessWidget {
|
|||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return ListView(
|
||||
return ListView.builder(
|
||||
reverse: true,
|
||||
padding: EdgeInsets.zero,
|
||||
addAutomaticKeepAlives: false,
|
||||
controller: chatStore.scrollController,
|
||||
children: chatStore.renderMessages.reversed
|
||||
.map((e) => ChatMessage(
|
||||
ircMessage: e,
|
||||
chatStore: chatStore,
|
||||
))
|
||||
.toList(),
|
||||
itemCount: chatStore.renderMessages.length,
|
||||
itemBuilder: (context, index) => ChatMessage(
|
||||
ircMessage: chatStore.renderMessages.reversed.toList()[index],
|
||||
chatStore: chatStore,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@ part 'chat_store.g.dart';
|
|||
class ChatStore = ChatStoreBase with _$ChatStore;
|
||||
|
||||
abstract class ChatStoreBase with Store {
|
||||
static const _messageLimit = 5000;
|
||||
static const _renderMessageLimit = 88;
|
||||
/// The total maximum amount of messages in chat.
|
||||
static const _messageLimit = 10000;
|
||||
|
||||
/// The maximum ammount of messages to render when autoscroll is enabled.
|
||||
static const _renderMessageLimit = 100;
|
||||
|
||||
/// The provided auth store to determine login status, get the token, and use the headers for requests.
|
||||
final AuthStore auth;
|
||||
|
|
@ -71,19 +74,22 @@ abstract class ChatStoreBase with Store {
|
|||
/// The list of chat messages to render and display.
|
||||
final messages = ObservableList<IRCMessage>();
|
||||
|
||||
/// The list of reaction disposer functions that will be used later when disposing.
|
||||
final reactions = <ReactionDisposer>[];
|
||||
|
||||
/// The periodic timer used for batching chat message re-renders.
|
||||
late final Timer _messageBufferTimer;
|
||||
|
||||
@computed
|
||||
List<IRCMessage> get renderMessages {
|
||||
// If autoscroll is disabled, render ALL messages in chat.
|
||||
// The second condition is to prevent an out of index error with sublist.
|
||||
//
|
||||
// Else, when autoscroll is enabled, only show the first [_renderMessageLimit] messages.
|
||||
if (!_autoScroll || messages.length < _renderMessageLimit) return messages;
|
||||
|
||||
// When autoscroll is enabled, only show the first [_renderMessageLimit] messages.
|
||||
// This will improve performance by only rendering a limited amount of messages
|
||||
// instead of the entire history at all times.
|
||||
if (!_autoScroll || messages.length < _renderMessageLimit) {
|
||||
return messages;
|
||||
} else {
|
||||
return messages.sublist(messages.length - _renderMessageLimit);
|
||||
}
|
||||
return messages.sublist(messages.length - _renderMessageLimit);
|
||||
}
|
||||
|
||||
/// If the chat should automatically scroll/jump to the latest message.
|
||||
|
|
@ -106,8 +112,6 @@ abstract class ChatStoreBase with Store {
|
|||
@observable
|
||||
var expandChat = false;
|
||||
|
||||
final reactions = <ReactionDisposer>[];
|
||||
|
||||
ChatStoreBase({
|
||||
required this.auth,
|
||||
required this.chatDetailsStore,
|
||||
|
|
@ -127,25 +131,16 @@ abstract class ChatStoreBase with Store {
|
|||
(_) => _channel?.sink.close(1001),
|
||||
));
|
||||
|
||||
// Create a reaction that will add buffered messages to the chat
|
||||
// when autoscroll is changed.
|
||||
reactions.add(reaction(
|
||||
(_) => _autoScroll,
|
||||
(_) {
|
||||
if (_messageBuffer.isNotEmpty) {
|
||||
messages.addAll(_messageBuffer);
|
||||
_messageBuffer.clear();
|
||||
}
|
||||
},
|
||||
));
|
||||
// Create a timer that will add messages from the buffer every 200 milliseconds.
|
||||
_messageBufferTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) => addMessages());
|
||||
|
||||
assetsStore.init();
|
||||
chatDetailsStore.updateChatters();
|
||||
|
||||
messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
|
||||
if (settings.chatDelay > 0) {
|
||||
messages.add(IRCMessage.createNotice(
|
||||
_messageBuffer.add(IRCMessage.createNotice(
|
||||
message: 'Waiting ${settings.chatDelay.toInt()} ${settings.chatDelay == 1.0 ? 'second' : 'seconds'} due to message delay setting...'));
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +190,7 @@ abstract class ChatStoreBase with Store {
|
|||
// The IRC data can contain more than one message separated by CRLF.
|
||||
// To account for this, split by CRLF, then loop and process each message.
|
||||
for (final message in data.trimRight().split('\r\n')) {
|
||||
debugPrint('$message\n');
|
||||
// debugPrint('$message\n');
|
||||
if (message.startsWith('@')) {
|
||||
final parsedIRCMessage = IRCMessage.fromString(message, userLogin: auth.user.details?.login);
|
||||
|
||||
|
|
@ -208,12 +203,7 @@ abstract class ChatStoreBase with Store {
|
|||
case Command.privateMessage:
|
||||
case Command.notice:
|
||||
case Command.userNotice:
|
||||
if (_autoScroll) {
|
||||
messages.add(parsedIRCMessage);
|
||||
} else {
|
||||
// If audoscroll is disabled, buffer the message to prevent the chat from shifting.
|
||||
_messageBuffer.add(parsedIRCMessage);
|
||||
}
|
||||
_messageBuffer.add(parsedIRCMessage);
|
||||
break;
|
||||
case Command.clearChat:
|
||||
IRCMessage.clearChat(
|
||||
|
|
@ -235,12 +225,7 @@ abstract class ChatStoreBase with Store {
|
|||
case Command.userState:
|
||||
_userState = _userState.fromIRCMessage(parsedIRCMessage);
|
||||
if (toSend != null) {
|
||||
if (_autoScroll) {
|
||||
messages.add(toSend!);
|
||||
} else {
|
||||
// If audoscroll is disabled, buffer the sent message to prevent the chat from shifting.
|
||||
_messageBuffer.add(toSend!);
|
||||
}
|
||||
_messageBuffer.add(toSend!);
|
||||
toSend = null;
|
||||
}
|
||||
break;
|
||||
|
|
@ -268,7 +253,7 @@ abstract class ChatStoreBase with Store {
|
|||
_channel?.sink.add('PONG :tmi.twitch.tv');
|
||||
return;
|
||||
} else if (message.contains('Welcome, GLHF!')) {
|
||||
messages.add(IRCMessage.createNotice(message: "Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!"));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: "Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!"));
|
||||
|
||||
getAssets();
|
||||
|
||||
|
|
@ -323,7 +308,7 @@ abstract class ChatStoreBase with Store {
|
|||
if (_backoffTime > 0) {
|
||||
// Add notice that chat was disconnected and then wait the backoff time before reconnecting.
|
||||
final notice = 'Disconnected from chat, waiting $_backoffTime ${_backoffTime == 1 ? 'second' : 'seconds'} before reconnecting...';
|
||||
messages.add(IRCMessage.createNotice(message: notice));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: notice));
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(seconds: _backoffTime));
|
||||
|
|
@ -333,7 +318,7 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
// Increment the retry count and attempt the reconnect.
|
||||
_retries++;
|
||||
messages.add(IRCMessage.createNotice(message: 'Reconnecting to chat (attempt $_retries)...'));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: 'Reconnecting to chat (attempt $_retries)...'));
|
||||
connectToChat();
|
||||
},
|
||||
);
|
||||
|
|
@ -360,6 +345,14 @@ abstract class ChatStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void addMessages() {
|
||||
if (!_autoScroll || _messageBuffer.isEmpty) return;
|
||||
|
||||
messages.addAll(_messageBuffer);
|
||||
_messageBuffer.clear();
|
||||
}
|
||||
|
||||
/// Sends the given string message by the logged-in user and adds it to [_messages].
|
||||
@action
|
||||
void sendMessage(String message) {
|
||||
|
|
@ -367,7 +360,7 @@ abstract class ChatStoreBase with Store {
|
|||
if (message.isEmpty) return;
|
||||
|
||||
if (_channel == null || _channel?.closeCode != null) {
|
||||
messages.add(IRCMessage.createNotice(message: 'Failed to send message: disconnected from chat.'));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: 'Failed to send message: disconnected from chat.'));
|
||||
} else {
|
||||
// Send the message to the IRC chat room.
|
||||
_channel?.sink.add('PRIVMSG #$channelName :$message');
|
||||
|
|
@ -416,6 +409,8 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
/// Closes and disposes all the channels and controllers used by the store.
|
||||
void dispose() {
|
||||
_messageBufferTimer.cancel();
|
||||
|
||||
_channel?.sink.close(1001);
|
||||
_channel = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,17 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addMessages() {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
name: 'ChatStoreBase.addMessages');
|
||||
try {
|
||||
return super.addMessages();
|
||||
} finally {
|
||||
_$ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void sendMessage(String message) {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue