From 7f9f6ba63b6fdd065e992e7ba3d7e959c1e7870d Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 03:48:45 -0400 Subject: [PATCH] perf(#306): batch per-contact setting loads instead of notifying 600 times Diagnosed from the app's own log. After SELF_INFO the client went completely silent for 43.7s, then flushed a burst of radio frames all sharing the identical timestamp - queued while the event loop was blocked, not a slow radio. Cause: loadContactCache() looped over every cached contact calling _ensureContactSmazSettingLoaded and _ensureContactCyr2LatSettingLoaded, and each of those notified on completion. With 300 contacts that is 600 notifyListeners() calls in a burst, each rebuilding the whole widget tree, and each rebuild itself walks the contact list. O(contacts^2) on the UI isolate. Both loaders are now awaitable and take notify: false for bulk use. loadContactCache awaits them all and notifies once. Measured on the reporting device (radio 4f6585264e, TCP): 300 contacts, 1090 discovered contacts, 12 channels holding 1841 messages, 6.9 MB prefs file. Log evidence: 43.7s stall between "Pulled battery" and the first channel response, followed by same-millisecond frame delivery. This is the Windows-visible freeze in #306. The platform asymmetry is consistent with contact-list size rather than anything desktop-specific, so whether Android is genuinely faster or simply has a smaller address book on its paired radio is still open - #306 stays open pending a like-for-like re-measure. flutter analyze clean, dart format clean, 469 tests pass. --- lib/connector/meshcore_connector.dart | 49 ++++++++++++++++++--------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a27c2f4..d86443b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1191,10 +1191,17 @@ class MeshCoreConnector extends ChangeNotifier { _contacts ..clear() ..addAll(cached); - for (final contact in cached) { - _ensureContactSmazSettingLoaded(contact.publicKeyHex); - _ensureContactCyr2LatSettingLoaded(contact.publicKeyHex); - } + // Load every contact's settings without notifying per contact, then + // notify once. Per-contact notification rebuilt the whole tree 2x per + // contact (600 rebuilds for 300 contacts), and each rebuild walks the + // contact list, which is what stalled the UI for ~44s on connect. + await Future.wait([ + for (final contact in cached) ...[ + _ensureContactSmazSettingLoaded(contact.publicKeyHex, notify: false), + _ensureContactCyr2LatSettingLoaded(contact.publicKeyHex, notify: false), + ], + ]); + notifyListeners(); } Future _loadDiscoveredContactCache() async { @@ -5625,22 +5632,32 @@ class MeshCoreConnector extends ChangeNotifier { return frame.sublist(prefixOffset, prefixOffset + prefixLen); } - void _ensureContactSmazSettingLoaded(String contactKeyHex) { + /// [notify] is false for bulk loads, which notify once at the end instead. + /// Notifying per contact rebuilds the whole tree once per contact, and each + /// rebuild itself walks the contact list, so a large address book turns this + /// into O(contacts^2) work on the UI isolate. + Future _ensureContactSmazSettingLoaded( + String contactKeyHex, { + bool notify = true, + }) async { if (_contactSmazEnabled.containsKey(contactKeyHex)) return; - _contactSettingsStore.loadSmazEnabled(contactKeyHex).then((enabled) { - if (_contactSmazEnabled[contactKeyHex] == enabled) return; - _contactSmazEnabled[contactKeyHex] = enabled; - notifyListeners(); - }); + final enabled = await _contactSettingsStore.loadSmazEnabled(contactKeyHex); + if (_contactSmazEnabled[contactKeyHex] == enabled) return; + _contactSmazEnabled[contactKeyHex] = enabled; + if (notify) notifyListeners(); } - void _ensureContactCyr2LatSettingLoaded(String contactKeyHex) { + Future _ensureContactCyr2LatSettingLoaded( + String contactKeyHex, { + bool notify = true, + }) async { if (_contactCyr2LatEnabled.containsKey(contactKeyHex)) return; - _contactSettingsStore.loadCyr2LatEnabled(contactKeyHex).then((enabled) { - if (_contactCyr2LatEnabled[contactKeyHex] == enabled) return; - _contactCyr2LatEnabled[contactKeyHex] = enabled; - notifyListeners(); - }); + final enabled = await _contactSettingsStore.loadCyr2LatEnabled( + contactKeyHex, + ); + if (_contactCyr2LatEnabled[contactKeyHex] == enabled) return; + _contactCyr2LatEnabled[contactKeyHex] = enabled; + if (notify) notifyListeners(); } void _ensureContactCyr2LatProfileLoaded(String contactKeyHex) {