From a7ef7b33257bd1c1e0eba87522fd1701dcc3d9af Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 04:40:00 -0400 Subject: [PATCH] perf(#306): measure the per-contact conversation load The startup loads are now clean (all steps ~0ms, down from 37792ms), but the contact pull still stalls in multi-second bursts: 8.0s and 7.7s at +25s..+52s on the last run, matching the reported freeze from ~15s to ~49s. No frame handler exceeds 100ms, because the work is not in one. _loadMessagesForContact is fired unawaited from the contact handler, so it runs after the handler returns and escapes that timing entirely. This times it directly and reports cumulative store time versus merge+notify time every 25 contacts, so the next run says which half is responsible rather than requiring another guess. Two candidates it will discriminate: - store: prefs read + jsonDecode per contact - merge+notify: the per-contact notifyListeners, i.e. one full widget tree rebuild per contact, each of which walks the contact list Diagnostics only, no behaviour change. flutter analyze clean, dart format clean. --- lib/connector/meshcore_connector.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 1d297c8..2bfb1ca 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -703,11 +703,30 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } + /// Aggregate cost of the per-contact conversation load, which runs once per + /// contact during a pull. It is fired unawaited from the contact handler, so + /// it escapes the frame-handler timing and has to be measured here. + int _convLoadCount = 0; + int _convLoadStoreMs = 0; + int _convLoadMergeMs = 0; + Future _loadMessagesForContact(String contactKeyHex) async { if (_loadedConversationKeys.contains(contactKeyHex)) return; _loadedConversationKeys.add(contactKeyHex); + final storeWatch = Stopwatch()..start(); final allMessages = await _messageStore.loadMessages(contactKeyHex); + storeWatch.stop(); + final mergeWatch = Stopwatch()..start(); + _convLoadCount++; + _convLoadStoreMs += storeWatch.elapsedMilliseconds; + if (_convLoadCount % 25 == 0) { + appLogger.info( + 'conversation loads: $_convLoadCount contacts, ' + 'store=${_convLoadStoreMs}ms merge=${_convLoadMergeMs}ms cumulative', + tag: 'Perf', + ); + } if (allMessages.isNotEmpty) { // Keep only the most recent N messages in memory to bound memory usage final windowedMessages = allMessages.length > _messageWindowSize @@ -748,6 +767,8 @@ class MeshCoreConnector extends ChangeNotifier { _conversations[contactKeyHex] = windowedMergedMessages; notifyListeners(); } + mergeWatch.stop(); + _convLoadMergeMs += mergeWatch.elapsedMilliseconds; } String _messageMergeKey(Message message) {