From 03282eb4788fbef6b175c1d96f58c10063a9182e Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Thu, 26 Mar 2026 02:45:16 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20sync=20=D0=B8=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2:=20heartbeat=20=D0=B0=D0=BD=D1=82=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=B0=D0=BC=20+=20Connection=20Logs=20=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=20rosettadev2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rosetta/messenger/MainActivity.kt | 5 +++ .../messenger/data/MessageRepository.kt | 7 ++-- .../com/rosetta/messenger/network/Protocol.kt | 41 ++++++++++++++++--- .../messenger/network/ProtocolManager.kt | 27 ++++++++++-- .../ui/chats/ConnectionLogsScreen.kt | 11 ++++- .../messenger/ui/chats/SearchScreen.kt | 8 +++- .../rosetta/messenger/utils/MessageLogger.kt | 11 +++-- 7 files changed, 91 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index bb26358..823917f 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -1379,6 +1379,11 @@ fun MainScreen( }, onNavigateToCrashLogs = { navStack = navStack.filterNot { it is Screen.Search } + Screen.CrashLogs + }, + onNavigateToConnectionLogs = { + navStack = + navStack.filterNot { it is Screen.Search } + + Screen.ConnectionLogs } ) } diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index 0f11295..74ab40e 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -885,11 +885,10 @@ class MessageRepository private constructor(private val context: Context) { unreadCount = dialog?.unreadCount ?: 0 ) - // 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа - // Desktop parity: always re-fetch on incoming message so renamed contacts - // get their new name/username updated in the chat list. + // 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа. + // Важно: не форсим повторный запрос на каждый входящий пакет — это создает + // шторм PacketSearch во время sync и заметно тормозит обработку. if (!isGroupDialogKey(dialogOpponentKey)) { - requestedUserInfoKeys.remove(dialogOpponentKey) requestUserInfo(dialogOpponentKey) } else { applyGroupDisplayNameToDialog(account, dialogOpponentKey) diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index 760e3e7..8f8c7ef 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -33,6 +33,9 @@ class Protocol( private const val RECONNECT_INTERVAL = 5000L // 5 seconds (как в Архиве) private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds private const val MIN_PACKET_ID_BITS = 18 // Stream.readInt16() = 2 * readInt8() (9 bits each) + private const val DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 15 + private const val MIN_HEARTBEAT_SEND_INTERVAL_MS = 2_000L + private const val HEARTBEAT_OK_LOG_THROTTLE_MS = 30_000L } private fun log(message: String) { @@ -112,6 +115,9 @@ class Protocol( // Heartbeat private var heartbeatJob: Job? = null + @Volatile private var heartbeatPeriodMs: Long = 0L + @Volatile private var lastHeartbeatOkLogAtMs: Long = 0L + @Volatile private var heartbeatOkSuppressedCount: Int = 0 // Supported packets private val supportedPackets = mapOf( @@ -179,11 +185,24 @@ class Protocol( * Как в Архиве - отправляем text "heartbeat" СРАЗУ и потом с интервалом */ private fun startHeartbeat(intervalSeconds: Int) { + val normalizedServerIntervalSec = + if (intervalSeconds > 0) intervalSeconds else DEFAULT_HEARTBEAT_INTERVAL_SECONDS + // Отправляем чаще - каждые 1/3 интервала, но с нижним лимитом чтобы исключить tight-loop. + val intervalMs = + ((normalizedServerIntervalSec * 1000L) / 3).coerceAtLeast(MIN_HEARTBEAT_SEND_INTERVAL_MS) + + if (heartbeatJob?.isActive == true && heartbeatPeriodMs == intervalMs) { + return + } + heartbeatJob?.cancel() - - // Отправляем чаще - каждые 1/3 интервала (чтобы не терять соединение) - val intervalMs = (intervalSeconds * 1000L) / 3 - log("💓 HEARTBEAT START: server=${intervalSeconds}s, sending=${intervalMs/1000}s, state=${_state.value}") + heartbeatPeriodMs = intervalMs + lastHeartbeatOkLogAtMs = 0L + heartbeatOkSuppressedCount = 0 + log( + "💓 HEARTBEAT START: server=${intervalSeconds}s(normalized=${normalizedServerIntervalSec}s), " + + "sending=${intervalMs / 1000}s, state=${_state.value}" + ) heartbeatJob = scope.launch { // ⚡ СРАЗУ отправляем первый heartbeat (как в Архиве) @@ -210,7 +229,17 @@ class Protocol( ) { val sent = webSocket?.send("heartbeat") ?: false if (sent) { - log("💓 Heartbeat OK (socket=$socketAlive, state=$currentState)") + val now = System.currentTimeMillis() + if (now - lastHeartbeatOkLogAtMs >= HEARTBEAT_OK_LOG_THROTTLE_MS) { + val suppressed = heartbeatOkSuppressedCount + heartbeatOkSuppressedCount = 0 + lastHeartbeatOkLogAtMs = now + val suffix = + if (suppressed > 0) ", +$suppressed suppressed" else "" + log("💓 Heartbeat OK (socket=$socketAlive, state=$currentState$suffix)") + } else { + heartbeatOkSuppressedCount++ + } } else { log("💔 HEARTBEAT FAILED: socket=$socketAlive, state=$currentState, manuallyClosed=$isManuallyClosed") // Триггерим reconnect если heartbeat не прошёл @@ -573,6 +602,7 @@ class Protocol( handshakeComplete = false handshakeJob?.cancel() heartbeatJob?.cancel() + heartbeatPeriodMs = 0L // Автоматический reconnect с защитой от бесконечных попыток if (!isManuallyClosed) { @@ -628,6 +658,7 @@ class Protocol( reconnectJob = null handshakeJob?.cancel() heartbeatJob?.cancel() + heartbeatPeriodMs = 0L webSocket?.close(1000, "User disconnected") webSocket = null _state.value = ProtocolState.DISCONNECTED diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index 3a160b9..41d24c0 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -6,6 +6,7 @@ import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.GroupRepository import com.rosetta.messenger.data.MessageRepository import com.rosetta.messenger.data.isPlaceholderAccountName +import com.rosetta.messenger.utils.MessageLogger import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -30,6 +31,7 @@ object ProtocolManager { private const val SYNC_REQUEST_TIMEOUT_MS = 12_000L private const val MAX_DEBUG_LOGS = 600 private const val DEBUG_LOG_FLUSH_DELAY_MS = 60L + private const val HEARTBEAT_OK_LOG_MIN_INTERVAL_MS = 5_000L private const val TYPING_INDICATOR_TIMEOUT_MS = 3_000L private const val PACKET_SIGNAL_PEER = 0x1A private const val PACKET_WEB_RTC = 0x1B @@ -61,6 +63,8 @@ object ProtocolManager { private val debugLogsLock = Any() @Volatile private var debugFlushJob: Job? = null private val debugFlushPending = AtomicBoolean(false) + @Volatile private var lastHeartbeatOkLogAtMs: Long = 0L + @Volatile private var suppressedHeartbeatOkLogs: Int = 0 // Typing status private val _typingUsers = MutableStateFlow>(emptySet()) @@ -92,8 +96,8 @@ object ProtocolManager { private fun normalizeSearchQuery(value: String): String = value.trim().removePrefix("@").lowercase(Locale.ROOT) - // UI logs are enabled by default; updates are throttled and bounded by MAX_DEBUG_LOGS. - private var uiLogsEnabled = true + // Keep heavy protocol/message UI logs disabled by default. + private var uiLogsEnabled = false private var lastProtocolState: ProtocolState? = null @Volatile private var syncBatchInProgress = false private val _syncInProgress = MutableStateFlow(false) @@ -131,9 +135,23 @@ object ProtocolManager { fun addLog(message: String) { if (!uiLogsEnabled) return + var normalizedMessage = message + val isHeartbeatOk = normalizedMessage.startsWith("💓 Heartbeat OK") + if (isHeartbeatOk) { + val now = System.currentTimeMillis() + if (now - lastHeartbeatOkLogAtMs < HEARTBEAT_OK_LOG_MIN_INTERVAL_MS) { + suppressedHeartbeatOkLogs++ + return + } + if (suppressedHeartbeatOkLogs > 0) { + normalizedMessage = "$normalizedMessage (+${suppressedHeartbeatOkLogs} skipped)" + suppressedHeartbeatOkLogs = 0 + } + lastHeartbeatOkLogAtMs = now + } val timestamp = java.text.SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()).format(Date()) - val line = "[$timestamp] $message" + val line = "[$timestamp] $normalizedMessage" synchronized(debugLogsLock) { if (debugLogsBuffer.size >= MAX_DEBUG_LOGS) { debugLogsBuffer.removeFirst() @@ -145,6 +163,7 @@ object ProtocolManager { fun enableUILogs(enabled: Boolean) { uiLogsEnabled = enabled + MessageLogger.setEnabled(enabled) if (enabled) { val snapshot = synchronized(debugLogsLock) { debugLogsBuffer.toList() } _debugLogs.value = snapshot @@ -157,6 +176,8 @@ object ProtocolManager { synchronized(debugLogsLock) { debugLogsBuffer.clear() } + suppressedHeartbeatOkLogs = 0 + lastHeartbeatOkLogAtMs = 0L _debugLogs.value = emptyList() } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ConnectionLogsScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ConnectionLogsScreen.kt index 4c93f32..d78bbc5 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ConnectionLogsScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ConnectionLogsScreen.kt @@ -40,9 +40,16 @@ fun ConnectionLogsScreen( val listState = rememberLazyListState() val scope = rememberCoroutineScope() + DisposableEffect(Unit) { + ProtocolManager.enableUILogs(true) + onDispose { + ProtocolManager.enableUILogs(false) + } + } + LaunchedEffect(logs.size) { if (logs.isNotEmpty()) { - listState.animateScrollToItem(logs.size - 1) + listState.scrollToItem(logs.size - 1) } } @@ -89,7 +96,7 @@ fun ConnectionLogsScreen( IconButton(onClick = { scope.launch { - if (logs.isNotEmpty()) listState.animateScrollToItem(logs.size - 1) + if (logs.isNotEmpty()) listState.scrollToItem(logs.size - 1) } }) { Icon( diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt index a4a100b..ea756cf 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt @@ -101,7 +101,8 @@ fun SearchScreen( protocolState: ProtocolState, onBackClick: () -> Unit, onUserSelect: (SearchUser) -> Unit, - onNavigateToCrashLogs: () -> Unit = {} + onNavigateToCrashLogs: () -> Unit = {}, + onNavigateToConnectionLogs: () -> Unit = {} ) { // Context и View для мгновенного закрытия клавиатуры val context = LocalContext.current @@ -150,6 +151,11 @@ fun SearchScreen( if (searchQuery.trim().equals("rosettadev1", ignoreCase = true)) { searchViewModel.clearSearchQuery() onNavigateToCrashLogs() + return@LaunchedEffect + } + if (searchQuery.trim().equals("rosettadev2", ignoreCase = true)) { + searchViewModel.clearSearchQuery() + onNavigateToConnectionLogs() } } diff --git a/app/src/main/java/com/rosetta/messenger/utils/MessageLogger.kt b/app/src/main/java/com/rosetta/messenger/utils/MessageLogger.kt index 037d924..19a84d8 100644 --- a/app/src/main/java/com/rosetta/messenger/utils/MessageLogger.kt +++ b/app/src/main/java/com/rosetta/messenger/utils/MessageLogger.kt @@ -13,10 +13,13 @@ import com.rosetta.messenger.network.ProtocolManager */ object MessageLogger { private const val TAG = "RosettaMsg" - - // Всегда включён — вывод идёт только в ProtocolManager.addLog() (in-memory UI), - // не в logcat, безопасно для release - private val isEnabled: Boolean = true + + @Volatile + private var isEnabled: Boolean = false + + fun setEnabled(enabled: Boolean) { + isEnabled = enabled + } /** * Добавить лог в UI (Debug Logs в чате)