From 28626d9742443004748efb8e572332ab03557ee4 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sat, 11 Jul 2026 19:08:29 +0000 Subject: [PATCH 1/4] Fix HUD CPU usage: measure /proc/stat load instead of clock-frequency ratio Compute CPU% from /proc/stat busy vs idle jiffie deltas (on-screen HUD and Task Manager pane, aggregate and per-core) instead of scaling_cur_freq/max which does not reflect actual utilization. Add Mali GPU load fallbacks (/sys/kernel/gpu/gpu_busy, /proc/mtk_mali/utilization). --- .../display/XServerDisplayActivity.java | 43 +++------ .../main/runtime/display/ui/FrameRating.java | 38 +++++--- app/src/main/runtime/system/CPUStatus.java | 96 +++++++++++++++++++ 3 files changed, 137 insertions(+), 40 deletions(-) diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index cc095b365..e0e761e47 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -440,7 +440,7 @@ private boolean isAnyControllerConnected() { private final ArrayList taskManagerAccum = new ArrayList<>(); private boolean taskManagerCpuExpanded = false; private boolean taskManagerPaneVisible = false; - private short[] cachedMaxClockSpeeds; + private CPUStatus.CpuSample prevTaskCpuSample; private boolean drawerEdgeGesturePossible = false; private float drawerEdgeGestureStartX = 0f; private float drawerEdgeGestureStartY = 0f; @@ -4959,6 +4959,7 @@ private void stopTaskManagerPolling() { if (winHandler != null) winHandler.setOnGetProcessInfoListener(null); taskManagerAccum.clear(); taskManagerCpuExpanded = false; + prevTaskCpuSample = null; if (drawerStateHolder != null) { drawerStateHolder.setTaskManagerState(new TaskManagerPaneState( new ArrayList<>(), 0, 0, new ArrayList<>(), 0, "")); @@ -5005,36 +5006,20 @@ private void handleTaskManagerProcessInfo(int index, int numProcesses, ProcessIn private void pushTaskManagerSystemStats() { if (drawerStateHolder == null) return; - short[] clockSpeeds = CPUStatus.getCurrentClockSpeeds(); - if (cachedMaxClockSpeeds == null || cachedMaxClockSpeeds.length != clockSpeeds.length) { - short[] maxes = new short[clockSpeeds.length]; - for (int i = 0; i < clockSpeeds.length; i++) maxes[i] = CPUStatus.getMaxClockSpeed(i); - cachedMaxClockSpeeds = maxes; - } - - int totalClock = 0; - short maxClock = 0; - for (int i = 0; i < clockSpeeds.length; i++) { - totalClock += clockSpeeds[i]; - if (cachedMaxClockSpeeds[i] > maxClock) maxClock = cachedMaxClockSpeeds[i]; - } + CPUStatus.CpuSample cpuSample = CPUStatus.readCpuSample(); + int coreCount = cpuSample != null ? cpuSample.coreCount() : 0; int cpuPercent = 0; - if (clockSpeeds.length > 0 && maxClock > 0) { - int avg = totalClock / clockSpeeds.length; - cpuPercent = (int) (((float) avg / maxClock) * 100.0f); - } - - ArrayList corePercents; - if (taskManagerCpuExpanded) { - corePercents = new ArrayList<>(clockSpeeds.length); - for (int i = 0; i < clockSpeeds.length; i++) { - short maxFor = cachedMaxClockSpeeds[i]; - int corePercent = maxFor > 0 ? (int) (((float) clockSpeeds[i] / maxFor) * 100.0f) : 0; - corePercents.add(corePercent); + ArrayList corePercents = new ArrayList<>(); + if (cpuSample != null && prevTaskCpuSample != null) { + int agg = cpuSample.percentSince(prevTaskCpuSample); + if (agg >= 0) cpuPercent = agg; + if (taskManagerCpuExpanded) { + for (int i = 0; i < coreCount; i++) { + corePercents.add(cpuSample.corePercentSince(prevTaskCpuSample, i)); + } } - } else { - corePercents = new ArrayList<>(); } + if (cpuSample != null) prevTaskCpuSample = cpuSample; ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); @@ -5047,7 +5032,7 @@ private void pushTaskManagerSystemStats() { drawerStateHolder.setTaskManagerState(new TaskManagerPaneState( current.getProcesses(), cpuPercent, - clockSpeeds.length, + coreCount, corePercents, memPercent, memDetail)); diff --git a/app/src/main/runtime/display/ui/FrameRating.java b/app/src/main/runtime/display/ui/FrameRating.java index 1e0c90740..28d140109 100644 --- a/app/src/main/runtime/display/ui/FrameRating.java +++ b/app/src/main/runtime/display/ui/FrameRating.java @@ -95,6 +95,7 @@ public class FrameRating extends LinearLayout implements Runnable { private boolean canReadGpu; private Context context; private int cpuFailCount; + private CPUStatus.CpuSample prevCpuSample; private volatile int cpuPercent; private volatile int cpuTemp; private volatile int cpuSensorTemp; @@ -210,6 +211,7 @@ public FrameRating( this.enableTemp = true; this.enableRenderer = true; this.cpuPercent = -1; + this.prevCpuSample = null; this.gpuLoad = -1; this.batteryWatts = -1.0f; this.cpuTemp = -1; @@ -1304,7 +1306,8 @@ private int calculateGPULoad() throws Exception { File[] gpuFiles = { new File("/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage"), new File("/sys/class/kgsl/kgsl-3d0/devfreq/gpu_load"), - new File("/sys/class/misc/mali0/device/utilisation") + new File("/sys/class/misc/mali0/device/utilisation"), + new File("/sys/kernel/gpu/gpu_busy") }; for (File f : gpuFiles) { @@ -1319,6 +1322,24 @@ private int calculateGPULoad() throws Exception { } } + File mtkMali = new File("/proc/mtk_mali/utilization"); + if (mtkMali.exists() && mtkMali.canRead()) { + try (BufferedReader reader = new BufferedReader(new FileReader(mtkMali))) { + String line; + while ((line = reader.readLine()) != null) { + int idx = line.indexOf("ACTIVE="); + if (idx >= 0) { + int start = idx + 7; + int end = line.indexOf(' ', start); + String num = (end > start ? line.substring(start, end) : line.substring(start)) + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) return Integer.parseInt(num); + } + } + } catch (Exception ignored) { + } + } + File gpubusy = new File("/sys/class/kgsl/kgsl-3d0/gpubusy"); if (gpubusy.exists() && gpubusy.canRead()) { try (BufferedReader reader = new BufferedReader(new FileReader(gpubusy))) { @@ -1358,18 +1379,13 @@ private void calculateStats() { } if (this.enableCpu && this.canReadCpu) { try { - short[] clocks = CPUStatus.getCurrentClockSpeeds(); - if (clocks != null && clocks.length > 0) { - long cur = 0; - long max = 0; - for (int i = 0; i < clocks.length; i++) { - cur += clocks[i]; - max += CPUStatus.getMaxClockSpeed(i); - } - if (max > 0) { - this.cpuPercent = (int) ((cur * 100) / max); + CPUStatus.CpuSample sample = CPUStatus.readCpuSample(); + if (sample != null) { + if (this.prevCpuSample != null) { + this.cpuPercent = sample.percentSince(this.prevCpuSample); this.cpuFailCount = 0; } + this.prevCpuSample = sample; } } catch (Exception e) { this.cpuPercent = -1; diff --git a/app/src/main/runtime/system/CPUStatus.java b/app/src/main/runtime/system/CPUStatus.java index 05cde0788..6b11bf499 100644 --- a/app/src/main/runtime/system/CPUStatus.java +++ b/app/src/main/runtime/system/CPUStatus.java @@ -1,7 +1,9 @@ package com.winlator.cmod.runtime.system; import com.winlator.cmod.shared.io.FileUtils; +import java.io.BufferedReader; import java.io.File; +import java.io.FileReader; import java.util.ArrayList; import java.util.Collections; import java.util.Locale; @@ -85,4 +87,98 @@ private static int rankCpuZone(String type) { if (type.contains("big") || type.contains("little")) return 8; return -1; } + + public static final class CpuSample { + private final long aggBusy; + private final long aggIdle; + private final long[] coreBusy; + private final long[] coreIdle; + + private CpuSample(long aggBusy, long aggIdle, long[] coreBusy, long[] coreIdle) { + this.aggBusy = aggBusy; + this.aggIdle = aggIdle; + this.coreBusy = coreBusy; + this.coreIdle = coreIdle; + } + + public int coreCount() { + return coreBusy.length; + } + + public int percentSince(CpuSample prev) { + if (prev == null) return -1; + return percent(aggBusy - prev.aggBusy, aggIdle - prev.aggIdle); + } + + public int corePercentSince(CpuSample prev, int core) { + if (prev == null || core < 0 || core >= coreBusy.length || core >= prev.coreBusy.length) + return 0; + return percent(coreBusy[core] - prev.coreBusy[core], coreIdle[core] - prev.coreIdle[core]); + } + + private static int percent(long busyDelta, long idleDelta) { + long total = busyDelta + idleDelta; + if (total <= 0) return 0; + long p = (100 * busyDelta) / total; + if (p < 0) return 0; + if (p > 100) return 100; + return (int) p; + } + } + + public static CpuSample readCpuSample() { + int numProcessors = Runtime.getRuntime().availableProcessors(); + long[] coreBusy = new long[numProcessors]; + long[] coreIdle = new long[numProcessors]; + long aggBusy = 0; + long aggIdle = 0; + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/stat"))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("cpu")) break; + String[] f = line.trim().split("\\s+"); + if (f.length < 5) continue; + long user = parseTick(f, 1); + long nice = parseTick(f, 2); + long system = parseTick(f, 3); + long idle = parseTick(f, 4); + long iowait = parseTick(f, 5); + long irq = parseTick(f, 6); + long softirq = parseTick(f, 7); + long steal = parseTick(f, 8); + long idleAll = idle + iowait; + long busy = user + nice + system + irq + softirq + steal; + if (f[0].equals("cpu")) { + aggBusy = busy; + aggIdle = idleAll; + } else { + int idx = parseCoreIndex(f[0]); + if (idx >= 0 && idx < numProcessors) { + coreBusy[idx] = busy; + coreIdle[idx] = idleAll; + } + } + } + } catch (Exception e) { + return null; + } + return new CpuSample(aggBusy, aggIdle, coreBusy, coreIdle); + } + + private static long parseTick(String[] fields, int index) { + if (index >= fields.length) return 0; + try { + return Long.parseLong(fields[index]); + } catch (NumberFormatException e) { + return 0; + } + } + + private static int parseCoreIndex(String label) { + try { + return Integer.parseInt(label.substring(3)); + } catch (Exception e) { + return -1; + } + } } From 842aaf3093b55a1b0f2c812365687d08f8af2bb9 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sun, 12 Jul 2026 00:42:12 +0000 Subject: [PATCH 2/4] HUD: show first CPU reading faster, drop stale value on re-show Poll the second CPU sample after 500ms during warm-up (then settle to 1s) and force a redraw when the first real value lands, so the on-screen HUD stops showing CPU N/A within about half a second instead of a full second. Reset the previous sample when stats start so re-showing the HUD no longer reports a delta spanning the hidden period. --- app/src/main/runtime/display/ui/FrameRating.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/runtime/display/ui/FrameRating.java b/app/src/main/runtime/display/ui/FrameRating.java index 28d140109..34be9ee36 100644 --- a/app/src/main/runtime/display/ui/FrameRating.java +++ b/app/src/main/runtime/display/ui/FrameRating.java @@ -96,6 +96,7 @@ public class FrameRating extends LinearLayout implements Runnable { private Context context; private int cpuFailCount; private CPUStatus.CpuSample prevCpuSample; + private boolean cpuWarmedUp; private volatile int cpuPercent; private volatile int cpuTemp; private volatile int cpuSensorTemp; @@ -164,6 +165,7 @@ public void setFrameObserver(FrameObserver observer) { private static final long FALLBACK_SUPPRESSION_NS = 2000000000L; private static final long FPS_CALC_INTERVAL_NS = 1000000000L; private static final long HUD_REFRESH_MS = 1000L; + private static final long CPU_WARMUP_POLL_MS = 500L; private static final long MIN_FRAME_INTERVAL_NS = 1000000L; private static final int MAX_FRAME_SAMPLES = 1024; @@ -212,6 +214,7 @@ public FrameRating( this.enableRenderer = true; this.cpuPercent = -1; this.prevCpuSample = null; + this.cpuWarmedUp = false; this.gpuLoad = -1; this.batteryWatts = -1.0f; this.cpuTemp = -1; @@ -301,7 +304,8 @@ public void run() { if (isStatsRunning) { calculateStats(); if (statsHandler != null) { - statsHandler.postDelayed(this, 1000L); + long next = (prevCpuSample != null && !cpuWarmedUp) ? CPU_WARMUP_POLL_MS : 1000L; + statsHandler.postDelayed(this, next); } } } @@ -313,6 +317,8 @@ private void startStatsUpdate() { return; } this.isStatsRunning = true; + this.prevCpuSample = null; + this.cpuWarmedUp = false; this.statsThread = new HandlerThread("HardwareStatsThread"); this.statsThread.start(); this.statsHandler = new Handler(this.statsThread.getLooper()); @@ -1384,6 +1390,11 @@ private void calculateStats() { if (this.prevCpuSample != null) { this.cpuPercent = sample.percentSince(this.prevCpuSample); this.cpuFailCount = 0; + if (!this.cpuWarmedUp) { + this.cpuWarmedUp = true; + Handler refresh = this.uiRefreshHandler; + if (refresh != null) refresh.post(this); + } } this.prevCpuSample = sample; } From c554358b439bba2ac7e3b315e1ecb3283ed864ee Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sun, 12 Jul 2026 01:21:18 +0000 Subject: [PATCH 3/4] HUD: fall back to clock-frequency CPU load when /proc/stat is unreadable On devices where the app cannot read /proc/stat, the delta sampler returned no data and the HUD showed CPU N/A. Try /proc/stat first (accurate) and fall back to the clock-frequency estimate so a value always shows, on both the on-screen HUD and the Task Manager pane. --- .../display/XServerDisplayActivity.java | 26 +++++++++++++----- .../main/runtime/display/ui/FrameRating.java | 22 ++++++++------- app/src/main/runtime/system/CPUStatus.java | 27 +++++++++++++++++++ 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 6f7bc09e1..5c902dd87 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -5013,19 +5013,33 @@ private void pushTaskManagerSystemStats() { if (drawerStateHolder == null) return; CPUStatus.CpuSample cpuSample = CPUStatus.readCpuSample(); - int coreCount = cpuSample != null ? cpuSample.coreCount() : 0; + int coreCount; int cpuPercent = 0; ArrayList corePercents = new ArrayList<>(); - if (cpuSample != null && prevTaskCpuSample != null) { - int agg = cpuSample.percentSince(prevTaskCpuSample); - if (agg >= 0) cpuPercent = agg; + if (cpuSample != null) { + coreCount = cpuSample.coreCount(); + if (prevTaskCpuSample != null) { + int agg = cpuSample.percentSince(prevTaskCpuSample); + if (agg >= 0) cpuPercent = agg; + if (taskManagerCpuExpanded) { + for (int i = 0; i < coreCount; i++) { + corePercents.add(cpuSample.corePercentSince(prevTaskCpuSample, i)); + } + } + } + prevTaskCpuSample = cpuSample; + } else { + prevTaskCpuSample = null; + short[] clocks = CPUStatus.getCurrentClockSpeeds(); + coreCount = clocks != null ? clocks.length : 0; + int agg = CPUStatus.getClockFreqLoadPercent(); + cpuPercent = agg >= 0 ? agg : 0; if (taskManagerCpuExpanded) { for (int i = 0; i < coreCount; i++) { - corePercents.add(cpuSample.corePercentSince(prevTaskCpuSample, i)); + corePercents.add(CPUStatus.getClockFreqCorePercent(i)); } } } - if (cpuSample != null) prevTaskCpuSample = cpuSample; ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); diff --git a/app/src/main/runtime/display/ui/FrameRating.java b/app/src/main/runtime/display/ui/FrameRating.java index 34be9ee36..860c556a4 100644 --- a/app/src/main/runtime/display/ui/FrameRating.java +++ b/app/src/main/runtime/display/ui/FrameRating.java @@ -1386,17 +1386,21 @@ private void calculateStats() { if (this.enableCpu && this.canReadCpu) { try { CPUStatus.CpuSample sample = CPUStatus.readCpuSample(); + int pct = -1; if (sample != null) { - if (this.prevCpuSample != null) { - this.cpuPercent = sample.percentSince(this.prevCpuSample); - this.cpuFailCount = 0; - if (!this.cpuWarmedUp) { - this.cpuWarmedUp = true; - Handler refresh = this.uiRefreshHandler; - if (refresh != null) refresh.post(this); - } - } + if (this.prevCpuSample != null) pct = sample.percentSince(this.prevCpuSample); this.prevCpuSample = sample; + } else { + pct = CPUStatus.getClockFreqLoadPercent(); + } + if (pct >= 0) { + this.cpuPercent = pct; + this.cpuFailCount = 0; + if (!this.cpuWarmedUp) { + this.cpuWarmedUp = true; + Handler refresh = this.uiRefreshHandler; + if (refresh != null) refresh.post(this); + } } } catch (Exception e) { this.cpuPercent = -1; diff --git a/app/src/main/runtime/system/CPUStatus.java b/app/src/main/runtime/system/CPUStatus.java index 6b11bf499..3bec623d0 100644 --- a/app/src/main/runtime/system/CPUStatus.java +++ b/app/src/main/runtime/system/CPUStatus.java @@ -181,4 +181,31 @@ private static int parseCoreIndex(String label) { return -1; } } + + public static int getClockFreqLoadPercent() { + short[] clocks = getCurrentClockSpeeds(); + if (clocks == null || clocks.length == 0) return -1; + long cur = 0; + long max = 0; + for (int i = 0; i < clocks.length; i++) { + cur += clocks[i]; + max += getMaxClockSpeed(i); + } + if (max <= 0) return -1; + return clampPercent((int) ((cur * 100) / max)); + } + + public static int getClockFreqCorePercent(int core) { + short[] clocks = getCurrentClockSpeeds(); + if (clocks == null || core < 0 || core >= clocks.length) return 0; + int max = getMaxClockSpeed(core); + if (max <= 0) return 0; + return clampPercent((int) (((float) clocks[core] / max) * 100.0f)); + } + + private static int clampPercent(int p) { + if (p < 0) return 0; + if (p > 100) return 100; + return p; + } } From e885e9e58e3ff61a37ae00a0682337678e45a921 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sun, 12 Jul 2026 01:44:37 +0000 Subject: [PATCH 4/4] HUD: show 0% GPU when idle instead of N/A The kgsl gpubusy counter reads busy=0 total=0 at idle; dividing by total==0 fell through to the read-failure path and showed N/A. Return 0 for that case. --- app/src/main/runtime/display/ui/FrameRating.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/runtime/display/ui/FrameRating.java b/app/src/main/runtime/display/ui/FrameRating.java index 860c556a4..8b185a203 100644 --- a/app/src/main/runtime/display/ui/FrameRating.java +++ b/app/src/main/runtime/display/ui/FrameRating.java @@ -1355,7 +1355,7 @@ private int calculateGPULoad() throws Exception { if (parts.length >= 2) { long busy = Long.parseLong(parts[0]); long total = Long.parseLong(parts[1]); - if (total != 0) return (int) ((100 * busy) / total); + return total != 0 ? (int) ((100 * busy) / total) : 0; } } } catch (Exception ignored) {