Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ ServerPulse is an open-source, real-time performance monitoring tool for Minecra
<details>
<summary>📊 View Dashboard Examples</summary>

![ServerPulse Grafana Dashboard Example1](img/dashboard.png)
*Example dashboard view 1: General Server Overview*
![ServerPulse Grafana Dashboard Example1](img/dash1.png)
*Example dashboard view 1: System Metrics*

![ServerPulse Grafana Dashboard Example2](img/dashboard2.png)
*Example dashboard view 2: Per-World Details*
![ServerPulse Grafana Dashboard Example2](img/dash2.png)
*Example dashboard view 2: System Metrics 2*

![ServerPulse Grafana Dashboard Example3](img/dashboard3.png)
*Example dashboard view 3: Players Ping Overview*
![ServerPulse Grafana Dashboard Example3](img/dash3.png)
*Example dashboard view 3: System & World Metrics*

![ServerPulse Grafana Dashboard Example4](img/dash4.png)
*Example dashboard view 4: Player Metrics*
</details>

## 🚀 Why Choose ServerPulse?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@ public class AsyncMetricsSnapshot {
private final long maxPing;
private final long avgPing;

private final double mspt1m;
private final double mspt5m;
private final double mspt15m;

public AsyncMetricsSnapshot(long usedHeap, long commitedHeap, long totalDisk, long usableDisk, long minPing, long maxPing, long avgPing) {
private final double lastMSPT;
private final double minMSPT;
private final double maxMSPT;


public AsyncMetricsSnapshot(long usedHeap, long commitedHeap,
long totalDisk, long usableDisk,
long minPing, long maxPing, long avgPing,
double mspt1m, double mspt5m, double mspt15m,
double lastMSPT, double minMSPT, double maxMSPT) {
this.usedHeap = usedHeap;
this.commitedHeap = commitedHeap;

Expand All @@ -24,6 +36,14 @@ public AsyncMetricsSnapshot(long usedHeap, long commitedHeap, long totalDisk, lo
this.minPing = minPing;
this.maxPing = maxPing;
this.avgPing = avgPing;

this.mspt1m = mspt1m;
this.mspt5m = mspt5m;
this.mspt15m = mspt15m;

this.lastMSPT = lastMSPT;
this.minMSPT = minMSPT;
this.maxMSPT = maxMSPT;
}

/**
Expand Down Expand Up @@ -88,4 +108,58 @@ public long getMaxPing() {
public long getAvgPing() {
return avgPing;
}

/**
* Gets the average server tick duration over the last 1 minute in milliseconds.
*
* @return the average tick duration for the last 1 minute
*/
public double getMspt1m() {
return mspt1m;
}

/**
* Gets the average server tick duration over the last 5 minutes in milliseconds.
*
* @return the average tick duration for the last 5 minutes
*/
public double getMspt5m() {
return mspt5m;
}

/**
* Gets the average server tick duration over the last 15 minutes in milliseconds.
*
* @return the average tick duration for the last 15 minutes
*/
public double getMspt15m() {
return mspt15m;
}

/**
* Gets the last server tick duration in milliseconds.
*
* @return the last tick duration
*/
public double getLastMSPT() {
return lastMSPT;
}

/**
* Gets the minimum server tick duration recorded in milliseconds.
*
* @return the minimum tick duration
*/
public double getMinMSPT() {
return minMSPT;
}

/**
* Gets the maximum server tick duration recorded in milliseconds.
*
* @return the maximum tick duration
*/
public double getMaxMSPT() {
return maxMSPT;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package it.renvins.serverpulse.api.metrics;

public interface IMSPTRetriever {

/**
* Retrieves the last tick duration in milliseconds.
*
* @return The last tick duration in milliseconds, or 0.0 if no ticks are recorded.
*/
double getLastMSPT();

/**
* Retrieves the average tick duration over the last specified number of ticks.
*
* @param ticksCount The number of ticks to consider for the average.
* @return The average tick duration in milliseconds, or 0.0 if no ticks are recorded or ticksCount is invalid.
*/
double getAverageMSPT(int ticksCount);

/**
* Retrieves the minimum tick duration over the last specified number of ticks.
*
* @param ticksCount The number of ticks to consider for the minimum.
* @return The minimum tick duration in milliseconds, or 0.0 if no ticks are recorded.
*/
double getMinMSPT(int ticksCount);

/**
* Retrieves the maximum tick duration over the last specified number of ticks.
*
* @param ticksCount The number of ticks to consider for the maximum.
* @return The maximum tick duration in milliseconds, or 0.0 if no ticks are recorded.
*/
double getMaxMSPT(int ticksCount);
}
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group = "it.renvins"
version = "0.4.5-SNAPSHOT"
version = "0.5.0-SNAPSHOT"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import it.renvins.serverpulse.api.ServerPulseProvider;
import it.renvins.serverpulse.api.metrics.IDiskRetriever;
import it.renvins.serverpulse.api.metrics.IMSPTRetriever;
import it.renvins.serverpulse.api.metrics.IPingRetriever;
import it.renvins.serverpulse.api.metrics.ITPSRetriever;
import it.renvins.serverpulse.api.service.IDatabaseService;
import it.renvins.serverpulse.api.service.IMetricsService;
import it.renvins.serverpulse.api.service.Service;
import it.renvins.serverpulse.bukkit.logger.BukkitLogger;
import it.renvins.serverpulse.bukkit.metrics.BukkitTPSRetriever;
import it.renvins.serverpulse.bukkit.metrics.PaperMSPTRetriever;
import it.renvins.serverpulse.common.DatabaseService;
import it.renvins.serverpulse.common.config.GeneralConfiguration;
import it.renvins.serverpulse.bukkit.commands.ServerPulseCommand;
Expand All @@ -23,6 +25,7 @@
import it.renvins.serverpulse.common.MetricsService;
import it.renvins.serverpulse.common.metrics.LineProtocolFormatter;
import it.renvins.serverpulse.common.metrics.MetricsCollector;
import it.renvins.serverpulse.common.metrics.UnsupportedMSPTRetriever;
import it.renvins.serverpulse.common.platform.Platform;
import it.renvins.serverpulse.common.scheduler.TaskScheduler;

Expand All @@ -40,6 +43,7 @@ public class ServerPulseBukkitLoader implements Service {
private final ITPSRetriever tpsRetriever;
private final IDiskRetriever diskRetriever;
private final IPingRetriever pingRetriever;
private final IMSPTRetriever msptRetriever;

private final IMetricsService metricsService;

Expand All @@ -58,13 +62,15 @@ public ServerPulseBukkitLoader(ServerPulseBukkit plugin) {

if (isPaper()) {
this.tpsRetriever = new PaperTPSRetriever();
this.msptRetriever = new PaperMSPTRetriever();
} else {
this.tpsRetriever = new BukkitTPSRetriever(plugin);
this.msptRetriever = new UnsupportedMSPTRetriever();
}
this.diskRetriever = new DiskRetriever(plugin.getDataFolder());
this.pingRetriever = new BukkitPingRetriever();

MetricsCollector collector = new MetricsCollector(logger, platform, tpsRetriever, diskRetriever, pingRetriever);
MetricsCollector collector = new MetricsCollector(logger, platform, tpsRetriever, diskRetriever, pingRetriever, msptRetriever);
LineProtocolFormatter formatter = new LineProtocolFormatter(config);

this.metricsService = new MetricsService(logger, collector, formatter, taskScheduler, databaseService);
Expand All @@ -86,6 +92,10 @@ public void load() {
LOGGER.info("Starting tick monitoring task...");
((BukkitTPSRetriever) tpsRetriever).startTickMonitor();
}
if (msptRetriever instanceof PaperMSPTRetriever) {
LOGGER.info("Registering PaperMSPTRetriever event listener...");
plugin.getServer().getPluginManager().registerEvents((PaperMSPTRetriever) msptRetriever, plugin);
}
metricsService.load();

long intervalTicks = config.getConfig().getLong("metrics.interval", 5) * 20L;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
package it.renvins.serverpulse.bukkit.metrics;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;

import it.renvins.serverpulse.api.metrics.ITPSRetriever;
import it.renvins.serverpulse.bukkit.ServerPulseBukkit;
import org.bukkit.scheduler.BukkitRunnable;

public class BukkitTPSRetriever implements ITPSRetriever {

private static final int TICKS_PER_SECOND = 20;
private static final int ONE_MINUTE_TICKS = 60 * TICKS_PER_SECOND; // 1200
private static final int FIVE_MINUTES_TICKS = 5 * ONE_MINUTE_TICKS; // 6000
private static final int FIFTEEN_MINUTES_TICKS = 15 * ONE_MINUTE_TICKS; // 18000

private static final int MAX_HISTORY_SIZE = FIFTEEN_MINUTES_TICKS;
private static final int MAX_HISTORY_SIZE = 15 * 60 * 20; // 15 minutes at 20 ticks per second

// Queue for FIFO
private final Queue<Long> tickDurations = new LinkedList<>();
private long lastTickTimeNano = -1;

private double tps1m = 20.0;
private double tps5m = 20.0;
private double tps15m = 20.0;

private final Queue<Long> tickTimestamps = new ConcurrentLinkedDeque<>();
private final ServerPulseBukkit plugin;

public BukkitTPSRetriever(ServerPulseBukkit plugin) {
Expand All @@ -32,71 +21,46 @@ public BukkitTPSRetriever(ServerPulseBukkit plugin) {

@Override
public double[] getTPS() {
calculateAverages();
return new double[]{tps1m, tps5m, tps15m};
long currentTime = System.currentTimeMillis();

long oneMinuteAgo = currentTime - (60 * 1000);
long fiveMinutesAgo = currentTime - (5 * 60 * 1000);
long fifteenMinutesAgo = currentTime - (15 * 60 * 1000);

int ticks1m = 0;
int ticks5m = 0;
int ticks15m = 0;

for (long timeStamp : tickTimestamps) {
if (timeStamp >= oneMinuteAgo) {
ticks1m++;
}
if (timeStamp >= fiveMinutesAgo) {
ticks5m++;
}
if (timeStamp >= fifteenMinutesAgo) {
ticks15m++;
}
}
double tps1m = ticks1m / 60.0;
double tps5m = ticks5m / 300.0;
double tps15m = ticks15m / 900.0;

return new double[]{Math.min(20.0, tps1m), Math.min(20.0, tps5m), Math.min(20.0, tps15m)};
}

public void startTickMonitor() {
lastTickTimeNano = System.nanoTime();

new BukkitRunnable() {

@Override
public void run() {
long currentTimeNano = System.nanoTime();
long elapsedNano = currentTimeNano - lastTickTimeNano;
lastTickTimeNano = currentTimeNano;
tickTimestamps.offer(System.currentTimeMillis());

tickDurations.offer(elapsedNano);
if (tickDurations.size() > MAX_HISTORY_SIZE) {
tickDurations.poll();
if (tickTimestamps.size() > MAX_HISTORY_SIZE) {
tickTimestamps.poll();
}
}
}.runTaskTimer(plugin, 1L, 1L);
}
private void calculateAverages() {
double sum1m = 0, sum5m = 0, sum15m = 0;
int count1m = 0, count5m = 0, count15m = 0;

int i = 0;
Object[] durationsArray = tickDurations.toArray();

for (int j = durationsArray.length - 1; j >= 0; j--) {
if (!(durationsArray[j] instanceof Long)) continue;
long durationNano = (Long) durationsArray[j];

if (i < ONE_MINUTE_TICKS) {
sum1m += durationNano;
count1m++;
}
if (i < FIVE_MINUTES_TICKS) {
sum5m += durationNano;
count5m++;
}
if (i < FIFTEEN_MINUTES_TICKS) {
sum15m += durationNano;
count15m++;
} else {
break;
}
i++;
}

tps1m = calculateTPSFromAvgNano(sum1m, count1m);
tps5m = calculateTPSFromAvgNano(sum5m, count5m);
tps15m = calculateTPSFromAvgNano(sum15m, count15m);
}

private double calculateTPSFromAvgNano(double totalNano, int count) {
if (count == 0) {
return 20.0;
}
double avgTickTimeMillis = (double) totalNano / count / 1_000_000.0;
if (avgTickTimeMillis <= 0) {
return 20.0;
}
// TPS: 1 second (1000ms) / avg tick time (ms)
double tps = 1000.0 / avgTickTimeMillis;
return Math.min(tps, 20.0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package it.renvins.serverpulse.bukkit.metrics;

import com.destroystokyo.paper.event.server.ServerTickEndEvent;
import it.renvins.serverpulse.common.metrics.CommonMSPTRetriever;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

public class PaperMSPTRetriever extends CommonMSPTRetriever implements Listener {

@EventHandler
private void startTickMonitor(ServerTickEndEvent event) {
addTickDuration(event.getTickDuration());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import it.renvins.serverpulse.api.ServerPulseProvider;
import it.renvins.serverpulse.api.metrics.IDiskRetriever;
import it.renvins.serverpulse.api.metrics.IMSPTRetriever;
import it.renvins.serverpulse.api.metrics.IPingRetriever;
import it.renvins.serverpulse.api.metrics.ITPSRetriever;
import it.renvins.serverpulse.api.service.IDatabaseService;
Expand All @@ -19,6 +20,7 @@
import it.renvins.serverpulse.common.disk.DiskRetriever;
import it.renvins.serverpulse.common.metrics.LineProtocolFormatter;
import it.renvins.serverpulse.common.metrics.MetricsCollector;
import it.renvins.serverpulse.common.metrics.UnsupportedMSPTRetriever;
import it.renvins.serverpulse.common.metrics.UnsupportedTPSRetriever;
import it.renvins.serverpulse.common.platform.Platform;
import it.renvins.serverpulse.common.scheduler.TaskScheduler;
Expand Down Expand Up @@ -58,8 +60,9 @@ public ServerPulseBungeeCordLoader(ServerPulseBungeeCord plugin) {
this.pingRetriever =new BungeeCordPingRetriever(plugin);

ITPSRetriever tpsRetriever = new UnsupportedTPSRetriever();
IMSPTRetriever msptRetriever = new UnsupportedMSPTRetriever();

MetricsCollector collector = new MetricsCollector(pulseLogger, platform, tpsRetriever, diskRetriever, pingRetriever);
MetricsCollector collector = new MetricsCollector(pulseLogger, platform, tpsRetriever, diskRetriever, pingRetriever, msptRetriever);
LineProtocolFormatter formatter = new LineProtocolFormatter(config);

this.metricsService = new MetricsService(pulseLogger, collector, formatter, scheduler, databaseService);
Expand Down
Loading