Skip to content

Commit 6700e49

Browse files
jamakr4codex
andcommitted
feat(dev): add system performance overlay
Add an opt-in overlay showing system and app CPU/RAM usage, load average, and thermal state. Wire its lifecycle through SignalService, expose the toggle in Dev settings, cover metric calculations with unit tests, and bump the beta version to 0.8.9. Co-authored-by: Codex <codex@openai.com>
1 parent c4612e2 commit 6700e49

13 files changed

Lines changed: 785 additions & 2 deletions

File tree

‎app/build.gradle‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ android {
1414
applicationId "com.drivehub.kamera"
1515
minSdk 28
1616
targetSdk 36
17-
versionCode 16
18-
versionName "0.8.8"
17+
versionCode 20
18+
versionName "0.8.9"
1919
buildConfigField "boolean", "IS_BETA", "true"
2020

2121
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

‎app/src/main/AndroidManifest.xml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
1414
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
1515
<uses-permission android:name="android.permission.CAMERA" />
16+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
1617
<!-- Declare camera permission but mark the hardware feature as optional
1718
to avoid Play Store filtering – this device has physical cameras. -->
1819
<uses-feature

‎app/src/main/java/com/drivehub/kamera/dev/DevSettingsController.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import android.widget.Button;
1414
import android.widget.EditText;
1515
import android.widget.SeekBar;
16+
import android.widget.Switch;
1617
import android.widget.TextView;
1718
import android.widget.Toast;
1819

@@ -26,6 +27,7 @@ public DevSettingsController() {
2627

2728
public void bind(
2829
SharedPreferences prefs,
30+
Switch swPerformanceOverlay,
2931
SeekBar seekOverlayTopInsetPx,
3032
EditText etOverlayTopInsetPx,
3133
EditText etForegroundModePollMs,
@@ -39,6 +41,7 @@ public void bind(
3941
Button btnBrowseFolder,
4042
Button btnResetDefaults
4143
) {
44+
bindPerformanceOverlay(prefs, swPerformanceOverlay);
4245
bindIntSlider(
4346
prefs,
4447
seekOverlayTopInsetPx,
@@ -102,6 +105,13 @@ public void bind(
102105
btnResetDefaults);
103106
}
104107

108+
private void bindPerformanceOverlay(SharedPreferences prefs, Switch performanceOverlaySwitch) {
109+
if (performanceOverlaySwitch == null) return;
110+
performanceOverlaySwitch.setChecked(UiPrefs.isDevPerformanceOverlayEnabled(prefs));
111+
performanceOverlaySwitch.setOnCheckedChangeListener((buttonView, checked) ->
112+
SignalService.setPerformanceOverlayEnabled(buttonView.getContext(), checked));
113+
}
114+
105115
private interface CapacityWriter {
106116
void write(SharedPreferences prefs, int value);
107117
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package com.drivehub.kamera.dev;
2+
3+
import java.util.Locale;
4+
5+
/** Pure calculations and formatting used by the head-unit performance overlay. */
6+
final class PerformanceMetrics {
7+
8+
private static final double BYTES_PER_MIB = 1024d * 1024d;
9+
private static final double BYTES_PER_GIB = 1024d * 1024d * 1024d;
10+
11+
private PerformanceMetrics() {
12+
}
13+
14+
static final class CpuTimes {
15+
final long total;
16+
final long idle;
17+
18+
CpuTimes(long total, long idle) {
19+
this.total = total;
20+
this.idle = idle;
21+
}
22+
}
23+
24+
static final class Snapshot {
25+
final double systemCpuPercent;
26+
final double appCpuPercent;
27+
final long usedRamBytes;
28+
final long totalRamBytes;
29+
final long appPssBytes;
30+
final double loadAverageOneMinute;
31+
final double cpuTemperatureCelsius;
32+
final int thermalStatus;
33+
34+
Snapshot(
35+
double systemCpuPercent,
36+
double appCpuPercent,
37+
long usedRamBytes,
38+
long totalRamBytes,
39+
long appPssBytes,
40+
double loadAverageOneMinute,
41+
double cpuTemperatureCelsius,
42+
int thermalStatus
43+
) {
44+
this.systemCpuPercent = systemCpuPercent;
45+
this.appCpuPercent = appCpuPercent;
46+
this.usedRamBytes = usedRamBytes;
47+
this.totalRamBytes = totalRamBytes;
48+
this.appPssBytes = appPssBytes;
49+
this.loadAverageOneMinute = loadAverageOneMinute;
50+
this.cpuTemperatureCelsius = cpuTemperatureCelsius;
51+
this.thermalStatus = thermalStatus;
52+
}
53+
54+
String format(String thermalText) {
55+
return formatCpuLine(systemCpuPercent, appCpuPercent)
56+
+ "\n" + formatRamLine(usedRamBytes, totalRamBytes, appPssBytes)
57+
+ "\n" + formatLoadLine(loadAverageOneMinute, thermalText);
58+
}
59+
}
60+
61+
static CpuTimes parseProcStatCpuLine(String line) {
62+
if (line == null) return null;
63+
String trimmed = line.trim();
64+
if (!trimmed.startsWith("cpu ") && !trimmed.startsWith("cpu\t")) return null;
65+
String[] fields = trimmed.split("\\s+");
66+
if (fields.length < 5 || !"cpu".equals(fields[0])) return null;
67+
try {
68+
// Linux reports guest/guest_nice inside user/nice already, so only sum through steal.
69+
long total = 0L;
70+
int lastIncludedField = Math.min(fields.length - 1, 8);
71+
for (int i = 1; i <= lastIncludedField; i++) {
72+
long value = Long.parseLong(fields[i]);
73+
if (value < 0L) return null;
74+
total = Math.addExact(total, value);
75+
}
76+
long idle = Long.parseLong(fields[4]);
77+
if (idle < 0L) return null;
78+
if (fields.length > 5) {
79+
long ioWait = Long.parseLong(fields[5]);
80+
if (ioWait < 0L) return null;
81+
idle = Math.addExact(idle, ioWait);
82+
}
83+
return total > 0L && idle <= total ? new CpuTimes(total, idle) : null;
84+
} catch (ArithmeticException | NumberFormatException ignored) {
85+
return null;
86+
}
87+
}
88+
89+
static double calculateSystemCpuPercent(CpuTimes previous, CpuTimes current) {
90+
if (previous == null || current == null) return Double.NaN;
91+
long totalDelta = current.total - previous.total;
92+
long idleDelta = current.idle - previous.idle;
93+
if (totalDelta <= 0L || idleDelta < 0L) return Double.NaN;
94+
return clampPercent(((totalDelta - idleDelta) * 100d) / totalDelta);
95+
}
96+
97+
static double calculateAppCpuPercent(
98+
long previousCpuMs,
99+
long currentCpuMs,
100+
long previousElapsedMs,
101+
long currentElapsedMs,
102+
int processorCount
103+
) {
104+
long cpuDelta = currentCpuMs - previousCpuMs;
105+
long elapsedDelta = currentElapsedMs - previousElapsedMs;
106+
if (previousCpuMs < 0L || previousElapsedMs < 0L || cpuDelta < 0L
107+
|| elapsedDelta <= 0L || processorCount <= 0) {
108+
return Double.NaN;
109+
}
110+
double deviceCapacityMs = elapsedDelta * (double) processorCount;
111+
return clampPercent((cpuDelta * 100d) / deviceCapacityMs);
112+
}
113+
114+
static double calculateRamPercent(long usedBytes, long totalBytes) {
115+
if (usedBytes < 0L || totalBytes <= 0L || usedBytes > totalBytes) return Double.NaN;
116+
return clampPercent((usedBytes * 100d) / totalBytes);
117+
}
118+
119+
static String formatCpuLine(double systemPercent, double appPercent) {
120+
return "CPU " + formatPercent(systemPercent) + " · App " + formatPercent(appPercent);
121+
}
122+
123+
static String formatRamLine(long usedBytes, long totalBytes, long appPssBytes) {
124+
String systemRam = "–";
125+
double ramPercent = calculateRamPercent(usedBytes, totalBytes);
126+
if (!Double.isNaN(ramPercent)) {
127+
systemRam = String.format(Locale.US, "%.1f/%.1f GB %s",
128+
usedBytes / BYTES_PER_GIB,
129+
totalBytes / BYTES_PER_GIB,
130+
formatPercent(ramPercent));
131+
}
132+
String appRam = appPssBytes >= 0L
133+
? String.format(Locale.US, "%.0f MB", appPssBytes / BYTES_PER_MIB)
134+
: "–";
135+
return "RAM " + systemRam + " · App " + appRam;
136+
}
137+
138+
static String formatLoadLine(double loadAverageOneMinute, String thermalText) {
139+
String load = isFiniteInRange(loadAverageOneMinute, 0d, 10_000d)
140+
? String.format(Locale.US, "%.2f", loadAverageOneMinute)
141+
: "–";
142+
String safeThermalText = thermalText == null || thermalText.trim().isEmpty()
143+
? "Temp –"
144+
: thermalText;
145+
return "Load 1m " + load + " · " + safeThermalText;
146+
}
147+
148+
private static String formatPercent(double value) {
149+
return isFiniteInRange(value, 0d, 100d)
150+
? String.format(Locale.US, "%.0f %%", value)
151+
: "–";
152+
}
153+
154+
private static double clampPercent(double value) {
155+
if (!Double.isFinite(value)) return Double.NaN;
156+
return Math.max(0d, Math.min(100d, value));
157+
}
158+
159+
static boolean isFiniteInRange(double value, double minimum, double maximum) {
160+
return Double.isFinite(value) && value >= minimum && value <= maximum;
161+
}
162+
}

0 commit comments

Comments
 (0)