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.
integration/290-306-335
Strycher 1 day ago
parent 77625db575
commit 2004459556

@ -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) {

Loading…
Cancel
Save

Powered by TurnKey Linux.