mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
fix: give the session a live /proc/stat and read GPU load and clock where other overlays do
This commit is contained in:
1 parent
48054a86ab
commit
b29ece82c8
4 files changed
+153
-22
No files matched your search
@@ -231,7 +231,10 @@ public final class LinuxRuntime {
|
||||
};
|
||||
for (String[] entry : procFiles) {
|
||||
File fake = new File(fakeProc, entry[0]);
|
||||
if (fake.isFile() && !new File(entry[1]).canRead()) {
|
||||
// A live stand-in the session binds itself (CpuStatComponent's /proc/stat) wins.
|
||||
boolean live = false;
|
||||
if (extraBinds != null) for (String spec : extraBinds) live |= spec.endsWith(":" + entry[1]);
|
||||
if (!live && fake.isFile() && !new File(entry[1]).canRead()) {
|
||||
bind(cmd, fake.getPath() + ":" + entry[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.SessionPart
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* A `/proc/stat` that moves.
|
||||
*
|
||||
* Android has refused apps `/proc/stat` since 8.0, and the session's stand-in for it was one
|
||||
* `cpu0` line of zeros - enough for glibc and Steam to start, but every CPU load in the session
|
||||
* read 0 % (the performance overlay showed one core, idle). The kernel still lets apps read each
|
||||
* core's idle residency (`cpuidle/stateN/time`, µs, the same label as the cpufreq files), which is
|
||||
* how overlays on Android work out CPU load: a core's busy time is the time that passed less the
|
||||
* time it spent idle. This writes `/proc/stat` from that, one line per core, refreshed twice a
|
||||
* second. Counters only grow - a sample's idle is never more than the time that passed - so a
|
||||
* reader taking the difference of two reads never sees a negative. Where `/proc/stat` is readable,
|
||||
* or the idle times are not, nothing is done.
|
||||
*/
|
||||
class CpuStatComponent(val file: File) : SessionPart() {
|
||||
@Volatile private var running = false
|
||||
private var thread: Thread? = null
|
||||
private val cores = (0 until coreCount()).toList()
|
||||
private val busy = LongArray(cores.size)
|
||||
private val idle = LongArray(cores.size)
|
||||
private val lastIdleUs = LongArray(cores.size)
|
||||
private var lastMs = 0L
|
||||
private val bootSeconds = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) / 1000
|
||||
|
||||
/** Writes the first sample; true when it should be bound over `/proc/stat`. */
|
||||
fun prepare(): Boolean {
|
||||
if (File("/proc/stat").canRead() || cores.isEmpty() || idleUs(0) == null) return false
|
||||
lastMs = SystemClock.elapsedRealtime()
|
||||
for (i in cores) lastIdleUs[i] = idleUs(i) ?: 0L
|
||||
write()
|
||||
Log.i(TAG, "hud: /proc/stat from the cores' idle times (${cores.size} cores)")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
running = true
|
||||
thread = Thread({
|
||||
while (running) {
|
||||
try { Thread.sleep(PERIOD_MS) } catch (e: InterruptedException) { break }
|
||||
if (!running) break
|
||||
sample()
|
||||
write()
|
||||
}
|
||||
}, "cpu-stat").apply { isDaemon = true; start() }
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
running = false
|
||||
thread?.interrupt()
|
||||
thread = null
|
||||
}
|
||||
|
||||
private fun sample() {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val elapsed = (now - lastMs) / MS_PER_TICK
|
||||
if (elapsed <= 0) return
|
||||
lastMs += elapsed * MS_PER_TICK
|
||||
for (i in cores) {
|
||||
// An offline core has no idle residency to count and runs nothing: all idle.
|
||||
val us = idleUs(i)
|
||||
val idleTicks = if (us == null) elapsed else ((us - lastIdleUs[i]) / US_PER_TICK).coerceIn(0, elapsed)
|
||||
if (us != null) lastIdleUs[i] = us
|
||||
idle[i] += idleTicks
|
||||
busy[i] += elapsed - idleTicks
|
||||
}
|
||||
}
|
||||
|
||||
/** The whole file at once, renamed into place: a reader opens either the last sample or this one. */
|
||||
private fun write() {
|
||||
val text = StringBuilder()
|
||||
text.append("cpu ${busy.sum()} 0 0 ${idle.sum()} 0 0 0 0 0 0\n")
|
||||
for (i in cores) text.append("cpu$i ${busy[i]} 0 0 ${idle[i]} 0 0 0 0 0 0\n")
|
||||
text.append("intr 0\nctxt 0\nbtime $bootSeconds\nprocesses 1\nprocs_running 1\nprocs_blocked 0\n")
|
||||
try {
|
||||
val tmp = File(file.parentFile, file.name + ".tmp")
|
||||
tmp.writeText(text.toString())
|
||||
if (!tmp.renameTo(file)) tmp.delete()
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SessionService"
|
||||
private const val PERIOD_MS = 500L
|
||||
/** /proc/stat counts in USER_HZ, 100 a second on every Linux the guest runs. */
|
||||
private const val MS_PER_TICK = 10L
|
||||
private const val US_PER_TICK = 10_000L
|
||||
|
||||
private fun coreCount(): Int =
|
||||
File("/sys/devices/system/cpu").list()?.count { it.matches(Regex("cpu\\d+")) } ?: 0
|
||||
|
||||
/** The core's total idle residency in µs, or null when it has none to read (offline). */
|
||||
private fun idleUs(core: Int): Long? {
|
||||
val states = File("/sys/devices/system/cpu/cpu$core/cpuidle").listFiles { f -> f.name.startsWith("state") }
|
||||
?: return null
|
||||
if (states.isEmpty()) return null
|
||||
var total = 0L
|
||||
for (state in states) {
|
||||
total += runCatching { File(state, "time").readText().trim().toLong() }.getOrNull() ?: return null
|
||||
}
|
||||
return total
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,10 @@ import java.io.RandomAccessFile
|
||||
* process on a file there it may not read - not a file that is missing, which it skips. Under an
|
||||
* enforcing SELinux policy (every retail phone) KGSL's sysfs is refused to apps, so mangoapp died
|
||||
* on start and the session ran with no overlay. Where that is so, a directory of our own is bound
|
||||
* there instead: `gpu_busy_percentage` worked out from `gpubusy` (busy and total cycles of the
|
||||
* last sample) and `clock_mhz` from `gpuclk` - files with labels of their own that policies often
|
||||
* leave to apps for GPU profilers - when they are readable, and `temp` linked to the GPU's
|
||||
* there instead: `gpu_busy_percentage` and `clock_mhz` from Qualcomm's `/sys/kernel/gpu` (load
|
||||
* and MHz, under the plain sysfs label - where other Android overlays read them), else from KGSL's
|
||||
* `gpubusy` (busy and total cycles of the last sample) and `gpuclk` (Hz), files with labels of
|
||||
* their own that policies often leave to apps for GPU profilers; and `temp` linked to the GPU's
|
||||
* thermal zone when there is one. What is not readable is left out, and the overlay shows no line
|
||||
* for it. Where KGSL's own files are readable, nothing is done.
|
||||
*/
|
||||
@@ -32,14 +33,12 @@ class GpuStatsComponent(val dir: File) : SessionPart() {
|
||||
return try {
|
||||
dir.mkdirs()
|
||||
dir.listFiles()?.forEach { java.nio.file.Files.deleteIfExists(it.toPath()) }
|
||||
val busy = File(KGSL, "gpubusy").canRead()
|
||||
if (busy) writeLoad(0)
|
||||
val clk = File(KGSL, "gpuclk").canRead()
|
||||
if (clk) readClock()?.let { write(clock, String.format("%5d\n", it)) }
|
||||
val busy = readBusy()?.also { writeLoad(it) } != null
|
||||
val clk = readClock()?.also { write(clock, String.format("%5d\n", it)) } != null
|
||||
val temp = LinuxRuntime.gpuTempSource()
|
||||
if (temp != null) Os.symlink(temp, File(dir, "temp").path)
|
||||
Log.i(TAG, "hud: kgsl stats from the session's own: load " + (if (busy) "from gpubusy" else "none") +
|
||||
", clock " + (if (clk) "from gpuclk" else "none") + ", temp " + (temp ?: "none"))
|
||||
Log.i(TAG, "hud: kgsl stats from the session's own: load " + (if (busy) loadFrom else "none") +
|
||||
", clock " + (if (clk) clockFrom else "none") + ", temp " + (temp ?: "none"))
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "hud: could not stand in for kgsl stats: $e")
|
||||
@@ -66,20 +65,30 @@ class GpuStatsComponent(val dir: File) : SessionPart() {
|
||||
thread = null
|
||||
}
|
||||
|
||||
private fun readBusy(): Int? = try {
|
||||
val parts = File(KGSL, "gpubusy").readText().trim().split(Regex("\\s+"))
|
||||
val busy = parts[0].toLong()
|
||||
val total = parts[1].toLong()
|
||||
if (total > 0) (busy * 100 / total).toInt().coerceIn(0, 100) else 0
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
private var loadFrom = ""
|
||||
private var clockFrom = ""
|
||||
|
||||
/** GPU load in percent: /sys/kernel/gpu's "NN %", else KGSL's busy and total cycles. */
|
||||
private fun readBusy(): Int? {
|
||||
runCatching { File(QCOM_GPU, "gpu_busy").readText().trim().removeSuffix("%").trim().toInt() }.getOrNull()?.let {
|
||||
loadFrom = "$QCOM_GPU/gpu_busy"
|
||||
return it.coerceIn(0, 100)
|
||||
}
|
||||
return runCatching {
|
||||
val parts = File(KGSL, "gpubusy").readText().trim().split(Regex("\\s+"))
|
||||
val total = parts[1].toLong()
|
||||
if (total > 0) (parts[0].toLong() * 100 / total).toInt().coerceIn(0, 100) else 0
|
||||
}.getOrNull()?.also { loadFrom = "$KGSL/gpubusy" }
|
||||
}
|
||||
|
||||
/** The GPU clock in MHz; gpuclk is in Hz. */
|
||||
private fun readClock(): Int? = try {
|
||||
(File(KGSL, "gpuclk").readText().trim().toLong() / 1_000_000).toInt()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
/** GPU clock in MHz: /sys/kernel/gpu's, else KGSL's gpuclk in Hz. */
|
||||
private fun readClock(): Int? {
|
||||
runCatching { File(QCOM_GPU, "gpu_clock").readText().trim().toInt() }.getOrNull()?.let {
|
||||
clockFrom = "$QCOM_GPU/gpu_clock"
|
||||
return it
|
||||
}
|
||||
return runCatching { (File(KGSL, "gpuclk").readText().trim().toLong() / 1_000_000).toInt() }.getOrNull()
|
||||
?.also { clockFrom = "$KGSL/gpuclk" }
|
||||
}
|
||||
|
||||
private fun writeLoad(percent: Int) = write(load, String.format("%3d %%\n", percent))
|
||||
@@ -95,6 +104,8 @@ class GpuStatsComponent(val dir: File) : SessionPart() {
|
||||
companion object {
|
||||
private const val TAG = "SessionService"
|
||||
const val KGSL = "/sys/class/kgsl/kgsl-3d0"
|
||||
/** Qualcomm's own GPU summary, beside KGSL in vendor kernels. */
|
||||
private const val QCOM_GPU = "/sys/kernel/gpu"
|
||||
private const val PERIOD_MS = 500L
|
||||
}
|
||||
}
|
||||
@@ -688,6 +688,13 @@ class SessionService : Service() {
|
||||
battery.attach(this)
|
||||
components.add(battery)
|
||||
binds.add(battery.dir.path + ":/sys/class/power_supply")
|
||||
// CPU load for everything in the session that reads /proc/stat, the overlay among them.
|
||||
val cpuStat = CpuStatComponent(File(filesDir, "session/proc-stat"))
|
||||
if (cpuStat.prepare()) {
|
||||
cpuStat.attach(this)
|
||||
components.add(cpuStat)
|
||||
binds.add(cpuStat.file.path + ":/proc/stat")
|
||||
}
|
||||
// The GPU's load and temperature for the performance overlay, where KGSL's sysfs is refused.
|
||||
val gpuStats = GpuStatsComponent(File(filesDir, "session/sys/kgsl-3d0"))
|
||||
if (gpuStats.prepare()) {
|
||||
|
||||
Reference in new issue
Block a user