Measure uasync CPU load and display it in desktop and Android GUIs
Sample thread CPU usage without waking idle loops, expose a one-second moving average, and refresh compact GUI indicators at 3 Hz while visible on Android. Add load and bridge regression coverage, streaming notes, and task list cleanup.
This commit is contained in:
1 parent
8bd6e4fb2d
commit
5a9e61164b
25 files changed
+692
-587
No files matched your search
@@ -358,11 +358,14 @@ class MainActivity : AppCompatActivity() {
|
||||
val channels by vm.channels.collectAsState()
|
||||
val dmConversations by vm.dmConversations.collectAsState()
|
||||
val stuck by vm.isStuck.collectAsState()
|
||||
val cpuLoad by vm.cpuLoad.collectAsState()
|
||||
ChannelListScreen(
|
||||
channels = channels,
|
||||
dmConversations = dmConversations,
|
||||
connected = connected,
|
||||
isStuck = stuck,
|
||||
cpuLoad = cpuLoad,
|
||||
onCpuLoadVisible = vm::setCpuLoadVisible,
|
||||
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat },
|
||||
onSelectDm = { dm -> vm.openDm(dm.convId, dm.peerNodeId, dm.peerName); screen = Screen.Dm },
|
||||
onCreateChannel = { name -> vm.createChannel(name, "channel_1") },
|
||||
|
||||
@@ -84,6 +84,7 @@ object NativeLib {
|
||||
fun requestInviteLink(channelId: String, targetNodeId: Long, requestId: Long) { nativeRequestInviteLink(channelId, targetNodeId, requestId) }
|
||||
|
||||
fun ping() { nativePing() }
|
||||
fun requestCpuLoad(): Boolean = nativeRequestCpuLoad()
|
||||
fun isResponsive(): Boolean = nativeIsResponsive()
|
||||
|
||||
/* ── App foreground/background (Android lifecycle) ── */
|
||||
@@ -253,6 +254,7 @@ object NativeLib {
|
||||
private external fun nativeInit(dbPath: String, controlPort: Int, config: Any, logCb: Any): Boolean
|
||||
private external fun nativeStart(configText: String): Boolean
|
||||
private external fun nativeStop()
|
||||
private external fun nativeRequestCpuLoad(): Boolean
|
||||
private external fun nativeSetEventCallback(cb: Any)
|
||||
private external fun nativeDestroy()
|
||||
private external fun nativeSendMessage(channelId: String, text: ByteArray, replyTo: ByteArray, requestId: Long): Boolean
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.utun.chat.ui.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/* Градиент привязан ко всей ширине: низкая нагрузка зелёная, полная доходит до красного. */
|
||||
@Composable
|
||||
fun CpuLoadIndicator(percent: Int, modifier: Modifier = Modifier) {
|
||||
val value = percent.coerceIn(0, 100)
|
||||
Box(modifier.size(60.dp, 24.dp).semantics { contentDescription = "CPU $value%" }, contentAlignment = Alignment.Center) {
|
||||
Canvas(Modifier.matchParentSize()) {
|
||||
val shape = Path().apply { addRoundRect(RoundRect(0f, 0f, size.width, size.height, CornerRadius(5.dp.toPx()))) }
|
||||
clipPath(shape) {
|
||||
drawRect(Color(0xFFE4E9E6))
|
||||
clipRect(right = size.width * value / 100f) {
|
||||
drawRect(Brush.horizontalGradient(listOf(Color(0xFF66BB6A), Color(0xFFFFCA28), Color(0xFFEF5350)), endX = size.width))
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("$value%", color = Color(0xFF172019), fontSize = 12.sp, fontWeight = FontWeight.Bold, maxLines = 1)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import com.utun.chat.ui.components.PeerStatusDot
|
||||
import com.utun.chat.ui.components.CpuLoadIndicator
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import com.utun.chat.ui.components.GroupAvatar
|
||||
import com.utun.chat.ui.components.UserAvatar
|
||||
import com.utun.chat.ui.components.GroupPropertiesDialog
|
||||
@@ -43,6 +45,8 @@ fun ChannelListScreen(
|
||||
dmConversations: List<DmConversation> = emptyList(),
|
||||
connected: Boolean,
|
||||
isStuck: Boolean = false,
|
||||
cpuLoad: Int = 0,
|
||||
onCpuLoadVisible: (Boolean) -> Unit = {},
|
||||
onSelectChannel: (Channel) -> Unit,
|
||||
onSelectDm: (DmConversation) -> Unit = {},
|
||||
onCreateChannel: (String) -> Unit,
|
||||
@@ -53,6 +57,10 @@ fun ChannelListScreen(
|
||||
onLogs: () -> Unit,
|
||||
onRefresh: () -> Unit = {}
|
||||
) {
|
||||
LifecycleResumeEffect(Unit) {
|
||||
onCpuLoadVisible(true)
|
||||
onPauseOrDispose { onCpuLoadVisible(false) }
|
||||
}
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
var menuChannelId by remember { mutableStateOf<String?>(null) }
|
||||
var optionsChannel by remember { mutableStateOf<Channel?>(null) }
|
||||
@@ -81,6 +89,7 @@ fun ChannelListScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CpuLoadIndicator(cpuLoad, Modifier.padding(end = 12.dp))
|
||||
Text(
|
||||
text = "v${BuildConfig.VERSION_NAME}",
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
|
||||
@@ -250,6 +250,13 @@ class ChatViewModel : ViewModel() {
|
||||
private val _isStuck = MutableStateFlow(false)
|
||||
val isStuck: StateFlow<Boolean> = _isStuck
|
||||
|
||||
private val _cpuLoad = MutableStateFlow(0)
|
||||
val cpuLoad: StateFlow<Int> = _cpuLoad
|
||||
private val _cpuLoadVisible = MutableStateFlow(false)
|
||||
private var cpuLoadPending = false
|
||||
|
||||
fun setCpuLoadVisible(visible: Boolean) { _cpuLoadVisible.value = visible }
|
||||
|
||||
private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap())
|
||||
val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses
|
||||
|
||||
@@ -324,6 +331,21 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
_cpuLoadVisible.collectLatest { visible ->
|
||||
if (!visible) return@collectLatest
|
||||
LogManager.addLog("DEBUG", "CPU", "GUI polling started: period=333ms")
|
||||
try {
|
||||
while (isActive) {
|
||||
if (!NativeLib.isReady()) { cpuLoadPending = false; _cpuLoad.value = 0 }
|
||||
else if (!cpuLoadPending) cpuLoadPending = NativeLib.requestCpuLoad()
|
||||
delay(333)
|
||||
}
|
||||
} finally {
|
||||
LogManager.addLog("DEBUG", "CPU", "GUI polling stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
AppEventHandler.events.collect { (type, data) -> handleEvent(type, data) }
|
||||
}
|
||||
@@ -386,11 +408,21 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
private fun handleEvent(type: Int, data: ByteArray?) {
|
||||
if (type == 13) {
|
||||
cpuLoadPending = false; _cpuLoad.value = 0
|
||||
dmPresenceRefreshJob?.cancel()
|
||||
_dmCallAvailable.value = false
|
||||
_dmConversations.value = _dmConversations.value.map { it.copy(online = false) }
|
||||
} else if (type in listOf(4, 5, 12, 16, 17, 24, 25, 31, 33)) refreshDmCallRoute()
|
||||
when (type) {
|
||||
57 -> { /* CPU_LOAD: valid:1, average_percent:1. */
|
||||
cpuLoadPending = false
|
||||
if (data == null || data.size != 2 || data[0].toInt() !in 0..1 || data[1].toInt() !in 0..100) {
|
||||
LogManager.addLog("ERROR", "CPU", "Invalid CPU load response: size=${data?.size}")
|
||||
_cpuLoad.value = 0
|
||||
return
|
||||
}
|
||||
if (_cpuLoadVisible.value) _cpuLoad.value = if (data[0].toInt() == 1) data[1].toInt() else 0
|
||||
}
|
||||
56 -> { /* MSG_SUBMITTED: request_id:8LE, success. */
|
||||
if (data == null || data.size != 9) { LogManager.addLog("ERROR", "Reply", "invalid submission result"); return }
|
||||
val id = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN).long
|
||||
|
||||
Reference in new issue
Block a user