mirror of
https://github.com/openflocon/Flocon.git
synced 2026-05-22 19:09:45 +00:00
feat: [PERFORMANCES] add a performances monitor
This commit is contained in:
parent
3631f0dc97
commit
f02fc5718f
8 changed files with 67 additions and 45 deletions
|
|
@ -3,16 +3,16 @@ package io.github.openflocon.flocondesktop.features.performance
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MetricEvent(
|
||||
data class MetricEventUiModel(
|
||||
val timestamp: String,
|
||||
val ramMb: String,
|
||||
val ramMb: String?,
|
||||
val fps: String,
|
||||
val jankPercentage: String,
|
||||
val battery: String,
|
||||
val screenshotPath: String?,
|
||||
)
|
||||
|
||||
fun previewMetricsEvent() = MetricEvent(
|
||||
fun previewMetricsEvent() = MetricEventUiModel(
|
||||
timestamp = "10:55:38.123",
|
||||
ramMb = "150",
|
||||
fps = "60.0",
|
||||
|
|
@ -18,7 +18,7 @@ internal sealed interface PerformanceRoutes : FloconRoute {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
data class Detail(val event: MetricEvent) : PerformanceRoutes, WindowRoute {
|
||||
data class Detail(val event: MetricEventUiModel) : PerformanceRoutes, WindowRoute {
|
||||
override val singleTopKey = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package io.github.openflocon.flocondesktop.features.performance
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.github.openflocon.domain.common.ByteFormatter
|
||||
import io.github.openflocon.domain.common.DispatcherProvider
|
||||
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
|
||||
import io.github.openflocon.domain.performance.usecase.FetchPerformanceMetricsUseCase
|
||||
import io.github.openflocon.domain.performance.usecase.GetAdbDevicesUseCase
|
||||
import io.github.openflocon.domain.performance.usecase.GetDeviceRefreshRateUseCase
|
||||
|
|
@ -10,10 +12,7 @@ import io.github.openflocon.navigation.MainFloconNavigationState
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -24,6 +23,7 @@ import java.time.format.DateTimeFormatter
|
|||
|
||||
class PerformanceViewModel(
|
||||
private val fetchPerformanceMetricsUseCase: FetchPerformanceMetricsUseCase,
|
||||
private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
|
||||
private val getAdbDevicesUseCase: GetAdbDevicesUseCase,
|
||||
private val getDeviceRefreshRateUseCase: GetDeviceRefreshRateUseCase,
|
||||
private val navigationState: MainFloconNavigationState,
|
||||
|
|
@ -42,7 +42,7 @@ class PerformanceViewModel(
|
|||
private val _intervalMs = MutableStateFlow(1000L)
|
||||
val intervalMs = _intervalMs.asStateFlow()
|
||||
|
||||
private val _metrics = MutableStateFlow<List<MetricEvent>>(emptyList())
|
||||
private val _metrics = MutableStateFlow<List<MetricEventUiModel>>(emptyList())
|
||||
val metrics = _metrics.asStateFlow()
|
||||
|
||||
private val _isMonitoring = MutableStateFlow(false)
|
||||
|
|
@ -62,6 +62,11 @@ class PerformanceViewModel(
|
|||
_selectedDevice.value = deviceList.first()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch(dispatcherProvider.viewModel) {
|
||||
getCurrentDeviceIdAndPackageNameUseCase()?.let { (_, packageName) ->
|
||||
_packageName.value = packageName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeviceSelected(deviceId: String) {
|
||||
|
|
@ -95,7 +100,7 @@ class PerformanceViewModel(
|
|||
if (deviceSerial != null) {
|
||||
refreshRate = getDeviceRefreshRateUseCase(deviceSerial)
|
||||
}
|
||||
|
||||
|
||||
while (isActive) {
|
||||
fetchMetrics()
|
||||
delay(_intervalMs.value)
|
||||
|
|
@ -124,10 +129,13 @@ class PerformanceViewModel(
|
|||
lastFrameCount = domainModel.totalFrames
|
||||
lastFetchTime = domainModel.timestamp
|
||||
|
||||
val event = MetricEvent(
|
||||
timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(domainModel.timestamp), ZoneId.systemDefault())
|
||||
val event = MetricEventUiModel(
|
||||
timestamp = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(domainModel.timestamp),
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")),
|
||||
ramMb = domainModel.ramMb,
|
||||
ramMb = domainModel.ramMb?.let { ByteFormatter.formatBytes(it) },
|
||||
fps = if (domainModel.fps > 0) String.format("%.1f", domainModel.fps) else "0",
|
||||
jankPercentage = String.format("%.1f%%", domainModel.jankPercentage),
|
||||
battery = domainModel.battery,
|
||||
|
|
@ -137,7 +145,7 @@ class PerformanceViewModel(
|
|||
_metrics.update { listOf(event) + it }
|
||||
}
|
||||
|
||||
fun onEventClicked(event: MetricEvent) {
|
||||
fun onEventClicked(event: MetricEventUiModel) {
|
||||
navigationState.navigate(PerformanceRoutes.Detail(event))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import io.github.openflocon.flocondesktop.common.ui.isInPreview
|
||||
import io.github.openflocon.flocondesktop.features.performance.MetricEvent
|
||||
import io.github.openflocon.flocondesktop.features.performance.MetricEventUiModel
|
||||
import io.github.openflocon.flocondesktop.features.performance.previewMetricsEvent
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconCircularProgressIndicator
|
||||
import io.github.openflocon.library.designsystem.components.FloconSurface
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
|
|
@ -29,8 +28,8 @@ private val imageSize = 40.dp
|
|||
|
||||
@Composable
|
||||
internal fun MetricItemView(
|
||||
event: MetricEvent,
|
||||
onClick: (MetricEvent) -> Unit,
|
||||
event: MetricEventUiModel,
|
||||
onClick: (MetricEventUiModel) -> Unit,
|
||||
) {
|
||||
val bodySmall = FloconTheme.typography.bodySmall.copy(fontSize = 11.sp)
|
||||
|
||||
|
|
@ -81,7 +80,7 @@ internal fun MetricItemView(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text("RAM: ${event.ramMb} MB", style = bodySmall)
|
||||
event.ramMb?.let { Text("RAM: $it", style = bodySmall) }
|
||||
Text("Battery: ${event.battery}", style = bodySmall)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import io.github.openflocon.flocondesktop.features.performance.MetricEvent
|
||||
import io.github.openflocon.flocondesktop.features.performance.MetricEventUiModel
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PerformanceDetailScreen(
|
||||
event: MetricEvent,
|
||||
event: MetricEventUiModel,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
|
@ -62,7 +62,7 @@ fun PerformanceDetailScreen(
|
|||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MetricText(label = "RAM Usage", value = "${event.ramMb} MB")
|
||||
event.ramMb?.let { MetricText(label = "RAM Usage", value = "$it MB") }
|
||||
MetricText(label = "FPS", value = event.fps)
|
||||
MetricText(label = "Jank Percentage", value = event.jankPercentage)
|
||||
MetricText(label = "Battery", value = event.battery)
|
||||
|
|
@ -113,7 +113,7 @@ private fun MetricText(label: String, value: String) {
|
|||
private fun PerformanceDetailScreenPreview() {
|
||||
FloconTheme {
|
||||
PerformanceDetailScreen(
|
||||
event = MetricEvent(
|
||||
event = MetricEventUiModel(
|
||||
timestamp = "10:55:38.123",
|
||||
ramMb = "150",
|
||||
fps = "60.0",
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
package io.github.openflocon.flocondesktop.features.performance.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
|
|
@ -12,16 +14,13 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import io.github.openflocon.flocondesktop.features.performance.MetricEvent
|
||||
import io.github.openflocon.flocondesktop.features.performance.PerformanceViewModel
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.*
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun PerformanceScreen() {
|
||||
|
|
@ -33,7 +32,7 @@ fun PerformanceScreen() {
|
|||
val metrics by viewModel.metrics.collectAsStateWithLifecycle()
|
||||
val isMonitoring by viewModel.isMonitoring.collectAsStateWithLifecycle()
|
||||
|
||||
FloconScaffold() { paddingValues ->
|
||||
FloconScaffold { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -54,7 +53,8 @@ fun PerformanceScreen() {
|
|||
onExpandedChange = { expanded = it }
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
text = selectedDevice ?: "Select Device",
|
||||
//onValueChange = {},
|
||||
//readOnly = true,
|
||||
|
|
@ -137,17 +137,34 @@ fun PerformanceScreen() {
|
|||
|
||||
HorizontalDivider()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.clip(FloconTheme.shapes.medium)
|
||||
.background(FloconTheme.colorPalette.primary),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
items(metrics) { event ->
|
||||
MetricItemView(
|
||||
event = event,
|
||||
onClick = viewModel::onEventClicked
|
||||
)
|
||||
val listState = rememberLazyListState()
|
||||
val scrollAdapter = rememberFloconScrollbarAdapter(listState)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(metrics) { event ->
|
||||
MetricItemView(
|
||||
event = event,
|
||||
onClick = viewModel::onEventClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
FloconVerticalScrollbar(
|
||||
adapter = scrollAdapter,
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package io.github.openflocon.domain.performance.model
|
||||
|
||||
data class PerformanceMetricsDomainModel(
|
||||
val ramMb: String,
|
||||
val ramMb: Long?,
|
||||
val fps: Double,
|
||||
val jankPercentage: Double,
|
||||
val battery: String,
|
||||
|
|
|
|||
|
|
@ -7,19 +7,17 @@ import io.github.openflocon.domain.common.getOrNull
|
|||
class GetRamUsageUseCase(
|
||||
private val executeAdbCommandUseCase: ExecuteAdbCommandUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(deviceSerial: String, packageName: String): String {
|
||||
if (packageName.isEmpty()) return "N/A"
|
||||
suspend operator fun invoke(deviceSerial: String, packageName: String): Long? {
|
||||
if (packageName.isEmpty()) return null
|
||||
return executeAdbCommandUseCase(
|
||||
target = AdbCommandTargetDomainModel.DeviceSerial(deviceSerial),
|
||||
command = "shell dumpsys meminfo $packageName"
|
||||
).mapSuccess { output ->
|
||||
val pssRegex = Regex("(?:TOTAL PSS:|TOTAL)\\s+(\\d+)", RegexOption.IGNORE_CASE)
|
||||
val pssKb = pssRegex.find(output)?.groupValues?.get(1)?.toLongOrNull()
|
||||
if (pssKb != null) {
|
||||
(pssKb / 1024).toString()
|
||||
} else {
|
||||
"N/A"
|
||||
pssKb?.let {
|
||||
it * 1024
|
||||
}
|
||||
}.getOrNull() ?: "N/A"
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue