Skip to content

Commit eec7c5a

Browse files
committed
refactor: split ServerMetricsCollector into focused classes
- Extract TickTracker for TPS/MSPT tracking - Extract CpuMetrics for CPU usage tracking (async warmup) - ServerMetricsCollector now acts as facade delegating to specialists - Add CPU/memory permanent caching in SystemInfoCollector - Make warmUpCpu async to avoid blocking server init
1 parent 4b5cebf commit eec7c5a

4 files changed

Lines changed: 180 additions & 124 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.damon233.performtrackermod.collector;
2+
3+
import java.lang.management.ManagementFactory;
4+
import java.lang.management.OperatingSystemMXBean;
5+
import java.util.concurrent.atomic.AtomicReference;
6+
7+
public class CpuMetrics {
8+
private static final double CPU_SMOOTHING_FACTOR = 0.3;
9+
10+
private final OperatingSystemMXBean osBean;
11+
private final AtomicReference<Double> smoothedCpuUsage;
12+
private volatile boolean warmedUp = false;
13+
14+
public CpuMetrics() {
15+
this.osBean = ManagementFactory.getOperatingSystemMXBean();
16+
this.smoothedCpuUsage = new AtomicReference<>(0.0);
17+
startWarmUpAsync();
18+
}
19+
20+
private void startWarmUpAsync() {
21+
Thread.ofVirtual().name("PerformTracker-CpuWarmup").start(() -> {
22+
for (int i = 0; i < 10; i++) {
23+
double raw = getRawCpuUsage();
24+
if (raw >= 0) {
25+
smoothedCpuUsage.set(raw);
26+
warmedUp = true;
27+
return;
28+
}
29+
try {
30+
Thread.sleep(100);
31+
} catch (InterruptedException ignored) {
32+
return;
33+
}
34+
}
35+
warmedUp = true;
36+
});
37+
}
38+
39+
private double getRawCpuUsage() {
40+
try {
41+
if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
42+
double load = sunOsBean.getCpuLoad();
43+
if (load >= 0) {
44+
return load * 100.0;
45+
}
46+
}
47+
long[] ticks = getSystemCpuLoadTicks();
48+
if (ticks != null && ticks.length >= 4) {
49+
long total = ticks[0] + ticks[1] + ticks[2] + ticks[3];
50+
if (total > 0) {
51+
return ((double) (total - ticks[3]) / total) * 100.0;
52+
}
53+
}
54+
} catch (Exception ignored) {
55+
}
56+
return -1;
57+
}
58+
59+
private long[] getSystemCpuLoadTicks() {
60+
try {
61+
java.lang.reflect.Method method = osBean.getClass().getMethod("getSystemCpuLoadTicks");
62+
method.setAccessible(true);
63+
return (long[]) method.invoke(osBean);
64+
} catch (Exception ignored) {
65+
return null;
66+
}
67+
}
68+
69+
public double getCpuUsagePercent() {
70+
if (!warmedUp) {
71+
return -1;
72+
}
73+
74+
double rawUsage = getRawCpuUsage();
75+
if (rawUsage < 0) {
76+
return rawUsage;
77+
}
78+
79+
double current = smoothedCpuUsage.get();
80+
double smoothed = current * (1 - CPU_SMOOTHING_FACTOR) + rawUsage * CPU_SMOOTHING_FACTOR;
81+
smoothedCpuUsage.set(smoothed);
82+
return smoothed;
83+
}
84+
}
Lines changed: 11 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,36 @@
11
package org.damon233.performtrackermod.collector;
22

3-
import java.lang.management.ManagementFactory;
4-
import java.lang.management.OperatingSystemMXBean;
5-
import java.util.concurrent.ConcurrentLinkedDeque;
6-
import java.util.concurrent.atomic.AtomicBoolean;
7-
import java.util.concurrent.atomic.AtomicReference;
8-
9-
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
10-
113
public class ServerMetricsCollector {
12-
private static final double TARGET_TPS = 20.0;
13-
private static final double NANOS_TO_SECONDS = 1_000_000_000.0;
14-
private static final long WINDOW_SECONDS = 5;
15-
private static final long WINDOW_NANOS = WINDOW_SECONDS * 1_000_000_000L;
16-
private static final double CPU_SMOOTHING_FACTOR = 0.3;
17-
18-
private final ConcurrentLinkedDeque<Long> tickTimes = new ConcurrentLinkedDeque<>();
19-
private final AtomicBoolean enabled = new AtomicBoolean(true);
20-
private final OperatingSystemMXBean osBean;
21-
private final AtomicReference<Double> smoothedCpuUsage;
4+
private final TickTracker tickTracker;
5+
private final CpuMetrics cpuMetrics;
226

237
public ServerMetricsCollector() {
24-
this.osBean = ManagementFactory.getOperatingSystemMXBean();
25-
this.smoothedCpuUsage = new AtomicReference<>(0.0);
26-
warmUpCpu();
27-
ServerTickEvents.END_SERVER_TICK.register(this::onEndServerTick);
28-
}
29-
30-
private void warmUpCpu() {
31-
for (int i = 0; i < 10; i++) {
32-
double raw = getRawCpuUsage();
33-
if (raw >= 0) {
34-
smoothedCpuUsage.set(raw);
35-
return;
36-
}
37-
try {
38-
Thread.sleep(100);
39-
} catch (InterruptedException ignored) {
40-
}
41-
}
42-
}
43-
44-
private double getRawCpuUsage() {
45-
try {
46-
if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
47-
double load = sunOsBean.getCpuLoad();
48-
if (load >= 0) {
49-
return load * 100.0;
50-
}
51-
}
52-
long[] ticks = getSystemCpuLoadTicks();
53-
if (ticks != null && ticks.length >= 4) {
54-
long total = ticks[0] + ticks[1] + ticks[2] + ticks[3];
55-
if (total > 0) {
56-
return ((double) (total - ticks[3]) / total) * 100.0;
57-
}
58-
}
59-
} catch (Exception ignored) {
60-
}
61-
return -1;
62-
}
63-
64-
private long[] getSystemCpuLoadTicks() {
65-
try {
66-
java.lang.reflect.Method method = osBean.getClass().getMethod("getSystemCpuLoadTicks");
67-
method.setAccessible(true);
68-
return (long[]) method.invoke(osBean);
69-
} catch (Exception ignored) {
70-
return null;
71-
}
72-
}
73-
74-
private void onEndServerTick(net.minecraft.server.MinecraftServer server) {
75-
if (!enabled.get()) {
76-
return;
77-
}
78-
79-
long currentTime = System.nanoTime();
80-
tickTimes.add(currentTime);
81-
82-
long cutoff = currentTime - WINDOW_NANOS;
83-
while (!tickTimes.isEmpty() && tickTimes.peekFirst() < cutoff) {
84-
tickTimes.pollFirst();
85-
}
8+
this.tickTracker = new TickTracker();
9+
this.cpuMetrics = new CpuMetrics();
8610
}
8711

8812
public double getTps() {
89-
if (tickTimes.isEmpty()) {
90-
return TARGET_TPS;
91-
}
92-
93-
long first = tickTimes.peekFirst();
94-
long current = System.nanoTime();
95-
double elapsedSeconds = (current - first) / NANOS_TO_SECONDS;
96-
97-
if (elapsedSeconds <= 0) {
98-
return TARGET_TPS;
99-
}
100-
101-
int count = tickTimes.size();
102-
return Math.min(TARGET_TPS, count / elapsedSeconds);
13+
return tickTracker.getTps();
10314
}
10415

10516
public double getMspt() {
106-
double tps = getTps();
107-
if (tps <= 0) {
108-
return 1000.0 / TARGET_TPS;
109-
}
110-
return 1000.0 / tps;
17+
return tickTracker.getMspt();
11118
}
112-
19+
11320
public double getHeapUsedMB() {
11421
Runtime runtime = Runtime.getRuntime();
11522
return (runtime.totalMemory() - runtime.freeMemory()) / (1024.0 * 1024.0);
11623
}
117-
24+
11825
public double getHeapMaxMB() {
11926
return Runtime.getRuntime().maxMemory() / (1024.0 * 1024.0);
12027
}
121-
28+
12229
public double getCpuUsagePercent() {
123-
double rawUsage = getRawCpuUsage();
124-
if (rawUsage < 0) {
125-
return rawUsage;
126-
}
127-
128-
double current = smoothedCpuUsage.get();
129-
double smoothed = current * (1 - CPU_SMOOTHING_FACTOR) + rawUsage * CPU_SMOOTHING_FACTOR;
130-
smoothedCpuUsage.set(smoothed);
131-
return smoothed;
30+
return cpuMetrics.getCpuUsagePercent();
13231
}
13332

13433
public void reset() {
135-
tickTimes.clear();
34+
tickTracker.reset();
13635
}
13736
}

