PerformanceStatsScreen.kt (8188B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2026 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.wallet.settings 18 19 import androidx.compose.foundation.layout.Arrangement 20 import androidx.compose.foundation.layout.Box 21 import androidx.compose.foundation.layout.Row 22 import androidx.compose.foundation.layout.Spacer 23 import androidx.compose.foundation.layout.fillMaxWidth 24 import androidx.compose.foundation.layout.padding 25 import androidx.compose.foundation.layout.fillMaxSize 26 import androidx.compose.foundation.layout.size 27 import androidx.compose.foundation.lazy.LazyColumn 28 import androidx.compose.foundation.lazy.itemsIndexed 29 import androidx.compose.material.icons.Icons 30 import androidx.compose.material.icons.filled.Refresh 31 import androidx.compose.material3.Badge 32 import androidx.compose.material3.Button 33 import androidx.compose.material3.ButtonDefaults 34 import androidx.compose.material3.Icon 35 import androidx.compose.material3.ListItem 36 import androidx.compose.material3.MaterialTheme 37 import androidx.compose.material3.Text 38 import androidx.compose.runtime.Composable 39 import androidx.compose.runtime.LaunchedEffect 40 import androidx.compose.runtime.getValue 41 import androidx.compose.runtime.remember 42 import androidx.compose.ui.Alignment 43 import androidx.compose.ui.Modifier 44 import androidx.compose.ui.res.stringResource 45 import androidx.compose.ui.text.font.FontFamily 46 import androidx.compose.ui.text.font.FontWeight 47 import androidx.compose.ui.unit.dp 48 import kotlinx.serialization.json.Json 49 import net.taler.wallet.BottomInsetsSpacer 50 import net.taler.wallet.R 51 import net.taler.wallet.balances.SectionHeader 52 import net.taler.wallet.compose.EmptyComposable 53 import net.taler.wallet.compose.GlobalScaffold 54 import net.taler.wallet.compose.ShareButton 55 import net.taler.wallet.compose.TalerSurface 56 import net.taler.wallet.compose.collectAsStateLifecycleAware 57 import net.taler.wallet.main.MainViewModel 58 59 @Composable 60 fun PerformanceStatsScreen( 61 model: MainViewModel, 62 onNavigateBack: () -> Unit, 63 ) { 64 val settingsManager = model.settingsManager 65 val stats by settingsManager.performanceTable.collectAsStateLifecycleAware() 66 67 LaunchedEffect(Unit) { 68 settingsManager.loadPerformanceStats() 69 } 70 71 TalerSurface { 72 GlobalScaffold( 73 model = model, 74 modifier = Modifier.fillMaxSize(), 75 title = { Text(stringResource(R.string.performance_stats_title)) }, 76 onNavigateBack = onNavigateBack, 77 ) { paddingValues -> 78 val it = stats 79 if (it == null) { 80 EmptyComposable(Modifier.padding(paddingValues)) 81 } else { 82 PerformanceTableComposable( 83 modifier = Modifier.padding(paddingValues), 84 stats = it, 85 onReload = { settingsManager.loadPerformanceStats() }, 86 ) 87 } 88 } 89 } 90 } 91 92 @Composable 93 fun PerformanceTableComposable( 94 stats: PerformanceTable, 95 onReload: () -> Unit, 96 modifier: Modifier = Modifier, 97 ) { 98 val json = remember { Json { 99 prettyPrint = true 100 ignoreUnknownKeys = true 101 coerceInputValues = true 102 } } 103 104 LazyColumn(modifier = modifier) { 105 item { 106 Row( 107 modifier = Modifier 108 .fillMaxWidth() 109 .padding(16.dp), 110 verticalAlignment = Alignment.CenterVertically, 111 horizontalArrangement = Arrangement.SpaceAround, 112 ) { 113 ShareButton(json.encodeToString(stats)) 114 115 Button(onClick = onReload) { 116 Icon( 117 Icons.Default.Refresh, 118 contentDescription = null, 119 modifier = Modifier.size(ButtonDefaults.IconSize), 120 ) 121 Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 122 Text(stringResource(R.string.reload)) 123 } 124 } 125 } 126 127 if (stats.httpFetch.isNotEmpty()) { 128 stickyHeader { 129 SectionHeader { Text(stringResource(R.string.performance_stats_http_fetch)) } 130 } 131 132 itemsIndexed(stats.httpFetch) { i, stat -> 133 PerformanceStatItem(i + 1, stat) 134 } 135 } 136 137 if (stats.dbQuery.isNotEmpty()) { 138 stickyHeader { 139 SectionHeader { Text(stringResource(R.string.performance_stats_db_query)) } 140 } 141 142 itemsIndexed(stats.dbQuery) { i, stat -> 143 PerformanceStatItem(i + 1, stat) 144 } 145 } 146 147 148 if (stats.crypto.isNotEmpty()) { 149 stickyHeader { 150 SectionHeader { Text(stringResource(R.string.performance_stats_crypto)) } 151 } 152 153 itemsIndexed(stats.crypto) { i, stat -> 154 PerformanceStatItem(i + 1, stat) 155 } 156 } 157 158 if (stats.walletRequest.isNotEmpty()) { 159 stickyHeader { 160 SectionHeader { Text(stringResource(R.string.performance_stats_wallet_request)) } 161 } 162 163 itemsIndexed(stats.walletRequest) { i, stat -> 164 PerformanceStatItem(i + 1, stat) 165 } 166 } 167 168 if (stats.walletTask.isNotEmpty()) { 169 stickyHeader { 170 SectionHeader { Text(stringResource(R.string.performance_stats_wallet_task)) } 171 } 172 173 itemsIndexed(stats.walletTask) { i, stat -> 174 PerformanceStatItem(i + 1, stat) 175 } 176 } 177 178 item { 179 BottomInsetsSpacer() 180 } 181 } 182 } 183 184 @Composable 185 fun PerformanceStatItem( 186 ranking: Int, 187 stat: PerformanceStat, 188 ) { 189 ListItem( 190 leadingContent = { 191 Text( 192 text = stringResource(R.string.ranking, ranking), 193 fontWeight = FontWeight.ExtraBold, 194 style = MaterialTheme.typography.titleLarge, 195 ) 196 }, 197 198 headlineContent = { 199 Row( 200 modifier = Modifier.fillMaxWidth(), 201 verticalAlignment = Alignment.CenterVertically, 202 203 ) { 204 Box(Modifier.weight(1f, fill = false)) { 205 when (stat) { 206 is PerformanceStat.HttpFetch -> Text( 207 text = stat.url, 208 style = MaterialTheme.typography.bodySmall, 209 ) 210 211 is PerformanceStat.DbQuery -> Text(stat.name) 212 is PerformanceStat.Crypto -> Text(stat.operation) 213 is PerformanceStat.WalletRequest -> Text(stat.operation) 214 is PerformanceStat.WalletTask -> Text( 215 text = stat.taskId, 216 style = MaterialTheme.typography.bodySmall, 217 fontFamily = FontFamily.Monospace, 218 ) 219 } 220 } 221 222 Badge(Modifier.padding(start = 10.dp)) { 223 Text(stat.count.toString()) 224 } 225 } 226 }, 227 228 supportingContent = { 229 when(stat) { 230 is PerformanceStat.DbQuery -> Text( 231 text = stat.location, 232 style = MaterialTheme.typography.bodySmall, 233 fontFamily = FontFamily.Monospace, 234 ) 235 else -> {} 236 } 237 }, 238 239 trailingContent = { 240 Text(stringResource(R.string.millisecond, stat.maxDurationMs)) 241 }, 242 ) 243 }