1
mirror of https://github.com/StarWishsama/Slimefun4.git synced 2024-09-19 19:25:48 +00:00

Add new analytics service (#4067)

Co-authored-by: Alessio Colombo <37039432+Sfiguz7@users.noreply.github.com>
This commit is contained in:
Daniel Walsh 2024-02-17 16:23:39 +00:00 committed by GitHub
parent 8666bbc3d1
commit bf402068f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 342 additions and 30 deletions

View File

@ -134,6 +134,14 @@ For more info see [bStats' Privacy Policy](https://bstats.org/privacy-policy)
Our [bStats Module](https://github.com/Slimefun/MetricsModule) is downloaded automatically when installing this Plugin, this module will automatically update on server starts independently from the main plugin. This way we can automatically roll out updates to the bStats module, in cases of severe performance issues for example where live data and insight into what is impacting performance can be crucial.
These updates can of course be disabled under `/plugins/Slimefun/config.yml`. To disable metrics collection as a whole, see the paragraph above.
---
Slimefun also uses its own analytics system to collect anonymous information about the performance of this plugin.<br>
This is solely for statistical purposes, as we are interested in how it's performing for all servers.<br>
All available data is anonymous and aggregated, at no point can we see individual server information.<br>
You can also disable this behaviour under `/plugins/Slimefun/config.yml`.<br>
</details>
<details>

View File

@ -1,23 +0,0 @@
package io.github.thebusybiscuit.slimefun4;
import javax.annotation.ParametersAreNonnullByDefault;
import org.bukkit.plugin.java.JavaPlugin;
public class Threads {
@ParametersAreNonnullByDefault
public static void newThread(JavaPlugin plugin, String name, Runnable runnable) {
// TODO: Change to thread pool
new Thread(runnable, plugin.getName() + " - " + name).start();
}
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();
}
}

View File

@ -30,7 +30,6 @@ import com.google.common.collect.ImmutableSet;
import io.github.bakedlibs.dough.common.ChatColors;
import io.github.bakedlibs.dough.common.CommonPatterns;
import io.github.bakedlibs.dough.config.Config;
import io.github.thebusybiscuit.slimefun4.Threads;
import io.github.thebusybiscuit.slimefun4.api.events.AsyncProfileLoadEvent;
import io.github.thebusybiscuit.slimefun4.api.gps.Waypoint;
import io.github.thebusybiscuit.slimefun4.api.items.HashedArmorpiece;
@ -383,7 +382,6 @@ public class PlayerProfile {
// See #4011, #4116
if (loading.containsKey(uuid)) {
Debug.log(TestCase.PLAYER_PROFILE_DATA, "Attempted to get PlayerProfile ({}) while loading", uuid);
Debug.log(TestCase.PLAYER_PROFILE_DATA, "Caller: {}", Threads.getCaller());
// We can't easily consume the callback so we will throw it away in this case
// This will mean that if a user has attempted to do an action like open a block while
@ -394,7 +392,7 @@ public class PlayerProfile {
}
loading.put(uuid, true);
Threads.newThread(Slimefun.instance(), "PlayerProfile#get(" + uuid + ")", () -> {
Slimefun.getThreadService().newThread(Slimefun.instance(), "PlayerProfile#get(" + uuid + ")", () -> {
PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(p.getUniqueId());
loading.remove(uuid);
@ -428,14 +426,13 @@ public class PlayerProfile {
// See #4011, #4116
if (loading.containsKey(uuid)) {
Debug.log(TestCase.PLAYER_PROFILE_DATA, "Attempted to request PlayerProfile ({}) while loading", uuid);
Debug.log(TestCase.PLAYER_PROFILE_DATA, "Caller: {}", Threads.getCaller());
return false;
}
if (!Slimefun.getRegistry().getPlayerProfiles().containsKey(uuid)) {
loading.put(uuid, true);
// Should probably prevent multiple requests for the same profile in the future
Threads.newThread(Slimefun.instance(), "PlayerProfile#request(" + uuid + ")", () -> {
Slimefun.getThreadService().newThread(Slimefun.instance(), "PlayerProfile#request(" + uuid + ")", () -> {
PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(uuid);
loading.remove(uuid);

View File

@ -6,6 +6,8 @@ import java.util.Locale;
import javax.annotation.Nonnull;
import io.github.thebusybiscuit.slimefun4.core.services.AnalyticsService;
/**
* Test cases in Slimefun. These are very useful for debugging why behavior is happening.
* Server owners can enable these with {@code /sf debug <test-case>}
@ -25,7 +27,12 @@ public enum TestCase {
* Debug information regarding player profile loading, saving and handling.
* This is an area we're currently changing quite a bit and this will help ensure we're doing it safely
*/
PLAYER_PROFILE_DATA;
PLAYER_PROFILE_DATA,
/**
* Debug information regarding our {@link AnalyticsService}.
*/
ANALYTICS;
public static final List<String> VALUES_LIST = Arrays.stream(values()).map(TestCase::toString).toList();

View File

@ -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}.
* <p>
* 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());
}
});
}
}

View File

@ -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 - <x> - <plugin>" 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 - <x> - <plugin>" 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();
}
}

View File

@ -22,6 +22,8 @@ import org.bukkit.Server;
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 @@ public class SlimefunProfiler {
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 class SlimefunProfiler {
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);
}
}

View File

@ -42,6 +42,7 @@ import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile;
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.MetricsService;
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 @@ public class Slimefun extends JavaPlugin implements SlimefunAddon {
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 class Slimefun extends JavaPlugin implements SlimefunAddon {
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 class Slimefun extends JavaPlugin implements SlimefunAddon {
public static @Nonnull Storage getPlayerStorage() {
return instance().playerStorage;
}
/**
* This method returns the {@link ThreadService} of Slimefun.
* <b>Do not use this if you're an addon. Please make your own {@link ThreadService}.</b>
*
* @return The {@link ThreadService} for Slimefun
*/
public static @Nonnull ThreadService getThreadService() {
return instance().threadService;
}
}

View File

@ -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 class LegacyStorage implements Storage {
}
}
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 class LegacyStorage implements Storage {
// Save files
playerFile.save();
waypointsFile.save();
long end = System.nanoTime();
Slimefun.getAnalyticsService().recordPlayerProfileDataTime("legacy", false, end - start);
}
}

View File

@ -50,6 +50,7 @@ talismans:
metrics:
auto-update: true
analytics: true
research-ranks:
- Chicken