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
20 changes: 20 additions & 0 deletions bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
import dev.faststats.bukkit.BukkitMetrics;
import dev.faststats.core.Metrics;
import dev.faststats.core.chart.Chart;
import dev.faststats.core.ErrorTracker;
import org.bukkit.plugin.java.JavaPlugin;

import java.net.URI;

public class ExamplePlugin extends JavaPlugin {
// context-aware error tracker, automatically tracks errors in the same class loader
public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware();

// context-unaware error tracker, does not automatically track errors
public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware();

private final Metrics metrics = BukkitMetrics.factory()
.url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only

Expand All @@ -20,6 +27,10 @@ public class ExamplePlugin extends JavaPlugin {
.addChart(Chart.numberArray("example_number_array", () -> new Number[]{1, 2, 3}))
.addChart(Chart.booleanArray("example_boolean_array", () -> new Boolean[]{true, false}))

// Attach an error tracker
// This must be enabled in the project settings
.errorTracker(ERROR_TRACKER)

.debug(true) // Enable debug mode for development and testing

.token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project
Expand All @@ -29,4 +40,13 @@ public class ExamplePlugin extends JavaPlugin {
public void onDisable() {
metrics.shutdown();
}

public void doSomethingWrong() {
try {
// Do something that might throw an error
throw new RuntimeException("Something went wrong!");
} catch (Exception e) {
CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e);
}
}
}
119 changes: 119 additions & 0 deletions core/src/main/java/dev/faststats/core/ErrorTracker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package dev.faststats.core;

import dev.faststats.core.concurrent.TrackingExecutors;
import dev.faststats.core.concurrent.TrackingBase;
import dev.faststats.core.concurrent.TrackingThreadFactory;
import dev.faststats.core.concurrent.TrackingThreadPoolExecutor;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;

/**
* An error tracker.
*
* @since 0.10.0
*/
public sealed interface ErrorTracker permits SimpleErrorTracker {
/**
* Create and attach a new context-aware error tracker.
* <p>
* This tracker will automatically track errors that occur in the same class loader as the tracker itself.
* <p>
* You can still manually track errors using {@code #trackError}.
*
* @return the error tracker
* @see #contextUnaware()
* @see #trackError(String)
* @see #trackError(Throwable)
* @since 0.10.0
*/
@Contract(value = " -> new")
static ErrorTracker contextAware() {
var tracker = new SimpleErrorTracker();
tracker.attachErrorContext(ErrorTracker.class.getClassLoader());
return tracker;
}

/**
* Create a new context-unaware error tracker.
* <p>
* This tracker will not automatically track any errors.
* <p>
* You have to manually track errors using {@code #trackError}.
*
* @return the error tracker
* @see #contextAware()
* @see #trackError(String)
* @see #trackError(Throwable)
* @since 0.10.0
*/
@Contract(value = " -> new")
static ErrorTracker contextUnaware() {
return new SimpleErrorTracker();
}

/**
* Tracks an error.
*
* @param message the error message
* @see #trackError(Throwable)
* @since 0.10.0
*/
@Contract(mutates = "this")
void trackError(String message);

/**
* Tracks an error.
*
* @param error the error
* @since 0.10.0
*/
@Contract(mutates = "this")
void trackError(Throwable error);

/**
* Attaches an error context to the tracker.
*
* @param loader the class loader
* @since 0.10.0
*/
void attachErrorContext(@Nullable ClassLoader loader);

/**
* Returns the tracking base.
*
* @return the tracking base
* @since 0.10.0
*/
@Contract(pure = true)
TrackingBase base();

/**
* Returns the tracking equivalent to {@link java.util.concurrent.Executors}.
*
* @return the tracking executors
* @see java.util.concurrent.Executors
* @since 0.10.0
*/
@Contract(pure = true)
TrackingExecutors executors();

/**
* Returns the tracking equivalent to {@link java.util.concurrent.ThreadFactory}.
*
* @return the tracking thread factory
* @see java.util.concurrent.ThreadFactory
* @since 0.10.0
*/
@Contract(pure = true)
TrackingThreadFactory threadFactory();

/**
* Returns the tracking equivalent to {@link java.util.concurrent.ThreadPoolExecutor}.
*
* @return the tracking thread pool executor
* @see java.util.concurrent.ThreadPoolExecutor
* @since 0.10.0
*/
@Contract(pure = true)
TrackingThreadPoolExecutor threadPoolExecutor();
}
20 changes: 20 additions & 0 deletions core/src/main/java/dev/faststats/core/Metrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.jetbrains.annotations.Contract;

import java.net.URI;
import java.util.Optional;
import java.util.UUID;

/**
Expand All @@ -23,6 +24,15 @@ public interface Metrics {
@Contract(pure = true)
String getToken();

/**
* Get the error tracker for this metrics instance.
*
* @return the error tracker
* @since 0.10.0
*/
@Contract(pure = true)
Optional<ErrorTracker> getErrorTracker();

/**
* Get the metrics configuration.
*
Expand Down Expand Up @@ -60,6 +70,16 @@ interface Factory<T> {
@Contract(mutates = "this")
Factory<T> addChart(Chart<?> chart) throws IllegalArgumentException;

/**
* Sets the error tracker for this metrics instance.
*
* @param tracker the error tracker
* @return the metrics factory
* @since 0.10.0
*/
@Contract(mutates = "this")
Factory<T> errorTracker(ErrorTracker tracker);

/**
* Enables or disabled debug mode for this metrics instance.
* <p>
Expand Down
182 changes: 182 additions & 0 deletions core/src/main/java/dev/faststats/core/MurmurHash3.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package dev.faststats.core;

import org.jetbrains.annotations.Contract;

import java.nio.charset.StandardCharsets;

/**
* Implementation of the MurmurHash3 128-bit hash algorithm.
* <p>
* MurmurHash is a non-cryptographic hash function suitable for general hash-based lookup.
* It provides excellent distribution and performance while minimizing collisions.
* </p>
* <p>
* This implementation follows the MurmurHash3_x64_128 variant as described at:
* <a href="https://en.wikipedia.org/wiki/MurmurHash">https://en.wikipedia.org/wiki/MurmurHash</a>
* </p>
* <p>
* Original algorithm by Austin Appleby. The name comes from the two elementary operations
* it uses: multiply (MU) and rotate (R).
* </p>
*/
final class MurmurHash3 {
/**
* Computes the 128-bit MurmurHash3 hash of the input string.
* <p>
* The string is encoded to UTF-8 bytes before hashing. The result is returned
* as an array of two long values (64 bits each), combined they form a 128-bit hash.
* </p>
*
* @param data the input string to hash
* @return a 2-element array containing the lower 64 bits at index 0 and upper 64 bits at index 1
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash on Wikipedia</a>
*/
@Contract(value = "_ -> new", pure = true)
public static long[] hash(String data) {
var bytes = data.getBytes(StandardCharsets.UTF_8);
var h1 = 0L;
var h2 = 0L;
final var c1 = 0x87c37b91114253d5L;
final var c2 = 0x4cf5ad432745937fL;
var length = bytes.length;
var blocks = length / 16;

// Process 128-bit blocks
for (int i = 0; i < blocks; i++) {
var k1 = getInt(bytes, i * 16);
var k2 = getInt(bytes, i * 16 + 4);
var k3 = getInt(bytes, i * 16 + 8);
var k4 = getInt(bytes, i * 16 + 12);

k1 *= (int) c1;
k1 = Integer.rotateLeft(k1, 31);
k1 *= (int) c2;
h1 ^= k1;

h1 = Long.rotateLeft(h1, 27);
h1 += h2;
h1 = h1 * 5 + 0x52dce729;

k2 *= (int) c2;
k2 = Integer.rotateLeft(k2, 33);
k2 *= (int) c1;
h2 ^= k2;

h2 = Long.rotateLeft(h2, 31);
h2 += h1;
h2 = h2 * 5 + 0x38495ab5;
}

// Tail
var k1 = 0;
var k2 = 0;
var k3 = 0;
var k4 = 0;
var tail = blocks * 16;

switch (length & 15) {
case 15:
k4 ^= (bytes[tail + 14] & 0xff) << 16;
case 14:
k4 ^= (bytes[tail + 13] & 0xff) << 8;
case 13:
k4 ^= (bytes[tail + 12] & 0xff);
k4 *= (int) c2;
k4 = Integer.rotateLeft(k4, 33);
k4 *= (int) c1;
h2 ^= k4;
case 12:
k3 ^= (bytes[tail + 11] & 0xff) << 24;
case 11:
k3 ^= (bytes[tail + 10] & 0xff) << 16;
case 10:
k3 ^= (bytes[tail + 9] & 0xff) << 8;
case 9:
k3 ^= (bytes[tail + 8] & 0xff);
k3 *= (int) c1;
k3 = Integer.rotateLeft(k3, 31);
k3 *= (int) c2;
h1 ^= k3;
case 8:
k2 ^= (bytes[tail + 7] & 0xff) << 24;
case 7:
k2 ^= (bytes[tail + 6] & 0xff) << 16;
case 6:
k2 ^= (bytes[tail + 5] & 0xff) << 8;
case 5:
k2 ^= (bytes[tail + 4] & 0xff);
k2 *= (int) c2;
k2 = Integer.rotateLeft(k2, 33);
k2 *= (int) c1;
h2 ^= k2;
case 4:
k1 ^= (bytes[tail + 3] & 0xff) << 24;
case 3:
k1 ^= (bytes[tail + 2] & 0xff) << 16;
case 2:
k1 ^= (bytes[tail + 1] & 0xff) << 8;
case 1:
k1 ^= (bytes[tail] & 0xff);
k1 *= (int) c1;
k1 = Integer.rotateLeft(k1, 31);
k1 *= (int) c2;
h1 ^= k1;
}

// Finalization
h1 ^= length;
h2 ^= length;

h1 += h2;
h2 += h1;

h1 = fmix64(h1);
h2 = fmix64(h2);

h1 += h2;
h2 += h1;

return new long[]{h1, h2};
}

/**
* Finalization mix function to avalanche the bits in the hash.
* <p>
* This function improves the distribution of the hash by XORing and multiplying
* with carefully chosen constants, ensuring that similar inputs produce very
* different outputs (avalanche effect).
* </p>
*
* @param k the 64-bit value to mix
* @return the mixed 64-bit value
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash#Algorithm">MurmurHash Algorithm on Wikipedia</a>
*/
@Contract(pure = true)
private static long fmix64(long k) {
k ^= k >>> 33;
k *= 0xff51afd7ed558ccdL;
k ^= k >>> 33;
k *= 0xc4ceb9fe1a85ec53L;
k ^= k >>> 33;
return k;
}

/**
* Reads a 32-bit little-endian integer from the byte array at the specified offset.
* <p>
* This helper method extracts four consecutive bytes and combines them into a
* single integer using little-endian byte order.
* </p>
*
* @param bytes the byte array to read from
* @param offset the starting index in the byte array (must have at least 4 bytes from offset)
* @return the 32-bit integer value read in little-endian order
*/
@Contract(pure = true)
private static int getInt(byte[] bytes, int offset) {
return (bytes[offset] & 0xff) |
((bytes[offset + 1] & 0xff) << 8) |
((bytes[offset + 2] & 0xff) << 16) |
((bytes[offset + 3] & 0xff) << 24);
}
}
Loading
Loading