From 2004459556ce4bb669446c6162963ff185c9e79d Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 13:45:29 -0400 Subject: [PATCH] perf(#306): throttle the per-contact notify during a bulk pull Measured, not guessed. Cumulative per-code frame timing named the owner: frame handler cumulative (2002ms total sync): code=3 2002ms/105calls code=3 is RESP_CODE_CONTACT, and it accounted for ALL of the synchronous frame time at ~19ms per contact. That is why a per-call 100ms threshold never fired: no single call is slow, the volume is. The 19ms is _handleContact calling notifyListeners() for every contact. Each notification is a synchronous full-tree rebuild, and each rebuild walks the contact list, so the cost grows with the address book. The channel handler directly alongside already guards its notify with _isLoadingChannels; the contact handler had no equivalent. Notifications are now throttled to one per 250ms while _isLoadingContacts is set, and remain immediate outside a pull so single adverts still update live. Throttled rather than suppressed so the sync progress bar keeps advancing. The existing notifyListeners() when the pull completes still fires, so the final state cannot be stale. Scope of what this accounts for, stated honestly: measured synchronous frame work was ~2s per report window and the pull showed ~4s total, against a ~30s observed freeze. This removes the largest measured contributor. If the stall persists, the remainder is outside the frame handlers and the cumulative counters will show frame time has dropped, which narrows it further. flutter analyze clean, dart format clean, 494 tests pass. --- lib/connector/meshcore_connector.dart | 31 ++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index af5b8a3..fdcfc98 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -5101,6 +5101,28 @@ class MeshCoreConnector extends ChangeNotifier { return physicsMax; } + /// Coalesces notifications during a bulk contact pull. + /// + /// Outside a pull this notifies immediately, preserving live-update + /// behaviour for adverts arriving one at a time. + DateTime? _lastContactPullNotify; + static const Duration _contactPullNotifyInterval = Duration( + milliseconds: 250, + ); + + void _notifyContactPullThrottled() { + if (!_isLoadingContacts) { + notifyListeners(); + return; + } + final now = DateTime.now(); + final last = _lastContactPullNotify; + if (last == null || now.difference(last) >= _contactPullNotifyInterval) { + _lastContactPullNotify = now; + notifyListeners(); + } + } + void _handleContact(Uint8List frame, {bool isContact = true}) { final contactTmp = Contact.fromFrame(frame); if (contactTmp != null) { @@ -5188,7 +5210,14 @@ class MeshCoreConnector extends ChangeNotifier { _pathHistoryService!.handlePathUpdated(contact); } - notifyListeners(); + // During a bulk pull this fired once per contact, and each notification + // is a synchronous full-tree rebuild that itself walks the contact list. + // Measured at ~19ms per contact, which never tripped a per-call slow + // threshold but dominated total isolate time. The channel handler + // already guards its notify with _isLoadingChannels; this is the same + // guard, throttled rather than suppressed so the sync progress bar still + // advances while the pull runs. + _notifyContactPullThrottled(); // Show notification for new contact (advertisement) if (isNewContact && _appSettingsService != null) {