VALUES_LIST = Arrays.stream(values()).map(TestCase::toString).toList();
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AnalyticsService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AnalyticsService.java
new file mode 100644
index 0000000000..b0afd40657
--- /dev/null
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AnalyticsService.java
@@ -0,0 +1,158 @@
+package io.github.thebusybiscuit.slimefun4.core.services;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nonnull;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import io.github.thebusybiscuit.slimefun4.core.debug.Debug;
+import io.github.thebusybiscuit.slimefun4.core.debug.TestCase;
+import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+
+/**
+ * This class represents an analytics service that sends data.
+ * This data is used to analyse performance of this {@link Plugin}.
+ *
+ * You can find more info in the README file of this Project on GitHub.
+ *
+ * @author WalshyDev
+ */
+public class AnalyticsService {
+
+ private static final int VERSION = 1;
+ private static final String API_URL = "https://analytics.slimefun.dev/ingest";
+
+ private final JavaPlugin plugin;
+ private final HttpClient client = HttpClient.newHttpClient();
+
+ private boolean enabled;
+
+ public AnalyticsService(JavaPlugin plugin) {
+ this.plugin = plugin;
+ }
+
+ public void start() {
+ this.enabled = Slimefun.getCfg().getBoolean("metrics.analytics");
+
+ if (enabled) {
+ plugin.getLogger().info("Enabled Analytics Service");
+
+ // Send the timings data every minute
+ Slimefun.getThreadService().newScheduledThread(
+ plugin,
+ "AnalyticsService - Timings",
+ sendTimingsAnalytics(),
+ 1,
+ 1,
+ TimeUnit.MINUTES
+ );
+ }
+ }
+
+ // We'll send some timing data every minute.
+ // To date, we collect the tick interval, the avg timing per tick and avg timing per machine
+ @Nonnull
+ private Runnable sendTimingsAnalytics() {
+ return () -> {
+ double tickInterval = Slimefun.getTickerTask().getTickRate();
+ // This is currently used by bStats in a ranged way, we'll move this
+ double totalTimings = Slimefun.getProfiler().getAndResetAverageNanosecondTimings();
+ double avgPerMachine = Slimefun.getProfiler().getAverageTimingsPerMachine();
+
+ if (totalTimings == 0 || avgPerMachine == 0) {
+ Debug.log(TestCase.ANALYTICS, "Ignoring analytics data for server_timings as no data was found"
+ + " - total: " + totalTimings + ", avg: " + avgPerMachine);
+ // Ignore if no data
+ return;
+ }
+
+ send("server_timings", new double[]{
+ // double1 is schema version
+ tickInterval, // double2
+ totalTimings, // double3
+ avgPerMachine // double4
+ }, null);
+ };
+ }
+
+ public void recordPlayerProfileDataTime(@Nonnull String backend, boolean load, long nanoseconds) {
+ send(
+ "player_profile_data_load_time",
+ new double[]{
+ // double1 is schema version
+ nanoseconds, // double2
+ load ? 1 : 0 // double3 - 1 if load, 0 if save
+ },
+ new String[]{
+ // blob1 is version
+ backend // blob2
+ }
+ );
+ }
+
+ // Important: Keep the order of these doubles and blobs the same unless you increment the version number
+ // If a value is no longer used, just send null or replace it with a new value - don't shift the order
+ @ParametersAreNonnullByDefault
+ private void send(String id, double[] doubles, String[] blobs) {
+ // If not enabled or not official build (e.g. local build) or a unit test, just ignore.
+ if (
+ !enabled
+ || !Slimefun.getUpdater().getBranch().isOfficial()
+ || Slimefun.instance().isUnitTest()
+ ) return;
+
+ JsonObject object = new JsonObject();
+ // Up to 1 index
+ JsonArray indexes = new JsonArray();
+ indexes.add(id);
+ object.add("indexes", indexes);
+
+ // Up to 20 doubles (including the version)
+ JsonArray doublesArray = new JsonArray();
+ doublesArray.add(VERSION);
+ if (doubles != null) {
+ for (double d : doubles) {
+ doublesArray.add(d);
+ }
+ }
+ object.add("doubles", doublesArray);
+
+ // Up to 20 blobs (including the version)
+ JsonArray blobsArray = new JsonArray();
+ blobsArray.add(Slimefun.getVersion());
+ if (blobs != null) {
+ for (String s : blobs) {
+ blobsArray.add(s);
+ }
+ }
+ object.add("blobs", blobsArray);
+
+ Debug.log(TestCase.ANALYTICS, "Sending analytics data for " + id);
+ Debug.log(TestCase.ANALYTICS, object.toString());
+
+ // Send async, we do not care about the result. If it fails, that's fine.
+ client.sendAsync(HttpRequest.newBuilder()
+ .uri(URI.create(API_URL))
+ .header("User-Agent", "Mozilla/5.0 Slimefun4 AnalyticsService")
+ .POST(HttpRequest.BodyPublishers.ofString(object.toString()))
+ .build(),
+ HttpResponse.BodyHandlers.discarding()
+ ).thenAcceptAsync((res) -> {
+ if (res.statusCode() == 200) {
+ Debug.log(TestCase.ANALYTICS, "Analytics data for " + id + " sent successfully");
+ } else {
+ Debug.log(TestCase.ANALYTICS, "Analytics data for " + id + " failed to send - " + res.statusCode());
+ }
+ });
+ }
+}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/ThreadService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/ThreadService.java
new file mode 100644
index 0000000000..772b65d3fe
--- /dev/null
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/ThreadService.java
@@ -0,0 +1,99 @@
+package io.github.thebusybiscuit.slimefun4.core.services;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitScheduler;
+
+public final class ThreadService {
+
+ private final ThreadGroup group;
+ private final ExecutorService cachedPool;
+ private final ScheduledExecutorService scheduledPool;
+
+ public ThreadService(JavaPlugin plugin) {
+ this.group = new ThreadGroup(plugin.getName());
+ this.cachedPool = Executors.newCachedThreadPool(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(group, r, plugin.getName() + " - ThreadService");
+ }
+ });
+
+ this.scheduledPool = Executors.newScheduledThreadPool(1, new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(group, r, plugin.getName() + " - ScheduledThreadService");
+ }
+ });
+ }
+
+ /**
+ * Invoke a new thread from the cached thread pool with the given name.
+ * This is a much better alternative to using
+ * {@link BukkitScheduler#runTaskAsynchronously(org.bukkit.plugin.Plugin, Runnable)}
+ * as this will show not only the plugin but a useful name.
+ * By default, Bukkit will use "Craft Scheduler Thread - - " which is nice to show the plugin but
+ * it's impossible to track exactly what thread that is.
+ *
+ * @param plugin The {@link JavaPlugin} that is creating this thread
+ * @param name The name of this thread, this will be prefixed with the plugin's name
+ * @param runnable The {@link Runnable} to execute
+ */
+ @ParametersAreNonnullByDefault
+ public void newThread(JavaPlugin plugin, String name, Runnable runnable) {
+ cachedPool.submit(() -> {
+ // This is a bit of a hack, but it's the only way to have the thread name be as desired
+ Thread.currentThread().setName(plugin.getName() + " - " + name);
+ runnable.run();
+ });
+ }
+
+ /**
+ * Invoke a new scheduled thread from the cached thread pool with the given name.
+ * This is a much better alternative to using
+ * {@link BukkitScheduler#runTaskTimerAsynchronously(org.bukkit.plugin.Plugin, Runnable, long, long)}
+ * as this will show not only the plugin but a useful name.
+ * By default, Bukkit will use "Craft Scheduler Thread - - " which is nice to show the plugin but
+ * it's impossible to track exactly what thread that is.
+ *
+ * @param plugin The {@link JavaPlugin} that is creating this thread
+ * @param name The name of this thread, this will be prefixed with the plugin's name
+ * @param runnable The {@link Runnable} to execute
+ */
+ @ParametersAreNonnullByDefault
+ public void newScheduledThread(
+ JavaPlugin plugin,
+ String name,
+ Runnable runnable,
+ long delay,
+ long period,
+ TimeUnit unit
+ ) {
+ this.scheduledPool.scheduleWithFixedDelay(() -> {
+ // This is a bit of a hack, but it's the only way to have the thread name be as desired
+ Thread.currentThread().setName(plugin.getName() + " - " + name);
+ runnable.run();
+ }, delay, delay, unit);
+ }
+
+ /**
+ * Get the caller of a given method, this should only be used for debugging purposes and is not performant.
+ *
+ * @return The caller of the method that called this method.
+ */
+ public static String getCaller() {
+ // First item will be getting the call stack
+ // Second item will be this call
+ // Third item will be the func we care about being called
+ // And finally will be the caller
+ StackTraceElement element = Thread.currentThread().getStackTrace()[3];
+ return element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber();
+ }
+}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java
index 2a1292225a..408fdc439e 100644
--- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/profiler/SlimefunProfiler.java
@@ -22,6 +22,8 @@
import org.bukkit.block.Block;
import org.bukkit.scheduler.BukkitScheduler;
+import com.google.common.util.concurrent.AtomicDouble;
+
import io.github.thebusybiscuit.slimefun4.api.SlimefunAddon;
import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem;
import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
@@ -87,6 +89,8 @@ public class SlimefunProfiler {
private final AtomicLong totalMsTicked = new AtomicLong();
private final AtomicInteger ticksPassed = new AtomicInteger();
+ private final AtomicLong totalNsTicked = new AtomicLong();
+ private final AtomicDouble averageTimingsPerMachine = new AtomicDouble();
/**
* This method terminates the {@link SlimefunProfiler}.
@@ -222,11 +226,14 @@ private void finishReport() {
totalElapsedTime = timings.values().stream().mapToLong(Long::longValue).sum();
+ averageTimingsPerMachine.getAndSet(timings.values().stream().mapToLong(Long::longValue).average().orElse(0));
+
/*
* We log how many milliseconds have been ticked, and how many ticks have passed
* This is so when bStats requests the average timings, they're super quick to figure out
*/
totalMsTicked.addAndGet(TimeUnit.NANOSECONDS.toMillis(totalElapsedTime));
+ totalNsTicked.addAndGet(totalElapsedTime);
ticksPassed.incrementAndGet();
if (!requests.isEmpty()) {
@@ -416,4 +423,26 @@ public long getAndResetAverageTimings() {
return l;
}
+
+ /**
+ * Get and reset the average nanosecond timing for this {@link SlimefunProfiler}.
+ *
+ * @return The average nanosecond timing for this {@link SlimefunProfiler}.
+ */
+ public double getAndResetAverageNanosecondTimings() {
+ long l = totalNsTicked.get() / ticksPassed.get();
+ totalNsTicked.set(0);
+ ticksPassed.set(0);
+
+ return l;
+ }
+
+ /**
+ * Get and reset the average millisecond timing for each machine.
+ *
+ * @return The average millisecond timing for each machine.
+ */
+ public double getAverageTimingsPerMachine() {
+ return averageTimingsPerMachine.getAndSet(0);
+ }
}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java
index 28233ea741..ae065bc062 100644
--- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java
@@ -42,6 +42,7 @@
import io.github.thebusybiscuit.slimefun4.core.SlimefunRegistry;
import io.github.thebusybiscuit.slimefun4.core.commands.SlimefunCommand;
import io.github.thebusybiscuit.slimefun4.core.networks.NetworkManager;
+import io.github.thebusybiscuit.slimefun4.core.services.AnalyticsService;
import io.github.thebusybiscuit.slimefun4.core.services.AutoSavingService;
import io.github.thebusybiscuit.slimefun4.core.services.BackupService;
import io.github.thebusybiscuit.slimefun4.core.services.BlockDataService;
@@ -52,6 +53,7 @@
import io.github.thebusybiscuit.slimefun4.core.services.MinecraftRecipeService;
import io.github.thebusybiscuit.slimefun4.core.services.PerWorldSettingsService;
import io.github.thebusybiscuit.slimefun4.core.services.PermissionsService;
+import io.github.thebusybiscuit.slimefun4.core.services.ThreadService;
import io.github.thebusybiscuit.slimefun4.core.services.UpdaterService;
import io.github.thebusybiscuit.slimefun4.core.services.github.GitHubService;
import io.github.thebusybiscuit.slimefun4.core.services.holograms.HologramsService;
@@ -182,6 +184,8 @@ public class Slimefun extends JavaPlugin implements SlimefunAddon {
private final MinecraftRecipeService recipeService = new MinecraftRecipeService(this);
private final HologramsService hologramsService = new HologramsService(this);
private final SoundService soundService = new SoundService(this);
+ private final ThreadService threadService = new ThreadService(this);
+ private final AnalyticsService analyticsService = new AnalyticsService(this);
// Some other things we need
private final IntegrationsManager integrations = new IntegrationsManager(this);
@@ -309,8 +313,9 @@ private void onPluginStart() {
playerStorage = new LegacyStorage();
logger.log(Level.INFO, "Using legacy storage for player data");
- // Setting up bStats
+ // Setting up bStats and analytics
new Thread(metricsService::start, "Slimefun Metrics").start();
+ analyticsService.start();
// Starting the Auto-Updater
if (config.getBoolean("options.auto-update")) {
@@ -901,6 +906,17 @@ public static SoundService getSoundService() {
return instance.metricsService;
}
+ /**
+ * This method returns the {@link AnalyticsService} of Slimefun.
+ * It is used to handle sending analytic information.
+ *
+ * @return The {@link AnalyticsService} for Slimefun
+ */
+ public static @Nonnull AnalyticsService getAnalyticsService() {
+ validateInstance();
+ return instance.analyticsService;
+ }
+
/**
* This method returns the {@link GitHubService} of Slimefun.
* It is used to retrieve data from GitHub repositories.
@@ -1068,4 +1084,14 @@ public static boolean isNewlyInstalled() {
public static @Nonnull Storage getPlayerStorage() {
return instance().playerStorage;
}
+
+ /**
+ * This method returns the {@link ThreadService} of Slimefun.
+ * Do not use this if you're an addon. Please make your own {@link ThreadService}.
+ *
+ * @return The {@link ThreadService} for Slimefun
+ */
+ public static @Nonnull ThreadService getThreadService() {
+ return instance().threadService;
+ }
}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java
index 59b0c82b96..f051a3b846 100644
--- a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java
@@ -27,6 +27,8 @@ public class LegacyStorage implements Storage {
@Override
public PlayerData loadPlayerData(@Nonnull UUID uuid) {
+ long start = System.nanoTime();
+
Config playerFile = new Config("data-storage/Slimefun/Players/" + uuid + ".yml");
// Not too sure why this is its own file
Config waypointsFile = new Config("data-storage/Slimefun/waypoints/" + uuid + ".yml");
@@ -73,12 +75,17 @@ public PlayerData loadPlayerData(@Nonnull UUID uuid) {
}
}
+ long end = System.nanoTime();
+ Slimefun.getAnalyticsService().recordPlayerProfileDataTime("legacy", true, end - start);
+
return new PlayerData(researches, backpacks, waypoints);
}
// The current design of saving all at once isn't great, this will be refined.
@Override
public void savePlayerData(@Nonnull UUID uuid, @Nonnull PlayerData data) {
+ long start = System.nanoTime();
+
Config playerFile = new Config("data-storage/Slimefun/Players/" + uuid + ".yml");
// Not too sure why this is its own file
Config waypointsFile = new Config("data-storage/Slimefun/waypoints/" + uuid + ".yml");
@@ -133,5 +140,8 @@ public void savePlayerData(@Nonnull UUID uuid, @Nonnull PlayerData data) {
// Save files
playerFile.save();
waypointsFile.save();
+
+ long end = System.nanoTime();
+ Slimefun.getAnalyticsService().recordPlayerProfileDataTime("legacy", false, end - start);
}
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index cb133170e4..5e36b700b7 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -50,6 +50,7 @@ talismans:
metrics:
auto-update: true
+ analytics: true
research-ranks:
- Chicken