Skip to content

Commit

Permalink
Add new analytics service (#4067)
Browse files Browse the repository at this point in the history
Co-authored-by: Alessio Colombo <[email protected]>
  • Loading branch information
WalshyDev and Sfiguz7 authored Feb 17, 2024
1 parent 8666bbc commit bf40206
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 30 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
23 changes: 0 additions & 23 deletions src/main/java/io/github/thebusybiscuit/slimefun4/Threads.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
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;
Expand Down Expand Up @@ -383,7 +382,6 @@ public static boolean get(@Nonnull OfflinePlayer p, @Nonnull Consumer<PlayerProf
// 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
Expand All @@ -394,7 +392,7 @@ public static boolean get(@Nonnull OfflinePlayer p, @Nonnull Consumer<PlayerProf
}

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);

Expand Down Expand Up @@ -428,14 +426,13 @@ public static boolean request(@Nonnull OfflinePlayer p) {
// 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

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>}
Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit bf40206

Please sign in to comment.