оптимизация: слегоньца набурмалдил

This commit is contained in:
Jganenok
2026-06-03 13:55:14 +07:00
parent 3341888ad5
commit 570a4396eb
9 changed files with 229 additions and 123 deletions
+8 -4
View File
@@ -512,12 +512,15 @@ class ChatsModule {
if (accountId == null) return;
final dialogRows = await AppDatabase.loadDialogChats(accountId);
final byParticipant = <int, List<Map<String, dynamic>>>{};
final byParticipant =
<int, List<({Map<String, dynamic> row, CachedChat cached})>>{};
for (final row in dialogRows) {
final cached = CachedChat.fromDbRow(row);
for (final pid in cached.participants.keys) {
if (pid == accountId) continue;
byParticipant.putIfAbsent(pid, () => []).add(row);
byParticipant
.putIfAbsent(pid, () => [])
.add((row: row, cached: cached));
}
}
@@ -529,8 +532,9 @@ class ChatsModule {
final options = ContactCache.getOptions(contactId) ?? const <String>{};
final affected = byParticipant[contactId];
if (affected == null) continue;
for (final row in affected) {
final cached = CachedChat.fromDbRow(row);
for (final entry in affected) {
final row = entry.row;
final cached = entry.cached;
final sameTitle = cached.title == name;
final sameAvatar = (cached.iconUrl ?? '') == (avatar ?? '');
final sameOptions = cached.options.length == options.length &&
+6 -3
View File
@@ -44,10 +44,13 @@ class FoldersModule {
List<dynamic>? foldersOrder,
) {
if (foldersOrder == null || foldersOrder.isEmpty) return;
final orderedIds = foldersOrder.map((id) => id.toString()).toList();
final orderIndex = <String, int>{};
for (var i = 0; i < foldersOrder.length; i++) {
orderIndex.putIfAbsent(foldersOrder[i].toString(), () => i);
}
folders.sort((a, b) {
final aIndex = orderedIds.indexOf(a.id);
final bIndex = orderedIds.indexOf(b.id);
final aIndex = orderIndex[a.id] ?? -1;
final bIndex = orderIndex[b.id] ?? -1;
if (aIndex == -1 && bIndex == -1) return 0;
if (aIndex == -1) return 1;
if (bIndex == -1) return -1;
+21 -15
View File
@@ -92,25 +92,31 @@ const int _compressionThreshold = 32;
/// `0` — без сжатия, иначе `(rawLen ~/ compLen) + 1` (множитель размера, по
/// которому получатель выделяет буфер под распаковку).
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload, {int seq = 0}) {
final header = ByteData(headerSize);
final Uint8List raw = msgpack.serialize(payload);
final List<int> body;
final int flag;
if (raw.length < _compressionThreshold) {
body = raw;
flag = 0;
} else {
body = lz4Compress(raw);
flag = (raw.length ~/ body.length) + 1;
}
final out = Uint8List(headerSize + body.length);
final header = ByteData.view(out.buffer, out.offsetInBytes, headerSize);
header.setUint8(0, 10);
header.setUint8(1, CmdType.request);
header.setUint16(2, seq, Endian.big);
header.setUint16(4, opcode, Endian.big);
final raw = Uint8List.fromList(msgpack.serialize(payload));
if (raw.length < _compressionThreshold) {
header.setUint32(6, raw.length & 0xFFFFFF, Endian.big);
return Uint8List.fromList(header.buffer.asUint8List() + raw);
}
final compressed = lz4Compress(raw);
final compLen = compressed.length;
final flag = (raw.length ~/ compLen) + 1;
header.setUint32(6, ((flag & 0xFF) << 24) | (compLen & 0xFFFFFF), Endian.big);
return Uint8List.fromList(header.buffer.asUint8List() + compressed);
header.setUint32(
6,
((flag & 0xFF) << 24) | (body.length & 0xFFFFFF),
Endian.big,
);
out.setRange(headerSize, out.length, body);
return out;
}
/// Распаковка пакета от сервера
+12 -9
View File
@@ -435,17 +435,20 @@ class AppDatabase {
// Chats cache
static Future<void> saveChats(List<Map<String, dynamic>> rows) async {
if (rows.isEmpty) return;
try {
final db = await _instance;
final batch = db.batch();
for (final row in rows) {
batch.insert(
'chats_cache',
row,
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
await db.transaction((txn) async {
final batch = txn.batch();
for (final row in rows) {
batch.insert(
'chats_cache',
row,
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
});
} catch (e) {
logger.e("Ошибка при сохранении чата: $e");
}
+38 -11
View File
@@ -7,46 +7,73 @@ import '../utils/logger.dart';
/// Копит сырые байты из сокета, нарезает их на байтовые срезы целых пакетов.
class PacketReceiver {
Uint8List _buffer = Uint8List(0);
int _start = 0;
int _end = 0;
static const int _maxBufferSize = 2 * 1024 * 1024; // 2 мегабуйта
/// Добавляет байты в буфер и возвращает все собранные пакеты как сырые срезы.
/// Полностью синхронный — нарезка не блокируется на распаковке, поэтому
/// конкурентные вызовы из stream-листенера не могут пересечься на `_buffer`.
///
/// Накопление идёт без перекопирования всего буфера на каждый чанк: целые
/// пакеты отдаются как `sublistView`, а потреблённый префикс отбрасывается
/// сдвигом указателя `_start`, а не пересборкой буфера.
List<Uint8List> feed(Uint8List data) {
final newBuffer = Uint8List(_buffer.length + data.length);
newBuffer.setAll(0, _buffer);
newBuffer.setAll(_buffer.length, data);
_buffer = newBuffer;
_append(data);
if (_buffer.length > _maxBufferSize) {
if (_end - _start > _maxBufferSize) {
logger.e(
'PacketReceiver: переполнение буфера (${_buffer.length} B), сброс',
'PacketReceiver: переполнение буфера (${_end - _start} B), сброс',
);
reset();
return const [];
}
final packets = <Uint8List>[];
while (_buffer.length >= headerSize) {
while (_end - _start >= headerSize) {
final bd = ByteData.view(
_buffer.buffer,
_buffer.offsetInBytes,
_buffer.offsetInBytes + _start,
headerSize,
);
final packedLen = bd.getUint32(6, Endian.big);
final payloadLength = packedLen & 0xFFFFFF;
final totalLength = headerSize + payloadLength;
if (_buffer.length < totalLength) break;
if (_end - _start < totalLength) break;
packets.add(Uint8List.sublistView(_buffer, 0, totalLength));
_buffer = _buffer.sublist(totalLength);
packets.add(Uint8List.sublistView(_buffer, _start, _start + totalLength));
_start += totalLength;
}
if (_start == _end) {
_start = 0;
_end = 0;
}
return packets;
}
void _append(Uint8List data) {
final pending = _end - _start;
if (pending == 0) {
_buffer = Uint8List.fromList(data);
_start = 0;
_end = data.length;
return;
}
final total = pending + data.length;
final newBuffer = Uint8List(total);
newBuffer.setRange(0, pending, _buffer, _start);
newBuffer.setRange(pending, total, data);
_buffer = newBuffer;
_start = 0;
_end = total;
}
void reset() {
_buffer = Uint8List(0);
_start = 0;
_end = 0;
}
}
+33 -6
View File
@@ -15,6 +15,7 @@ class MediaCache {
static int get maxBytes => AppMediaCacheLimit.current.value;
static Directory? _dir;
static int? _cachedSize;
static Future<Directory> _cacheDir() async {
final cached = _dir;
@@ -81,6 +82,12 @@ class MediaCache {
}
await sink.close();
await part.rename(file.path);
final known = _cachedSize;
if (known != null) {
try {
_cachedSize = known + await file.length();
} catch (_) {}
}
await _enforceLimit();
return file;
} catch (_) {
@@ -96,7 +103,18 @@ class MediaCache {
}
/// Суммарный размер кэша в байтах.
///
/// Результат держится в памяти и поддерживается инкрементально при
/// загрузке/очистке/вытеснении — повторные вызовы не пересканируют каталог.
static Future<int> currentSize() async {
final cached = _cachedSize;
if (cached != null) return cached;
final total = await _scanSize();
_cachedSize = total;
return total;
}
static Future<int> _scanSize() async {
final dir = await _cacheDir();
var total = 0;
await for (final entity in dir.list()) {
@@ -121,34 +139,43 @@ class MediaCache {
} catch (_) {}
}
}
_cachedSize = 0;
return freed;
}
/// Вытесняет старые файлы (по mtime), пока размер превышает [maxBytes].
///
/// Под лимитом — ранний выход без сканирования каталога (частый случай).
/// Каталог обходится только когда лимит реально превышен.
static Future<void> _enforceLimit() async {
final limit = maxBytes;
if (limit <= 0) return;
var total = _cachedSize ?? await _scanSize();
if (total <= limit) {
_cachedSize = total;
return;
}
final dir = await _cacheDir();
final files = <File>[];
var total = 0;
await for (final entity in dir.list()) {
if (entity is File && !entity.path.endsWith('.part')) {
files.add(entity);
try {
total += await entity.length();
} catch (_) {}
}
}
if (maxBytes <= 0 || total <= maxBytes) return;
files.sort((a, b) =>
a.statSync().modified.compareTo(b.statSync().modified));
for (final file in files) {
if (total <= maxBytes) break;
if (total <= limit) break;
try {
total -= await file.length();
await file.delete();
} catch (_) {}
}
_cachedSize = total;
}
static String _sanitize(String name) {
@@ -183,8 +183,12 @@ class _ChatListScreenState extends State<ChatListScreen>
List<CachedChat> _selectedChatObjects() {
if (_selectedChats.isEmpty) return const [];
final ids = _selectedChats;
return _chats.where((c) => ids.contains(c.id.toString())).toList();
final ids = <int>{};
for (final s in _selectedChats) {
final v = int.tryParse(s);
if (v != null) ids.add(v);
}
return _chats.where((c) => ids.contains(c.id)).toList();
}
_DeleteKind _categorizeChat(CachedChat c, int myId) {
+82 -55
View File
@@ -136,8 +136,10 @@ class _ChatScreenState extends State<ChatScreen>
Timer? _shimmerStartTimer;
bool _historyKickedOff = false;
List<CachedMessage> _messages = [];
int _messagesRevision = 0;
List<Object>? _combinedItemsCache;
int? _combinedItemsKey;
bool _floatingDateScheduled = false;
int _myId = 0;
CachedChat? chat;
@@ -146,7 +148,6 @@ class _ChatScreenState extends State<ChatScreen>
late final AnimationController _floatingDateAnimController;
late final CurvedAnimation _floatingDateCurved;
final Map<int, GlobalKey> _separatorKeys = {};
double _lastScrollOffset = 0;
String? _lastSentId;
@override
@@ -226,6 +227,7 @@ class _ChatScreenState extends State<ChatScreen>
.toList();
setState(() {
_messages = first;
_messagesRevision++;
_isLoading = false;
_onLoadingFinished();
});
@@ -349,7 +351,10 @@ class _ChatScreenState extends State<ChatScreen>
final changed = !_listsEquivalent(_messages, merged);
if (!changed && !markLoaded) return;
setState(() {
if (changed) _messages = merged;
if (changed) {
_messages = merged;
_messagesRevision++;
}
if (markLoaded) {
_isLoading = false;
_onLoadingFinished();
@@ -590,6 +595,7 @@ class _ChatScreenState extends State<ChatScreen>
setState(() {
_lastSentId = message.id;
_messages.add(message);
_messagesRevision++;
});
_clearTyping(message.senderId);
Haptics.tap();
@@ -598,11 +604,17 @@ class _ChatScreenState extends State<ChatScreen>
case MessageEditedEvent(:final message):
final idx = _messages.indexWhere((m) => m.id == message.id);
if (idx == -1) return;
setState(() => _messages[idx] = message);
setState(() {
_messages[idx] = message;
_messagesRevision++;
});
case MessageRemovedEvent(:final messageId):
final idx = _messages.indexWhere((m) => m.id == messageId);
if (idx == -1) return;
setState(() => _messages.removeAt(idx));
setState(() {
_messages.removeAt(idx);
_messagesRevision++;
});
_reactionNotifiers.remove(messageId)?.dispose();
case MessageReactionsChangedEvent(:final messageId, :final reactionInfo):
_reactionNotifiers[messageId]?.value = reactionInfo;
@@ -722,6 +734,7 @@ class _ChatScreenState extends State<ChatScreen>
setState(() {
_lastSentId = tempId;
_messages.add(tempMessage);
_messagesRevision++;
_messageController.clear();
});
unawaited(_persistOutgoing(tempMessage));
@@ -748,6 +761,7 @@ class _ChatScreenState extends State<ChatScreen>
);
setState(() {
_messages[index] = sent;
_messagesRevision++;
});
unawaited(_persistOutgoing(sent, removeId: tempId));
}
@@ -776,6 +790,7 @@ class _ChatScreenState extends State<ChatScreen>
);
setState(() {
_messages[index] = failed;
_messagesRevision++;
});
unawaited(_persistOutgoing(failed));
}
@@ -806,48 +821,61 @@ class _ChatScreenState extends State<ChatScreen>
}
if (forwardIds.isEmpty) return;
final resolved = <int, ({String name, String? avatar})>{};
for (final id in forwardIds) {
final name = await messagesModule.searchContactById(id);
final avatar = ContactCache.getAvatar(id);
if (name != null && mounted) {
setState(() {
for (var i = 0; i < _messages.length; i++) {
final msg = _messages[i];
if (msg.attachments != null) {
final newAttaches = msg.attachments!.map((a) {
if (a is ForwardedMessageAttachment &&
a.originalSenderId == id &&
a.originalSenderName == null) {
return ForwardedMessageAttachment(
originalSenderId: id,
originalSenderName: name,
originalSenderAvatar: avatar,
originalMessageId: a.originalMessageId,
originalTime: a.originalTime,
originalText: a.originalText,
originalChatId: a.originalChatId,
originalAttachments: a.originalAttachments,
originalContact: a.originalContact,
);
}
return a;
}).toList();
_messages[i] = CachedMessage(
id: msg.id,
accountId: msg.accountId,
chatId: msg.chatId,
senderId: msg.senderId,
text: msg.text,
time: msg.time,
status: msg.status,
payload: msg.payload,
attachments: newAttaches,
);
}
}
});
if (name != null) {
resolved[id] = (name: name, avatar: ContactCache.getAvatar(id));
}
}
if (resolved.isEmpty || !mounted) return;
var anyChanged = false;
for (var i = 0; i < _messages.length; i++) {
final msg = _messages[i];
final attaches = msg.attachments;
if (attaches == null) continue;
var msgChanged = false;
final newAttaches = attaches.map((a) {
if (a is ForwardedMessageAttachment &&
a.originalSenderName == null &&
resolved.containsKey(a.originalSenderId)) {
final r = resolved[a.originalSenderId]!;
msgChanged = true;
return ForwardedMessageAttachment(
originalSenderId: a.originalSenderId,
originalSenderName: r.name,
originalSenderAvatar: r.avatar,
originalMessageId: a.originalMessageId,
originalTime: a.originalTime,
originalText: a.originalText,
originalChatId: a.originalChatId,
originalAttachments: a.originalAttachments,
originalContact: a.originalContact,
);
}
return a;
}).toList();
if (!msgChanged) continue;
anyChanged = true;
_messages[i] = CachedMessage(
id: msg.id,
accountId: msg.accountId,
chatId: msg.chatId,
senderId: msg.senderId,
text: msg.text,
time: msg.time,
status: msg.status,
payload: msg.payload,
attachments: newAttaches,
);
}
if (anyChanged) {
setState(() => _messagesRevision++);
}
}
void _scrollToBottom() {
@@ -863,7 +891,7 @@ class _ChatScreenState extends State<ChatScreen>
}
List<Object> _buildCombinedItems() {
final key = Object.hashAll(_messages.map(identityHashCode));
final key = Object.hash(_messagesRevision, _messages.length);
final cached = _combinedItemsCache;
if (cached != null && _combinedItemsKey == key) return cached;
@@ -906,25 +934,22 @@ class _ChatScreenState extends State<ChatScreen>
void _onScrollForDate() {
if (!_scrollController.hasClients) return;
final currentOffset = _scrollController.position.pixels;
final scrollingUp = currentOffset > _lastScrollOffset;
_lastScrollOffset = currentOffset;
_floatingDateTimer?.cancel();
if (!scrollingUp) {
_floatingDateAnimController.reverse();
return;
}
_floatingDateTimer = Timer(const Duration(seconds: 2), () {
_floatingDateTimer = Timer(const Duration(seconds: 1), () {
if (mounted) _floatingDateAnimController.reverse();
});
WidgetsBinding.instance.addPostFrameCallback((_) => _updateFloatingDate());
if (_floatingDateScheduled) return;
_floatingDateScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_floatingDateScheduled = false;
_updateFloatingDate();
});
}
void _updateFloatingDate() {
if (!mounted) return;
if (!mounted || _separatorKeys.isEmpty) return;
DateTime? result;
final listRenderBox = _listKey.currentContext?.findRenderObject();
@@ -1611,6 +1636,7 @@ class _ChatScreenState extends State<ChatScreen>
setState(() {
_lastSentId = tempId;
_messages.add(msg);
_messagesRevision++;
});
Haptics.send();
_scrollToBottom();
@@ -1638,6 +1664,7 @@ class _ChatScreenState extends State<ChatScreen>
payload: old.payload,
attachments: attachment != null ? [attachment] : old.attachments,
);
_messagesRevision++;
});
}
+23 -18
View File
@@ -1924,7 +1924,7 @@ class _VoiceMessageBubble extends StatefulWidget {
class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
bool _isPlaying = false;
double _progress = 0.0;
final ValueNotifier<double> _progress = ValueNotifier(0.0);
bool _transcriptionVisible = false;
String? _transcriptionText;
bool _transcriptionLoading = false;
@@ -1935,6 +1935,12 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
_transcriptionText = widget.preloadedText;
}
@override
void dispose() {
_progress.dispose();
super.dispose();
}
String _formatDuration(int seconds) {
final min = seconds ~/ 60;
final sec = seconds % 60;
@@ -2027,18 +2033,14 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
builder: (context, constraints) {
return GestureDetector(
onTapDown: (details) {
setState(() {
_progress = (details.localPosition.dx /
constraints.maxWidth)
.clamp(0.0, 1.0);
});
_progress.value =
(details.localPosition.dx / constraints.maxWidth)
.clamp(0.0, 1.0);
},
onHorizontalDragUpdate: (details) {
setState(() {
_progress = (details.localPosition.dx /
constraints.maxWidth)
.clamp(0.0, 1.0);
});
_progress.value =
(details.localPosition.dx / constraints.maxWidth)
.clamp(0.0, 1.0);
},
child: Container(
height: 4,
@@ -2046,13 +2048,16 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
color: waveInactiveColor,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: _progress.clamp(0.0, 1.0),
child: Container(
decoration: BoxDecoration(
color: waveActiveColor,
borderRadius: BorderRadius.circular(2),
child: ValueListenableBuilder<double>(
valueListenable: _progress,
builder: (context, progress, _) => FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress.clamp(0.0, 1.0),
child: Container(
decoration: BoxDecoration(
color: waveActiveColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),