Skip to content

Instantly share code, notes, and snippets.

@kibotu
Created March 11, 2025 12:39
Show Gist options
  • Save kibotu/721087055164c20b01bbf39f1159446a to your computer and use it in GitHub Desktop.
Save kibotu/721087055164c20b01bbf39f1159446a to your computer and use it in GitHub Desktop.
Threads Watchdog
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