src/main/java/org/damon233/performtrackermod/collector/SystemInfoCollector.java

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public class SystemInfoCollector {
2121
private static List<String> phoneChips;
2222
private static List<String> serverChips;
2323

24+
// Permanent caches - CPU info and memory never change at runtime
25+
private static String cachedCpuName;
26+
private static long cachedPhysicalMemory = -1;
27+
2428
public static SystemInfo collect() {
2529
if (cachedInfo != null) {
2630
return cachedInfo;
@@ -71,18 +75,23 @@ private static String getGpuName() {
7175
}
7276

7377
private static String getCpuName() {
78+
if (cachedCpuName != null) {
79+
return cachedCpuName;
80+
}
81+
7482
String osName = System.getProperty("os.name").toLowerCase();
7583

7684
if (osName.contains("windows")) {
77-
return getWindowsCpuName();
78-
}
79-
if (osName.contains("linux")) {
80-
return getLinuxCpuName();
81-
}
82-
if (osName.contains("mac") || osName.contains("darwin")) {
83-
return getMacCpuName();
85+
cachedCpuName = getWindowsCpuName();
86+
} else if (osName.contains("linux")) {
87+
cachedCpuName = getLinuxCpuName();
88+
} else if (osName.contains("mac") || osName.contains("darwin")) {
89+
cachedCpuName = getMacCpuName();
90+
} else {
91+
cachedCpuName = System.getProperty("os.arch");
8492
}
85-
return System.getProperty("os.arch");
93+
94+
return cachedCpuName;
8695
}
8796

8897
private static String getWindowsCpuName() {
@@ -156,6 +165,10 @@ private static String readFile(String path) {
156165
}
157166

158167
private static long getPhysicalMemory() {
168+
if (cachedPhysicalMemory > 0) {
169+
return cachedPhysicalMemory;
170+
}
171+
159172
String osName = System.getProperty("os.name").toLowerCase();
160173

161174
if (osName.contains("linux")) {
@@ -166,7 +179,8 @@ private static long getPhysicalMemory() {
166179
String[] parts = line.split("\\s+");
167180
if (parts.length >= 2) {
168181
try {
169-
return Long.parseLong(parts[1]) * 1024;
182+
cachedPhysicalMemory = Long.parseLong(parts[1]) * 1024;
183+
return cachedPhysicalMemory;
170184
} catch (NumberFormatException ignored) {
171185
}
172186
}
@@ -180,12 +194,14 @@ private static long getPhysicalMemory() {
180194
java.lang.reflect.Method method = osBean.getClass().getMethod("getTotalMemorySize");
181195
Object result = method.invoke(osBean);
182196
if (result instanceof Number num && num.longValue() > 0) {
183-
return num.longValue();
197+
cachedPhysicalMemory = num.longValue();
198+
return cachedPhysicalMemory;
184199
}
185200
} catch (Exception ignored) {
186201
}
187202

188-
return Runtime.getRuntime().maxMemory();
203+
cachedPhysicalMemory = Runtime.getRuntime().maxMemory();
204+
return cachedPhysicalMemory;
189205
}
190206

191207
private static DeviceType classifyDevice() {
@@ -214,9 +230,9 @@ private static DeviceType classifyDevice() {
214230
private static DeviceType classifyArmDevice(String cpuInfo, String gpuInfo) {
215231
loadChipRules();
216232

217-
DeviceType result;
218233
String upper = cpuInfo != null ? cpuInfo.toUpperCase() : "";
219234
String gpuUpper = gpuInfo != null ? gpuInfo.toUpperCase() : "";
235+
DeviceType result;
220236

221237
if (upper.contains("APPLE")) {
222238
result = DeviceType.MAC;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.damon233.performtrackermod.collector;
2+
3+
import java.util.concurrent.ConcurrentLinkedDeque;
4+
5+
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
6+
7+
public class TickTracker {
8+
private static final double TARGET_TPS = 20.0;
9+
private static final double NANOS_TO_SECONDS = 1_000_000_000.0;
10+
private static final long WINDOW_SECONDS = 5;
11+
private static final long WINDOW_NANOS = WINDOW_SECONDS * 1_000_000_000L;
12+
13+
private final ConcurrentLinkedDeque<Long> tickTimes = new ConcurrentLinkedDeque<>();
14+
15+
public TickTracker() {
16+
ServerTickEvents.END_SERVER_TICK.register(this::onEndServerTick);
17+
}
18+
19+
private void onEndServerTick(net.minecraft.server.MinecraftServer server) {
20+
long currentTime = System.nanoTime();
21+
tickTimes.add(currentTime);
22+
23+
long cutoff = currentTime - WINDOW_NANOS;
24+
while (!tickTimes.isEmpty() && tickTimes.peekFirst() < cutoff) {
25+
tickTimes.pollFirst();
26+
}
27+
}
28+
29+
public double getTps() {
30+
if (tickTimes.isEmpty()) {
31+
return TARGET_TPS;
32+
}
33+
34+
long first = tickTimes.peekFirst();
35+
long current = System.nanoTime();
36+
double elapsedSeconds = (current - first) / NANOS_TO_SECONDS;
37+
38+
if (elapsedSeconds <= 0) {
39+
return TARGET_TPS;
40+
}
41+
42+
int count = tickTimes.size();
43+
return Math.min(TARGET_TPS, count / elapsedSeconds);
44+
}
45+
46+
public double getMspt() {
47+
double tps = getTps();
48+
if (tps <= 0) {
49+
return 1000.0 / TARGET_TPS;
50+
}
51+
return 1000.0 / tps;
52+
}
53+
54+
public void reset() {
55+
tickTimes.clear();
56+
}
57+
}

0 commit comments

Comments
 (0)