Created
March 11, 2025 12:39
-
-
Save kibotu/721087055164c20b01bbf39f1159446a to your computer and use it in GitHub Desktop.
Threads Watchdog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ThreadsWatchDogInitializer: Initializer<Unit> { | |
@Inject lateinit var threads : ThreadsRepository | |
override fun create(context: Context) { | |
C24CoreApplication.inject(this) | |
threads.start() | |
} | |
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(DependencyInitializer::class.java) | |
} | |
class ThreadsRepository { | |
val threads = MutableStateFlow<List<ThreadInfo>>(emptyList()) | |
init { | |
start() | |
} | |
fun start() { | |
CoreServices.services.applicationScope.launch(Dispatchers.Default) { | |
while (true) { | |
threads.value = Thread.getAllStackTraces().keys.map { | |
ThreadInfo( | |
id = it.id, | |
name = it.name, | |
state = it.state, | |
priority = it.priority, | |
isDaemon = it.isDaemon, | |
stackTrace = it.stackTrace, | |
isAlive = it.isAlive, | |
isInterrupted = it.isInterrupted, | |
threadGroup = it.threadGroup?.name, | |
threadGroupIsDaemon = it.threadGroup?.isDaemon | |
) | |
} | |
delay(16) | |
} | |
} | |
} | |
} | |
data class ThreadInfo( | |
val id: Long, | |
val name: String, | |
val state: Thread.State, | |
val priority: Int, | |
val isDaemon : Boolean, | |
val stackTrace: Array<StackTraceElement>, | |
val isAlive: Boolean, | |
val isInterrupted: Boolean, | |
val threadGroup: String?, | |
val threadGroupIsDaemon: Boolean?, | |
) | |
class ThreadWatchdogViewModel @Inject constructor(private val threadsRepository: ThreadsRepository) : ViewModel() { | |
val state = ThreadWatchdogStateHolder() | |
init { | |
viewModelScope.launch { | |
threadsRepository.threads.collectLatest { | |
state.threads.value = it | |
} | |
} | |
} | |
} | |
private enum class DaemonState { | |
IS_DAEMON, | |
IS_NOT_A_DAEMON | |
} | |
@OptIn(ExperimentalMaterialApi::class) | |
@Composable | |
fun ThreadWatchdogScreen(modifier: Modifier = Modifier, state: ThreadWatchdogStateHolder) { | |
var selectedThread by remember { mutableStateOf<ThreadInfo?>(null) } | |
// Filter states | |
var daemonState = remember { mutableStateOf<String?>(null) } | |
var isAlive by remember { mutableStateOf(true) } | |
var selectedState = remember { mutableStateOf<String?>(null) } | |
var sortBy = remember { mutableStateOf<String?>("Name") } // Options: "name", "priority", "isDaemon" | |
// Filtered and Sorted Thread List | |
val filteredThreads = state | |
.threads | |
.value | |
.filter { | |
when (daemonState.value) { | |
DaemonState.IS_DAEMON.name.snakeCaseToWords() -> it.isDaemon | |
DaemonState.IS_NOT_A_DAEMON.name.snakeCaseToWords() -> !it.isDaemon | |
else -> true | |
} | |
} | |
.filter { it.isAlive == isAlive } | |
.filter { selectedState.value == null || it.state.name.snakeCaseToWords() == selectedState.value } | |
.sortedWith(when (sortBy.value) { | |
"Priority" -> compareByDescending<ThreadInfo> { it.priority }.thenBy { it.name } | |
"Is Daemon" -> compareByDescending<ThreadInfo> { it.isDaemon }.thenBy { it.name } | |
else -> compareBy { it.name } | |
}) | |
Surface( | |
modifier = modifier | |
.fillMaxSize() | |
.background(MaterialTheme.colors.surface) | |
) { | |
Column { | |
Text( | |
modifier = Modifier | |
.clickable { | |
val threads = Thread.getAllStackTraces().keys.sortedBy { it.name }.joinToString("\n") | |
ProfiLogger.v("Threads", threads) | |
} | |
.padding(16.dp), | |
text = "Threads: ${filteredThreads.size}" | |
) | |
// Filter Chips Row | |
LazyRow( | |
modifier = Modifier | |
.fillMaxWidth(), | |
horizontalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
item { | |
DropDownFilterChip( | |
modifier = Modifier.padding(start = 8.dp), | |
selectedState, | |
options = Thread.State.entries.map { it.name.snakeCaseToWords() }, | |
empty = "State" | |
) | |
} | |
item { | |
DropDownFilterChip( | |
selectedState = daemonState, | |
options = DaemonState.entries.map { it.name.snakeCaseToWords() }, | |
empty = "Daemon" | |
) | |
} | |
item { | |
DropDownFilterChip( | |
selectedState = sortBy, | |
options = listOf("Priority", "Is Daemon"), | |
empty = "Name", | |
prefix = "Sort by: " | |
) | |
} | |
item { | |
FilterChip( | |
selected = isAlive, | |
onClick = { isAlive = !isAlive }, | |
text = "Alive", | |
) | |
} | |
} | |
LazyColumn( | |
modifier = Modifier.weight(1f), | |
contentPadding = PaddingValues(16.dp), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
items(filteredThreads) { | |
CompactThreadItem(thread = it) { | |
selectedThread = it | |
} | |
} | |
} | |
} | |
} | |
selectedThread?.let { threadInfo -> | |
ThreadInfoScreenDialog( | |
threadInfo = threadInfo, | |
onDismiss = { selectedThread = null } | |
) | |
} | |
} | |
@Composable | |
fun CompactThreadItem( | |
thread: ThreadInfo, | |
modifier: Modifier = Modifier, | |
onClick: () -> Unit | |
) { | |
Card( | |
modifier = modifier | |
.fillMaxWidth() | |
.clickable(onClick = onClick) | |
.height(48.dp), | |
elevation = 0.dp, | |
shape = RoundedCornerShape(4.dp), | |
backgroundColor = if (thread.isDaemon) { | |
MaterialTheme.colors.primary.copy(alpha = 0.08f) | |
} else { | |
MaterialTheme.colors.surface | |
}, | |
border = BorderStroke( | |
width = 1.dp, | |
color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) | |
) | |
) { | |
val threadGroupName = if (thread.threadGroup == "main") { | |
"" | |
} else { | |
"- ${thread.threadGroup}" | |
} | |
Row( | |
modifier = Modifier | |
.padding(horizontal = 12.dp, vertical = 8.dp), | |
verticalAlignment = Alignment.CenterVertically, | |
horizontalArrangement = Arrangement.SpaceBetween | |
) { | |
Text( | |
text = "${thread.name} ${threadGroupName}", | |
style = MaterialTheme.typography.body2, | |
color = MaterialTheme.colors.onSurface, | |
maxLines = 2, | |
overflow = TextOverflow.Ellipsis, | |
modifier = Modifier.weight(1f) | |
) | |
Spacer(modifier = Modifier.width(8.dp)) | |
Text( | |
text = "${thread.state} (${thread.priority})", | |
style = MaterialTheme.typography.caption, | |
color = if (thread.isDaemon) { | |
MaterialTheme.colors.onSurface.copy(alpha = 0.8f) | |
} else { | |
MaterialTheme.colors.onSurface.copy(alpha = 0.7f) | |
}, | |
maxLines = 1 | |
) | |
} | |
} | |
} | |
@OptIn(ExperimentalMaterialApi::class) | |
@Composable | |
fun DropDownFilterChip(modifier: Modifier = Modifier, selectedState: MutableState<String?>, options: List<String>, empty: String, prefix: String = "") { | |
var isStateMenuExpanded by remember { mutableStateOf(false) } | |
Box(modifier = modifier) { | |
Chip( | |
onClick = { isStateMenuExpanded = true }, | |
colors = ChipDefaults.chipColors( | |
backgroundColor = if (selectedState.value != null) | |
MaterialTheme.colors.primary.copy(alpha = 0.12f) | |
else MaterialTheme.colors.surface, | |
contentColor = MaterialTheme.colors.onSurface | |
), | |
border = BorderStroke( | |
width = 1.dp, | |
color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) | |
) | |
) { | |
Text("${prefix}${selectedState.value ?: empty}") | |
} | |
DropdownMenu( | |
expanded = isStateMenuExpanded, | |
onDismissRequest = { isStateMenuExpanded = false } | |
) { | |
// Add "All" option to clear filter | |
DropdownMenuItem( | |
onClick = { | |
selectedState.value = null | |
isStateMenuExpanded = false | |
} | |
) { | |
Text(empty) | |
} | |
// Add all Thread.State options | |
options.forEach { state -> | |
DropdownMenuItem( | |
onClick = { | |
selectedState.value = state | |
isStateMenuExpanded = false | |
} | |
) { | |
Text(state) | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment