fix: Widen worker stats graph

This commit is contained in:
Janez T
2026-04-04 20:20:36 +02:00
parent 5320fa1a2b
commit 1baab06627
13 changed files with 355 additions and 1345 deletions

View File

@@ -41,26 +41,22 @@ import '../utils/log_rx_route_decoder.dart';
class _DirectMessageRouteSession {
final PathSelection currentSelection;
final ParsedContactRoute? originalRoute;
final bool usedManualOverride;
final bool routerFallbackAttempted;
const _DirectMessageRouteSession({
required this.currentSelection,
required this.originalRoute,
required this.usedManualOverride,
required this.routerFallbackAttempted,
});
_DirectMessageRouteSession copyWith({
PathSelection? currentSelection,
ParsedContactRoute? originalRoute,
bool? usedManualOverride,
bool? routerFallbackAttempted,
}) {
return _DirectMessageRouteSession(
currentSelection: currentSelection ?? this.currentSelection,
originalRoute: originalRoute ?? this.originalRoute,
usedManualOverride: usedManualOverride ?? this.usedManualOverride,
routerFallbackAttempted:
routerFallbackAttempted ?? this.routerFallbackAttempted,
);
@@ -199,9 +195,6 @@ class AppProvider with ChangeNotifier {
bool get isVoiceNoiseSuppressionEnabled => _isVoiceNoiseSuppressionEnabled;
double _messageFontScale = 1.0;
double get messageFontScale => _messageFontScale;
bool _autoRouteRotationEnabled =
MessagingRoutePreferences.defaultAutoRouteRotationEnabled;
bool get autoRouteRotationEnabled => _autoRouteRotationEnabled;
bool _clearPathOnMaxRetry =
MessagingRoutePreferences.defaultClearPathOnMaxRetry;
bool get clearPathOnMaxRetry => _clearPathOnMaxRetry;
@@ -213,6 +206,7 @@ class AppProvider with ChangeNotifier {
const NearestRouterSelector();
final Map<String, _DirectMessageRouteSession> _directMessageRouteSessions =
{};
final Set<String> _pendingDeliveredRouteRefreshContacts = <String>{};
static const Duration _packetRetryDelay = Duration(milliseconds: 1200);
static const Duration _mediaSwarmResponseWindow = Duration(seconds: 10);
@@ -873,8 +867,7 @@ class AppProvider with ChangeNotifier {
Future<void> _loadMessagingRouteSettings() async {
try {
_autoRouteRotationEnabled =
await MessagingRoutePreferences.getAutoRouteRotationEnabled();
await MessagingRoutePreferences.cleanupLegacySettings();
_clearPathOnMaxRetry =
await MessagingRoutePreferences.getClearPathOnMaxRetry();
_nearestRelayFallbackEnabled =
@@ -885,16 +878,6 @@ class AppProvider with ChangeNotifier {
}
}
Future<void> toggleAutoRouteRotationEnabled(bool enabled) async {
try {
_autoRouteRotationEnabled = enabled;
await MessagingRoutePreferences.setAutoRouteRotationEnabled(enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving auto route rotation setting: $e');
}
}
Future<void> toggleClearPathOnMaxRetry(bool enabled) async {
try {
_clearPathOnMaxRetry = enabled;
@@ -1048,6 +1031,14 @@ class AppProvider with ChangeNotifier {
contact,
devicePublicKey: devicePublicKey,
);
final updatedContact =
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
if (_pendingDeliveredRouteRefreshContacts.remove(
updatedContact.publicKeyHex,
)) {
messagesProvider.applyDeliveredMessageRouteFromContact(updatedContact);
}
};
// When all contacts are received
@@ -1254,18 +1245,6 @@ class AppProvider with ChangeNotifier {
enrichedMessage,
);
final receivedPathBytes = receptionDetailsSnapshot?.pathBytes;
if (senderContact != null &&
enrichedMessage.isChannelMessage &&
receivedPathBytes != null &&
receivedPathBytes.isNotEmpty) {
unawaited(
_learnPathFromPublicMessage(
contact: senderContact,
pathBytes: receivedPathBytes,
),
);
}
// Estimate location for contacts without GPS using received path
if (senderContact != null &&
senderContact.displayLocation == null &&
@@ -1685,6 +1664,7 @@ class AppProvider with ChangeNotifier {
// When a contact's routing path is updated in the mesh network
connectionProvider.onPathUpdated = (publicKey) {
_pendingDeliveredRouteRefreshContacts.add(_publicKeyHex(publicKey));
debugPrint(
'🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
);
@@ -1850,28 +1830,18 @@ class AppProvider with ChangeNotifier {
.getManualSelectionForContact(latestContact);
final selection =
manualSelection ??
await _pathHistoryService.getSelectionForContact(
latestContact,
autoRouteRotationEnabled: _autoRouteRotationEnabled,
);
await _pathHistoryService.getSelectionForContact(latestContact);
session = _DirectMessageRouteSession(
currentSelection: selection,
originalRoute: ContactRouteCodec.fromContact(latestContact),
usedManualOverride: manualSelection != null,
routerFallbackAttempted: false,
);
}
if (!session.routerFallbackAttempted) {
final currentSignature = session.currentSelection.hasDirectPath
? session.currentSelection.pathBytes
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join()
: null;
final selection = await _resolveDirectMessageSelectionForRetry(
latestContact,
retryAttempt: retryAttempt,
currentSignature: currentSignature,
fallbackSelection: session.currentSelection,
);
session = session.copyWith(currentSelection: selection);
@@ -1891,26 +1861,17 @@ class AppProvider with ChangeNotifier {
Future<PathSelection> _resolveDirectMessageSelectionForRetry(
Contact contact, {
required int retryAttempt,
required String? currentSignature,
required PathSelection fallbackSelection,
}) async {
if (retryAttempt == 2) {
return PathSelection.flood();
}
if (retryAttempt >= 3) {
final historicalSelection = await _pathHistoryService
.getLastSuccessfulDirectSelection(
contact,
excludeSignature: currentSignature,
senderLatitude: locationTrackingService.currentPosition?.latitude,
senderLongitude: locationTrackingService.currentPosition?.longitude,
recipientLatitude: contact.displayLocation?.latitude,
recipientLongitude: contact.displayLocation?.longitude,
);
if (historicalSelection != null) {
return historicalSelection;
}
if (retryAttempt < 2 &&
!fallbackSelection.hasDirectPath &&
contact.routeHasPath &&
contact.routeHopCount > 0) {
return _pathHistoryService.getSelectionForContact(contact);
}
return fallbackSelection;
@@ -2020,32 +1981,19 @@ class AppProvider with ChangeNotifier {
final latestContact =
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
final manualSelection = await _pathHistoryService.getManualSelectionForContact(
latestContact,
);
final session =
_directMessageRouteSessions[messageId] ??
_DirectMessageRouteSession(
currentSelection:
await _pathHistoryService.getManualSelectionForContact(
latestContact,
) ??
await _pathHistoryService.getSelectionForContact(
latestContact,
autoRouteRotationEnabled: _autoRouteRotationEnabled,
),
manualSelection ??
await _pathHistoryService.getSelectionForContact(latestContact),
originalRoute: ContactRouteCodec.fromContact(latestContact),
usedManualOverride:
await _pathHistoryService.getManualSelectionForContact(
latestContact,
) !=
null,
routerFallbackAttempted: false,
);
await _pathHistoryService.recordPathResult(
latestContact.publicKeyHex,
session.currentSelection,
success: false,
);
final repeater = _nearestRouterSelector.select(
senderPosition: locationTrackingService.currentPosition,
repeaters: contactsProvider.repeaters,
@@ -2089,33 +2037,10 @@ class AppProvider with ChangeNotifier {
return;
}
unawaited(
() async {
await _pathHistoryService.recordPathResult(
contact.publicKeyHex,
session.currentSelection,
success: true,
roundTripTimeMs: roundTripTimeMs,
senderLatitude: locationTrackingService.currentPosition?.latitude,
senderLongitude: locationTrackingService.currentPosition?.longitude,
recipientLatitude: contact.displayLocation?.latitude,
recipientLongitude: contact.displayLocation?.longitude,
);
if (!session.usedManualOverride) {
return;
}
if (session.currentSelection.mode == PathSelectionMode.directCurrent ||
session.currentSelection.mode ==
PathSelectionMode.directHistorical) {
await _pathHistoryService.setManualSelectionFor(
contact.publicKeyHex,
session.currentSelection,
);
return;
}
await _pathHistoryService.clearManualRouteFor(contact.publicKeyHex);
}(),
);
if (session.currentSelection.usesFlood ||
session.currentSelection.mode == PathSelectionMode.nearestRouter) {
messagesProvider.queueDeliveredMessageRouteRefresh(messageId, contact);
}
}
Future<void> _handleDirectMessageFinalFailure({
@@ -2126,16 +2051,6 @@ class AppProvider with ChangeNotifier {
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
final session = _directMessageRouteSessions.remove(messageId);
if (session != null) {
await _pathHistoryService.recordPathResult(
latestContact.publicKeyHex,
session.currentSelection,
success: false,
);
if (session.usedManualOverride) {
await _pathHistoryService.clearManualRouteFor(
latestContact.publicKeyHex,
);
}
if (session.routerFallbackAttempted) {
await _restoreRouteOnDevice(latestContact, session.originalRoute);
}
@@ -2157,6 +2072,12 @@ class AppProvider with ChangeNotifier {
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}
String _publicKeyHex(Uint8List publicKey) {
return publicKey
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join();
}
/// Estimate contact location from the received message path.
///
/// When we receive a message, the path bytes describe how it traveled:
@@ -2207,22 +2128,6 @@ class AppProvider with ChangeNotifier {
);
}
Future<void> _learnPathFromPublicMessage({
required Contact contact,
required List<int> pathBytes,
}) async {
final preferred = await RouteHashPreferences.getHashSize();
final inferredHashSize = _inferReceivedPathHashSize(
pathBytes,
preferredHashSize: preferred,
);
await _pathHistoryService.recordReceivedBytePath(
contact.publicKeyHex,
pathBytes,
inferredHashSize,
);
}
Future<void> _retainAdvertRxPath(Uint8List publicKey) async {
final decoded = _findBestMatchingAdvertRxRoute(publicKey);
if (decoded == null || decoded.pathBytes.isEmpty) {
@@ -2248,11 +2153,6 @@ class AppProvider with ChangeNotifier {
paddedPathBytes: parsedRoute.paddedPathBytes,
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
await _pathHistoryService.recordReceivedBytePath(
publicKey.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(),
decoded.pathBytes,
decoded.hashSize,
);
}
/// Handle pushAdvert (0x80) — matches the official MeshCore app flow:
@@ -2593,22 +2493,6 @@ class AppProvider with ChangeNotifier {
}
}
int _inferReceivedPathHashSize(
List<int> pathBytes, {
required int preferredHashSize,
}) {
final preferred = preferredHashSize;
final candidates = {preferred, 3, 2, 1}.toList();
for (final candidate in candidates) {
if (candidate >= 1 &&
candidate <= 3 &&
pathBytes.length % candidate == 0) {
return candidate;
}
}
return 1;
}
/// Initialize the app (load contacts, sync time, etc.)
Future<void> initialize() async {
if (!connectionProvider.deviceInfo.isConnected) return;

View File

@@ -24,6 +24,7 @@ typedef DisplayMessageEntry = ({Message message, int occurrenceCount});
class MessagesProvider with ChangeNotifier {
static const Duration _channelEchoWarningDelay = Duration(seconds: 12);
static const Duration _receivedDuplicateWindow = Duration(seconds: 5);
static const Duration _deliveredRouteRefreshWindow = Duration(seconds: 30);
final List<Message> _messages = [];
final Map<String, SarMarker> _sarMarkers = {};
@@ -38,6 +39,9 @@ class MessagesProvider with ChangeNotifier {
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
final Map<String, List<_PendingDeliveredRouteRefresh>>
_pendingDeliveredRouteRefreshByContact =
<String, List<_PendingDeliveredRouteRefresh>>{};
String? _storageNamespace;
// Track pending sent messages by expected ACK/TAG
@@ -199,6 +203,83 @@ class MessagesProvider with ChangeNotifier {
notifyListeners();
}
void queueDeliveredMessageRouteRefresh(String messageId, Contact contact) {
if (messageId.isEmpty || contact.publicKeyHex.isEmpty) {
return;
}
_prunePendingDeliveredRouteRefresh();
final queue = _pendingDeliveredRouteRefreshByContact.putIfAbsent(
contact.publicKeyHex,
() => <_PendingDeliveredRouteRefresh>[],
);
queue.removeWhere((entry) => entry.messageId == messageId);
queue.add(
_PendingDeliveredRouteRefresh(
messageId: messageId,
queuedAt: DateTime.now(),
),
);
}
bool applyDeliveredMessageRouteFromContact(Contact contact) {
if (!contact.routeHasPath ||
contact.routeHopCount <= 0 ||
contact.publicKeyHex.isEmpty) {
return false;
}
_prunePendingDeliveredRouteRefresh();
final queue = _pendingDeliveredRouteRefreshByContact[contact.publicKeyHex];
if (queue == null || queue.isEmpty) {
return false;
}
while (queue.isNotEmpty) {
final pending = queue.removeAt(0);
final index = _messages.indexWhere(
(message) => message.id == pending.messageId,
);
if (index == -1) {
continue;
}
final message = _messages[index];
if (!message.isContactMessage ||
message.deliveryStatus != MessageDeliveryStatus.delivered) {
continue;
}
final existingMetadata = _messageRouteMetadata[pending.messageId];
_messageRouteMetadata[pending.messageId] = MessageRouteMetadata(
mode: PathSelectionMode.directCurrent,
routerFallbackAttempted:
existingMetadata?.routerFallbackAttempted ?? false,
canonicalPath: contact.routeCanonicalText.isEmpty
? null
: contact.routeCanonicalText,
hopCount: contact.routeHopCount,
);
_messages[index] = message.copyWith(
pathLen: contact.routeHopCount,
usedFloodFallback: false,
);
if (queue.isEmpty) {
_pendingDeliveredRouteRefreshByContact.remove(contact.publicKeyHex);
}
_persistMessages();
notifyListeners();
return true;
}
if (queue.isEmpty) {
_pendingDeliveredRouteRefreshByContact.remove(contact.publicKeyHex);
}
return false;
}
/// Set localizations for notifications
void setLocalizations(AppLocalizations localizations) {
_localizations = localizations;
@@ -2555,6 +2636,7 @@ class MessagesProvider with ChangeNotifier {
_pendingSentMessages.remove(message.expectedAckTag);
}
_clearAckHistoryForMessage(messageId);
_removePendingDeliveredRouteRefresh(messageId);
// Clear retry tracking
_retryManager.clearRetry(messageId);
@@ -2595,6 +2677,7 @@ class MessagesProvider with ChangeNotifier {
_pendingSentMessages.remove(message.expectedAckTag);
}
_clearAckHistoryForMessage(messageId);
_removePendingDeliveredRouteRefresh(messageId);
_retryManager.clearRetry(messageId);
_messageRouteMetadata.remove(messageId);
onManualRetryPreparedCallback?.call(messageId);
@@ -2767,4 +2850,51 @@ class MessagesProvider with ChangeNotifier {
}
}
}
void _prunePendingDeliveredRouteRefresh() {
if (_pendingDeliveredRouteRefreshByContact.isEmpty) {
return;
}
final cutoff = DateTime.now().subtract(_deliveredRouteRefreshWindow);
final emptyKeys = <String>[];
for (final entry in _pendingDeliveredRouteRefreshByContact.entries) {
entry.value.removeWhere(
(pending) => pending.queuedAt.isBefore(cutoff),
);
if (entry.value.isEmpty) {
emptyKeys.add(entry.key);
}
}
for (final key in emptyKeys) {
_pendingDeliveredRouteRefreshByContact.remove(key);
}
}
void _removePendingDeliveredRouteRefresh(String messageId) {
if (_pendingDeliveredRouteRefreshByContact.isEmpty) {
return;
}
final emptyKeys = <String>[];
for (final entry in _pendingDeliveredRouteRefreshByContact.entries) {
entry.value.removeWhere((pending) => pending.messageId == messageId);
if (entry.value.isEmpty) {
emptyKeys.add(entry.key);
}
}
for (final key in emptyKeys) {
_pendingDeliveredRouteRefreshByContact.remove(key);
}
}
}
class _PendingDeliveredRouteRefresh {
final String messageId;
final DateTime queuedAt;
const _PendingDeliveredRouteRefresh({
required this.messageId,
required this.queuedAt,
});
}