PaperMC/Spigot-API-Patches/0004-Timings-v2.patch
Aikar 2712c68885
[Auto] Updated Upstream (Bukkit/CraftBukkit)
Upstream has released updates that appears to apply and compile correctly.
This update has not been tested by PaperMC and as with ANY update, please do your own testing

Bukkit Changes:
f1e73b03 #525: Add contributors plugin.yml field.
ef0999fe #529: Added getRecipe() method to retrieve a Recipe by it's NamespacedKey

CraftBukkit Changes:
8b831a965 #714: Added getRecipe() method to retrieve a Recipe by it's NamespacedKey
2020-07-22 04:26:59 -04:00

3739 lines
137 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Mon, 29 Feb 2016 18:48:17 -0600
Subject: [PATCH] Timings v2
diff --git a/src/main/java/co/aikar/timings/FullServerTickHandler.java b/src/main/java/co/aikar/timings/FullServerTickHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..dfaa266ff53e43ad48dc5a5a5657fe70600f539a
--- /dev/null
+++ b/src/main/java/co/aikar/timings/FullServerTickHandler.java
@@ -0,0 +1,85 @@
+package co.aikar.timings;
+
+import static co.aikar.timings.TimingsManager.*;
+
+import org.bukkit.Bukkit;
+import org.jetbrains.annotations.NotNull;
+
+public class FullServerTickHandler extends TimingHandler {
+ private static final TimingIdentifier IDENTITY = new TimingIdentifier("Minecraft", "Full Server Tick", null);
+ final TimingData minuteData;
+ double avgFreeMemory = -1D;
+ double avgUsedMemory = -1D;
+ FullServerTickHandler() {
+ super(IDENTITY);
+ minuteData = new TimingData(id);
+
+ TIMING_MAP.put(IDENTITY, this);
+ }
+
+ @NotNull
+ @Override
+ public Timing startTiming() {
+ if (TimingsManager.needsFullReset) {
+ TimingsManager.resetTimings();
+ } else if (TimingsManager.needsRecheckEnabled) {
+ TimingsManager.recheckEnabled();
+ }
+ return super.startTiming();
+ }
+
+ @Override
+ public void stopTiming() {
+ super.stopTiming();
+ if (!isEnabled() || Bukkit.isStopping()) {
+ return;
+ }
+ if (TimingHistory.timedTicks % 20 == 0) {
+ final Runtime runtime = Runtime.getRuntime();
+ double usedMemory = runtime.totalMemory() - runtime.freeMemory();
+ double freeMemory = runtime.maxMemory() - usedMemory;
+ if (this.avgFreeMemory == -1) {
+ this.avgFreeMemory = freeMemory;
+ } else {
+ this.avgFreeMemory = (this.avgFreeMemory * (59 / 60D)) + (freeMemory * (1 / 60D));
+ }
+
+ if (this.avgUsedMemory == -1) {
+ this.avgUsedMemory = usedMemory;
+ } else {
+ this.avgUsedMemory = (this.avgUsedMemory * (59 / 60D)) + (usedMemory * (1 / 60D));
+ }
+ }
+
+ long start = System.nanoTime();
+ TimingsManager.tick();
+ long diff = System.nanoTime() - start;
+ TIMINGS_TICK.addDiff(diff, null);
+ // addDiff for TIMINGS_TICK incremented this, bring it back down to 1 per tick.
+ record.setCurTickCount(record.getCurTickCount()-1);
+
+ minuteData.setCurTickTotal(record.getCurTickTotal());
+ minuteData.setCurTickCount(1);
+
+ boolean violated = isViolated();
+ minuteData.processTick(violated);
+ TIMINGS_TICK.processTick(violated);
+ processTick(violated);
+
+
+ if (TimingHistory.timedTicks % 1200 == 0) {
+ MINUTE_REPORTS.add(new TimingHistory.MinuteReport());
+ TimingHistory.resetTicks(false);
+ minuteData.reset();
+ }
+ if (TimingHistory.timedTicks % Timings.getHistoryInterval() == 0) {
+ TimingsManager.HISTORY.add(new TimingHistory());
+ TimingsManager.resetTimings();
+ }
+ Bukkit.getUnsafe().reportTimings();
+ }
+
+ boolean isViolated() {
+ return record.getCurTickTotal() > 50000000;
+ }
+}
diff --git a/src/main/java/co/aikar/timings/NullTimingHandler.java b/src/main/java/co/aikar/timings/NullTimingHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b45ce887b9172f30302b83fe24b99b76b16dac3
--- /dev/null
+++ b/src/main/java/co/aikar/timings/NullTimingHandler.java
@@ -0,0 +1,68 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class NullTimingHandler implements Timing {
+ public static final Timing NULL = new NullTimingHandler();
+ @NotNull
+ @Override
+ public Timing startTiming() {
+ return this;
+ }
+
+ @Override
+ public void stopTiming() {
+
+ }
+
+ @NotNull
+ @Override
+ public Timing startTimingIfSync() {
+ return this;
+ }
+
+ @Override
+ public void stopTimingIfSync() {
+
+ }
+
+ @Override
+ public void abort() {
+
+ }
+
+ @Nullable
+ @Override
+ public TimingHandler getTimingHandler() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimedEventExecutor.java b/src/main/java/co/aikar/timings/TimedEventExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e6e1b8e8aeb07e34536941d2cbfc25e5cfa6c27
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimedEventExecutor.java
@@ -0,0 +1,83 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import org.bukkit.Bukkit;
+import org.bukkit.event.Event;
+import org.bukkit.event.EventException;
+import org.bukkit.event.Listener;
+import org.bukkit.plugin.EventExecutor;
+import org.bukkit.plugin.Plugin;
+
+import java.lang.reflect.Method;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class TimedEventExecutor implements EventExecutor {
+
+ private final EventExecutor executor;
+ private final Timing timings;
+
+ /**
+ * Wraps an event executor and associates a timing handler to it.
+ *
+ * @param executor Executor to wrap
+ * @param plugin Owning plugin
+ * @param method EventHandler method
+ * @param eventClass Owning class
+ */
+ public TimedEventExecutor(@NotNull EventExecutor executor, @NotNull Plugin plugin, @Nullable Method method, @NotNull Class<? extends Event> eventClass) {
+ this.executor = executor;
+ String id;
+
+ if (method == null) {
+ if (executor.getClass().getEnclosingClass() != null) { // Oh Skript, how we love you
+ method = executor.getClass().getEnclosingMethod();
+ }
+ }
+
+ if (method != null) {
+ id = method.getDeclaringClass().getName();
+ } else {
+ id = executor.getClass().getName();
+ }
+
+
+ final String eventName = eventClass.getSimpleName();
+ boolean verbose = "BlockPhysicsEvent".equals(eventName);
+ this.timings = Timings.ofSafe(plugin, (verbose ? "## " : "") +
+ "Event: " + id + " (" + eventName + ")");
+ }
+
+ @Override
+ public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException {
+ if (event.isAsynchronous() || !Timings.timingsEnabled || !Bukkit.isPrimaryThread()) {
+ executor.execute(listener, event);
+ return;
+ }
+ try (Timing ignored = timings.startTiming()){
+ executor.execute(listener, event);
+ }
+ }
+}
diff --git a/src/main/java/co/aikar/timings/Timing.java b/src/main/java/co/aikar/timings/Timing.java
new file mode 100644
index 0000000000000000000000000000000000000000..a21e5ead5024fd0058c5e3302d8201dd249d32bc
--- /dev/null
+++ b/src/main/java/co/aikar/timings/Timing.java
@@ -0,0 +1,83 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Provides an ability to time sections of code within the Minecraft Server
+ */
+public interface Timing extends AutoCloseable {
+ /**
+ * Starts timing the execution until {@link #stopTiming()} is called.
+ *
+ * @return Timing
+ */
+ @NotNull
+ Timing startTiming();
+
+ /**
+ * <p>Stops timing and records the data. Propagates the data up to group handlers.</p>
+ *
+ * Will automatically be called when this Timing is used with try-with-resources
+ */
+ void stopTiming();
+
+ /**
+ * Starts timing the execution until {@link #stopTiming()} is called.
+ *
+ * But only if we are on the primary thread.
+ *
+ * @return Timing
+ */
+ @NotNull
+ Timing startTimingIfSync();
+
+ /**
+ * <p>Stops timing and records the data. Propagates the data up to group handlers.</p>
+ *
+ * <p>Will automatically be called when this Timing is used with try-with-resources</p>
+ *
+ * But only if we are on the primary thread.
+ */
+ void stopTimingIfSync();
+
+ /**
+ * @deprecated Doesn't do anything - Removed
+ */
+ @Deprecated
+ void abort();
+
+ /**
+ * Used internally to get the actual backing Handler in the case of delegated Handlers
+ *
+ * @return TimingHandler
+ */
+ @Nullable
+ TimingHandler getTimingHandler();
+
+ @Override
+ void close();
+}
diff --git a/src/main/java/co/aikar/timings/TimingData.java b/src/main/java/co/aikar/timings/TimingData.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5d13a1e44edb861f45c83a9b4309fbf799d407d
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingData.java
@@ -0,0 +1,122 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+
+import static co.aikar.util.JSONUtil.toArray;
+
+/**
+ * <p>Lightweight object for tracking timing data</p>
+ *
+ * This is broken out to reduce memory usage
+ */
+class TimingData {
+ private final int id;
+ private int count = 0;
+ private int lagCount = 0;
+ private long totalTime = 0;
+ private long lagTotalTime = 0;
+ private int curTickCount = 0;
+ private long curTickTotal = 0;
+
+ TimingData(int id) {
+ this.id = id;
+ }
+
+ private TimingData(TimingData data) {
+ this.id = data.id;
+ this.totalTime = data.totalTime;
+ this.lagTotalTime = data.lagTotalTime;
+ this.count = data.count;
+ this.lagCount = data.lagCount;
+ }
+
+ void add(long diff) {
+ ++curTickCount;
+ curTickTotal += diff;
+ }
+
+ void processTick(boolean violated) {
+ totalTime += curTickTotal;
+ count += curTickCount;
+ if (violated) {
+ lagTotalTime += curTickTotal;
+ lagCount += curTickCount;
+ }
+ curTickTotal = 0;
+ curTickCount = 0;
+ }
+
+ void reset() {
+ count = 0;
+ lagCount = 0;
+ curTickTotal = 0;
+ curTickCount = 0;
+ totalTime = 0;
+ lagTotalTime = 0;
+ }
+
+ protected TimingData clone() {
+ return new TimingData(this);
+ }
+
+ @NotNull
+ List<Object> export() {
+ List<Object> list = toArray(
+ id,
+ count,
+ totalTime);
+ if (lagCount > 0) {
+ list.add(lagCount);
+ list.add(lagTotalTime);
+ }
+ return list;
+ }
+
+ boolean hasData() {
+ return count > 0;
+ }
+
+ long getTotalTime() {
+ return totalTime;
+ }
+
+ int getCurTickCount() {
+ return curTickCount;
+ }
+
+ void setCurTickCount(int curTickCount) {
+ this.curTickCount = curTickCount;
+ }
+
+ long getCurTickTotal() {
+ return curTickTotal;
+ }
+
+ void setCurTickTotal(long curTickTotal) {
+ this.curTickTotal = curTickTotal;
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimingHandler.java b/src/main/java/co/aikar/timings/TimingHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..199789d56d22fcb1b77ebd56805cc28aa5a5ab0a
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingHandler.java
@@ -0,0 +1,226 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import co.aikar.util.LoadingIntMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+class TimingHandler implements Timing {
+
+ private static AtomicInteger idPool = new AtomicInteger(1);
+ private static Deque<TimingHandler> TIMING_STACK = new ArrayDeque<>();
+ final int id = idPool.getAndIncrement();
+
+ final TimingIdentifier identifier;
+ private final boolean verbose;
+
+ private final Int2ObjectOpenHashMap<TimingData> children = new LoadingIntMap<>(TimingData::new);
+
+ final TimingData record;
+ private TimingHandler startParent;
+ private final TimingHandler groupHandler;
+
+ private long start = 0;
+ private int timingDepth = 0;
+ private boolean added;
+ private boolean timed;
+ private boolean enabled;
+
+ TimingHandler(@NotNull TimingIdentifier id) {
+ this.identifier = id;
+ this.verbose = id.name.startsWith("##");
+ this.record = new TimingData(this.id);
+ this.groupHandler = id.groupHandler;
+
+ TimingIdentifier.getGroup(id.group).handlers.add(this);
+ checkEnabled();
+ }
+
+ final void checkEnabled() {
+ enabled = Timings.timingsEnabled && (!verbose || Timings.verboseEnabled);
+ }
+
+ void processTick(boolean violated) {
+ if (timingDepth != 0 || record.getCurTickCount() == 0) {
+ timingDepth = 0;
+ start = 0;
+ return;
+ }
+
+ record.processTick(violated);
+ for (TimingData handler : children.values()) {
+ handler.processTick(violated);
+ }
+ }
+
+ @NotNull
+ @Override
+ public Timing startTimingIfSync() {
+ startTiming();
+ return this;
+ }
+
+ @Override
+ public void stopTimingIfSync() {
+ stopTiming();
+ }
+
+ @NotNull
+ public Timing startTiming() {
+ if (!enabled || !Bukkit.isPrimaryThread()) {
+ return this;
+ }
+ if (++timingDepth == 1) {
+ startParent = TIMING_STACK.peekLast();
+ start = System.nanoTime();
+ }
+ TIMING_STACK.addLast(this);
+ return this;
+ }
+
+ public void stopTiming() {
+ if (!enabled || timingDepth <= 0 || start == 0 || !Bukkit.isPrimaryThread()) {
+ return;
+ }
+
+ popTimingStack();
+ if (--timingDepth == 0) {
+ addDiff(System.nanoTime() - start, startParent);
+ startParent = null;
+ start = 0;
+ }
+ }
+
+ private void popTimingStack() {
+ TimingHandler last;
+ while ((last = TIMING_STACK.removeLast()) != this) {
+ last.timingDepth = 0;
+ if ("Minecraft".equalsIgnoreCase(last.identifier.group)) {
+ Logger.getGlobal().log(Level.SEVERE, "TIMING_STACK_CORRUPTION - Look above this for any errors and report this to Paper unless it has a plugin in the stack trace (" + last.identifier + " did not stopTiming)");
+ } else {
+ Logger.getGlobal().log(Level.SEVERE, "TIMING_STACK_CORRUPTION - Report this to the plugin " + last.identifier.group + " (Look for errors above this in the logs) (" + last.identifier + " did not stopTiming)", new Throwable());
+ }
+
+ boolean found = TIMING_STACK.contains(this);
+ if (!found) {
+ // We aren't even in the stack... Don't pop everything
+ TIMING_STACK.addLast(last);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public final void abort() {
+
+ }
+
+ void addDiff(long diff, @Nullable TimingHandler parent) {
+ if (parent != null) {
+ parent.children.get(id).add(diff);
+ }
+
+ record.add(diff);
+ if (!added) {
+ added = true;
+ timed = true;
+ TimingsManager.HANDLERS.add(this);
+ }
+ if (groupHandler != null) {
+ groupHandler.addDiff(diff, parent);
+ groupHandler.children.get(id).add(diff);
+ }
+ }
+
+ /**
+ * Reset this timer, setting all values to zero.
+ */
+ void reset(boolean full) {
+ record.reset();
+ if (full) {
+ timed = false;
+ }
+ start = 0;
+ timingDepth = 0;
+ added = false;
+ children.clear();
+ checkEnabled();
+ }
+
+ @NotNull
+ @Override
+ public TimingHandler getTimingHandler() {
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (this == o);
+ }
+
+ @Override
+ public int hashCode() {
+ return id;
+ }
+
+ /**
+ * This is simply for the Closeable interface so it can be used with try-with-resources ()
+ */
+ @Override
+ public void close() {
+ stopTimingIfSync();
+ }
+
+ public boolean isSpecial() {
+ return this == TimingsManager.FULL_SERVER_TICK || this == TimingsManager.TIMINGS_TICK;
+ }
+
+ boolean isTimed() {
+ return timed;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @NotNull
+ TimingData[] cloneChildren() {
+ final TimingData[] clonedChildren = new TimingData[children.size()];
+ int i = 0;
+ for (TimingData child : children.values()) {
+ clonedChildren[i++] = child.clone();
+ }
+ return clonedChildren;
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimingHistory.java b/src/main/java/co/aikar/timings/TimingHistory.java
new file mode 100644
index 0000000000000000000000000000000000000000..ddaed81275fcc12d1671b668697acf318e96888b
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingHistory.java
@@ -0,0 +1,354 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import co.aikar.timings.TimingHistory.RegionData.RegionId;
+import com.google.common.base.Function;
+import com.google.common.collect.Sets;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.BlockState;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import co.aikar.util.LoadingMap;
+import co.aikar.util.MRUMapCache;
+
+import java.lang.management.ManagementFactory;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static co.aikar.timings.TimingsManager.FULL_SERVER_TICK;
+import static co.aikar.timings.TimingsManager.MINUTE_REPORTS;
+import static co.aikar.util.JSONUtil.*;
+
+@SuppressWarnings({"deprecation", "SuppressionAnnotation", "Convert2Lambda", "Anonymous2MethodRef"})
+public class TimingHistory {
+ public static long lastMinuteTime;
+ public static long timedTicks;
+ public static long playerTicks;
+ public static long entityTicks;
+ public static long tileEntityTicks;
+ public static long activatedEntityTicks;
+ private static int worldIdPool = 1;
+ static Map<String, Integer> worldMap = LoadingMap.newHashMap(new Function<String, Integer>() {
+ @NotNull
+ @Override
+ public Integer apply(@Nullable String input) {
+ return worldIdPool++;
+ }
+ });
+ private final long endTime;
+ private final long startTime;
+ private final long totalTicks;
+ private final long totalTime; // Represents all time spent running the server this history
+ private final MinuteReport[] minuteReports;
+
+ private final TimingHistoryEntry[] entries;
+ final Set<Material> tileEntityTypeSet = Sets.newHashSet();
+ final Set<EntityType> entityTypeSet = Sets.newHashSet();
+ private final Map<Object, Object> worlds;
+
+ TimingHistory() {
+ this.endTime = System.currentTimeMillis() / 1000;
+ this.startTime = TimingsManager.historyStart / 1000;
+ if (timedTicks % 1200 != 0 || MINUTE_REPORTS.isEmpty()) {
+ this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[MINUTE_REPORTS.size() + 1]);
+ this.minuteReports[this.minuteReports.length - 1] = new MinuteReport();
+ } else {
+ this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[MINUTE_REPORTS.size()]);
+ }
+ long ticks = 0;
+ for (MinuteReport mp : this.minuteReports) {
+ ticks += mp.ticksRecord.timed;
+ }
+ this.totalTicks = ticks;
+ this.totalTime = FULL_SERVER_TICK.record.getTotalTime();
+ this.entries = new TimingHistoryEntry[TimingsManager.HANDLERS.size()];
+
+ int i = 0;
+ for (TimingHandler handler : TimingsManager.HANDLERS) {
+ entries[i++] = new TimingHistoryEntry(handler);
+ }
+
+ // Information about all loaded chunks/entities
+ //noinspection unchecked
+ this.worlds = toObjectMapper(Bukkit.getWorlds(), new Function<World, JSONPair>() {
+ @NotNull
+ @Override
+ public JSONPair apply(World world) {
+ Map<RegionId, RegionData> regions = LoadingMap.newHashMap(RegionData.LOADER);
+
+ for (Chunk chunk : world.getLoadedChunks()) {
+ RegionData data = regions.get(new RegionId(chunk.getX(), chunk.getZ()));
+
+ for (Entity entity : chunk.getEntities()) {
+ if (entity == null) {
+ Bukkit.getLogger().warning("Null entity detected in chunk at position x: " + chunk.getX() + ", z: " + chunk.getZ());
+ continue;
+ }
+
+ data.entityCounts.get(entity.getType()).increment();
+ }
+
+ for (BlockState tileEntity : chunk.getTileEntities()) {
+ if (tileEntity == null) {
+ Bukkit.getLogger().warning("Null tileentity detected in chunk at position x: " + chunk.getX() + ", z: " + chunk.getZ());
+ continue;
+ }
+
+ data.tileEntityCounts.get(tileEntity.getBlock().getType()).increment();
+ }
+ }
+ return pair(
+ worldMap.get(world.getName()),
+ toArrayMapper(regions.values(),new Function<RegionData, Object>() {
+ @NotNull
+ @Override
+ public Object apply(RegionData input) {
+ return toArray(
+ input.regionId.x,
+ input.regionId.z,
+ toObjectMapper(input.entityCounts.entrySet(),
+ new Function<Map.Entry<EntityType, Counter>, JSONPair>() {
+ @NotNull
+ @Override
+ public JSONPair apply(Map.Entry<EntityType, Counter> entry) {
+ entityTypeSet.add(entry.getKey());
+ return pair(
+ String.valueOf(entry.getKey().ordinal()),
+ entry.getValue().count()
+ );
+ }
+ }
+ ),
+ toObjectMapper(input.tileEntityCounts.entrySet(),
+ new Function<Map.Entry<Material, Counter>, JSONPair>() {
+ @NotNull
+ @Override
+ public JSONPair apply(Map.Entry<Material, Counter> entry) {
+ tileEntityTypeSet.add(entry.getKey());
+ return pair(
+ String.valueOf(entry.getKey().ordinal()),
+ entry.getValue().count()
+ );
+ }
+ }
+ )
+ );
+ }
+ })
+ );
+ }
+ });
+ }
+ static class RegionData {
+ final RegionId regionId;
+ @SuppressWarnings("Guava")
+ static Function<RegionId, RegionData> LOADER = new Function<RegionId, RegionData>() {
+ @NotNull
+ @Override
+ public RegionData apply(@NotNull RegionId id) {
+ return new RegionData(id);
+ }
+ };
+ RegionData(@NotNull RegionId id) {
+ this.regionId = id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ RegionData that = (RegionData) o;
+
+ return regionId.equals(that.regionId);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return regionId.hashCode();
+ }
+
+ @SuppressWarnings("unchecked")
+ final Map<EntityType, Counter> entityCounts = MRUMapCache.of(LoadingMap.of(
+ new EnumMap<EntityType, Counter>(EntityType.class), k -> new Counter()
+ ));
+ @SuppressWarnings("unchecked")
+ final Map<Material, Counter> tileEntityCounts = MRUMapCache.of(LoadingMap.of(
+ new EnumMap<Material, Counter>(Material.class), k -> new Counter()
+ ));
+
+ static class RegionId {
+ final int x, z;
+ final long regionId;
+ RegionId(int x, int z) {
+ this.x = x >> 5 << 5;
+ this.z = z >> 5 << 5;
+ this.regionId = ((long) (this.x) << 32) + (this.z >> 5 << 5) - Integer.MIN_VALUE;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RegionId regionId1 = (RegionId) o;
+
+ return regionId == regionId1.regionId;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (regionId ^ (regionId >>> 32));
+ }
+ }
+ }
+ static void resetTicks(boolean fullReset) {
+ if (fullReset) {
+ // Non full is simply for 1 minute reports
+ timedTicks = 0;
+ }
+ lastMinuteTime = System.nanoTime();
+ playerTicks = 0;
+ tileEntityTicks = 0;
+ entityTicks = 0;
+ activatedEntityTicks = 0;
+ }
+
+ @NotNull
+ Object export() {
+ return createObject(
+ pair("s", startTime),
+ pair("e", endTime),
+ pair("tk", totalTicks),
+ pair("tm", totalTime),
+ pair("w", worlds),
+ pair("h", toArrayMapper(entries, new Function<TimingHistoryEntry, Object>() {
+ @Nullable
+ @Override
+ public Object apply(TimingHistoryEntry entry) {
+ TimingData record = entry.data;
+ if (!record.hasData()) {
+ return null;
+ }
+ return entry.export();
+ }
+ })),
+ pair("mp", toArrayMapper(minuteReports, new Function<MinuteReport, Object>() {
+ @NotNull
+ @Override
+ public Object apply(MinuteReport input) {
+ return input.export();
+ }
+ }))
+ );
+ }
+
+ static class MinuteReport {
+ final long time = System.currentTimeMillis() / 1000;
+
+ final TicksRecord ticksRecord = new TicksRecord();
+ final PingRecord pingRecord = new PingRecord();
+ final TimingData fst = TimingsManager.FULL_SERVER_TICK.minuteData.clone();
+ final double tps = 1E9 / ( System.nanoTime() - lastMinuteTime ) * ticksRecord.timed;
+ final double usedMemory = TimingsManager.FULL_SERVER_TICK.avgUsedMemory;
+ final double freeMemory = TimingsManager.FULL_SERVER_TICK.avgFreeMemory;
+ final double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
+
+ @NotNull
+ List<Object> export() {
+ return toArray(
+ time,
+ Math.round(tps * 100D) / 100D,
+ Math.round(pingRecord.avg * 100D) / 100D,
+ fst.export(),
+ toArray(ticksRecord.timed,
+ ticksRecord.player,
+ ticksRecord.entity,
+ ticksRecord.activatedEntity,
+ ticksRecord.tileEntity
+ ),
+ usedMemory,
+ freeMemory,
+ loadAvg
+ );
+ }
+ }
+
+ private static class TicksRecord {
+ final long timed;
+ final long player;
+ final long entity;
+ final long tileEntity;
+ final long activatedEntity;
+
+ TicksRecord() {
+ timed = timedTicks - (TimingsManager.MINUTE_REPORTS.size() * 1200);
+ player = playerTicks;
+ entity = entityTicks;
+ tileEntity = tileEntityTicks;
+ activatedEntity = activatedEntityTicks;
+ }
+
+ }
+
+ private static class PingRecord {
+ final double avg;
+
+ PingRecord() {
+ final Collection<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers();
+ int totalPing = 0;
+ for (Player player : onlinePlayers) {
+ totalPing += player.spigot().getPing();
+ }
+ avg = onlinePlayers.isEmpty() ? 0 : totalPing / onlinePlayers.size();
+ }
+ }
+
+
+ private static class Counter {
+ private int count = 0;
+ public int increment() {
+ return ++count;
+ }
+ public int count() {
+ return count;
+ }
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimingHistoryEntry.java b/src/main/java/co/aikar/timings/TimingHistoryEntry.java
new file mode 100644
index 0000000000000000000000000000000000000000..86d5ac6bd0d7d0003688761aceb3f3343575319f
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingHistoryEntry.java
@@ -0,0 +1,58 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import com.google.common.base.Function;
+
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+
+import static co.aikar.util.JSONUtil.toArrayMapper;
+
+class TimingHistoryEntry {
+ final TimingData data;
+ private final TimingData[] children;
+
+ TimingHistoryEntry(@NotNull TimingHandler handler) {
+ this.data = handler.record.clone();
+ children = handler.cloneChildren();
+ }
+
+ @NotNull
+ List<Object> export() {
+ List<Object> result = data.export();
+ if (children.length > 0) {
+ result.add(
+ toArrayMapper(children, new Function<TimingData, Object>() {
+ @NotNull
+ @Override
+ public Object apply(TimingData child) {
+ return child.export();
+ }
+ })
+ );
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimingIdentifier.java b/src/main/java/co/aikar/timings/TimingIdentifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..df142a89b8c43acb81eb383eac0ef048a1f49a6e
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingIdentifier.java
@@ -0,0 +1,116 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import co.aikar.util.LoadingMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * <p>Used as a basis for fast HashMap key comparisons for the Timing Map.</p>
+ *
+ * This class uses interned strings giving us the ability to do an identity check instead of equals() on the strings
+ */
+final class TimingIdentifier {
+ /**
+ * Holds all groups. Autoloads on request for a group by name.
+ */
+ static final Map<String, TimingGroup> GROUP_MAP = LoadingMap.of(new ConcurrentHashMap<>(64, .5F), TimingGroup::new);
+ private static final TimingGroup DEFAULT_GROUP = getGroup("Minecraft");
+ final String group;
+ final String name;
+ final TimingHandler groupHandler;
+ private final int hashCode;
+
+ TimingIdentifier(@Nullable String group, @NotNull String name, @Nullable Timing groupHandler) {
+ this.group = group != null ? group: DEFAULT_GROUP.name;
+ this.name = name;
+ this.groupHandler = groupHandler != null ? groupHandler.getTimingHandler() : null;
+ this.hashCode = (31 * this.group.hashCode()) + this.name.hashCode();
+ }
+
+ @NotNull
+ static TimingGroup getGroup(@Nullable String groupName) {
+ if (groupName == null) {
+ //noinspection ConstantConditions
+ return DEFAULT_GROUP;
+ }
+
+ return GROUP_MAP.get(groupName);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) {
+ return false;
+ }
+
+ TimingIdentifier that = (TimingIdentifier) o;
+ return Objects.equals(group, that.group) && Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return "TimingIdentifier{id=" + group + ":" + name +'}';
+ }
+
+ static class TimingGroup {
+
+ private static AtomicInteger idPool = new AtomicInteger(1);
+ final int id = idPool.getAndIncrement();
+
+ final String name;
+ final List<TimingHandler> handlers = Collections.synchronizedList(new ArrayList<>(64));
+
+ private TimingGroup(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TimingGroup that = (TimingGroup) o;
+ return id == that.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return id;
+ }
+ }
+}
diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java
new file mode 100644
index 0000000000000000000000000000000000000000..da76e1aaee1dee794e38ddd4e0a28e0071e90bbf
--- /dev/null
+++ b/src/main/java/co/aikar/timings/Timings.java
@@ -0,0 +1,296 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.EvictingQueue;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.Validate;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.plugin.Plugin;
+
+import java.util.List;
+import java.util.Queue;
+import java.util.logging.Level;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@SuppressWarnings({"UnusedDeclaration", "WeakerAccess", "SameParameterValue"})
+public final class Timings {
+
+ final static List<CommandSender> requestingReport = Lists.newArrayList();
+ private static final int MAX_HISTORY_FRAMES = 12;
+ public static final Timing NULL_HANDLER = new NullTimingHandler();
+ static boolean timingsEnabled = false;
+ static boolean verboseEnabled = false;
+ private static int historyInterval = -1;
+ private static int historyLength = -1;
+
+ private Timings() {}
+
+ /**
+ * Returns a Timing for a plugin corresponding to a name.
+ *
+ * @param plugin Plugin to own the Timing
+ * @param name Name of Timing
+ * @return Handler
+ */
+ @NotNull
+ public static Timing of(@NotNull Plugin plugin, @NotNull String name) {
+ Timing pluginHandler = null;
+ if (plugin != null) {
+ pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER);
+ }
+ return of(plugin, name, pluginHandler);
+ }
+
+ /**
+ * <p>Returns a handler that has a groupHandler timer handler. Parent timers should not have their
+ * start/stop methods called directly, as the children will call it for you.</p>
+ *
+ * Parent Timers are used to group multiple subsections together and get a summary of them combined
+ * Parent Handler can not be changed after first call
+ *
+ * @param plugin Plugin to own the Timing
+ * @param name Name of Timing
+ * @param groupHandler Parent handler to mirror .start/stop calls to
+ * @return Timing Handler
+ */
+ @NotNull
+ public static Timing of(@NotNull Plugin plugin, @NotNull String name, @Nullable Timing groupHandler) {
+ Preconditions.checkNotNull(plugin, "Plugin can not be null");
+ return TimingsManager.getHandler(plugin.getName(), name, groupHandler);
+ }
+
+ /**
+ * Returns a Timing object after starting it, useful for Java7 try-with-resources.
+ *
+ * try (Timing ignored = Timings.ofStart(plugin, someName)) {
+ * // timed section
+ * }
+ *
+ * @param plugin Plugin to own the Timing
+ * @param name Name of Timing
+ * @return Timing Handler
+ */
+ @NotNull
+ public static Timing ofStart(@NotNull Plugin plugin, @NotNull String name) {
+ return ofStart(plugin, name, null);
+ }
+
+ /**
+ * Returns a Timing object after starting it, useful for Java7 try-with-resources.
+ *
+ * try (Timing ignored = Timings.ofStart(plugin, someName, groupHandler)) {
+ * // timed section
+ * }
+ *
+ * @param plugin Plugin to own the Timing
+ * @param name Name of Timing
+ * @param groupHandler Parent handler to mirror .start/stop calls to
+ * @return Timing Handler
+ */
+ @NotNull
+ public static Timing ofStart(@NotNull Plugin plugin, @NotNull String name, @Nullable Timing groupHandler) {
+ Timing timing = of(plugin, name, groupHandler);
+ timing.startTiming();
+ return timing;
+ }
+
+ /**
+ * Gets whether or not the Spigot Timings system is enabled
+ *
+ * @return Enabled or not
+ */
+ public static boolean isTimingsEnabled() {
+ return timingsEnabled;
+ }
+
+ /**
+ * <p>Sets whether or not the Spigot Timings system should be enabled</p>
+ *
+ * Calling this will reset timing data.
+ *
+ * @param enabled Should timings be reported
+ */
+ public static void setTimingsEnabled(boolean enabled) {
+ timingsEnabled = enabled;
+ reset();
+ }
+
+ /**
+ * <p>Sets whether or not the Timings should monitor at Verbose level.</p>
+ *
+ * <p>When Verbose is disabled, high-frequency timings will not be available.</p>
+ *
+ * @return Enabled or not
+ */
+ public static boolean isVerboseTimingsEnabled() {
+ return verboseEnabled;
+ }
+
+ /**
+ * <p>Sets whether or not the Timings should monitor at Verbose level.</p>
+ *
+ * When Verbose is disabled, high-frequency timings will not be available.
+ * Calling this will reset timing data.
+ *
+ * @param enabled Should high-frequency timings be reported
+ */
+ public static void setVerboseTimingsEnabled(boolean enabled) {
+ verboseEnabled = enabled;
+ TimingsManager.needsRecheckEnabled = true;
+ }
+
+ /**
+ * <p>Gets the interval between Timing History report generation.</p>
+ *
+ * Defaults to 5 minutes (6000 ticks)
+ *
+ * @return Interval in ticks
+ */
+ public static int getHistoryInterval() {
+ return historyInterval;
+ }
+
+ /**
+ * <p>Sets the interval between Timing History report generations.</p>
+ *
+ * <p>Defaults to 5 minutes (6000 ticks)</p>
+ *
+ * This will recheck your history length, so lowering this value will lower your
+ * history length if you need more than 60 history windows.
+ *
+ * @param interval Interval in ticks
+ */
+ public static void setHistoryInterval(int interval) {
+ historyInterval = Math.max(20*60, interval);
+ // Recheck the history length with the new Interval
+ if (historyLength != -1) {
+ setHistoryLength(historyLength);
+ }
+ }
+
+ /**
+ * Gets how long in ticks Timings history is kept for the server.
+ *
+ * Defaults to 1 hour (72000 ticks)
+ *
+ * @return Duration in Ticks
+ */
+ public static int getHistoryLength() {
+ return historyLength;
+ }
+
+ /**
+ * Sets how long Timing History reports are kept for the server.
+ *
+ * Defaults to 1 hours(72000 ticks)
+ *
+ * This value is capped at a maximum of getHistoryInterval() * MAX_HISTORY_FRAMES (12)
+ *
+ * Will not reset Timing Data but may truncate old history if the new length is less than old length.
+ *
+ * @param length Duration in ticks
+ */
+ public static void setHistoryLength(int length) {
+ // Cap at 12 History Frames, 1 hour at 5 minute frames.
+ int maxLength = historyInterval * MAX_HISTORY_FRAMES;
+ // For special cases of servers with special permission to bypass the max.
+ // This max helps keep data file sizes reasonable for processing on Aikar's Timing parser side.
+ // Setting this will not help you bypass the max unless Aikar has added an exception on the API side.
+ if (System.getProperty("timings.bypassMax") != null) {
+ maxLength = Integer.MAX_VALUE;
+ }
+ historyLength = Math.max(Math.min(maxLength, length), historyInterval);
+ Queue<TimingHistory> oldQueue = TimingsManager.HISTORY;
+ int frames = (getHistoryLength() / getHistoryInterval());
+ if (length > maxLength) {
+ Bukkit.getLogger().log(Level.WARNING, "Timings Length too high. Requested " + length + ", max is " + maxLength + ". To get longer history, you must increase your interval. Set Interval to " + Math.ceil(length / MAX_HISTORY_FRAMES) + " to achieve this length.");
+ }
+ TimingsManager.HISTORY = EvictingQueue.create(frames);
+ TimingsManager.HISTORY.addAll(oldQueue);
+ }
+
+ /**
+ * Resets all Timing Data
+ */
+ public static void reset() {
+ TimingsManager.reset();
+ }
+
+ /**
+ * Generates a report and sends it to the specified command sender.
+ *
+ * If sender is null, ConsoleCommandSender will be used.
+ * @param sender The sender to send to, or null to use the ConsoleCommandSender
+ */
+ public static void generateReport(@Nullable CommandSender sender) {
+ if (sender == null) {
+ sender = Bukkit.getConsoleSender();
+ }
+ requestingReport.add(sender);
+ }
+
+ /**
+ * Generates a report and sends it to the specified listener.
+ * Use with {@link org.bukkit.command.BufferedCommandSender} to get full response when done!
+ * @param sender The listener to send responses too.
+ */
+ public static void generateReport(@NotNull TimingsReportListener sender) {
+ Validate.notNull(sender);
+ requestingReport.add(sender);
+ }
+
+ /*
+ =================
+ Protected API: These are for internal use only in Bukkit/CraftBukkit
+ These do not have isPrimaryThread() checks in the startTiming/stopTiming
+ =================
+ */
+ @NotNull
+ static TimingHandler ofSafe(@NotNull String name) {
+ return ofSafe(null, name, null);
+ }
+
+ @NotNull
+ static Timing ofSafe(@Nullable Plugin plugin, @NotNull String name) {
+ Timing pluginHandler = null;
+ if (plugin != null) {
+ pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER);
+ }
+ return ofSafe(plugin != null ? plugin.getName() : "Minecraft - Invalid Plugin", name, pluginHandler);
+ }
+
+ @NotNull
+ static TimingHandler ofSafe(@NotNull String name, @Nullable Timing groupHandler) {
+ return ofSafe(null, name, groupHandler);
+ }
+
+ @NotNull
+ static TimingHandler ofSafe(@Nullable String groupName, @NotNull String name, @Nullable Timing groupHandler) {
+ return TimingsManager.getHandler(groupName, name, groupHandler);
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimingsCommand.java b/src/main/java/co/aikar/timings/TimingsCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7c2245a310a084367ff25db539b3c967d5cb141
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingsCommand.java
@@ -0,0 +1,119 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.commons.lang.Validate;
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.defaults.BukkitCommand;
+import org.bukkit.util.StringUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+
+public class TimingsCommand extends BukkitCommand {
+ private static final List<String> TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste", "verbon", "verboff");
+ private long lastResetAttempt = 0;
+
+ public TimingsCommand(@NotNull String name) {
+ super(name);
+ this.description = "Manages Spigot Timings data to see performance of the server.";
+ this.usageMessage = "/timings <reset|report|on|off|verbon|verboff>";
+ this.setPermission("bukkit.command.timings");
+ }
+
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) {
+ if (!testPermission(sender)) {
+ return true;
+ }
+ if (args.length < 1) {
+ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage);
+ return true;
+ }
+ final String arg = args[0];
+ if ("on".equalsIgnoreCase(arg)) {
+ Timings.setTimingsEnabled(true);
+ sender.sendMessage("Enabled Timings & Reset");
+ return true;
+ } else if ("off".equalsIgnoreCase(arg)) {
+ Timings.setTimingsEnabled(false);
+ sender.sendMessage("Disabled Timings");
+ return true;
+ }
+
+ if (!Timings.isTimingsEnabled()) {
+ sender.sendMessage("Please enable timings by typing /timings on");
+ return true;
+ }
+
+ long now = System.currentTimeMillis();
+ if ("verbon".equalsIgnoreCase(arg)) {
+ Timings.setVerboseTimingsEnabled(true);
+ sender.sendMessage("Enabled Verbose Timings");
+ return true;
+ } else if ("verboff".equalsIgnoreCase(arg)) {
+ Timings.setVerboseTimingsEnabled(false);
+ sender.sendMessage("Disabled Verbose Timings");
+ return true;
+ } else if ("reset".equalsIgnoreCase(arg)) {
+ if (now - lastResetAttempt < 30000) {
+ TimingsManager.reset();
+ sender.sendMessage(ChatColor.RED + "Timings reset. Please wait 5-10 minutes before using /timings report.");
+ } else {
+ lastResetAttempt = now;
+ sender.sendMessage(ChatColor.RED + "WARNING: Timings v2 should not be reset. If you are encountering lag, please wait 3 minutes and then issue a report. The best timings will include 10+ minutes, with data before and after your lag period. If you really want to reset, run this command again within 30 seconds.");
+ }
+ } else if (
+ "paste".equalsIgnoreCase(arg) ||
+ "report".equalsIgnoreCase(arg) ||
+ "get".equalsIgnoreCase(arg) ||
+ "merged".equalsIgnoreCase(arg) ||
+ "separate".equalsIgnoreCase(arg)
+ ) {
+ Timings.generateReport(sender);
+ } else {
+ sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage);
+ }
+ return true;
+ }
+
+ @NotNull
+ @Override
+ public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) {
+ Validate.notNull(sender, "Sender cannot be null");
+ Validate.notNull(args, "Arguments cannot be null");
+ Validate.notNull(alias, "Alias cannot be null");
+
+ if (args.length == 1) {
+ return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS,
+ new ArrayList<String>(TIMINGS_SUBCOMMANDS.size()));
+ }
+ return ImmutableList.of();
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimingsManager.java b/src/main/java/co/aikar/timings/TimingsManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef824d701c97cad8b31e76ad98c94fc4367a7eda
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingsManager.java
@@ -0,0 +1,188 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import co.aikar.util.LoadingMap;
+import com.google.common.collect.EvictingQueue;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.bukkit.command.Command;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.PluginClassLoader;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class TimingsManager {
+ static final Map<TimingIdentifier, TimingHandler> TIMING_MAP = LoadingMap.of(
+ new ConcurrentHashMap<>(4096, .5F), TimingHandler::new
+ );
+ public static final FullServerTickHandler FULL_SERVER_TICK = new FullServerTickHandler();
+ public static final TimingHandler TIMINGS_TICK = Timings.ofSafe("Timings Tick", FULL_SERVER_TICK);
+ public static final Timing PLUGIN_GROUP_HANDLER = Timings.ofSafe("Plugins");
+ public static List<String> hiddenConfigs = new ArrayList<String>();
+ public static boolean privacy = false;
+
+ static final List<TimingHandler> HANDLERS = new ArrayList<>(1024);
+ static final List<TimingHistory.MinuteReport> MINUTE_REPORTS = new ArrayList<>(64);
+
+ static EvictingQueue<TimingHistory> HISTORY = EvictingQueue.create(12);
+ static long timingStart = 0;
+ static long historyStart = 0;
+ static boolean needsFullReset = false;
+ static boolean needsRecheckEnabled = false;
+
+ private TimingsManager() {}
+
+ /**
+ * Resets all timing data on the next tick
+ */
+ static void reset() {
+ needsFullReset = true;
+ }
+
+ /**
+ * Ticked every tick by CraftBukkit to count the number of times a timer
+ * caused TPS loss.
+ */
+ static void tick() {
+ if (Timings.timingsEnabled) {
+ boolean violated = FULL_SERVER_TICK.isViolated();
+
+ for (TimingHandler handler : HANDLERS) {
+ if (handler.isSpecial()) {
+ // We manually call this
+ continue;
+ }
+ handler.processTick(violated);
+ }
+
+ TimingHistory.playerTicks += Bukkit.getOnlinePlayers().size();
+ TimingHistory.timedTicks++;
+ // Generate TPS/Ping/Tick reports every minute
+ }
+ }
+ static void stopServer() {
+ Timings.timingsEnabled = false;
+ recheckEnabled();
+ }
+ static void recheckEnabled() {
+ synchronized (TIMING_MAP) {
+ for (TimingHandler timings : TIMING_MAP.values()) {
+ timings.checkEnabled();
+ }
+ }
+ needsRecheckEnabled = false;
+ }
+ static void resetTimings() {
+ if (needsFullReset) {
+ // Full resets need to re-check every handlers enabled state
+ // Timing map can be modified from async so we must sync on it.
+ synchronized (TIMING_MAP) {
+ for (TimingHandler timings : TIMING_MAP.values()) {
+ timings.reset(true);
+ }
+ }
+ Bukkit.getLogger().log(Level.INFO, "Timings Reset");
+ HISTORY.clear();
+ needsFullReset = false;
+ needsRecheckEnabled = false;
+ timingStart = System.currentTimeMillis();
+ } else {
+ // Soft resets only need to act on timings that have done something
+ // Handlers can only be modified on main thread.
+ for (TimingHandler timings : HANDLERS) {
+ timings.reset(false);
+ }
+ }
+
+ HANDLERS.clear();
+ MINUTE_REPORTS.clear();
+
+ TimingHistory.resetTicks(true);
+ historyStart = System.currentTimeMillis();
+ }
+
+ @NotNull
+ static TimingHandler getHandler(@Nullable String group, @NotNull String name, @Nullable Timing parent) {
+ return TIMING_MAP.get(new TimingIdentifier(group, name, parent));
+ }
+
+
+ /**
+ * <p>Due to access restrictions, we need a helper method to get a Command TimingHandler with String group</p>
+ *
+ * Plugins should never call this
+ *
+ * @param pluginName Plugin this command is associated with
+ * @param command Command to get timings for
+ * @return TimingHandler
+ */
+ @NotNull
+ public static Timing getCommandTiming(@Nullable String pluginName, @NotNull Command command) {
+ Plugin plugin = null;
+ final Server server = Bukkit.getServer();
+ if (!( server == null || pluginName == null ||
+ "minecraft".equals(pluginName) || "bukkit".equals(pluginName) ||
+ "spigot".equalsIgnoreCase(pluginName) || "paper".equals(pluginName)
+ )) {
+ plugin = server.getPluginManager().getPlugin(pluginName);
+ }
+ if (plugin == null) {
+ // Plugin is passing custom fallback prefix, try to look up by class loader
+ plugin = getPluginByClassloader(command.getClass());
+ }
+ if (plugin == null) {
+ return Timings.ofSafe("Command: " + pluginName + ":" + command.getTimingName());
+ }
+
+ return Timings.ofSafe(plugin, "Command: " + pluginName + ":" + command.getTimingName());
+ }
+
+ /**
+ * Looks up the class loader for the specified class, and if it is a PluginClassLoader, return the
+ * Plugin that created this class.
+ *
+ * @param clazz Class to check
+ * @return Plugin if created by a plugin
+ */
+ @Nullable
+ public static Plugin getPluginByClassloader(@Nullable Class<?> clazz) {
+ if (clazz == null) {
+ return null;
+ }
+ final ClassLoader classLoader = clazz.getClassLoader();
+ if (classLoader instanceof PluginClassLoader) {
+ PluginClassLoader pluginClassLoader = (PluginClassLoader) classLoader;
+ return pluginClassLoader.getPlugin();
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/co/aikar/timings/TimingsReportListener.java b/src/main/java/co/aikar/timings/TimingsReportListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf3e059fe06aae361b2ded451914ed19b5e970c5
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingsReportListener.java
@@ -0,0 +1,75 @@
+package co.aikar.timings;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.Validate;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.ConsoleCommandSender;
+import org.bukkit.command.MessageCommandSender;
+import org.bukkit.command.RemoteConsoleCommandSender;
+
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@SuppressWarnings("WeakerAccess")
+public class TimingsReportListener implements MessageCommandSender {
+ private final List<CommandSender> senders;
+ private final Runnable onDone;
+ private String timingsURL;
+
+ public TimingsReportListener(@NotNull CommandSender senders) {
+ this(senders, null);
+ }
+ public TimingsReportListener(@NotNull CommandSender sender, @Nullable Runnable onDone) {
+ this(Lists.newArrayList(sender), onDone);
+ }
+ public TimingsReportListener(@NotNull List<CommandSender> senders) {
+ this(senders, null);
+ }
+ public TimingsReportListener(@NotNull List<CommandSender> senders, @Nullable Runnable onDone) {
+ Validate.notNull(senders);
+ Validate.notEmpty(senders);
+
+ this.senders = Lists.newArrayList(senders);
+ this.onDone = onDone;
+ }
+
+ @Nullable
+ public String getTimingsURL() {
+ return timingsURL;
+ }
+
+ public void done() {
+ done(null);
+ }
+
+ public void done(@Nullable String url) {
+ this.timingsURL = url;
+ if (onDone != null) {
+ onDone.run();
+ }
+ for (CommandSender sender : senders) {
+ if (sender instanceof TimingsReportListener) {
+ ((TimingsReportListener) sender).done();
+ }
+ }
+ }
+
+ @Override
+ public void sendMessage(@NotNull String message) {
+ senders.forEach((sender) -> sender.sendMessage(message));
+ }
+
+ public void addConsoleIfNeeded() {
+ boolean hasConsole = false;
+ for (CommandSender sender : this.senders) {
+ if (sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) {
+ hasConsole = true;
+ }
+ }
+ if (!hasConsole) {
+ this.senders.add(Bukkit.getConsoleSender());
+ }
+ }
+}
diff --git a/src/main/java/co/aikar/timings/UnsafeTimingHandler.java b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..632c4961515f5052551f841cfa840e60bba7a257
--- /dev/null
+++ b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java
@@ -0,0 +1,53 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import org.bukkit.Bukkit;
+import org.jetbrains.annotations.NotNull;
+
+class UnsafeTimingHandler extends TimingHandler {
+
+ UnsafeTimingHandler(@NotNull TimingIdentifier id) {
+ super(id);
+ }
+
+ private static void checkThread() {
+ if (!Bukkit.isPrimaryThread()) {
+ throw new IllegalStateException("Calling Timings from Async Operation");
+ }
+ }
+
+ @NotNull
+ @Override
+ public Timing startTiming() {
+ checkThread();
+ return super.startTiming();
+ }
+
+ @Override
+ public void stopTiming() {
+ checkThread();
+ super.stopTiming();
+ }
+}
diff --git a/src/main/java/co/aikar/util/Counter.java b/src/main/java/co/aikar/util/Counter.java
new file mode 100644
index 0000000000000000000000000000000000000000..80155072d1004e34e04342d434cf7d75f0b7e29d
--- /dev/null
+++ b/src/main/java/co/aikar/util/Counter.java
@@ -0,0 +1,38 @@
+package co.aikar.util;
+
+import com.google.common.collect.ForwardingMap;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class Counter <T> extends ForwardingMap<T, Long> {
+ private final Map<T, Long> counts = new HashMap<>();
+
+ public long decrement(@Nullable T key) {
+ return increment(key, -1);
+ }
+ public long increment(@Nullable T key) {
+ return increment(key, 1);
+ }
+ public long decrement(@Nullable T key, long amount) {
+ return decrement(key, -amount);
+ }
+ public long increment(@Nullable T key, long amount) {
+ Long count = this.getCount(key);
+ count += amount;
+ this.counts.put(key, count);
+ return count;
+ }
+
+ public long getCount(@Nullable T key) {
+ return this.counts.getOrDefault(key, 0L);
+ }
+
+ @NotNull
+ @Override
+ protected Map<T, Long> delegate() {
+ return this.counts;
+ }
+}
diff --git a/src/main/java/co/aikar/util/JSONUtil.java b/src/main/java/co/aikar/util/JSONUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..190bf0598442c89c2a1c93ad7c8c1a29797304ae
--- /dev/null
+++ b/src/main/java/co/aikar/util/JSONUtil.java
@@ -0,0 +1,140 @@
+package co.aikar.util;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides Utility methods that assist with generating JSON Objects
+ */
+@SuppressWarnings({"rawtypes", "SuppressionAnnotation"})
+public final class JSONUtil {
+ private JSONUtil() {}
+
+ /**
+ * Creates a key/value "JSONPair" object
+ *
+ * @param key Key to use
+ * @param obj Value to use
+ * @return JSONPair
+ */
+ @NotNull
+ public static JSONPair pair(@NotNull String key, @Nullable Object obj) {
+ return new JSONPair(key, obj);
+ }
+
+ @NotNull
+ public static JSONPair pair(long key, @Nullable Object obj) {
+ return new JSONPair(String.valueOf(key), obj);
+ }
+
+ /**
+ * Creates a new JSON object from multiple JSONPair key/value pairs
+ *
+ * @param data JSONPairs
+ * @return Map
+ */
+ @NotNull
+ public static Map<String, Object> createObject(@NotNull JSONPair... data) {
+ return appendObjectData(new LinkedHashMap(), data);
+ }
+
+ /**
+ * This appends multiple key/value Obj pairs into a JSON Object
+ *
+ * @param parent Map to be appended to
+ * @param data Data to append
+ * @return Map
+ */
+ @NotNull
+ public static Map<String, Object> appendObjectData(@NotNull Map parent, @NotNull JSONPair... data) {
+ for (JSONPair JSONPair : data) {
+ parent.put(JSONPair.key, JSONPair.val);
+ }
+ return parent;
+ }
+
+ /**
+ * This builds a JSON array from a set of data
+ *
+ * @param data Data to build JSON array from
+ * @return List
+ */
+ @NotNull
+ public static List toArray(@NotNull Object... data) {
+ return Lists.newArrayList(data);
+ }
+
+ /**
+ * These help build a single JSON array using a mapper function
+ *
+ * @param collection Collection to apply to
+ * @param mapper Mapper to apply
+ * @param <E> Element Type
+ * @return List
+ */
+ @NotNull
+ public static <E> List toArrayMapper(@NotNull E[] collection, @NotNull Function<E, Object> mapper) {
+ return toArrayMapper(Lists.newArrayList(collection), mapper);
+ }
+
+ @NotNull
+ public static <E> List toArrayMapper(@NotNull Iterable<E> collection, @NotNull Function<E, Object> mapper) {
+ List array = Lists.newArrayList();
+ for (E e : collection) {
+ Object object = mapper.apply(e);
+ if (object != null) {
+ array.add(object);
+ }
+ }
+ return array;
+ }
+
+ /**
+ * These help build a single JSON Object from a collection, using a mapper function
+ *
+ * @param collection Collection to apply to
+ * @param mapper Mapper to apply
+ * @param <E> Element Type
+ * @return Map
+ */
+ @NotNull
+ public static <E> Map toObjectMapper(@NotNull E[] collection, @NotNull Function<E, JSONPair> mapper) {
+ return toObjectMapper(Lists.newArrayList(collection), mapper);
+ }
+
+ @NotNull
+ public static <E> Map toObjectMapper(@NotNull Iterable<E> collection, @NotNull Function<E, JSONPair> mapper) {
+ Map object = Maps.newLinkedHashMap();
+ for (E e : collection) {
+ JSONPair JSONPair = mapper.apply(e);
+ if (JSONPair != null) {
+ object.put(JSONPair.key, JSONPair.val);
+ }
+ }
+ return object;
+ }
+
+ /**
+ * Simply stores a key and a value, used internally by many methods below.
+ */
+ @SuppressWarnings("PublicInnerClass")
+ public static class JSONPair {
+ final String key;
+ final Object val;
+
+ JSONPair(@NotNull String key, @NotNull Object val) {
+ this.key = key;
+ this.val = val;
+ }
+ }
+}
diff --git a/src/main/java/co/aikar/util/LoadingIntMap.java b/src/main/java/co/aikar/util/LoadingIntMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..63a899c7dbdb69daa4876a2ce2a7dfb734b5af9d
--- /dev/null
+++ b/src/main/java/co/aikar/util/LoadingIntMap.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2015. Starlis LLC / dba Empire Minecraft
+ *
+ * This source code is proprietary software and must not be redistributed without Starlis LLC's approval
+ *
+ */
+package co.aikar.util;
+
+
+import com.google.common.base.Function;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Allows you to pass a Loader function that when a key is accessed that doesn't exist,
+ * automatically loads the entry into the map by calling the loader Function.
+ *
+ * .get() Will only return null if the Loader can return null.
+ *
+ * You may pass any backing Map to use.
+ *
+ * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed.
+ *
+ * Do not wrap the backing map with Collections.synchronizedMap.
+ *
+ * @param <V> Value
+ */
+public class LoadingIntMap<V> extends Int2ObjectOpenHashMap<V> {
+ private final Function<Integer, V> loader;
+
+ public LoadingIntMap(@NotNull Function<Integer, V> loader) {
+ super();
+ this.loader = loader;
+ }
+
+ public LoadingIntMap(int expectedSize, @NotNull Function<Integer, V> loader) {
+ super(expectedSize);
+ this.loader = loader;
+ }
+
+ public LoadingIntMap(int expectedSize, float loadFactor, @NotNull Function<Integer, V> loader) {
+ super(expectedSize, loadFactor);
+ this.loader = loader;
+ }
+
+
+ @Nullable
+ @Override
+ public V get(int key) {
+ V res = super.get(key);
+ if (res == null) {
+ res = loader.apply(key);
+ if (res != null) {
+ put(key, res);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Due to java stuff, you will need to cast it to (Function) for some cases
+ *
+ * @param <T> Type
+ */
+ public abstract static class Feeder <T> implements Function<T, T> {
+ @Nullable
+ @Override
+ public T apply(@Nullable Object input) {
+ return apply();
+ }
+
+ @Nullable
+ public abstract T apply();
+ }
+}
diff --git a/src/main/java/co/aikar/util/LoadingMap.java b/src/main/java/co/aikar/util/LoadingMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..aedbb03321886cb267879d7994653e447b485f6a
--- /dev/null
+++ b/src/main/java/co/aikar/util/LoadingMap.java
@@ -0,0 +1,368 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.util;
+
+import com.google.common.base.Preconditions;
+import java.lang.reflect.Constructor;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Allows you to pass a Loader function that when a key is accessed that doesn't exists,
+ * automatically loads the entry into the map by calling the loader Function.
+ *
+ * .get() Will only return null if the Loader can return null.
+ *
+ * You may pass any backing Map to use.
+ *
+ * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed.
+ *
+ * Do not wrap the backing map with Collections.synchronizedMap.
+ *
+ * @param <K> Key
+ * @param <V> Value
+ */
+public class LoadingMap <K, V> extends AbstractMap<K, V> {
+ private final Map<K, V> backingMap;
+ private final java.util.function.Function<K, V> loader;
+
+ /**
+ * Initializes an auto loading map using specified loader and backing map
+ * @param backingMap Map to wrap
+ * @param loader Loader
+ */
+ public LoadingMap(@NotNull Map<K, V> backingMap, @NotNull java.util.function.Function<K, V> loader) {
+ this.backingMap = backingMap;
+ this.loader = loader;
+ }
+
+ /**
+ * Creates a new LoadingMap with the specified map and loader
+ *
+ * @param backingMap Actual map being used.
+ * @param loader Loader to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map
+ */
+ @NotNull
+ public static <K, V> Map<K, V> of(@NotNull Map<K, V> backingMap, @NotNull Function<K, V> loader) {
+ return new LoadingMap<>(backingMap, loader);
+ }
+
+ /**
+ * Creates a LoadingMap with an auto instantiating loader.
+ *
+ * Will auto construct class of of Value when not found
+ *
+ * Since this uses Reflection, It is more effecient to define your own static loader
+ * than using this helper, but if performance is not critical, this is easier.
+ *
+ * @param backingMap Actual map being used.
+ * @param keyClass Class used for the K generic
+ * @param valueClass Class used for the V generic
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map that auto instantiates on .get()
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newAutoMap(@NotNull Map<K, V> backingMap, @Nullable final Class<? extends K> keyClass,
+ @NotNull final Class<? extends V> valueClass) {
+ return new LoadingMap<>(backingMap, new AutoInstantiatingLoader<>(keyClass, valueClass));
+ }
+ /**
+ * Creates a LoadingMap with an auto instantiating loader.
+ *
+ * Will auto construct class of of Value when not found
+ *
+ * Since this uses Reflection, It is more effecient to define your own static loader
+ * than using this helper, but if performance is not critical, this is easier.
+ *
+ * @param backingMap Actual map being used.
+ * @param valueClass Class used for the V generic
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map that auto instantiates on .get()
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newAutoMap(@NotNull Map<K, V> backingMap,
+ @NotNull final Class<? extends V> valueClass) {
+ return newAutoMap(backingMap, null, valueClass);
+ }
+
+ /**
+ * @see #newAutoMap
+ *
+ * new Auto initializing map using a HashMap.
+ *
+ * @param keyClass Class used for the K generic
+ * @param valueClass Class used for the V generic
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map that auto instantiates on .get()
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newHashAutoMap(@Nullable final Class<? extends K> keyClass, @NotNull final Class<? extends V> valueClass) {
+ return newAutoMap(new HashMap<>(), keyClass, valueClass);
+ }
+
+ /**
+ * @see #newAutoMap
+ *
+ * new Auto initializing map using a HashMap.
+ *
+ * @param valueClass Class used for the V generic
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map that auto instantiates on .get()
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newHashAutoMap(@NotNull final Class<? extends V> valueClass) {
+ return newHashAutoMap(null, valueClass);
+ }
+
+ /**
+ * @see #newAutoMap
+ *
+ * new Auto initializing map using a HashMap.
+ *
+ * @param keyClass Class used for the K generic
+ * @param valueClass Class used for the V generic
+ * @param initialCapacity Initial capacity to use
+ * @param loadFactor Load factor to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map that auto instantiates on .get()
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newHashAutoMap(@Nullable final Class<? extends K> keyClass, @NotNull final Class<? extends V> valueClass, int initialCapacity, float loadFactor) {
+ return newAutoMap(new HashMap<>(initialCapacity, loadFactor), keyClass, valueClass);
+ }
+
+ /**
+ * @see #newAutoMap
+ *
+ * new Auto initializing map using a HashMap.
+ *
+ * @param valueClass Class used for the V generic
+ * @param initialCapacity Initial capacity to use
+ * @param loadFactor Load factor to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map that auto instantiates on .get()
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newHashAutoMap(@NotNull final Class<? extends V> valueClass, int initialCapacity, float loadFactor) {
+ return newHashAutoMap(null, valueClass, initialCapacity, loadFactor);
+ }
+
+ /**
+ * Initializes an auto loading map using a HashMap
+ *
+ * @param loader Loader to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newHashMap(@NotNull Function<K, V> loader) {
+ return new LoadingMap<>(new HashMap<>(), loader);
+ }
+
+ /**
+ * Initializes an auto loading map using a HashMap
+ *
+ * @param loader Loader to use
+ * @param initialCapacity Initial capacity to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newHashMap(@NotNull Function<K, V> loader, int initialCapacity) {
+ return new LoadingMap<>(new HashMap<>(initialCapacity), loader);
+ }
+ /**
+ * Initializes an auto loading map using a HashMap
+ *
+ * @param loader Loader to use
+ * @param initialCapacity Initial capacity to use
+ * @param loadFactor Load factor to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newHashMap(@NotNull Function<K, V> loader, int initialCapacity, float loadFactor) {
+ return new LoadingMap<>(new HashMap<>(initialCapacity, loadFactor), loader);
+ }
+
+ /**
+ * Initializes an auto loading map using an Identity HashMap
+ *
+ * @param loader Loader to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newIdentityHashMap(@NotNull Function<K, V> loader) {
+ return new LoadingMap<>(new IdentityHashMap<>(), loader);
+ }
+
+ /**
+ * Initializes an auto loading map using an Identity HashMap
+ *
+ * @param loader Loader to use
+ * @param initialCapacity Initial capacity to use
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map
+ */
+ @NotNull
+ public static <K, V> Map<K, V> newIdentityHashMap(@NotNull Function<K, V> loader, int initialCapacity) {
+ return new LoadingMap<>(new IdentityHashMap<>(initialCapacity), loader);
+ }
+
+ @Override
+ public int size() {return backingMap.size();}
+
+ @Override
+ public boolean isEmpty() {return backingMap.isEmpty();}
+
+ @Override
+ public boolean containsKey(@Nullable Object key) {return backingMap.containsKey(key);}
+
+ @Override
+ public boolean containsValue(@Nullable Object value) {return backingMap.containsValue(value);}
+
+ @Nullable
+ @Override
+ public V get(@Nullable Object key) {
+ V v = backingMap.get(key);
+ if (v != null) {
+ return v;
+ }
+ return backingMap.computeIfAbsent((K) key, loader);
+ }
+
+ @Nullable
+ public V put(@Nullable K key, @Nullable V value) {return backingMap.put(key, value);}
+
+ @Nullable
+ @Override
+ public V remove(@Nullable Object key) {return backingMap.remove(key);}
+
+ public void putAll(@NotNull Map<? extends K, ? extends V> m) {backingMap.putAll(m);}
+
+ @Override
+ public void clear() {backingMap.clear();}
+
+ @NotNull
+ @Override
+ public Set<K> keySet() {return backingMap.keySet();}
+
+ @NotNull
+ @Override
+ public Collection<V> values() {return backingMap.values();}
+
+ @Override
+ public boolean equals(@Nullable Object o) {return backingMap.equals(o);}
+
+ @Override
+ public int hashCode() {return backingMap.hashCode();}
+
+ @NotNull
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ return backingMap.entrySet();
+ }
+
+ @NotNull
+ public LoadingMap<K, V> clone() {
+ return new LoadingMap<>(backingMap, loader);
+ }
+
+ private static class AutoInstantiatingLoader<K, V> implements Function<K, V> {
+ final Constructor<? extends V> constructor;
+ private final Class<? extends V> valueClass;
+
+ AutoInstantiatingLoader(@Nullable Class<? extends K> keyClass, @NotNull Class<? extends V> valueClass) {
+ try {
+ this.valueClass = valueClass;
+ if (keyClass != null) {
+ constructor = valueClass.getConstructor(keyClass);
+ } else {
+ constructor = null;
+ }
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(
+ valueClass.getName() + " does not have a constructor for " + (keyClass != null ? keyClass.getName() : null));
+ }
+ }
+
+ @NotNull
+ @Override
+ public V apply(@Nullable K input) {
+ try {
+ return (constructor != null ? constructor.newInstance(input) : valueClass.newInstance());
+ } catch (Exception e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return false;
+ }
+ }
+
+ /**
+ * Due to java stuff, you will need to cast it to (Function) for some cases
+ *
+ * @param <T> Type
+ */
+ public abstract static class Feeder <T> implements Function<T, T> {
+ @Nullable
+ @Override
+ public T apply(@Nullable Object input) {
+ return apply();
+ }
+
+ @Nullable
+ public abstract T apply();
+ }
+}
diff --git a/src/main/java/co/aikar/util/MRUMapCache.java b/src/main/java/co/aikar/util/MRUMapCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..5989ee21297935651b0edd44b8239e655eaef1d9
--- /dev/null
+++ b/src/main/java/co/aikar/util/MRUMapCache.java
@@ -0,0 +1,111 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.util;
+
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Implements a Most Recently Used cache in front of a backing map, to quickly access the last accessed result.
+ *
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ */
+public class MRUMapCache<K, V> extends AbstractMap<K, V> {
+ final Map<K, V> backingMap;
+ Object cacheKey;
+ V cacheValue;
+ public MRUMapCache(@NotNull final Map<K, V> backingMap) {
+ this.backingMap = backingMap;
+ }
+
+ public int size() {return backingMap.size();}
+
+ public boolean isEmpty() {return backingMap.isEmpty();}
+
+ public boolean containsKey(@Nullable Object key) {
+ return key != null && key.equals(cacheKey) || backingMap.containsKey(key);
+ }
+
+ public boolean containsValue(@Nullable Object value) {
+ return value != null && value == cacheValue || backingMap.containsValue(value);
+ }
+
+ @Nullable
+ public V get(@Nullable Object key) {
+ if (cacheKey != null && cacheKey.equals(key)) {
+ return cacheValue;
+ }
+ cacheKey = key;
+ return cacheValue = backingMap.get(key);
+ }
+
+ @Nullable
+ public V put(@Nullable K key, @Nullable V value) {
+ cacheKey = key;
+ return cacheValue = backingMap.put(key, value);
+ }
+
+ @Nullable
+ public V remove(@Nullable Object key) {
+ if (key != null && key.equals(cacheKey)) {
+ cacheKey = null;
+ }
+ return backingMap.remove(key);
+ }
+
+ public void putAll(@NotNull Map<? extends K, ? extends V> m) {backingMap.putAll(m);}
+
+ public void clear() {
+ cacheKey = null;
+ cacheValue = null;
+ backingMap.clear();
+ }
+
+ @NotNull
+ public Set<K> keySet() {return backingMap.keySet();}
+
+ @NotNull
+ public Collection<V> values() {return backingMap.values();}
+
+ @NotNull
+ public Set<Map.Entry<K, V>> entrySet() {return backingMap.entrySet();}
+
+ /**
+ * Wraps the specified map with a most recently used cache
+ *
+ * @param map Map to be wrapped
+ * @param <K> Key Type of the Map
+ * @param <V> Value Type of the Map
+ * @return Map
+ */
+ @NotNull
+ public static <K, V> Map<K, V> of(@NotNull Map<K, V> map) {
+ return new MRUMapCache<K, V>(map);
+ }
+}
diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
index 2e059809021ea639c27d8a202a2962de1dae8168..f1585968b13007b0f9b8c69ac3c2eff28a0fc89e 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -640,7 +640,6 @@ public final class Bukkit {
*/
public static void reload() {
server.reload();
- org.spigotmc.CustomTimingsHandler.reload(); // Spigot
}
/**
diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java
index e43ba4c97b4a61e2b49c58553c39d0757521418d..6cbc22c08ce421bcd8bf3efabc96c9cec447faee 100644
--- a/src/main/java/org/bukkit/Server.java
+++ b/src/main/java/org/bukkit/Server.java
@@ -1337,6 +1337,26 @@ public interface Server extends PluginMessageRecipient {
throw new UnsupportedOperationException("Not supported yet.");
}
+ // Paper start
+ @NotNull
+ public org.bukkit.configuration.file.YamlConfiguration getBukkitConfig()
+ {
+ throw new UnsupportedOperationException( "Not supported yet." );
+ }
+
+ @NotNull
+ public org.bukkit.configuration.file.YamlConfiguration getSpigotConfig()
+ {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @NotNull
+ public org.bukkit.configuration.file.YamlConfiguration getPaperConfig()
+ {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+ // Paper end
+
/**
* Sends the component to the player
*
diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java
index 247d194f86c00db11acbc58e7d163b2606db4f07..945b8b030d1b2a13afc0c4efad76997eb7bf00ba 100644
--- a/src/main/java/org/bukkit/UnsafeValues.java
+++ b/src/main/java/org/bukkit/UnsafeValues.java
@@ -18,6 +18,7 @@ import org.bukkit.plugin.PluginDescriptionFile;
@Deprecated
public interface UnsafeValues {
+ void reportTimings(); // Paper
Material toLegacy(Material material);
Material fromLegacy(Material material);
@@ -69,4 +70,12 @@ public interface UnsafeValues {
* @return true if a file matching this key was found and deleted
*/
boolean removeAdvancement(NamespacedKey key);
+
+ // Paper start
+ /**
+ * Server name to report to timings v2
+ * @return name
+ */
+ String getTimingsServerName();
+ // Paper end
}
diff --git a/src/main/java/org/bukkit/command/BufferedCommandSender.java b/src/main/java/org/bukkit/command/BufferedCommandSender.java
new file mode 100644
index 0000000000000000000000000000000000000000..f9a00aecca5ec41b460bf41dfe1c69694768cf98
--- /dev/null
+++ b/src/main/java/org/bukkit/command/BufferedCommandSender.java
@@ -0,0 +1,21 @@
+package org.bukkit.command;
+
+import org.jetbrains.annotations.NotNull;
+
+public class BufferedCommandSender implements MessageCommandSender {
+ private final StringBuffer buffer = new StringBuffer();
+ @Override
+ public void sendMessage(@NotNull String message) {
+ buffer.append(message);
+ buffer.append("\n");
+ }
+
+ @NotNull
+ public String getBuffer() {
+ return buffer.toString();
+ }
+
+ public void reset() {
+ this.buffer.setLength(0);
+ }
+}
diff --git a/src/main/java/org/bukkit/command/Command.java b/src/main/java/org/bukkit/command/Command.java
index 4bfc214685164a38ba4261b2bae7faa8a3bd297e..03bdc1622791e1206406c87065978688d602e39e 100644
--- a/src/main/java/org/bukkit/command/Command.java
+++ b/src/main/java/org/bukkit/command/Command.java
@@ -33,7 +33,8 @@ public abstract class Command {
protected String usageMessage;
private String permission;
private String permissionMessage;
- public org.spigotmc.CustomTimingsHandler timings; // Spigot
+ public co.aikar.timings.Timing timings; // Paper
+ @NotNull public String getTimingName() {return getName();} // Paper
protected Command(@NotNull String name) {
this(name, "", "/" + name, new ArrayList<String>());
@@ -47,7 +48,6 @@ public abstract class Command {
this.usageMessage = (usageMessage == null) ? "/" + name : usageMessage;
this.aliases = aliases;
this.activeAliases = new ArrayList<String>(aliases);
- this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot
}
/**
@@ -245,7 +245,6 @@ public abstract class Command {
}
this.nextLabel = name;
if (!isRegistered()) {
- this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot
this.label = name;
return true;
}
diff --git a/src/main/java/org/bukkit/command/FormattedCommandAlias.java b/src/main/java/org/bukkit/command/FormattedCommandAlias.java
index d6c8938b1e13b63116b7b0e074ea8ef5997f8dc3..a6ad94ef98a1df1d2842635d850bc990b0137849 100644
--- a/src/main/java/org/bukkit/command/FormattedCommandAlias.java
+++ b/src/main/java/org/bukkit/command/FormattedCommandAlias.java
@@ -9,6 +9,7 @@ public class FormattedCommandAlias extends Command {
public FormattedCommandAlias(@NotNull String alias, @NotNull String[] formatStrings) {
super(alias);
+ timings = co.aikar.timings.TimingsManager.getCommandTiming("minecraft", this); // Spigot
this.formatStrings = formatStrings;
}
@@ -113,6 +114,10 @@ public class FormattedCommandAlias extends Command {
return formatString;
}
+ @NotNull
+ @Override // Paper
+ public String getTimingName() {return "Command Forwarder - " + super.getTimingName();} // Paper
+
private static boolean inRange(int i, int j, int k) {
return i >= j && i <= k;
}
diff --git a/src/main/java/org/bukkit/command/MessageCommandSender.java b/src/main/java/org/bukkit/command/MessageCommandSender.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca1893e9fb41baae0d103f1a925e33f3dfa273be
--- /dev/null
+++ b/src/main/java/org/bukkit/command/MessageCommandSender.java
@@ -0,0 +1,114 @@
+package org.bukkit.command;
+
+import org.apache.commons.lang.NotImplementedException;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionAttachment;
+import org.bukkit.permissions.PermissionAttachmentInfo;
+import org.bukkit.plugin.Plugin;
+
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * For when all you care about is just messaging
+ */
+public interface MessageCommandSender extends CommandSender {
+
+ @Override
+ default void sendMessage(@NotNull String[] messages) {
+ for (String message : messages) {
+ sendMessage(message);
+ }
+ }
+
+ @NotNull
+ @Override
+ default Server getServer() {
+ return Bukkit.getServer();
+ }
+
+ @NotNull
+ @Override
+ default String getName() {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean isOp() {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default void setOp(boolean value) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean isPermissionSet(@NotNull String name) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean isPermissionSet(@NotNull Permission perm) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean hasPermission(@NotNull String name) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean hasPermission(@NotNull Permission perm) {
+ throw new NotImplementedException();
+ }
+
+ @NotNull
+ @Override
+ default PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) {
+ throw new NotImplementedException();
+ }
+
+ @NotNull
+ @Override
+ default PermissionAttachment addAttachment(@NotNull Plugin plugin) {
+ throw new NotImplementedException();
+ }
+
+ @NotNull
+ @Override
+ default PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) {
+ throw new NotImplementedException();
+ }
+
+ @NotNull
+ @Override
+ default PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default void removeAttachment(@NotNull PermissionAttachment attachment) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default void recalculatePermissions() {
+ throw new NotImplementedException();
+ }
+
+ @NotNull
+ @Override
+ default Set<PermissionAttachmentInfo> getEffectivePermissions() {
+ throw new NotImplementedException();
+ }
+
+ @NotNull
+ @Override
+ default Spigot spigot() {
+ throw new NotImplementedException();
+ }
+
+}
diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java
index 81e4fa57337f5a40c4b673136dd5eb595cce4629..f020cb04eba27a2e70fc7cf799ebbfb434b9d974 100644
--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java
+++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java
@@ -15,7 +15,6 @@ import org.bukkit.command.defaults.BukkitCommand;
import org.bukkit.command.defaults.HelpCommand;
import org.bukkit.command.defaults.PluginsCommand;
import org.bukkit.command.defaults.ReloadCommand;
-import org.bukkit.command.defaults.TimingsCommand;
import org.bukkit.command.defaults.VersionCommand;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
@@ -35,7 +34,7 @@ public class SimpleCommandMap implements CommandMap {
register("bukkit", new VersionCommand("version"));
register("bukkit", new ReloadCommand("reload"));
register("bukkit", new PluginsCommand("plugins"));
- register("bukkit", new TimingsCommand("timings"));
+ register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Paper
}
public void setFallbackCommands() {
@@ -67,6 +66,7 @@ public class SimpleCommandMap implements CommandMap {
*/
@Override
public boolean register(@NotNull String label, @NotNull String fallbackPrefix, @NotNull Command command) {
+ command.timings = co.aikar.timings.TimingsManager.getCommandTiming(fallbackPrefix, command); // Paper
label = label.toLowerCase(java.util.Locale.ENGLISH).trim();
fallbackPrefix = fallbackPrefix.toLowerCase(java.util.Locale.ENGLISH).trim();
boolean registered = register(label, command, false, fallbackPrefix);
@@ -143,16 +143,22 @@ public class SimpleCommandMap implements CommandMap {
return false;
}
+ // Paper start - Plugins do weird things to workaround normal registration
+ if (target.timings == null) {
+ target.timings = co.aikar.timings.TimingsManager.getCommandTiming(null, target);
+ }
+ // Paper end
+
try {
- target.timings.startTiming(); // Spigot
+ try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources
// Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false)
target.execute(sender, sentCommandLabel, Arrays.copyOfRange(args, 1, args.length));
- target.timings.stopTiming(); // Spigot
+ } // target.timings.stopTiming(); // Spigot // Paper
} catch (CommandException ex) {
- target.timings.stopTiming(); // Spigot
+ //target.timings.stopTiming(); // Spigot // Paper
throw ex;
} catch (Throwable ex) {
- target.timings.stopTiming(); // Spigot
+ //target.timings.stopTiming(); // Spigot // Paper
throw new CommandException("Unhandled exception executing '" + commandLine + "' in " + target, ex);
}
diff --git a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java b/src/main/java/org/bukkit/command/defaults/TimingsCommand.java
deleted file mode 100644
index 2a145d851ce30360aa39549745bd87590c034584..0000000000000000000000000000000000000000
--- a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java
+++ /dev/null
@@ -1,250 +0,0 @@
-package org.bukkit.command.defaults;
-
-import com.google.common.collect.ImmutableList;
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.List;
-import org.apache.commons.lang.Validate;
-import org.bukkit.Bukkit;
-import org.bukkit.ChatColor;
-import org.bukkit.command.CommandSender;
-import org.bukkit.event.Event;
-import org.bukkit.event.HandlerList;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.plugin.RegisteredListener;
-import org.bukkit.plugin.TimedRegisteredListener;
-import org.bukkit.util.StringUtil;
-import org.jetbrains.annotations.NotNull;
-
-// Spigot start
-// CHECKSTYLE:OFF
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.logging.Level;
-import org.bukkit.command.RemoteConsoleCommandSender;
-import org.bukkit.plugin.SimplePluginManager;
-import org.spigotmc.CustomTimingsHandler;
-// CHECKSTYLE:ON
-// Spigot end
-
-public class TimingsCommand extends BukkitCommand {
- private static final List<String> TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste"); // Spigot
- public static long timingStart = 0; // Spigot
-
- public TimingsCommand(@NotNull String name) {
- super(name);
- this.description = "Manages Spigot Timings data to see performance of the server."; // Spigot
- this.usageMessage = "/timings <reset|report|on|off|paste>"; // Spigot
- this.setPermission("bukkit.command.timings");
- }
-
- // Spigot start - redesigned Timings Command
- public void executeSpigotTimings(@NotNull CommandSender sender, @NotNull String[] args) {
- if ("on".equals(args[0])) {
- ((SimplePluginManager) Bukkit.getPluginManager()).useTimings(true);
- CustomTimingsHandler.reload();
- sender.sendMessage("Enabled Timings & Reset");
- return;
- } else if ("off".equals(args[0])) {
- ((SimplePluginManager) Bukkit.getPluginManager()).useTimings(false);
- sender.sendMessage("Disabled Timings");
- return;
- }
-
- if (!Bukkit.getPluginManager().useTimings()) {
- sender.sendMessage("Please enable timings by typing /timings on");
- return;
- }
-
- boolean paste = "paste".equals(args[0]);
- if ("reset".equals(args[0])) {
- CustomTimingsHandler.reload();
- sender.sendMessage("Timings reset");
- } else if ("merged".equals(args[0]) || "report".equals(args[0]) || paste) {
- long sampleTime = System.nanoTime() - timingStart;
- int index = 0;
- File timingFolder = new File("timings");
- timingFolder.mkdirs();
- File timings = new File(timingFolder, "timings.txt");
- ByteArrayOutputStream bout = (paste) ? new ByteArrayOutputStream() : null;
- while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt");
- PrintStream fileTimings = null;
- try {
- fileTimings = (paste) ? new PrintStream(bout) : new PrintStream(timings);
-
- CustomTimingsHandler.printTimings(fileTimings);
- fileTimings.println("Sample time " + sampleTime + " (" + sampleTime / 1E9 + "s)");
-
- fileTimings.println("<spigotConfig>");
- fileTimings.println(Bukkit.spigot().getConfig().saveToString());
- fileTimings.println("</spigotConfig>");
-
- if (paste) {
- new PasteThread(sender, bout).start();
- return;
- }
-
- sender.sendMessage("Timings written to " + timings.getPath());
- sender.sendMessage("Paste contents of file into form at http://www.spigotmc.org/go/timings to read results.");
-
- } catch (IOException e) {
- } finally {
- if (fileTimings != null) {
- fileTimings.close();
- }
- }
- }
- }
- // Spigot end
-
- @Override
- public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) {
- if (!testPermission(sender)) return true;
- if (args.length < 1) { // Spigot
- sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage);
- return false;
- }
- // Spigot start
- if (true) {
- executeSpigotTimings(sender, args);
- return true;
- }
- // Spigot end
- if (!sender.getServer().getPluginManager().useTimings()) {
- sender.sendMessage("Please enable timings by setting \"settings.plugin-profiling\" to true in bukkit.yml");
- return true;
- }
-
- boolean separate = "separate".equalsIgnoreCase(args[0]);
- if ("reset".equalsIgnoreCase(args[0])) {
- for (HandlerList handlerList : HandlerList.getHandlerLists()) {
- for (RegisteredListener listener : handlerList.getRegisteredListeners()) {
- if (listener instanceof TimedRegisteredListener) {
- ((TimedRegisteredListener) listener).reset();
- }
- }
- }
- sender.sendMessage("Timings reset");
- } else if ("merged".equalsIgnoreCase(args[0]) || separate) {
-
- int index = 0;
- int pluginIdx = 0;
- File timingFolder = new File("timings");
- timingFolder.mkdirs();
- File timings = new File(timingFolder, "timings.txt");
- File names = null;
- while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt");
- PrintStream fileTimings = null;
- PrintStream fileNames = null;
- try {
- fileTimings = new PrintStream(timings);
- if (separate) {
- names = new File(timingFolder, "names" + index + ".txt");
- fileNames = new PrintStream(names);
- }
- for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
- pluginIdx++;
- long totalTime = 0;
- if (separate) {
- fileNames.println(pluginIdx + " " + plugin.getDescription().getFullName());
- fileTimings.println("Plugin " + pluginIdx);
- } else {
- fileTimings.println(plugin.getDescription().getFullName());
- }
- for (RegisteredListener listener : HandlerList.getRegisteredListeners(plugin)) {
- if (listener instanceof TimedRegisteredListener) {
- TimedRegisteredListener trl = (TimedRegisteredListener) listener;
- long time = trl.getTotalTime();
- int count = trl.getCount();
- if (count == 0) continue;
- long avg = time / count;
- totalTime += time;
- Class<? extends Event> eventClass = trl.getEventClass();
- if (count > 0 && eventClass != null) {
- fileTimings.println(" " + eventClass.getSimpleName() + (trl.hasMultiple() ? " (and sub-classes)" : "") + " Time: " + time + " Count: " + count + " Avg: " + avg);
- }
- }
- }
- fileTimings.println(" Total time " + totalTime + " (" + totalTime / 1000000000 + "s)");
- }
- sender.sendMessage("Timings written to " + timings.getPath());
- if (separate) sender.sendMessage("Names written to " + names.getPath());
- } catch (IOException e) {
- } finally {
- if (fileTimings != null) {
- fileTimings.close();
- }
- if (fileNames != null) {
- fileNames.close();
- }
- }
- } else {
- sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage);
- return false;
- }
- return true;
- }
-
- @NotNull
- @Override
- public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) {
- Validate.notNull(sender, "Sender cannot be null");
- Validate.notNull(args, "Arguments cannot be null");
- Validate.notNull(alias, "Alias cannot be null");
-
- if (args.length == 1) {
- return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, new ArrayList<String>(TIMINGS_SUBCOMMANDS.size()));
- }
- return ImmutableList.of();
- }
-
- // Spigot start
- private static class PasteThread extends Thread {
-
- private final CommandSender sender;
- private final ByteArrayOutputStream bout;
-
- public PasteThread(@NotNull CommandSender sender, @NotNull ByteArrayOutputStream bout) {
- super("Timings paste thread");
- this.sender = sender;
- this.bout = bout;
- }
-
- @Override
- public synchronized void start() {
- if (sender instanceof RemoteConsoleCommandSender) {
- run();
- } else {
- super.start();
- }
- }
-
- @Override
- public void run() {
- try {
- HttpURLConnection con = (HttpURLConnection) new URL("https://timings.spigotmc.org/paste").openConnection();
- con.setDoOutput(true);
- con.setRequestMethod("POST");
- con.setInstanceFollowRedirects(false);
-
- OutputStream out = con.getOutputStream();
- out.write(bout.toByteArray());
- out.close();
-
- com.google.gson.JsonObject location = new com.google.gson.Gson().fromJson(new java.io.InputStreamReader(con.getInputStream()), com.google.gson.JsonObject.class);
- con.getInputStream().close();
-
- String pasteID = location.get("key").getAsString();
- sender.sendMessage(ChatColor.GREEN + "Timings results can be viewed at https://www.spigotmc.org/go/timings?url=" + pasteID);
- } catch (IOException ex) {
- sender.sendMessage(ChatColor.RED + "Error pasting timings, check your console for more information");
- Bukkit.getServer().getLogger().log(Level.WARNING, "Could not paste timings", ex);
- }
- }
- }
- // Spigot end
-}
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
index 0859d2bc226c4abf21ac1dc28f35ebe805046d89..39d196992bbcc263fa4bcd46c6187ba7b8adb935 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
@@ -1369,6 +1369,11 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
public void sendMessage(@NotNull net.md_5.bungee.api.ChatMessageType position, @NotNull net.md_5.bungee.api.chat.BaseComponent... components) {
throw new UnsupportedOperationException("Not supported yet.");
}
+
+ public int getPing()
+ {
+ throw new UnsupportedOperationException( "Not supported yet." );
+ }
}
@NotNull
diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
index 62d0017362204070465c8ff72e5c2ca07501f558..745eaa8f2f2ff83536301db8ca47a8af30df7a73 100644
--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
@@ -358,7 +358,6 @@ public final class SimplePluginManager implements PluginManager {
}
}
- org.bukkit.command.defaults.TimingsCommand.timingStart = System.nanoTime(); // Spigot
return result.toArray(new Plugin[result.size()]);
}
@@ -397,9 +396,9 @@ public final class SimplePluginManager implements PluginManager {
if (result != null) {
plugins.add(result);
- lookupNames.put(result.getDescription().getName(), result);
+ lookupNames.put(result.getDescription().getName().toLowerCase(java.util.Locale.ENGLISH), result); // Paper
for (String provided : result.getDescription().getProvides()) {
- lookupNames.putIfAbsent(provided, result);
+ lookupNames.putIfAbsent(provided.toLowerCase(java.util.Locale.ENGLISH), result); // Paper
}
}
@@ -428,7 +427,7 @@ public final class SimplePluginManager implements PluginManager {
@Override
@Nullable
public synchronized Plugin getPlugin(@NotNull String name) {
- return lookupNames.get(name.replace(' ', '_'));
+ return lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
}
@Override
@@ -646,7 +645,8 @@ public final class SimplePluginManager implements PluginManager {
throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
}
- if (useTimings) {
+ executor = new co.aikar.timings.TimedEventExecutor(executor, plugin, null, event); // Paper
+ if (false) { // Spigot - RL handles useTimings check now // Paper
getEventListeners(event).register(new TimedRegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
} else {
getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
@@ -860,7 +860,7 @@ public final class SimplePluginManager implements PluginManager {
@Override
public boolean useTimings() {
- return useTimings;
+ return co.aikar.timings.Timings.isTimingsEnabled(); // Spigot
}
/**
@@ -869,6 +869,6 @@ public final class SimplePluginManager implements PluginManager {
* @param use True if per event timing code should be used
*/
public void useTimings(boolean use) {
- useTimings = use;
+ co.aikar.timings.Timings.setTimingsEnabled(use); // Paper
}
}
diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
index 7b9ad3834c9c81220c74a16f1e66fb4da512e9f6..b6d739ca8ad8ebd4b1be7ebd129f9a7ae16b2a2a 100644
--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
@@ -53,7 +53,6 @@ public final class JavaPluginLoader implements PluginLoader {
private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")};
private final Map<String, Class<?>> classes = new ConcurrentHashMap<String, Class<?>>();
private final List<PluginClassLoader> loaders = new CopyOnWriteArrayList<PluginClassLoader>();
- public static final CustomTimingsHandler pluginParentTimer = new CustomTimingsHandler("** Plugins"); // Spigot
/**
* This class was not meant to be constructed explicitly
@@ -301,27 +300,21 @@ public final class JavaPluginLoader implements PluginLoader {
}
}
- final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName() + "(" + eventClass.getSimpleName() + ")", pluginParentTimer); // Spigot
- EventExecutor executor = new EventExecutor() {
+ EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Paper
@Override
- public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException {
+ public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { // Paper
try {
if (!eventClass.isAssignableFrom(event.getClass())) {
return;
}
- // Spigot start
- boolean isAsync = event.isAsynchronous();
- if (!isAsync) timings.startTiming();
method.invoke(listener, event);
- if (!isAsync) timings.stopTiming();
- // Spigot end
} catch (InvocationTargetException ex) {
throw new EventException(ex.getCause());
} catch (Throwable t) {
throw new EventException(t);
}
}
- };
+ }, plugin, method, eventClass); // Paper
if (false) { // Spigot - RL handles useTimings check now
eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
} else {
diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
index 5830e8b9b74d6107e54b6e19e03ab0e8c0da2f19..36f542a85e0f16e97c65c0ca64ec660ddf75d63e 100644
--- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
@@ -28,7 +28,8 @@ import org.jetbrains.annotations.Nullable;
/**
* A ClassLoader for plugins, to allow shared classes across multiple plugins
*/
-final class PluginClassLoader extends URLClassLoader {
+public final class PluginClassLoader extends URLClassLoader { // Spigot
+ public JavaPlugin getPlugin() { return plugin; } // Spigot
private final JavaPluginLoader loader;
private final Map<String, Class<?>> classes = new ConcurrentHashMap<String, Class<?>>();
private final PluginDescriptionFile description;
diff --git a/src/main/java/org/bukkit/util/CachedServerIcon.java b/src/main/java/org/bukkit/util/CachedServerIcon.java
index 5ca863b3692b2e1b58e7fb4d82f554a92cc4f01e..612958a331575d1da2715531ebdf6b1168f2e860 100644
--- a/src/main/java/org/bukkit/util/CachedServerIcon.java
+++ b/src/main/java/org/bukkit/util/CachedServerIcon.java
@@ -2,6 +2,7 @@ package org.bukkit.util;
import org.bukkit.Server;
import org.bukkit.event.server.ServerListPingEvent;
+import org.jetbrains.annotations.Nullable;
/**
* This is a cached version of a server-icon. It's internal representation
@@ -12,4 +13,9 @@ import org.bukkit.event.server.ServerListPingEvent;
* @see Server#loadServerIcon(java.io.File)
* @see ServerListPingEvent#setServerIcon(CachedServerIcon)
*/
-public interface CachedServerIcon {}
+public interface CachedServerIcon {
+
+ @Nullable
+ public String getData(); // Paper
+
+}
diff --git a/src/main/java/org/spigotmc/CustomTimingsHandler.java b/src/main/java/org/spigotmc/CustomTimingsHandler.java
index 44badfedcc3fdc26bdc293b85d8c781d6f659faa..3cbe5c2bb55dead7968a6f165ef267e3e2931061 100644
--- a/src/main/java/org/spigotmc/CustomTimingsHandler.java
+++ b/src/main/java/org/spigotmc/CustomTimingsHandler.java
@@ -1,3 +1,26 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
package org.spigotmc;
import java.io.PrintStream;
@@ -5,133 +28,84 @@ import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.bukkit.Bukkit;
import org.bukkit.World;
-import org.bukkit.command.defaults.TimingsCommand;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import org.bukkit.plugin.AuthorNagException;
+import org.bukkit.plugin.Plugin;
+import co.aikar.timings.Timing;
+import co.aikar.timings.Timings;
+import co.aikar.timings.TimingsManager;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.logging.Level;
/**
- * Provides custom timing sections for /timings merged.
+ * This is here for legacy purposes incase any plugin used it.
+ *
+ * If you use this, migrate ASAP as this will be removed in the future!
+ *
+ * @deprecated
+ * @see co.aikar.timings.Timings#of
*/
-public class CustomTimingsHandler {
-
- private static Queue<CustomTimingsHandler> HANDLERS = new ConcurrentLinkedQueue<CustomTimingsHandler>();
- /*========================================================================*/
- private final String name;
- private final CustomTimingsHandler parent;
- private long count = 0;
- private long start = 0;
- private long timingDepth = 0;
- private long totalTime = 0;
- private long curTickTotal = 0;
- private long violations = 0;
+@Deprecated
+public final class CustomTimingsHandler {
+ private final Timing handler;
+ private static Boolean sunReflectAvailable;
+ private static Method getCallerClass;
public CustomTimingsHandler(@NotNull String name) {
- this(name, null);
- }
+ if (sunReflectAvailable == null) {
+ String javaVer = System.getProperty("java.version");
+ String[] elements = javaVer.split("\\.");
- public CustomTimingsHandler(@NotNull String name, @Nullable CustomTimingsHandler parent) {
- this.name = name;
- this.parent = parent;
- HANDLERS.add(this);
- }
+ int major = Integer.parseInt(elements.length >= 2 ? elements[1] : javaVer);
+ if (major <= 8) {
+ sunReflectAvailable = true;
- /**
- * Prints the timings and extra data to the given stream.
- *
- * @param printStream output stream
- */
- public static void printTimings(@NotNull PrintStream printStream) {
- printStream.println("Minecraft");
- for (CustomTimingsHandler timings : HANDLERS) {
- long time = timings.totalTime;
- long count = timings.count;
- if (count == 0) {
- continue;
+ try {
+ Class<?> reflection = Class.forName("sun.reflect.Reflection");
+ getCallerClass = reflection.getMethod("getCallerClass", int.class);
+ } catch (ClassNotFoundException | NoSuchMethodException ignored) {
+ }
+ } else {
+ sunReflectAvailable = false;
}
- long avg = time / count;
-
- printStream.println(" " + timings.name + " Time: " + time + " Count: " + count + " Avg: " + avg + " Violations: " + timings.violations);
- }
- printStream.println("# Version " + Bukkit.getVersion());
- int entities = 0;
- int livingEntities = 0;
- for (World world : Bukkit.getWorlds()) {
- entities += world.getEntities().size();
- livingEntities += world.getLivingEntities().size();
}
- printStream.println("# Entities " + entities);
- printStream.println("# LivingEntities " + livingEntities);
- }
- /**
- * Resets all timings.
- */
- public static void reload() {
- if (Bukkit.getPluginManager().useTimings()) {
- for (CustomTimingsHandler timings : HANDLERS) {
- timings.reset();
+ Class calling = null;
+ if (sunReflectAvailable) {
+ try {
+ calling = (Class) getCallerClass.invoke(null, 2);
+ } catch (IllegalAccessException | InvocationTargetException ignored) {
}
}
- TimingsCommand.timingStart = System.nanoTime();
- }
- /**
- * Ticked every tick by CraftBukkit to count the number of times a timer
- * caused TPS loss.
- */
- public static void tick() {
- if (Bukkit.getPluginManager().useTimings()) {
- for (CustomTimingsHandler timings : HANDLERS) {
- if (timings.curTickTotal > 50000000) {
- timings.violations += Math.ceil(timings.curTickTotal / 50000000);
- }
- timings.curTickTotal = 0;
- timings.timingDepth = 0; // incase reset messes this up
- }
- }
- }
+ Timing timing;
- /**
- * Starts timing to track a section of code.
- */
- public void startTiming() {
- // If second condtion fails we are already timing
- if (Bukkit.getPluginManager().useTimings() && ++timingDepth == 1) {
- start = System.nanoTime();
- if (parent != null && ++parent.timingDepth == 1) {
- parent.start = start;
- }
- }
- }
+ Plugin plugin = null;
+ try {
+ plugin = TimingsManager.getPluginByClassloader(calling);
+ } catch (Exception ignored) {}
- /**
- * Stops timing a section of code.
- */
- public void stopTiming() {
- if (Bukkit.getPluginManager().useTimings()) {
- if (--timingDepth != 0 || start == 0) {
- return;
- }
- long diff = System.nanoTime() - start;
- totalTime += diff;
- curTickTotal += diff;
- count++;
- start = 0;
- if (parent != null) {
- parent.stopTiming();
+ new AuthorNagException("Deprecated use of CustomTimingsHandler. Please Switch to Timings.of ASAP").printStackTrace();
+ if (plugin != null) {
+ timing = Timings.of(plugin, "(Deprecated API) " + name);
+ } else {
+ try {
+ final Method ofSafe = TimingsManager.class.getDeclaredMethod("getHandler", String.class, String.class, Timing.class);
+ ofSafe.setAccessible(true);
+ timing = (Timing) ofSafe.invoke(null,"Minecraft", "(Deprecated API) " + name, null);
+ } catch (Exception e) {
+ e.printStackTrace();
+ Bukkit.getLogger().log(Level.SEVERE, "This handler could not be registered");
+ timing = Timings.NULL_HANDLER;
}
}
+ handler = timing;
}
- /**
- * Reset this timer, setting all values to zero.
- */
- public void reset() {
- count = 0;
- violations = 0;
- curTickTotal = 0;
- totalTime = 0;
- start = 0;
- timingDepth = 0;
- }
+ public void startTiming() { handler.startTiming(); }
+ public void stopTiming() { handler.stopTiming(); }
+
}