From 1ff6c50f8584c0a4808ca4291475ad52986148d7 Mon Sep 17 00:00:00 2001 From: Artem Bazhanov Date: Fri, 17 Oct 2025 03:58:52 +0300 Subject: [PATCH 1/8] Fix detekt findings for overlay performance metrics --- README.md | 2 +- module/logging/overlay-stub/build.gradle.kts | 1 + .../kick/module/overlay/OverlayModule.kt | 9 +- .../overlay/core/provider/OverlayProvider.kt | 14 +++ .../provider/PerformanceOverlayProvider.kt | 20 ++++ module/logging/overlay/build.gradle.kts | 1 + .../kick/module/overlay/OverlayUiTest.kt | 2 +- .../core/overlay/KickOverlay.android.kt | 2 +- .../provider/PerformanceSnapshot.android.kt | 98 ++++++++++++++++ .../ru/bartwell/kick/module/overlay/Kick.kt | 3 +- .../kick/module/overlay/OverlayModule.kt | 50 ++++++++- .../overlay/core/overlay/KickOverlay.kt | 2 +- .../overlay/core/provider/OverlayProvider.kt | 14 +++ .../provider/PerformanceOverlayProvider.kt | 99 ++++++++++++++++ .../core/provider/PerformanceSnapshot.kt | 9 ++ .../module/overlay/core/store/OverlayStore.kt | 65 +++++++++-- .../overlay/core/overlay/KickOverlay.ios.kt | 6 +- .../core/provider/PerformanceSnapshot.ios.kt | 106 ++++++++++++++++++ .../overlay/core/overlay/KickOverlay.jvm.kt | 2 +- .../core/provider/PerformanceSnapshot.jvm.kt | 22 ++++ .../overlay/OverlayCategoriesJvmTest.kt | 29 ++++- .../kick/module/overlay/OverlayJvmTest.kt | 2 +- .../core/overlay/KickOverlay.wasmJs.kt | 2 +- .../provider/PerformanceSnapshot.wasmJs.kt | 4 + .../kick/module/overlay/OverlayWasmJsTest.kt | 2 +- 25 files changed, 533 insertions(+), 33 deletions(-) create mode 100644 module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt create mode 100644 module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt create mode 100644 module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt create mode 100644 module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt create mode 100644 module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt create mode 100644 module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.kt create mode 100644 module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt create mode 100644 module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.jvm.kt create mode 100644 module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.wasmJs.kt diff --git a/README.md b/README.md index 58f8aa1..3c523b6 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,7 @@ Kick.overlay.set("isWsConnected", true) You can also show/hide the panel programmatically if needed: ```kotlin -Kick.overlay.show(context) // show floating panel +Kick.overlay.show() // show floating panel Kick.overlay.hide() // hide it ``` diff --git a/module/logging/overlay-stub/build.gradle.kts b/module/logging/overlay-stub/build.gradle.kts index 43088b0..9ee0a4c 100644 --- a/module/logging/overlay-stub/build.gradle.kts +++ b/module/logging/overlay-stub/build.gradle.kts @@ -49,6 +49,7 @@ kotlin { implementation(libs.decompose) implementation(libs.decompose.extensions.compose) implementation(libs.decompose.essenty.lifecycle.coroutines) + implementation(libs.kotlinx.coroutines.core) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 0af7f72..6244655 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -10,9 +10,14 @@ import ru.bartwell.kick.core.component.StubConfig import ru.bartwell.kick.core.data.Module import ru.bartwell.kick.core.data.ModuleDescription import ru.bartwell.kick.core.data.PlatformContext +import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider +import ru.bartwell.kick.module.overlay.core.provider.PerformanceOverlayProvider -@Suppress("UnusedPrivateProperty", "EmptyFunctionBlock") -public class OverlayModule(context: PlatformContext) : Module { +@Suppress("UnusedPrivateProperty", "EmptyFunctionBlock", "UNUSED_PARAMETER") +public class OverlayModule( + context: PlatformContext, + providers: List = listOf(PerformanceOverlayProvider()), +) : Module { override val description: ModuleDescription = ModuleDescription.OVERLAY override val startConfig: Config = StubConfig(description) diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt new file mode 100644 index 0000000..407ece7 --- /dev/null +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt @@ -0,0 +1,14 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope +import ru.bartwell.kick.module.overlay.OverlayAccessor + +public interface OverlayProvider { + public val categories: Set + + public fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle +} + +public fun interface OverlayProviderHandle { + public fun stop() +} diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt new file mode 100644 index 0000000..a1567a4 --- /dev/null +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt @@ -0,0 +1,20 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope +import ru.bartwell.kick.module.overlay.OverlayAccessor + +public class PerformanceOverlayProvider : OverlayProvider { + override val categories: Set = setOf(CATEGORY) + + override fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle { + return OverlayProviderHandle {} + } + + public companion object { + private const val KEY_SEPARATOR: String = "::" + public const val CATEGORY: String = "Performance" + public const val CPU_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "CPU" + public const val MEMORY_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "RAM" + public const val CUSTOM_KEY_PREFIX: String = CATEGORY + KEY_SEPARATOR + } +} diff --git a/module/logging/overlay/build.gradle.kts b/module/logging/overlay/build.gradle.kts index 5f04549..1edcf9f 100644 --- a/module/logging/overlay/build.gradle.kts +++ b/module/logging/overlay/build.gradle.kts @@ -51,6 +51,7 @@ kotlin { implementation(libs.decompose) implementation(libs.decompose.extensions.compose) implementation(libs.decompose.essenty.lifecycle.coroutines) + implementation(libs.kotlinx.coroutines.core) implementation(libs.settings) implementation(libs.settings.noArg) } diff --git a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt index 62f9f4d..96b5e44 100644 --- a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt +++ b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt @@ -81,7 +81,7 @@ class OverlayUiTest { component = DefaultOverlayComponent( componentContext = DefaultComponentContext(LifecycleRegistry()), onEnabledChangeCallback = { enabled -> - if (enabled) KickOverlay.show(platformContext) else KickOverlay.hide() + if (enabled) KickOverlay.show() else KickOverlay.hide() }, onBackCallback = {} ) diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt index 9195140..9c8ab45 100644 --- a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt @@ -20,7 +20,7 @@ public actual object KickOverlay { installed = true } - public actual fun show(context: PlatformContext) { + public actual fun show() { OverlaySettings.setEnabled(true) callbacks.currentActivity.get()?.let { callbacks.attach(it) } } diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt new file mode 100644 index 0000000..f7180d2 --- /dev/null +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt @@ -0,0 +1,98 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import java.io.File +import java.io.RandomAccessFile + +private const val PROC_STAT_PATH: String = "/proc/stat" +private const val PROC_MEMINFO_PATH: String = "/proc/meminfo" +private const val CPU_TOKEN_PREFIX: String = "cpu" +private const val CPU_MIN_TOKEN_COUNT: Int = 5 +private const val CPU_IDLE_INDEX: Int = 3 +private const val CPU_IOWAIT_INDEX: Int = 4 +private const val CPU_PERCENT_FACTOR: Double = 100.0 +private const val SPACE_DELIMITER: String = " " +private const val KEY_VALUE_DELIMITER: String = ":" +private const val KIB_IN_BYTES: Long = 1024L + +private data class CpuTimes(val idle: Long, val total: Long) + +private var previousCpuTimes: CpuTimes? = null + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { + val cpuUsage = readCpuUsage() + val memoryInfo = readMemoryInfo() + return PerformanceSnapshot( + cpuUsagePercent = cpuUsage, + usedMemoryBytes = memoryInfo?.used, + totalMemoryBytes = memoryInfo?.total, + ) +} + +private fun readCpuUsage(): Double? = + runCatching { + RandomAccessFile(PROC_STAT_PATH, "r").use { reader -> + val values = reader.readLine() + ?.split(SPACE_DELIMITER) + ?.filter { it.isNotBlank() } + ?.takeIf { it.size >= CPU_MIN_TOKEN_COUNT && it.first() == CPU_TOKEN_PREFIX } + ?.drop(1) + ?.mapNotNull(String::toLongOrNull) + + if (values.isNullOrEmpty()) { + null + } else { + val idle = (values.getOrNull(CPU_IDLE_INDEX) ?: 0L) + + (values.getOrNull(CPU_IOWAIT_INDEX) ?: 0L) + val total = values.sum() + + val current = CpuTimes(idle = idle, total = total) + val previous = previousCpuTimes + previousCpuTimes = current + + previous?.let { + val deltaIdle = idle - it.idle + val deltaTotal = total - it.total + if (deltaTotal > 0) { + (1.0 - deltaIdle.toDouble() / deltaTotal.toDouble()) * CPU_PERCENT_FACTOR + } else { + null + } + } + } + } + }.getOrNull() + +private data class MemoryInfo(val used: Long?, val total: Long?) + +private fun readMemoryInfo(): MemoryInfo? = + runCatching { + val memInfo = File(PROC_MEMINFO_PATH) + if (!memInfo.exists()) { + return@runCatching null + } + + val values = mutableMapOf() + memInfo.useLines { sequence -> + sequence.forEach { line -> + val parts = line.split(KEY_VALUE_DELIMITER, limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim().split(SPACE_DELIMITER).firstOrNull()?.toLongOrNull() + if (value != null) { + values[key] = value * KIB_IN_BYTES + } + } + } + } + + val total = values["MemTotal"] + val available = values["MemAvailable"] ?: run { + val free = values["MemFree"] ?: 0L + val buffers = values["Buffers"] ?: 0L + val cached = values["Cached"] ?: 0L + free + buffers + cached + } + + val used = if (total != null && available != null) total - available else null + MemoryInfo(used = used, total = total) + }.getOrNull() diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt index c78ba83..b0fe0ef 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt @@ -1,7 +1,6 @@ package ru.bartwell.kick.module.overlay import ru.bartwell.kick.Kick -import ru.bartwell.kick.core.data.PlatformContext import ru.bartwell.kick.module.overlay.core.overlay.KickOverlay import ru.bartwell.kick.module.overlay.core.store.OverlayStore @@ -27,6 +26,6 @@ public object OverlayAccessor { public fun set(key: String, value: String, category: String) { OverlayStore.set(key, value, category) } public fun set(key: String, value: Boolean, category: String) { OverlayStore.set(key, value.toString(), category) } - public fun show(context: PlatformContext) { KickOverlay.show(context) } + public fun show() { KickOverlay.show() } public fun hide() { KickOverlay.hide() } } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 6f416ae..6ef07f3 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -6,7 +6,13 @@ import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.pop +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.serialization.modules.PolymorphicModuleBuilder +import ru.bartwell.kick.Kick import ru.bartwell.kick.core.component.Child import ru.bartwell.kick.core.component.Config import ru.bartwell.kick.core.data.Module @@ -16,24 +22,62 @@ import ru.bartwell.kick.module.overlay.core.component.child.OverlayChild import ru.bartwell.kick.module.overlay.core.component.config.OverlayConfig import ru.bartwell.kick.module.overlay.core.overlay.KickOverlay import ru.bartwell.kick.module.overlay.core.persists.OverlaySettings +import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider +import ru.bartwell.kick.module.overlay.core.provider.OverlayProviderHandle +import ru.bartwell.kick.module.overlay.core.provider.PerformanceOverlayProvider import ru.bartwell.kick.module.overlay.core.store.OverlayStore import ru.bartwell.kick.module.overlay.feature.settings.presentation.DefaultOverlayComponent import ru.bartwell.kick.module.overlay.feature.settings.presentation.OverlayContent -public class OverlayModule(private val context: PlatformContext) : Module { +public class OverlayModule( + private val context: PlatformContext, + private val providers: List = listOf(PerformanceOverlayProvider()), +) : Module { override val description: ModuleDescription = ModuleDescription.OVERLAY override val startConfig: Config = OverlayConfig + private val providerScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val activeProviderHandles: MutableMap = mutableMapOf() + init { OverlaySettings(context) // Initialize selected category from persisted settings OverlayStore.selectCategory(OverlaySettings.getSelectedCategory()) KickOverlay.init(context) if (OverlaySettings.isEnabled()) { - KickOverlay.show(context) + KickOverlay.show() + } + + OverlayStore.declareCategories(providers.flatMap { it.categories }) + + providerScope.launch { + OverlayStore.selectedCategory.collectLatest { category -> + updateProvidersFor(category) + } } } + private fun updateProvidersFor(selectedCategory: String) { + providers.forEach { provider -> + val active = activeProviderHandles[provider] + if (provider.categories.contains(selectedCategory)) { + if (active?.category != selectedCategory) { + active?.handle?.stop() + val handle = provider.start(providerScope, Kick.overlay, selectedCategory) + activeProviderHandles[provider] = ActiveProvider(selectedCategory, handle) + } + } else if (active != null) { + active.handle.stop() + activeProviderHandles.remove(provider) + } + } + } + + private data class ActiveProvider( + val category: String, + val handle: OverlayProviderHandle, + ) + override fun getComponent( componentContext: ComponentContext, nav: StackNavigation, @@ -42,7 +86,7 @@ public class OverlayModule(private val context: PlatformContext) : Module { OverlayChild( DefaultOverlayComponent( componentContext = componentContext, - onEnabledChangeCallback = { enabled -> if (enabled) KickOverlay.show(context) else KickOverlay.hide() }, + onEnabledChangeCallback = { enabled -> if (enabled) KickOverlay.show() else KickOverlay.hide() }, onBackCallback = { nav.pop() }, ) ) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt index 9680358..1f87acf 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt @@ -4,6 +4,6 @@ import ru.bartwell.kick.core.data.PlatformContext public expect object KickOverlay { public fun init(context: PlatformContext) - public fun show(context: PlatformContext) + public fun show() public fun hide() } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt new file mode 100644 index 0000000..407ece7 --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt @@ -0,0 +1,14 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope +import ru.bartwell.kick.module.overlay.OverlayAccessor + +public interface OverlayProvider { + public val categories: Set + + public fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle +} + +public fun interface OverlayProviderHandle { + public fun stop() +} diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt new file mode 100644 index 0000000..0b20975 --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt @@ -0,0 +1,99 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import ru.bartwell.kick.module.overlay.OverlayAccessor +import kotlin.math.roundToInt + +public class PerformanceOverlayProvider( + private val updateIntervalMillis: Long = 1_000, +) : OverlayProvider { + + override val categories: Set = setOf(CATEGORY) + + override fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle { + require(category == CATEGORY) { "PerformanceOverlayProvider started for unexpected category: $category" } + + overlay.set(CPU_USAGE_KEY, NOT_AVAILABLE_VALUE) + overlay.set(MEMORY_USAGE_KEY, NOT_AVAILABLE_VALUE) + + val job = scope.launch { + while (isActive) { + val snapshot = readPerformanceSnapshot() + overlay.set(CPU_USAGE_KEY, snapshot.cpuUsagePercent?.let(::formatPercent) ?: NOT_AVAILABLE_VALUE) + overlay.set(MEMORY_USAGE_KEY, formatMemory(snapshot)) + delay(updateIntervalMillis) + } + } + + return OverlayProviderHandle { + job.cancel() + } + } + + private fun formatPercent(value: Double): String { + val normalized = if (!value.isFinite()) { + return NOT_AVAILABLE_VALUE + } else { + ((value * PERCENT_PRECISION_MULTIPLIER).roundToInt() / PERCENT_PRECISION_DIVISOR) + .coerceIn(MIN_PERCENT, MAX_PERCENT) + } + + val displayValue = normalized.takeIf { it >= 0 } ?: 0.0 + val formatted = displayValue.toString() + return if (formatted.contains(DECIMAL_SEPARATOR)) "$formatted %" else "$formatted$DEFAULT_DECIMAL_SUFFIX %" + } + + private fun formatMemory(snapshot: PerformanceSnapshot): String { + val used = snapshot.usedMemoryBytes + val total = snapshot.totalMemoryBytes + + return when { + used != null && total != null -> "${formatBytes(used)} / ${formatBytes(total)}" + used != null -> formatBytes(used) + total != null -> formatBytes(total) + else -> NOT_AVAILABLE_VALUE + } + } + + private fun formatBytes(value: Long): String { + if (value <= 0L) return "0 B" + + var unitIndex = 0 + var remaining = value.toDouble() + while (remaining >= UNIT_STEP && unitIndex < BYTE_UNITS.lastIndex) { + remaining /= UNIT_STEP + unitIndex++ + } + + val rounded = (remaining * PERCENT_PRECISION_MULTIPLIER).roundToInt() / PERCENT_PRECISION_DIVISOR + val normalized = if (rounded % 1.0 == 0.0) { + rounded.roundToInt().toString() + } else { + rounded.toString() + } + + return "$normalized ${BYTE_UNITS[unitIndex]}" + } + + private fun Double.isFinite(): Boolean = !isNaN() && !isInfinite() + + public companion object { + private const val KEY_SEPARATOR: String = "::" + private const val PERCENT_PRECISION_MULTIPLIER: Int = 10 + private const val PERCENT_PRECISION_DIVISOR: Double = 10.0 + private const val MIN_PERCENT: Double = 0.0 + private const val MAX_PERCENT: Double = 100.0 + private const val DECIMAL_SEPARATOR: String = "." + private const val DEFAULT_DECIMAL_SUFFIX: String = ".0" + private const val UNIT_STEP: Double = 1024.0 + public const val CATEGORY: String = "Performance" + public const val CPU_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "CPU" + public const val MEMORY_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "RAM" + public const val CUSTOM_KEY_PREFIX: String = CATEGORY + KEY_SEPARATOR + private const val NOT_AVAILABLE_VALUE: String = "—" + private val BYTE_UNITS = arrayOf("B", "KB", "MB", "GB", "TB", "PB") + } +} diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.kt new file mode 100644 index 0000000..aceb3d7 --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.kt @@ -0,0 +1,9 @@ +package ru.bartwell.kick.module.overlay.core.provider + +internal data class PerformanceSnapshot( + val cpuUsagePercent: Double?, + val usedMemoryBytes: Long?, + val totalMemoryBytes: Long?, +) + +internal expect fun readPerformanceSnapshot(): PerformanceSnapshot diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt index ac46566..2c7a079 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt @@ -8,6 +8,8 @@ internal const val DEFAULT_CATEGORY: String = "Default" internal object OverlayStore { private val categoriesMap: LinkedHashMap> = LinkedHashMap() + private val declaredCategories: LinkedHashSet = LinkedHashSet() + private const val KEY_CATEGORY_DELIMITER: String = "::" private val _items = MutableStateFlow>>(emptyList()) val items: StateFlow>> = _items.asStateFlow() @@ -19,42 +21,83 @@ internal object OverlayStore { val selectedCategory: StateFlow = _selectedCategory.asStateFlow() fun set(key: String, value: String) { - set(key = key, value = value, category = DEFAULT_CATEGORY) + val (category, normalizedKey) = splitKey(key) + setInternal( + key = normalizedKey, + value = value, + category = category, + ) } fun set(key: String, value: String, category: String) { - val cat = category.ifBlank { DEFAULT_CATEGORY } - val mapForCategory = categoriesMap.getOrPut(cat) { LinkedHashMap() } - mapForCategory[key] = value - updateCategoriesList(extra = cat) - updateItems() + setInternal( + key = key, + value = value, + category = category.ifBlank { DEFAULT_CATEGORY }, + ) } fun clear() { categoriesMap.clear() + declaredCategories.clear() updateCategoriesList() updateItems() } fun selectCategory(category: String) { - val cat = category.ifBlank { DEFAULT_CATEGORY } - _selectedCategory.value = cat - updateCategoriesList(extra = cat) + val resolvedCategory = category.ifBlank { DEFAULT_CATEGORY } + _selectedCategory.value = resolvedCategory + updateCategoriesList(extra = resolvedCategory) updateItems() } + fun declareCategories(categories: Collection) { + var changed = false + for (category in categories) { + val normalized = category.ifBlank { DEFAULT_CATEGORY } + if (declaredCategories.add(normalized)) { + changed = true + } + } + if (changed) { + updateCategoriesList() + } + } + private fun updateItems() { - val cat = _selectedCategory.value - val mapForCategory = categoriesMap[cat] + val category = _selectedCategory.value + val mapForCategory = categoriesMap[category] _items.value = mapForCategory?.entries?.map { it.key to it.value } ?: emptyList() } private fun updateCategoriesList(extra: String? = null) { val set = LinkedHashSet() set.add(DEFAULT_CATEGORY) + set.addAll(declaredCategories) set.addAll(categoriesMap.keys) extra?.let { set.add(it) } set.add(_selectedCategory.value) _categories.value = set.toList() } + + private fun setInternal(key: String, value: String, category: String) { + val mapForCategory = categoriesMap.getOrPut(category) { LinkedHashMap() } + mapForCategory[key] = value + updateCategoriesList(extra = category) + updateItems() + } + + private fun splitKey(key: String): Pair { + val delimiterIndex = key.indexOf(KEY_CATEGORY_DELIMITER) + if (delimiterIndex <= 0) { + return DEFAULT_CATEGORY to key + } + + val category = key.substring(0, delimiterIndex).ifBlank { DEFAULT_CATEGORY } + val normalizedKey = key.substring(delimiterIndex + KEY_CATEGORY_DELIMITER.length) + .takeIf { it.isNotEmpty() } + ?: key + + return category to normalizedKey + } } diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt index 89ee249..0d853fb 100644 --- a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt @@ -84,14 +84,14 @@ public actual object KickOverlay { queue = NSOperationQueue.mainQueue ) { _: NSNotification? -> if (OverlaySettings.isEnabled()) { - show(context) + show() } } } } } - public actual fun show(context: PlatformContext) { + public actual fun show() { dispatch_async(dispatch_get_main_queue()) { OverlaySettings.setEnabled(true) @@ -110,7 +110,7 @@ public actual object KickOverlay { queue = NSOperationQueue.mainQueue ) { _: NSNotification? -> if (overlayWindow == null && OverlaySettings.isEnabled()) { - show(context) + show() } windowObserver?.let { NSNotificationCenter.defaultCenter.removeObserver(it) } windowObserver = null diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt new file mode 100644 index 0000000..38947d5 --- /dev/null +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt @@ -0,0 +1,106 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) + +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.value +import platform.Foundation.NSProcessInfo +import platform.posix.CPU_STATE_IDLE +import platform.posix.CPU_STATE_NICE +import platform.posix.CPU_STATE_SYSTEM +import platform.posix.CPU_STATE_USER +import platform.posix.HOST_CPU_LOAD_INFO +import platform.posix.HOST_CPU_LOAD_INFO_COUNT +import platform.posix.KERN_SUCCESS +import platform.posix.MACH_TASK_BASIC_INFO +import platform.posix.MACH_TASK_BASIC_INFO_COUNT +import platform.posix.host_cpu_load_info +import platform.posix.host_statistics +import platform.posix.mach_host_self +import platform.posix.mach_msg_type_number_tVar +import platform.posix.mach_task_basic_info +import platform.posix.mach_task_self_ +import platform.posix.task_info +import kotlin.native.concurrent.ThreadLocal + +private data class CpuSample(val user: ULong, val nice: ULong, val system: ULong, val idle: ULong) + +private const val PERCENT_FACTOR: Double = 100.0 + +@ThreadLocal +private object CpuState { + var previous: CpuSample? = null +} + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { + val cpu = readCpuUsage() + val memory = readMemoryUsage() + + return PerformanceSnapshot( + cpuUsagePercent = cpu, + usedMemoryBytes = memory?.first, + totalMemoryBytes = memory?.second, + ) +} + +private fun readCpuUsage(): Double? = memScoped { + val cpuInfo = alloc() + val count = alloc().apply { value = HOST_CPU_LOAD_INFO_COUNT } + + val result = host_statistics( + mach_host_self(), + HOST_CPU_LOAD_INFO, + cpuInfo.ptr.reinterpret(), + count.ptr, + ) + + if (result != KERN_SUCCESS) { + return null + } + + val sample = CpuSample( + user = cpuInfo.cpu_ticks[CPU_STATE_USER], + nice = cpuInfo.cpu_ticks[CPU_STATE_NICE], + system = cpuInfo.cpu_ticks[CPU_STATE_SYSTEM], + idle = cpuInfo.cpu_ticks[CPU_STATE_IDLE], + ) + + val previousSample = CpuState.previous + CpuState.previous = sample + + if (previousSample == null) { + return null + } + + val userDelta = sample.activeTicks() - previousSample.activeTicks() + val totalDelta = userDelta + (sample.idle - previousSample.idle) + + if (totalDelta.toLong() <= 0L) { + return null + } + + userDelta.toDouble() / totalDelta.toDouble() * PERCENT_FACTOR +} + +private fun readMemoryUsage(): Pair? = memScoped { + val count = alloc().apply { value = MACH_TASK_BASIC_INFO_COUNT } + val info = alloc() + + val result = task_info( + target_task = mach_task_self_, + flavor = MACH_TASK_BASIC_INFO, + task_info_out = info.ptr.reinterpret(), + task_info_outCnt = count.ptr, + ) + + val used = if (result == KERN_SUCCESS) info.resident_size.toLong() else null + val total = NSProcessInfo.processInfo.physicalMemory.takeIf { it > 0uL }?.toLong() + + used to total +} + +private fun CpuSample.activeTicks(): ULong = user + nice + system diff --git a/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt index 4395384..56ea008 100644 --- a/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt +++ b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt @@ -29,7 +29,7 @@ public actual object KickOverlay { @Suppress("EmptyFunctionBlock") public actual fun init(context: PlatformContext) {} - public actual fun show(context: PlatformContext) { + public actual fun show() { if (window != null) { window!!.isVisible = true window!!.toFront() diff --git a/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.jvm.kt b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.jvm.kt new file mode 100644 index 0000000..9e3ce88 --- /dev/null +++ b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.jvm.kt @@ -0,0 +1,22 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import com.sun.management.OperatingSystemMXBean +import java.lang.management.ManagementFactory + +private const val PERCENT_FACTOR: Double = 100.0 + +private val osBean: OperatingSystemMXBean? = + ManagementFactory.getOperatingSystemMXBean() as? OperatingSystemMXBean + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { + val cpu = osBean?.systemCpuLoad?.takeIf { it >= 0 }?.let { it * PERCENT_FACTOR } + val totalMemory = osBean?.totalPhysicalMemorySize?.takeIf { it > 0 } + val freeMemory = osBean?.freePhysicalMemorySize?.takeIf { it >= 0 } + val usedMemory = if (totalMemory != null && freeMemory != null) totalMemory - freeMemory else null + + return PerformanceSnapshot( + cpuUsagePercent = cpu, + usedMemoryBytes = usedMemory, + totalMemoryBytes = totalMemory, + ) +} diff --git a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt index 3eb029a..20d9497 100644 --- a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt +++ b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt @@ -33,16 +33,17 @@ class OverlayCategoriesJvmTest { @Test fun set_withCategory_isIsolated_perCategory_and_switchingUpdatesItems() { + val category = "Performance" OverlayStore.set("k1", "v1") // Default - OverlayStore.set("k2", "v2", "Perf") + OverlayStore.set("k2", "v2", category) // Still on Default assertEquals(DEFAULT_CATEGORY, OverlayStore.selectedCategory.value) assertEquals(listOf("k1" to "v1"), OverlayStore.items.value) - assertTrue(OverlayStore.categories.value.containsAll(listOf(DEFAULT_CATEGORY, "Perf"))) + assertTrue(OverlayStore.categories.value.containsAll(listOf(DEFAULT_CATEGORY, category))) - // Switch to Perf - OverlayStore.selectCategory("Perf") + // Switch to the declared category + OverlayStore.selectCategory(category) assertEquals(listOf("k2" to "v2"), OverlayStore.items.value) } @@ -52,4 +53,24 @@ class OverlayCategoriesJvmTest { assertTrue(OverlayStore.categories.value.contains("NewCat")) assertTrue(OverlayStore.items.value.isEmpty()) } + + @Test + fun set_withEmbeddedCategoryPrefix_routesToCategory_and_usesNormalizedKey() { + val category = "Performance" + val key = "$category::fps" + + OverlayStore.set(key, "60") + + OverlayStore.selectCategory(category) + assertEquals(listOf("fps" to "60"), OverlayStore.items.value) + } + + @Test + fun declareCategories_addsThemToCategoriesList() { + val categories = listOf("Performance", "Analytics") + + OverlayStore.declareCategories(categories) + + assertTrue(OverlayStore.categories.value.containsAll(categories + DEFAULT_CATEGORY)) + } } diff --git a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt index 079ed9e..77303f2 100644 --- a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt +++ b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt @@ -24,7 +24,7 @@ class OverlayJvmTest { // Show KickOverlay.init(ctx) - KickOverlay.show(ctx) + KickOverlay.show() // Wait up to 2 seconds for window to appear run { val start = System.currentTimeMillis() diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt index 1650e26..745cf79 100644 --- a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt +++ b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt @@ -18,7 +18,7 @@ public actual object KickOverlay { public actual fun init(context: PlatformContext) {} @OptIn(ExperimentalComposeUiApi::class) - public actual fun show(context: PlatformContext) { + public actual fun show() { val existing = overlayRoot if (existing != null) { existing.style.display = "" diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.wasmJs.kt new file mode 100644 index 0000000..fe0b738 --- /dev/null +++ b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.wasmJs.kt @@ -0,0 +1,4 @@ +package ru.bartwell.kick.module.overlay.core.provider + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot = + PerformanceSnapshot(cpuUsagePercent = null, usedMemoryBytes = null, totalMemoryBytes = null) diff --git a/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt b/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt index f5be33e..532f794 100644 --- a/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt +++ b/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt @@ -21,7 +21,7 @@ class OverlayWasmJsTest { val ctx = getPlatformContext() KickOverlay.init(ctx) - KickOverlay.show(ctx) + KickOverlay.show() val el = document.getElementById(OVERLAY_ELEMENT_ID) as? HTMLElement assertNotNull(el) From eea024a1902fe54b338e0669593585d3ddda9e3c Mon Sep 17 00:00:00 2001 From: Artem Bazhanov Date: Fri, 17 Oct 2025 17:52:28 +0300 Subject: [PATCH 2/8] Safeguard overlay store mutations from concurrent providers --- .../module/overlay/core/store/OverlayStore.kt | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt index 2c7a079..7973b7b 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt @@ -3,12 +3,15 @@ package ru.bartwell.kick.module.overlay.core.store import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update internal const val DEFAULT_CATEGORY: String = "Default" internal object OverlayStore { - private val categoriesMap: LinkedHashMap> = LinkedHashMap() - private val declaredCategories: LinkedHashSet = LinkedHashSet() + private val categoriesMapState: MutableStateFlow>> = + MutableStateFlow(linkedMapOf>()) + private val declaredCategoriesState: MutableStateFlow> = + MutableStateFlow(linkedSetOf()) private const val KEY_CATEGORY_DELIMITER: String = "::" private val _items = MutableStateFlow>>(emptyList()) @@ -38,8 +41,8 @@ internal object OverlayStore { } fun clear() { - categoriesMap.clear() - declaredCategories.clear() + categoriesMapState.value = linkedMapOf>() + declaredCategoriesState.value = linkedSetOf() updateCategoriesList() updateItems() } @@ -53,11 +56,15 @@ internal object OverlayStore { fun declareCategories(categories: Collection) { var changed = false - for (category in categories) { - val normalized = category.ifBlank { DEFAULT_CATEGORY } - if (declaredCategories.add(normalized)) { - changed = true + declaredCategoriesState.update { current -> + val updated = LinkedHashSet(current) + for (category in categories) { + val normalized = category.ifBlank { DEFAULT_CATEGORY } + if (updated.add(normalized)) { + changed = true + } } + updated } if (changed) { updateCategoriesList() @@ -66,23 +73,30 @@ internal object OverlayStore { private fun updateItems() { val category = _selectedCategory.value - val mapForCategory = categoriesMap[category] + val mapForCategory = categoriesMapState.value[category] _items.value = mapForCategory?.entries?.map { it.key to it.value } ?: emptyList() } private fun updateCategoriesList(extra: String? = null) { val set = LinkedHashSet() set.add(DEFAULT_CATEGORY) - set.addAll(declaredCategories) - set.addAll(categoriesMap.keys) + set.addAll(declaredCategoriesState.value) + set.addAll(categoriesMapState.value.keys) extra?.let { set.add(it) } set.add(_selectedCategory.value) _categories.value = set.toList() } private fun setInternal(key: String, value: String, category: String) { - val mapForCategory = categoriesMap.getOrPut(category) { LinkedHashMap() } - mapForCategory[key] = value + categoriesMapState.update { current -> + val updated = LinkedHashMap>(current.size + 1) + for ((existingCategory, existingValues) in current) { + updated[existingCategory] = LinkedHashMap(existingValues) + } + val mapForCategory = updated.getOrPut(category) { LinkedHashMap() } + mapForCategory[key] = value + updated + } updateCategoriesList(extra = category) updateItems() } From 0d675f7fb231f652660581f08267c3566ea5a01a Mon Sep 17 00:00:00 2001 From: Artem Bazhanov Date: Sat, 18 Oct 2025 00:52:52 +0300 Subject: [PATCH 3/8] Fix Darwin imports and CPU ticks handling on iOS --- .../core/provider/PerformanceSnapshot.ios.kt | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt index 38947d5..706ae79 100644 --- a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt @@ -9,22 +9,22 @@ import kotlinx.cinterop.ptr import kotlinx.cinterop.reinterpret import kotlinx.cinterop.value import platform.Foundation.NSProcessInfo -import platform.posix.CPU_STATE_IDLE -import platform.posix.CPU_STATE_NICE -import platform.posix.CPU_STATE_SYSTEM -import platform.posix.CPU_STATE_USER -import platform.posix.HOST_CPU_LOAD_INFO -import platform.posix.HOST_CPU_LOAD_INFO_COUNT -import platform.posix.KERN_SUCCESS -import platform.posix.MACH_TASK_BASIC_INFO -import platform.posix.MACH_TASK_BASIC_INFO_COUNT -import platform.posix.host_cpu_load_info -import platform.posix.host_statistics -import platform.posix.mach_host_self -import platform.posix.mach_msg_type_number_tVar -import platform.posix.mach_task_basic_info -import platform.posix.mach_task_self_ -import platform.posix.task_info +import platform.darwin.CPU_STATE_IDLE +import platform.darwin.CPU_STATE_NICE +import platform.darwin.CPU_STATE_SYSTEM +import platform.darwin.CPU_STATE_USER +import platform.darwin.HOST_CPU_LOAD_INFO +import platform.darwin.HOST_CPU_LOAD_INFO_COUNT +import platform.darwin.KERN_SUCCESS +import platform.darwin.MACH_TASK_BASIC_INFO +import platform.darwin.MACH_TASK_BASIC_INFO_COUNT +import platform.darwin.host_cpu_load_info +import platform.darwin.host_statistics +import platform.darwin.mach_host_self +import platform.darwin.mach_msg_type_number_tVar +import platform.darwin.mach_task_basic_info +import platform.darwin.mach_task_self_ +import platform.darwin.task_info import kotlin.native.concurrent.ThreadLocal private data class CpuSample(val user: ULong, val nice: ULong, val system: ULong, val idle: ULong) @@ -62,11 +62,13 @@ private fun readCpuUsage(): Double? = memScoped { return null } + val ticks = cpuInfo.cpu_ticks + val sample = CpuSample( - user = cpuInfo.cpu_ticks[CPU_STATE_USER], - nice = cpuInfo.cpu_ticks[CPU_STATE_NICE], - system = cpuInfo.cpu_ticks[CPU_STATE_SYSTEM], - idle = cpuInfo.cpu_ticks[CPU_STATE_IDLE], + user = ticks[CPU_STATE_USER.toInt()].value.toULong(), + nice = ticks[CPU_STATE_NICE.toInt()].value.toULong(), + system = ticks[CPU_STATE_SYSTEM.toInt()].value.toULong(), + idle = ticks[CPU_STATE_IDLE.toInt()].value.toULong(), ) val previousSample = CpuState.previous From 9368f63e6493e7a0247271dc2fbcebe02613ae0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Mon, 20 Oct 2025 21:41:24 +0300 Subject: [PATCH 4/8] Refactor overlay module to support providers --- gradle/libs.versions.toml | 2 + module/logging/overlay/build.gradle.kts | 2 + .../overlay/OverlaySettingsCategoriesTest.kt | 2 +- .../kick/module/overlay/OverlayUiTest.kt | 2 +- .../kick/module/overlay/OverlayModule.kt | 47 ++++------ .../overlay/core/persists/OverlaySettings.kt | 13 ++- .../overlay/core/provider/OverlayProvider.kt | 12 ++- .../provider/PerformanceOverlayProvider.kt | 72 +++++++++------ .../module/overlay/core/store/OverlayStore.kt | 92 ++++--------------- .../settings/data/ProviderDescription.kt | 7 ++ .../presentation/DefaultOverlayComponent.kt | 24 ++++- .../settings/presentation/OverlayComponent.kt | 3 + .../settings/presentation/OverlayContent.kt | 14 ++- .../kick/sample/shared/TestDataInitializer.kt | 22 +++-- 14 files changed, 157 insertions(+), 157 deletions(-) create mode 100644 module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/data/ProviderDescription.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64d051f..89781d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,8 @@ room-driver = { module = "androidx.sqlite:sqlite-bundled", version.ref = "room-d napier = { module = "io.github.aakira:napier", version.ref = "napier" } settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } settings-noArg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" } +settings-make-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatform-settings" } +settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatform-settings" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } diff --git a/module/logging/overlay/build.gradle.kts b/module/logging/overlay/build.gradle.kts index 1edcf9f..df2dd62 100644 --- a/module/logging/overlay/build.gradle.kts +++ b/module/logging/overlay/build.gradle.kts @@ -53,6 +53,8 @@ kotlin { implementation(libs.decompose.essenty.lifecycle.coroutines) implementation(libs.kotlinx.coroutines.core) implementation(libs.settings) + implementation(libs.settings.make.observable) + implementation(libs.settings.coroutines) implementation(libs.settings.noArg) } commonTest.dependencies { diff --git a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt index 0fee8ea..c08d543 100644 --- a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt +++ b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt @@ -48,7 +48,7 @@ class OverlaySettingsCategoriesTest { component = DefaultOverlayComponent( componentContext = DefaultComponentContext(LifecycleRegistry()), onEnabledChangeCallback = {}, - onBackCallback = {} + onBackCallback = {}, ) activity.setContent { OverlayContent(component = component) } } diff --git a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt index 96b5e44..d106c68 100644 --- a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt +++ b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt @@ -83,7 +83,7 @@ class OverlayUiTest { onEnabledChangeCallback = { enabled -> if (enabled) KickOverlay.show() else KickOverlay.hide() }, - onBackCallback = {} + onBackCallback = {}, ) activity.setContent { OverlayContent(component = component) } } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 6ef07f3..b1b4c05 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -9,10 +9,9 @@ import com.arkivanov.decompose.router.stack.pop import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn import kotlinx.serialization.modules.PolymorphicModuleBuilder -import ru.bartwell.kick.Kick import ru.bartwell.kick.core.component.Child import ru.bartwell.kick.core.component.Config import ru.bartwell.kick.core.data.Module @@ -23,61 +22,46 @@ import ru.bartwell.kick.module.overlay.core.component.config.OverlayConfig import ru.bartwell.kick.module.overlay.core.overlay.KickOverlay import ru.bartwell.kick.module.overlay.core.persists.OverlaySettings import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider -import ru.bartwell.kick.module.overlay.core.provider.OverlayProviderHandle import ru.bartwell.kick.module.overlay.core.provider.PerformanceOverlayProvider import ru.bartwell.kick.module.overlay.core.store.OverlayStore import ru.bartwell.kick.module.overlay.feature.settings.presentation.DefaultOverlayComponent import ru.bartwell.kick.module.overlay.feature.settings.presentation.OverlayContent public class OverlayModule( - private val context: PlatformContext, + context: PlatformContext, private val providers: List = listOf(PerformanceOverlayProvider()), ) : Module { override val description: ModuleDescription = ModuleDescription.OVERLAY override val startConfig: Config = OverlayConfig private val providerScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val activeProviderHandles: MutableMap = mutableMapOf() init { OverlaySettings(context) - // Initialize selected category from persisted settings + observeFloatingWindowState() OverlayStore.selectCategory(OverlaySettings.getSelectedCategory()) KickOverlay.init(context) if (OverlaySettings.isEnabled()) { KickOverlay.show() } - - OverlayStore.declareCategories(providers.flatMap { it.categories }) - - providerScope.launch { - OverlayStore.selectedCategory.collectLatest { category -> - updateProvidersFor(category) - } - } } - private fun updateProvidersFor(selectedCategory: String) { - providers.forEach { provider -> - val active = activeProviderHandles[provider] - if (provider.categories.contains(selectedCategory)) { - if (active?.category != selectedCategory) { - active?.handle?.stop() - val handle = provider.start(providerScope, Kick.overlay, selectedCategory) - activeProviderHandles[provider] = ActiveProvider(selectedCategory, handle) + private fun observeFloatingWindowState() { + combine(OverlaySettings.observeEnabled(), OverlayStore.selectedCategory) { isWindowEnabled, currentCategory -> + providers.forEach { provider -> + provider.categories.forEach { providerCategory -> + OverlayStore.addCategory(providerCategory) + if (isWindowEnabled && providerCategory == currentCategory && provider.isAvailable) { + provider.start(providerScope) + } else { + provider.stop() + } } - } else if (active != null) { - active.handle.stop() - activeProviderHandles.remove(provider) } } + .launchIn(providerScope) } - private data class ActiveProvider( - val category: String, - val handle: OverlayProviderHandle, - ) - override fun getComponent( componentContext: ComponentContext, nav: StackNavigation, @@ -86,6 +70,7 @@ public class OverlayModule( OverlayChild( DefaultOverlayComponent( componentContext = componentContext, + providers = providers, onEnabledChangeCallback = { enabled -> if (enabled) KickOverlay.show() else KickOverlay.hide() }, onBackCallback = { nav.pop() }, ) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt index 3e251cf..e4bf415 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt @@ -1,20 +1,29 @@ package ru.bartwell.kick.module.overlay.core.persists -import com.russhwolf.settings.Settings +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.coroutines.getBooleanFlow +import com.russhwolf.settings.observable.makeObservable +import kotlinx.coroutines.flow.Flow import ru.bartwell.kick.core.data.PlatformContext import ru.bartwell.kick.module.overlay.core.store.DEFAULT_CATEGORY internal object OverlaySettings { - private lateinit var settings: Settings + private lateinit var settings: ObservableSettings private const val KEY_ENABLED = "enabled" private const val KEY_SELECTED_CATEGORY = "selected_category" + @OptIn(ExperimentalSettingsApi::class) operator fun invoke(context: PlatformContext) { settings = PlatformSettingsFactory.create(context = context, name = "kick_overlay_prefs") + .makeObservable() } fun isEnabled(): Boolean = settings.getBoolean(KEY_ENABLED, false) + @OptIn(ExperimentalSettingsApi::class) + fun observeEnabled(): Flow = settings.getBooleanFlow(KEY_ENABLED, false) + fun setEnabled(value: Boolean) { settings.putBoolean(KEY_ENABLED, value) } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt index 407ece7..fcfcba2 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt @@ -1,14 +1,18 @@ package ru.bartwell.kick.module.overlay.core.provider import kotlinx.coroutines.CoroutineScope -import ru.bartwell.kick.module.overlay.OverlayAccessor public interface OverlayProvider { + + /** + * Categories + * List all of categories used by provider. It is important for correct start/stop provider. + */ public val categories: Set - public fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle -} + public val isAvailable: Boolean + + public fun start(scope: CoroutineScope) -public fun interface OverlayProviderHandle { public fun stop() } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt index 0b20975..c42f2ad 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt @@ -1,36 +1,65 @@ package ru.bartwell.kick.module.overlay.core.provider import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import ru.bartwell.kick.module.overlay.OverlayAccessor +import ru.bartwell.kick.Kick +import ru.bartwell.kick.core.data.Platform +import ru.bartwell.kick.core.util.PlatformUtils +import ru.bartwell.kick.module.overlay.overlay import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +private const val PERCENT_PRECISION_MULTIPLIER: Int = 10 +private const val PERCENT_PRECISION_DIVISOR: Double = 10.0 +private const val MIN_PERCENT: Double = 0.0 +private const val MAX_PERCENT: Double = 100.0 +private const val DECIMAL_SEPARATOR: String = "." +private const val DEFAULT_DECIMAL_SUFFIX: String = ".0" +private const val UNIT_STEP: Double = 1024.0 +public const val CATEGORY: String = "Performance" +private const val CPU_USAGE_KEY: String = "CPU" +private const val MEMORY_USAGE_KEY: String = "RAM" +private const val NOT_AVAILABLE_VALUE: String = "—" +private val BYTE_UNITS = arrayOf("B", "KB", "MB", "GB", "TB", "PB") public class PerformanceOverlayProvider( - private val updateIntervalMillis: Long = 1_000, + private val updateIntervalMillis: Duration = 1.seconds, ) : OverlayProvider { override val categories: Set = setOf(CATEGORY) + override val isAvailable: Boolean + get() = PlatformUtils.getPlatform() != Platform.WEB + private var job: Job? = null - override fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle { - require(category == CATEGORY) { "PerformanceOverlayProvider started for unexpected category: $category" } + override fun start(scope: CoroutineScope) { + Kick.overlay.set(CPU_USAGE_KEY, NOT_AVAILABLE_VALUE, CATEGORY) + Kick.overlay.set(MEMORY_USAGE_KEY, NOT_AVAILABLE_VALUE, CATEGORY) - overlay.set(CPU_USAGE_KEY, NOT_AVAILABLE_VALUE) - overlay.set(MEMORY_USAGE_KEY, NOT_AVAILABLE_VALUE) - - val job = scope.launch { + job = scope.launch { while (isActive) { val snapshot = readPerformanceSnapshot() - overlay.set(CPU_USAGE_KEY, snapshot.cpuUsagePercent?.let(::formatPercent) ?: NOT_AVAILABLE_VALUE) - overlay.set(MEMORY_USAGE_KEY, formatMemory(snapshot)) + Kick.overlay.set( + key = CPU_USAGE_KEY, + value = snapshot.cpuUsagePercent?.let(::formatPercent) ?: NOT_AVAILABLE_VALUE, + category = CATEGORY, + ) + Kick.overlay.set( + key = MEMORY_USAGE_KEY, + value = formatMemory(snapshot), + category = CATEGORY, + ) delay(updateIntervalMillis) } } + } - return OverlayProviderHandle { - job.cancel() - } + override fun stop() { + job?.cancel() + job = null } private fun formatPercent(value: Double): String { @@ -79,21 +108,4 @@ public class PerformanceOverlayProvider( } private fun Double.isFinite(): Boolean = !isNaN() && !isInfinite() - - public companion object { - private const val KEY_SEPARATOR: String = "::" - private const val PERCENT_PRECISION_MULTIPLIER: Int = 10 - private const val PERCENT_PRECISION_DIVISOR: Double = 10.0 - private const val MIN_PERCENT: Double = 0.0 - private const val MAX_PERCENT: Double = 100.0 - private const val DECIMAL_SEPARATOR: String = "." - private const val DEFAULT_DECIMAL_SUFFIX: String = ".0" - private const val UNIT_STEP: Double = 1024.0 - public const val CATEGORY: String = "Performance" - public const val CPU_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "CPU" - public const val MEMORY_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "RAM" - public const val CUSTOM_KEY_PREFIX: String = CATEGORY + KEY_SEPARATOR - private const val NOT_AVAILABLE_VALUE: String = "—" - private val BYTE_UNITS = arrayOf("B", "KB", "MB", "GB", "TB", "PB") - } } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt index 7973b7b..f563d4d 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt @@ -3,115 +3,63 @@ package ru.bartwell.kick.module.overlay.core.store import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update internal const val DEFAULT_CATEGORY: String = "Default" internal object OverlayStore { - private val categoriesMapState: MutableStateFlow>> = - MutableStateFlow(linkedMapOf>()) - private val declaredCategoriesState: MutableStateFlow> = - MutableStateFlow(linkedSetOf()) - private const val KEY_CATEGORY_DELIMITER: String = "::" + private val categoriesMap: LinkedHashMap> = LinkedHashMap() private val _items = MutableStateFlow>>(emptyList()) val items: StateFlow>> = _items.asStateFlow() - private val _categories = MutableStateFlow>(listOf(DEFAULT_CATEGORY)) + private val _categories = MutableStateFlow(listOf(DEFAULT_CATEGORY)) val categories: StateFlow> = _categories.asStateFlow() private val _selectedCategory = MutableStateFlow(DEFAULT_CATEGORY) val selectedCategory: StateFlow = _selectedCategory.asStateFlow() + internal fun addCategory(category: String) { + categoriesMap.getOrPut(category) { LinkedHashMap() } + updateCategoriesList(category) + } + fun set(key: String, value: String) { - val (category, normalizedKey) = splitKey(key) - setInternal( - key = normalizedKey, - value = value, - category = category, - ) + set(key = key, value = value, category = DEFAULT_CATEGORY) } fun set(key: String, value: String, category: String) { - setInternal( - key = key, - value = value, - category = category.ifBlank { DEFAULT_CATEGORY }, - ) + val cat = category.ifBlank { DEFAULT_CATEGORY } + val mapForCategory = categoriesMap.getOrPut(cat) { LinkedHashMap() } + mapForCategory[key] = value + updateCategoriesList(extra = cat) + updateItems() } fun clear() { - categoriesMapState.value = linkedMapOf>() - declaredCategoriesState.value = linkedSetOf() + categoriesMap.clear() updateCategoriesList() updateItems() } fun selectCategory(category: String) { - val resolvedCategory = category.ifBlank { DEFAULT_CATEGORY } - _selectedCategory.value = resolvedCategory - updateCategoriesList(extra = resolvedCategory) + val cat = category.ifBlank { DEFAULT_CATEGORY } + _selectedCategory.value = cat + updateCategoriesList(extra = cat) updateItems() } - fun declareCategories(categories: Collection) { - var changed = false - declaredCategoriesState.update { current -> - val updated = LinkedHashSet(current) - for (category in categories) { - val normalized = category.ifBlank { DEFAULT_CATEGORY } - if (updated.add(normalized)) { - changed = true - } - } - updated - } - if (changed) { - updateCategoriesList() - } - } - private fun updateItems() { - val category = _selectedCategory.value - val mapForCategory = categoriesMapState.value[category] + val cat = _selectedCategory.value + val mapForCategory = categoriesMap[cat] _items.value = mapForCategory?.entries?.map { it.key to it.value } ?: emptyList() } private fun updateCategoriesList(extra: String? = null) { val set = LinkedHashSet() set.add(DEFAULT_CATEGORY) - set.addAll(declaredCategoriesState.value) - set.addAll(categoriesMapState.value.keys) + set.addAll(categoriesMap.keys) extra?.let { set.add(it) } set.add(_selectedCategory.value) _categories.value = set.toList() } - - private fun setInternal(key: String, value: String, category: String) { - categoriesMapState.update { current -> - val updated = LinkedHashMap>(current.size + 1) - for ((existingCategory, existingValues) in current) { - updated[existingCategory] = LinkedHashMap(existingValues) - } - val mapForCategory = updated.getOrPut(category) { LinkedHashMap() } - mapForCategory[key] = value - updated - } - updateCategoriesList(extra = category) - updateItems() - } - - private fun splitKey(key: String): Pair { - val delimiterIndex = key.indexOf(KEY_CATEGORY_DELIMITER) - if (delimiterIndex <= 0) { - return DEFAULT_CATEGORY to key - } - - val category = key.substring(0, delimiterIndex).ifBlank { DEFAULT_CATEGORY } - val normalizedKey = key.substring(delimiterIndex + KEY_CATEGORY_DELIMITER.length) - .takeIf { it.isNotEmpty() } - ?: key - - return category to normalizedKey - } } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/data/ProviderDescription.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/data/ProviderDescription.kt new file mode 100644 index 0000000..a3fa3eb --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/data/ProviderDescription.kt @@ -0,0 +1,7 @@ +package ru.bartwell.kick.module.overlay.feature.settings.data + +internal data class ProviderDescription( + val name: String, + val categories: Set, + val isAvailable: Boolean, +) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt index 2336670..ef5e217 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt @@ -4,23 +4,27 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import ru.bartwell.kick.core.data.PlatformContext +import ru.bartwell.kick.core.util.PlatformUtils import ru.bartwell.kick.module.overlay.core.persists.OverlaySettings +import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider import ru.bartwell.kick.module.overlay.core.store.OverlayStore +import ru.bartwell.kick.module.overlay.feature.settings.data.ProviderDescription internal class DefaultOverlayComponent( componentContext: ComponentContext, private val onEnabledChangeCallback: (Boolean) -> Unit, private val onBackCallback: () -> Unit, + providers: List, ) : OverlayComponent, ComponentContext by componentContext { - private val _model = MutableValue(OverlayState()) + private val _model = MutableValue(OverlayState(providers = providers.toDescriptions())) override val model: Value = _model override fun init(context: PlatformContext) { OverlaySettings(context) val enabled = OverlaySettings.isEnabled() val category = OverlaySettings.getSelectedCategory() - _model.value = OverlayState(enabled) + _model.value = model.value.copy(enabled = enabled) onEnabledChangeCallback(enabled) OverlayStore.selectCategory(category) } @@ -36,5 +40,21 @@ internal class DefaultOverlayComponent( override fun onCategoryChange(category: String) { OverlaySettings.setSelectedCategory(category) OverlayStore.selectCategory(category) + val unavailableProviders = model.value.providers.filter { it.categories.contains(category) && !it.isAvailable } + if (unavailableProviders.isEmpty()) { + _model.value = model.value.copy(warning = null) + } else { + val platform = PlatformUtils.getPlatform().name + val providersText = unavailableProviders.joinToString(", ") { it.name } + _model.value = model.value.copy(warning = "$providersText is not available on $platform") + } } } + +private fun List.toDescriptions(): List = map { provider -> + ProviderDescription( + name = provider::class.simpleName ?: "Unknown", + categories = provider.categories, + isAvailable = provider.isAvailable + ) +} diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt index de3a252..28ec68c 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt @@ -3,6 +3,7 @@ package ru.bartwell.kick.module.overlay.feature.settings.presentation import com.arkivanov.decompose.value.Value import ru.bartwell.kick.core.component.Component import ru.bartwell.kick.core.data.PlatformContext +import ru.bartwell.kick.module.overlay.feature.settings.data.ProviderDescription internal interface OverlayComponent : Component { val model: Value @@ -15,4 +16,6 @@ internal interface OverlayComponent : Component { internal data class OverlayState( val enabled: Boolean = false, + val providers: List, + val warning: String? = null, ) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt index b117889..202d170 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt @@ -85,6 +85,7 @@ internal fun OverlayContent( CategoryPicker( selectedCategory = selectedCategory, categories = categories, + warning = state.value.warning, onSelect = { component.onCategoryChange(it) } ) Spacer(modifier = Modifier.height(32.dp)) @@ -111,6 +112,7 @@ internal fun OverlayContent( private fun CategoryPicker( selectedCategory: String, categories: List, + warning: String?, onSelect: (String) -> Unit, ) { var expanded by rememberSaveable { mutableStateOf(false) } @@ -119,15 +121,17 @@ private fun CategoryPicker( onExpandedChange = { expanded = it }, ) { OutlinedTextField( - readOnly = true, + modifier = Modifier + .menuAnchor() + .padding(horizontal = 16.dp) + .fillMaxWidth(), value = selectedCategory, onValueChange = {}, + supportingText = { warning?.let { Text(text = it) } }, + isError = warning != null, label = { Text("Category") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier - .menuAnchor() - .padding(horizontal = 16.dp) - .fillMaxWidth() + readOnly = true, ) DropdownMenu( diff --git a/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/TestDataInitializer.kt b/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/TestDataInitializer.kt index 099673d..397f898 100644 --- a/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/TestDataInitializer.kt +++ b/sample/shared/src/commonMain/kotlin/ru/bartwell/kick/sample/shared/TestDataInitializer.kt @@ -40,12 +40,12 @@ private const val DEFAULT_MAX_ITEMS: Int = 5 private const val INPUT_MIN_INT: Int = 1 private const val INPUT_MAX_INT: Int = 10 -private const val PERF_KEY: String = "Performance" -private const val PERF_FPS_BASE: Int = 30 -private const val PERF_FPS_MOD: Int = 31 -private const val PERF_CPU_MOD: Int = 100 -private const val PERF_HEAP_BASE_MB: Int = 128 -private const val PERF_HEAP_MOD: Int = 64 +private const val OVERLAY_CATEGORY: String = "Test" +private const val OVERLAY_FPS_BASE: Int = 30 +private const val OVERLAY_FPS_MOD: Int = 31 +private const val OVERLAY_CPU_MOD: Int = 100 +private const val OVERLAY_HEAP_BASE_MB: Int = 128 +private const val OVERLAY_HEAP_MOD: Int = 64 class TestDataInitializer(context: PlatformContext) { @@ -124,9 +124,13 @@ class TestDataInitializer(context: PlatformContext) { Kick.overlay.set("config", true) Kick.overlay.set("timestamp", DateUtils.currentTimeMillis()) // Extra values under a separate category to demonstrate switching - Kick.overlay.set("fps", PERF_FPS_BASE + (counter % PERF_FPS_MOD).toInt(), PERF_KEY) - Kick.overlay.set("cpu", (counter % PERF_CPU_MOD).toInt(), PERF_KEY) - Kick.overlay.set("heapMb", PERF_HEAP_BASE_MB + (counter % PERF_HEAP_MOD).toInt(), PERF_KEY) + Kick.overlay.set("fps", OVERLAY_FPS_BASE + (counter % OVERLAY_FPS_MOD).toInt(), OVERLAY_CATEGORY) + Kick.overlay.set("cpu", (counter % OVERLAY_CPU_MOD).toInt(), OVERLAY_CATEGORY) + Kick.overlay.set( + "heapMb", + OVERLAY_HEAP_BASE_MB + (counter % OVERLAY_HEAP_MOD).toInt(), + OVERLAY_CATEGORY + ) counter++ delay(1.seconds) } From e2da2091a8f18b64869b890db7e3991486f1e03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Tue, 21 Oct 2025 00:42:00 +0300 Subject: [PATCH 5/8] Fix CPU data retrieving on iOS and Android --- kotlin-js-store/wasm/yarn.lock | 285 ++++++++++++++++++ .../kick/module/overlay/OverlayModule.kt | 2 +- .../overlay/core/provider/OverlayProvider.kt | 4 +- .../provider/PerformanceOverlayProvider.kt | 12 +- .../kick/module/overlay/core/data/CpuTimes.kt | 3 + .../module/overlay/core/data/MemoryInfo.kt | 3 + .../provider/PerformanceSnapshot.android.kt | 97 +++--- .../kick/module/overlay/OverlayModule.kt | 16 +- .../module/overlay/core/data/CpuSample.kt | 3 + .../kick/module/overlay/core/data/CpuState.kt | 8 + .../core/provider/PerformanceSnapshot.ios.kt | 40 +-- .../xcshareddata/xcschemes/iosSample.xcscheme | 4 + 12 files changed, 406 insertions(+), 71 deletions(-) create mode 100644 kotlin-js-store/wasm/yarn.lock create mode 100644 module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt create mode 100644 module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt create mode 100644 module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt create mode 100644 module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt diff --git a/kotlin-js-store/wasm/yarn.lock b/kotlin-js-store/wasm/yarn.lock new file mode 100644 index 0000000..5f0c8fa --- /dev/null +++ b/kotlin-js-store/wasm/yarn.lock @@ -0,0 +1,285 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cashapp/sqldelight-sqljs-worker@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@cashapp/sqldelight-sqljs-worker/-/sqldelight-sqljs-worker-2.1.0.tgz#4ab898698aca9487f47fc9a42107c606c3ce81c5" + integrity sha512-odvBljb1rUOCk3UUZgjdiAChEohYI4Fy6Tj3NUy3l6u3WV/we+tjDTJ/kC25CJKD4pv0ZlH5AL1sKsZ5clKCew== + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@types/json-schema@^7.0.8": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +copy-webpack-plugin@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz#2d2c460c4c4695ec0a58afb2801a1205256c4e6b" + integrity sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA== + dependencies: + fast-glob "^3.2.7" + glob-parent "^6.0.1" + globby "^11.0.3" + normalize-path "^3.0.0" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globby@^11.0.3: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +schema-utils@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +serialize-javascript@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +sql.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-1.8.0.tgz#cb45d957e17a2239662fe2f614c9b678990867a6" + integrity sha512-3HD8pSkZL+5YvYUI8nlvNILs61ALqq34xgmF+BHpqxe68yZIJ1H+sIVIODvni25+CcxHUxDyrTJUL0lE/m7afw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +ws@8.18.3: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 6244655..6dc0076 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -16,7 +16,7 @@ import ru.bartwell.kick.module.overlay.core.provider.PerformanceOverlayProvider @Suppress("UnusedPrivateProperty", "EmptyFunctionBlock", "UNUSED_PARAMETER") public class OverlayModule( context: PlatformContext, - providers: List = listOf(PerformanceOverlayProvider()), + providers: List = emptyList(), ) : Module { override val description: ModuleDescription = ModuleDescription.OVERLAY override val startConfig: Config = StubConfig(description) diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt index 407ece7..521fff0 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt @@ -6,9 +6,7 @@ import ru.bartwell.kick.module.overlay.OverlayAccessor public interface OverlayProvider { public val categories: Set - public fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle -} + public fun start(scope: CoroutineScope) -public fun interface OverlayProviderHandle { public fun stop() } diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt index a1567a4..a23ecc4 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt @@ -6,15 +6,13 @@ import ru.bartwell.kick.module.overlay.OverlayAccessor public class PerformanceOverlayProvider : OverlayProvider { override val categories: Set = setOf(CATEGORY) - override fun start(scope: CoroutineScope, overlay: OverlayAccessor, category: String): OverlayProviderHandle { - return OverlayProviderHandle {} - } + @Suppress("EmptyFunctionBlock") + override fun start(scope: CoroutineScope) {} + + @Suppress("EmptyFunctionBlock") + override fun stop() {} public companion object { - private const val KEY_SEPARATOR: String = "::" public const val CATEGORY: String = "Performance" - public const val CPU_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "CPU" - public const val MEMORY_USAGE_KEY: String = CATEGORY + KEY_SEPARATOR + "RAM" - public const val CUSTOM_KEY_PREFIX: String = CATEGORY + KEY_SEPARATOR } } diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt new file mode 100644 index 0000000..d924899 --- /dev/null +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt @@ -0,0 +1,3 @@ +package ru.bartwell.kick.module.overlay.core.data + +internal data class CpuTimes(val idle: Long, val total: Long) \ No newline at end of file diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt new file mode 100644 index 0000000..1427429 --- /dev/null +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt @@ -0,0 +1,3 @@ +package ru.bartwell.kick.module.overlay.core.data + +internal data class MemoryInfo(val used: Long?, val total: Long?) \ No newline at end of file diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt index f7180d2..566a31d 100644 --- a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt @@ -1,7 +1,11 @@ package ru.bartwell.kick.module.overlay.core.provider +import android.os.Build +import android.os.Process +import android.os.SystemClock +import ru.bartwell.kick.module.overlay.core.data.CpuTimes +import ru.bartwell.kick.module.overlay.core.data.MemoryInfo import java.io.File -import java.io.RandomAccessFile private const val PROC_STAT_PATH: String = "/proc/stat" private const val PROC_MEMINFO_PATH: String = "/proc/meminfo" @@ -14,9 +18,9 @@ private const val SPACE_DELIMITER: String = " " private const val KEY_VALUE_DELIMITER: String = ":" private const val KIB_IN_BYTES: Long = 1024L -private data class CpuTimes(val idle: Long, val total: Long) - private var previousCpuTimes: CpuTimes? = null +private var lastAppCpuTimeMs = 0L +private var lastWallTimeMs = 0L internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { val cpuUsage = readCpuUsage() @@ -28,44 +32,67 @@ internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { ) } -private fun readCpuUsage(): Double? = - runCatching { - RandomAccessFile(PROC_STAT_PATH, "r").use { reader -> - val values = reader.readLine() - ?.split(SPACE_DELIMITER) - ?.filter { it.isNotBlank() } - ?.takeIf { it.size >= CPU_MIN_TOKEN_COUNT && it.first() == CPU_TOKEN_PREFIX } +private fun readCpuUsage(): Double? { + var result: Double? = null + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val nowWall = SystemClock.uptimeMillis() + val nowCpu = Process.getElapsedCpuTime() + val prevWall = lastWallTimeMs + val prevCpu = lastAppCpuTimeMs + lastWallTimeMs = nowWall + lastAppCpuTimeMs = nowCpu + + if (prevWall != 0L) { + val deltaWall = (nowWall - prevWall).coerceAtLeast(1L) + val deltaCpu = (nowCpu - prevCpu).coerceAtLeast(0L) + val cores = Runtime.getRuntime().availableProcessors().coerceAtLeast(1) + val percent = deltaCpu.toDouble() / (deltaWall.toDouble() * cores) * CPU_PERCENT_FACTOR + result = percent.coerceIn(0.0, CPU_PERCENT_FACTOR) + } + } else { + result = runCatching { + val header = run { + var line: String? = null + File(PROC_STAT_PATH).useLines { seq -> + line = seq.firstOrNull { it.startsWith("$CPU_TOKEN_PREFIX ") } + } + line + } ?: return@runCatching null + + val tokens = header + .trim() + .split(Regex("\\s+")) + .takeIf { it.size >= CPU_MIN_TOKEN_COUNT && it.first() == CPU_TOKEN_PREFIX } ?.drop(1) ?.mapNotNull(String::toLongOrNull) + ?: return@runCatching null + + val idle = (tokens.getOrNull(CPU_IDLE_INDEX) ?: 0L) + + (tokens.getOrNull(CPU_IOWAIT_INDEX) ?: 0L) + val total = tokens.sum() - if (values.isNullOrEmpty()) { - null + val prev = previousCpuTimes + val current = CpuTimes(idle = idle, total = total) + previousCpuTimes = current + + if (prev != null) { + val deltaIdle = idle - prev.idle + val deltaTotal = total - prev.total + if (deltaTotal > 0) { + (1.0 - deltaIdle.toDouble() / deltaTotal.toDouble()) * CPU_PERCENT_FACTOR + } else null } else { - val idle = (values.getOrNull(CPU_IDLE_INDEX) ?: 0L) + - (values.getOrNull(CPU_IOWAIT_INDEX) ?: 0L) - val total = values.sum() - - val current = CpuTimes(idle = idle, total = total) - val previous = previousCpuTimes - previousCpuTimes = current - - previous?.let { - val deltaIdle = idle - it.idle - val deltaTotal = total - it.total - if (deltaTotal > 0) { - (1.0 - deltaIdle.toDouble() / deltaTotal.toDouble()) * CPU_PERCENT_FACTOR - } else { - null - } - } + (total - idle).toDouble() / total.toDouble() * CPU_PERCENT_FACTOR } - } - }.getOrNull() + }.getOrNull() + } + + return result +} -private data class MemoryInfo(val used: Long?, val total: Long?) -private fun readMemoryInfo(): MemoryInfo? = - runCatching { +private fun readMemoryInfo(): MemoryInfo? = runCatching { val memInfo = File(PROC_MEMINFO_PATH) if (!memInfo.exists()) { return@runCatching null @@ -93,6 +120,6 @@ private fun readMemoryInfo(): MemoryInfo? = free + buffers + cached } - val used = if (total != null && available != null) total - available else null + val used = if (total != null) total - available else null MemoryInfo(used = used, total = total) }.getOrNull() diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index b1b4c05..5914310 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -49,13 +49,15 @@ public class OverlayModule( private fun observeFloatingWindowState() { combine(OverlaySettings.observeEnabled(), OverlayStore.selectedCategory) { isWindowEnabled, currentCategory -> providers.forEach { provider -> - provider.categories.forEach { providerCategory -> - OverlayStore.addCategory(providerCategory) - if (isWindowEnabled && providerCategory == currentCategory && provider.isAvailable) { - provider.start(providerScope) - } else { - provider.stop() - } + provider.categories.forEach(OverlayStore::addCategory) + + val shouldStart = isWindowEnabled && provider.isAvailable && + provider.categories.any { it == currentCategory } + + if (shouldStart) { + provider.start(providerScope) + } else { + provider.stop() } } } diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt new file mode 100644 index 0000000..671b092 --- /dev/null +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt @@ -0,0 +1,3 @@ +package ru.bartwell.kick.module.overlay.core.data + +internal data class CpuSample(val user: ULong, val nice: ULong, val system: ULong, val idle: ULong) \ No newline at end of file diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt new file mode 100644 index 0000000..db3f9ee --- /dev/null +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt @@ -0,0 +1,8 @@ +package ru.bartwell.kick.module.overlay.core.data + +import kotlin.native.concurrent.ThreadLocal + +@ThreadLocal +internal object CpuState { + var previous: CpuSample? = null +} \ No newline at end of file diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt index 706ae79..2b87b7c 100644 --- a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt @@ -1,10 +1,15 @@ -@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) +@file:OptIn(ExperimentalForeignApi::class) package ru.bartwell.kick.module.overlay.core.provider +import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.UIntVar import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped +import kotlinx.cinterop.plus +import kotlinx.cinterop.pointed import kotlinx.cinterop.ptr import kotlinx.cinterop.reinterpret import kotlinx.cinterop.value @@ -18,24 +23,18 @@ import platform.darwin.HOST_CPU_LOAD_INFO_COUNT import platform.darwin.KERN_SUCCESS import platform.darwin.MACH_TASK_BASIC_INFO import platform.darwin.MACH_TASK_BASIC_INFO_COUNT -import platform.darwin.host_cpu_load_info +import platform.darwin.host_cpu_load_info_data_t import platform.darwin.host_statistics import platform.darwin.mach_host_self import platform.darwin.mach_msg_type_number_tVar import platform.darwin.mach_task_basic_info import platform.darwin.mach_task_self_ import platform.darwin.task_info -import kotlin.native.concurrent.ThreadLocal - -private data class CpuSample(val user: ULong, val nice: ULong, val system: ULong, val idle: ULong) +import ru.bartwell.kick.module.overlay.core.data.CpuSample +import ru.bartwell.kick.module.overlay.core.data.CpuState private const val PERCENT_FACTOR: Double = 100.0 -@ThreadLocal -private object CpuState { - var previous: CpuSample? = null -} - internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { val cpu = readCpuUsage() val memory = readMemoryUsage() @@ -48,7 +47,7 @@ internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { } private fun readCpuUsage(): Double? = memScoped { - val cpuInfo = alloc() + val cpuInfo = alloc() val count = alloc().apply { value = HOST_CPU_LOAD_INFO_COUNT } val result = host_statistics( @@ -62,15 +61,18 @@ private fun readCpuUsage(): Double? = memScoped { return null } - val ticks = cpuInfo.cpu_ticks + val ticksPtr: CPointer = cpuInfo.cpu_ticks + + fun tick(cpuStateIndex: Int) = (ticksPtr + cpuStateIndex)!!.pointed.value.toULong() val sample = CpuSample( - user = ticks[CPU_STATE_USER.toInt()].value.toULong(), - nice = ticks[CPU_STATE_NICE.toInt()].value.toULong(), - system = ticks[CPU_STATE_SYSTEM.toInt()].value.toULong(), - idle = ticks[CPU_STATE_IDLE.toInt()].value.toULong(), + user = tick(CPU_STATE_USER), + nice = tick(CPU_STATE_NICE), + system = tick(CPU_STATE_SYSTEM), + idle = tick(CPU_STATE_IDLE), ) + val previousSample = CpuState.previous CpuState.previous = sample @@ -89,12 +91,14 @@ private fun readCpuUsage(): Double? = memScoped { } private fun readMemoryUsage(): Pair? = memScoped { - val count = alloc().apply { value = MACH_TASK_BASIC_INFO_COUNT } + val count = alloc().apply { + value = MACH_TASK_BASIC_INFO_COUNT.convert() + } val info = alloc() val result = task_info( target_task = mach_task_self_, - flavor = MACH_TASK_BASIC_INFO, + flavor = MACH_TASK_BASIC_INFO.toUInt(), task_info_out = info.ptr.reinterpret(), task_info_outCnt = count.ptr, ) diff --git a/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme b/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme index 24e1e88..c7c9f26 100644 --- a/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme +++ b/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme @@ -50,6 +50,10 @@ ReferencedContainer = "container:iosSample.xcodeproj"> + + Date: Tue, 21 Oct 2025 01:09:30 +0300 Subject: [PATCH 6/8] Fix Detekt's issues --- .../kick/module/overlay/OverlayModule.kt | 1 - .../overlay/core/provider/OverlayProvider.kt | 1 - .../provider/PerformanceOverlayProvider.kt | 1 - .../kick/module/overlay/core/data/CpuTimes.kt | 2 +- .../module/overlay/core/data/MemoryInfo.kt | 2 +- .../provider/PerformanceSnapshot.android.kt | 55 ++++--- .../kick/module/overlay/OverlayModule.kt | 2 +- .../overlay/core/overlay/OverlayWindow.kt | 7 +- .../module/overlay/core/data/CpuSample.kt | 2 +- .../kick/module/overlay/core/data/CpuState.kt | 2 +- .../core/provider/PerformanceSnapshot.ios.kt | 7 +- .../core/overlay/AutosizeMeasure.wasmJs.kt | 43 ----- .../core/overlay/KickOverlay.wasmJs.kt | 155 ++++++++++++++---- .../overlay/core/overlay/Overlay.wasmJs.kt | 33 ---- 14 files changed, 161 insertions(+), 152 deletions(-) delete mode 100644 module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/AutosizeMeasure.wasmJs.kt delete mode 100644 module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/Overlay.wasmJs.kt diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 6dc0076..d2286c9 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -11,7 +11,6 @@ import ru.bartwell.kick.core.data.Module import ru.bartwell.kick.core.data.ModuleDescription import ru.bartwell.kick.core.data.PlatformContext import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider -import ru.bartwell.kick.module.overlay.core.provider.PerformanceOverlayProvider @Suppress("UnusedPrivateProperty", "EmptyFunctionBlock", "UNUSED_PARAMETER") public class OverlayModule( diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt index 521fff0..555c360 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt @@ -1,7 +1,6 @@ package ru.bartwell.kick.module.overlay.core.provider import kotlinx.coroutines.CoroutineScope -import ru.bartwell.kick.module.overlay.OverlayAccessor public interface OverlayProvider { public val categories: Set diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt index a23ecc4..117ad51 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt @@ -1,7 +1,6 @@ package ru.bartwell.kick.module.overlay.core.provider import kotlinx.coroutines.CoroutineScope -import ru.bartwell.kick.module.overlay.OverlayAccessor public class PerformanceOverlayProvider : OverlayProvider { override val categories: Set = setOf(CATEGORY) diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt index d924899..7ba4a77 100644 --- a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt @@ -1,3 +1,3 @@ package ru.bartwell.kick.module.overlay.core.data -internal data class CpuTimes(val idle: Long, val total: Long) \ No newline at end of file +internal data class CpuTimes(val idle: Long, val total: Long) diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt index 1427429..20eb026 100644 --- a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt @@ -1,3 +1,3 @@ package ru.bartwell.kick.module.overlay.core.data -internal data class MemoryInfo(val used: Long?, val total: Long?) \ No newline at end of file +internal data class MemoryInfo(val used: Long?, val total: Long?) diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt index 566a31d..8d0194a 100644 --- a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt @@ -69,7 +69,7 @@ private fun readCpuUsage(): Double? { ?: return@runCatching null val idle = (tokens.getOrNull(CPU_IDLE_INDEX) ?: 0L) + - (tokens.getOrNull(CPU_IOWAIT_INDEX) ?: 0L) + (tokens.getOrNull(CPU_IOWAIT_INDEX) ?: 0L) val total = tokens.sum() val prev = previousCpuTimes @@ -81,7 +81,9 @@ private fun readCpuUsage(): Double? { val deltaTotal = total - prev.total if (deltaTotal > 0) { (1.0 - deltaIdle.toDouble() / deltaTotal.toDouble()) * CPU_PERCENT_FACTOR - } else null + } else { + null + } } else { (total - idle).toDouble() / total.toDouble() * CPU_PERCENT_FACTOR } @@ -91,35 +93,34 @@ private fun readCpuUsage(): Double? { return result } - private fun readMemoryInfo(): MemoryInfo? = runCatching { - val memInfo = File(PROC_MEMINFO_PATH) - if (!memInfo.exists()) { - return@runCatching null - } + val memInfo = File(PROC_MEMINFO_PATH) + if (!memInfo.exists()) { + return@runCatching null + } - val values = mutableMapOf() - memInfo.useLines { sequence -> - sequence.forEach { line -> - val parts = line.split(KEY_VALUE_DELIMITER, limit = 2) - if (parts.size == 2) { - val key = parts[0].trim() - val value = parts[1].trim().split(SPACE_DELIMITER).firstOrNull()?.toLongOrNull() - if (value != null) { - values[key] = value * KIB_IN_BYTES - } + val values = mutableMapOf() + memInfo.useLines { sequence -> + sequence.forEach { line -> + val parts = line.split(KEY_VALUE_DELIMITER, limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim().split(SPACE_DELIMITER).firstOrNull()?.toLongOrNull() + if (value != null) { + values[key] = value * KIB_IN_BYTES } } } + } - val total = values["MemTotal"] - val available = values["MemAvailable"] ?: run { - val free = values["MemFree"] ?: 0L - val buffers = values["Buffers"] ?: 0L - val cached = values["Cached"] ?: 0L - free + buffers + cached - } + val total = values["MemTotal"] + val available = values["MemAvailable"] ?: run { + val free = values["MemFree"] ?: 0L + val buffers = values["Buffers"] ?: 0L + val cached = values["Cached"] ?: 0L + free + buffers + cached + } - val used = if (total != null) total - available else null - MemoryInfo(used = used, total = total) - }.getOrNull() + val used = if (total != null) total - available else null + MemoryInfo(used = used, total = total) +}.getOrNull() diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 5914310..77fa5f9 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -52,7 +52,7 @@ public class OverlayModule( provider.categories.forEach(OverlayStore::addCategory) val shouldStart = isWindowEnabled && provider.isAvailable && - provider.categories.any { it == currentCategory } + provider.categories.any { it == currentCategory } if (shouldStart) { provider.start(providerScope) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt index 345376b..a9e0fff 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.unit.dp import ru.bartwell.kick.module.overlay.core.store.OverlayStore @Composable -internal fun OverlayWindow(onCloseClick: () -> Unit, measureFull: Boolean = false) { +internal fun OverlayWindow(onCloseClick: () -> Unit) { val lines by OverlayStore.items.collectAsState() val shape = RectangleShape @@ -48,11 +48,6 @@ internal fun OverlayWindow(onCloseClick: () -> Unit, measureFull: Boolean = fals ) { Spacer(Modifier.height(2.dp)) lines.forEach { (k, v) -> - val (ml, sw, of) = if (measureFull) { - Triple(Int.MAX_VALUE, false, TextOverflow.Clip) - } else { - Triple(1, false, TextOverflow.Ellipsis) - } Text( text = "$k: $v", style = MaterialTheme.typography.bodySmall, diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt index 671b092..9cea769 100644 --- a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt @@ -1,3 +1,3 @@ package ru.bartwell.kick.module.overlay.core.data -internal data class CpuSample(val user: ULong, val nice: ULong, val system: ULong, val idle: ULong) \ No newline at end of file +internal data class CpuSample(val user: ULong, val nice: ULong, val system: ULong, val idle: ULong) diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt index db3f9ee..0294420 100644 --- a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt @@ -5,4 +5,4 @@ import kotlin.native.concurrent.ThreadLocal @ThreadLocal internal object CpuState { var previous: CpuSample? = null -} \ No newline at end of file +} diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt index 2b87b7c..5589fd9 100644 --- a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt @@ -66,13 +66,12 @@ private fun readCpuUsage(): Double? = memScoped { fun tick(cpuStateIndex: Int) = (ticksPtr + cpuStateIndex)!!.pointed.value.toULong() val sample = CpuSample( - user = tick(CPU_STATE_USER), - nice = tick(CPU_STATE_NICE), + user = tick(CPU_STATE_USER), + nice = tick(CPU_STATE_NICE), system = tick(CPU_STATE_SYSTEM), - idle = tick(CPU_STATE_IDLE), + idle = tick(CPU_STATE_IDLE), ) - val previousSample = CpuState.previous CpuState.previous = sample diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/AutosizeMeasure.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/AutosizeMeasure.wasmJs.kt deleted file mode 100644 index 812cc2b..0000000 --- a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/AutosizeMeasure.wasmJs.kt +++ /dev/null @@ -1,43 +0,0 @@ -package ru.bartwell.kick.module.overlay.core.overlay - -import androidx.compose.runtime.Composable -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.unit.IntSize - -@Composable -internal fun AutosizeMeasure( - onSizes: (desired: IntSize, actual: IntSize) -> Unit, - desiredContent: @Composable () -> Unit, - actualContent: @Composable () -> Unit, -) { - SubcomposeLayout { constraints -> - val desiredPlaceables = subcompose("desired", desiredContent).map { - it.measure( - constraints.copy( - minWidth = 0, - maxWidth = Int.MAX_VALUE, - minHeight = 0, - maxHeight = Int.MAX_VALUE - ) - ) - } - val desiredW = desiredPlaceables.maxOfOrNull { it.width } ?: 0 - val desiredH = desiredPlaceables.sumOf { it.height }.coerceAtLeast(0) - val desired = IntSize(desiredW, desiredH) - - val fixed = constraints.copy( - minWidth = desiredW, - maxWidth = desiredW, - minHeight = desiredH, - maxHeight = desiredH - ) - val actualPlaceables = subcompose("actual", actualContent).map { it.measure(fixed) } - val actual = IntSize(desiredW, desiredH) - - onSizes(desired, actual) - - layout(desiredW, desiredH) { - actualPlaceables.forEach { it.place(0, 0) } - } - } -} diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt index 745cf79..7835a16 100644 --- a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt +++ b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt @@ -1,11 +1,19 @@ package ru.bartwell.kick.module.overlay.core.overlay -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.ComposeViewport import kotlinx.browser.document +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.events.Event +import org.w3c.dom.events.MouseEvent import ru.bartwell.kick.core.data.PlatformContext import ru.bartwell.kick.module.overlay.core.persists.OverlaySettings +import ru.bartwell.kick.module.overlay.core.store.OverlayStore private const val INITIAL_WINDOW_X_PX = 50 private const val INITIAL_WINDOW_Y_PX = 200 @@ -13,16 +21,20 @@ private const val INITIAL_WINDOW_Y_PX = 200 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") public actual object KickOverlay { private var overlayRoot: HTMLElement? = null + private var overlayLines: HTMLElement? = null + private var mouseMoveListener: ((Event) -> Unit)? = null + private var mouseUpListener: ((Event) -> Unit)? = null + private var scope: CoroutineScope? = null + private var itemsJob: Job? = null @Suppress("EmptyFunctionBlock") public actual fun init(context: PlatformContext) {} - @OptIn(ExperimentalComposeUiApi::class) + @Suppress("LongMethod", "StringLiteralDuplication") public actual fun show() { - val existing = overlayRoot - if (existing != null) { - existing.style.display = "" - existing.style.visibility = "visible" + if (overlayRoot != null) { + overlayRoot!!.style.display = "" + overlayRoot!!.style.visibility = "visible" OverlaySettings.setEnabled(true) return } @@ -36,23 +48,73 @@ public actual object KickOverlay { top = "${INITIAL_WINDOW_Y_PX}px" left = "${INITIAL_WINDOW_X_PX}px" zIndex = "2147483647" - width = "180px" - height = "60px" - backgroundColor = "transparent" - borderRadius = "0px" setProperty("pointer-events", "auto") + setProperty("user-select", "none") + display = "inline-block" + // Auto-size to content + width = "max-content" + height = "max-content" + } + } + + val container = (document.createElement("div") as HTMLElement).apply { + id = "kick-overlay-content" + style.apply { + position = "relative" + backgroundColor = "white" + setProperty("border", "1px solid rgba(0,0,0,0.12)") + setProperty("box-shadow", "0 6px 24px rgba(0,0,0,0.15)") + borderRadius = "0px" + padding = "2px 16px 2px 6px" + color = "black" + fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, " + + "Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif" + fontSize = "12px" + setProperty("line-height", "16px") + setProperty("white-space", "nowrap") + } + } + + val close = (document.createElement("span") as HTMLSpanElement).apply { + id = "kick-overlay-close" + textContent = "\u00D7" + style.apply { + position = "absolute" + right = "2px" + top = "2px" + width = "20px" + height = "20px" + textAlign = "center" + cursor = "pointer" + borderRadius = "10px" + setProperty("line-height", "16px") + setProperty("user-select", "none") } + addEventListener("click") { _ -> onCloseClicked() } + addEventListener("mousedown") { e -> e.stopPropagation() } } + + val lines = (document.createElement("div") as HTMLElement).apply { + id = "kick-overlay-lines" + style.apply { setProperty("white-space", "nowrap") } + } + + container.appendChild(close) + container.appendChild(lines) + root.appendChild(container) document.body?.appendChild(root) + overlayRoot = root + overlayLines = lines + // Drag handling var dragging = false var startX = 0.0 var startY = 0.0 var offsetX = 0.0 var offsetY = 0.0 - val onMouseDown: (org.w3c.dom.events.MouseEvent) -> Unit = { ev -> + val onMouseDown: (MouseEvent) -> Unit = { ev -> dragging = true startX = ev.clientX.toDouble() startY = ev.clientY.toDouble() @@ -60,7 +122,7 @@ public actual object KickOverlay { offsetY = root.offsetTop.toDouble() ev.preventDefault() } - val onMouseMove: (org.w3c.dom.events.MouseEvent) -> Unit = { ev -> + val onMouseMove: (MouseEvent) -> Unit = { ev -> if (dragging) { val dx = ev.clientX - startX val dy = ev.clientY - startY @@ -68,32 +130,63 @@ public actual object KickOverlay { root.style.top = "${(offsetY + dy).toInt()}px" } } - val onMouseUp: (org.w3c.dom.events.MouseEvent) -> Unit = { _ -> dragging = false } + val onMouseUp: (MouseEvent) -> Unit = { _ -> dragging = false } - root.addEventListener("mousedown") { e: org.w3c.dom.events.Event -> - onMouseDown(e as org.w3c.dom.events.MouseEvent) - } - document.addEventListener("mousemove") { e: org.w3c.dom.events.Event -> - onMouseMove(e as org.w3c.dom.events.MouseEvent) - } - document.addEventListener("mouseup") { e: org.w3c.dom.events.Event -> - onMouseUp(e as org.w3c.dom.events.MouseEvent) - } + root.addEventListener("mousedown") { e: Event -> onMouseDown(e as MouseEvent) } + val moveListener: (Event) -> Unit = { e -> onMouseMove(e as MouseEvent) } + val upListener: (Event) -> Unit = { e -> onMouseUp(e as MouseEvent) } + document.addEventListener("mousemove", moveListener) + document.addEventListener("mouseup", upListener) + mouseMoveListener = moveListener + mouseUpListener = upListener - ComposeViewport(root) { - Overlay( - root = root, - onCloseClick = ::onCloseClicked, - ) + // Subscribe to data updates + scope = CoroutineScope(Dispatchers.Default).also { s -> + itemsJob = s.launch { + OverlayStore.items.collect { currentItems -> + renderLines(currentItems) + } + } } + + // Render initial state + renderLines(OverlayStore.items.value) } public actual fun hide() { OverlaySettings.setEnabled(false) - overlayRoot?.let { el -> - el.parentElement?.removeChild(el) - } + + itemsJob?.cancel() + itemsJob = null + scope?.cancel() + scope = null + + mouseMoveListener?.let { document.removeEventListener("mousemove", it) } + mouseUpListener?.let { document.removeEventListener("mouseup", it) } + mouseMoveListener = null + mouseUpListener = null + + overlayRoot?.let { el -> el.parentElement?.removeChild(el) } overlayRoot = null + overlayLines = null + } + + private fun renderLines(items: List>) { + val container = overlayLines ?: return + while (container.firstChild != null) { + container.removeChild(container.firstChild!!) + } + for ((key, value) in items) { + val line = document.createElement("div") as HTMLElement + line.textContent = "$key: $value" + line.style.apply { + setProperty("white-space", "nowrap") + setProperty("margin", "2px 0") + setProperty("overflow", "hidden") + setProperty("text-overflow", "clip") + } + container.appendChild(line) + } } private fun onCloseClicked() { diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/Overlay.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/Overlay.wasmJs.kt deleted file mode 100644 index 02c2f65..0000000 --- a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/Overlay.wasmJs.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ru.bartwell.kick.module.overlay.core.overlay - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.IntSize -import org.w3c.dom.HTMLElement -import kotlin.math.max - -@Composable -internal fun Overlay( - root: HTMLElement, - onCloseClick: () -> Unit, -) { - MaterialTheme { - var desiredPx by remember { mutableStateOf(IntSize(0, 0)) } - - AutosizeMeasure( - onSizes = { desired, _ -> desiredPx = desired }, - desiredContent = { OverlayWindow(onCloseClick = onCloseClick, measureFull = true) }, - actualContent = { OverlayWindow(onCloseClick = onCloseClick, measureFull = false) } - ) - - LaunchedEffect(desiredPx) { - root.style.width = "${max(1, desiredPx.width)}px" - root.style.height = "${max(1, desiredPx.height)}px" - } - } -} From 48067c5ab50df0ebd358881715aa1fe7fd497624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Tue, 21 Oct 2025 22:43:01 +0300 Subject: [PATCH 7/8] Update README --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 3c523b6..5f28ac2 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,28 @@ Kick.overlay.set("fps", 42, "Performance") Kick.overlay.set("isWsConnected", true, "Network") ``` +#### Providers + +Overlay modules can populate categories automatically through `OverlayProvider`s. By default `OverlayModule` registers the built-in `PerformanceOverlayProvider`, which exposes CPU and memory usage in the "Performance" category whenever the floating panel is visible.【F:module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt†L30-L55】【F:module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt†L25-L87】 + +Pass custom providers to `OverlayModule` to emit additional metrics: + +```kotlin +Kick.init(context) { + module( + OverlayModule( + context = context, + providers = listOf( + PerformanceOverlayProvider(), + MyCustomOverlayProvider(), // implements OverlayProvider + ), + ), + ) +} +``` + +Implement `OverlayProvider` to decide when your provider should run, which categories it contributes to, and how it updates values via `Kick.overlay.set` inside the supplied coroutine scope.【F:module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt†L3-L17】 + ### Advanced Module Configuration You don't need to add all the available modules. Just include the ones you need. Here only logging and network inspection are enabled: From 6c4fe2937b971530866d18e9007adae677971ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Tue, 21 Oct 2025 22:45:48 +0300 Subject: [PATCH 8/8] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f28ac2..cb1b019 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,7 @@ Kick.overlay.set("isWsConnected", true, "Network") #### Providers -Overlay modules can populate categories automatically through `OverlayProvider`s. By default `OverlayModule` registers the built-in `PerformanceOverlayProvider`, which exposes CPU and memory usage in the "Performance" category whenever the floating panel is visible.【F:module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt†L30-L55】【F:module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt†L25-L87】 +Overlay modules can populate categories automatically through `OverlayProvider`s. By default `OverlayModule` registers the built-in `PerformanceOverlayProvider`, which exposes CPU and memory usage in the "Performance" category whenever the floating panel is visible. Pass custom providers to `OverlayModule` to emit additional metrics: @@ -368,7 +368,7 @@ Kick.init(context) { } ``` -Implement `OverlayProvider` to decide when your provider should run, which categories it contributes to, and how it updates values via `Kick.overlay.set` inside the supplied coroutine scope.【F:module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt†L3-L17】 +Implement `OverlayProvider` to decide when your provider should run, which categories it contributes to, and how it updates values via `Kick.overlay.set` inside the supplied coroutine scope. ### Advanced Module Configuration