mirror of
https://github.com/PaperMC/Paper.git
synced 2024-11-23 02:55:47 +01:00
b6cf80ee66
This is for 2 reasons: 1) Ensuring our log4j is mostly loaded at OUR version. I've seen stack traces with line numbers that do not match our version. This means that some plugin has shaded in log4j and their loaded version is mixing with ours.... So by at least trying to load a bunch of log4j classes before we load plugins, we can be more sure mixed versions are not loading. 2) If the jar file is replaced while the server is runnimg class not found errors galore This will preloaod a bunch of classes commonly seen to error during shutdown due to this. The goal here is to help let the server shutdown gracefully as possible. Some plugins will still blow up here if they access a class that hadn't been loaded yet, but goal is to at least stop freezing the shutdown process as it does with JLine and Log4j errors requiring an external kill. Ideally you should not replace jars while the server is running, but it is something that happens in development for testing. Updated test server to do a copy though to avoid this happening in Paper development.
4093 lines
151 KiB
Diff
4093 lines
151 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..65ff5403cbc894215089626450252a050dd13119
|
|
--- /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();
|
|
+ }
|
|
+ TimingsExport.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..933ecf9bd232a376796d0f15babb0725cdeba3c9
|
|
--- /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.getName(), (verbose ? "## " : "") +
|
|
+ "Event: " + id + " (" + eventName + ")", null);
|
|
+ }
|
|
+
|
|
+ @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..0b34e0d0174b7d92a17699538d5f3fe5196f82b9
|
|
--- /dev/null
|
|
+++ b/src/main/java/co/aikar/timings/Timings.java
|
|
@@ -0,0 +1,293 @@
|
|
+/*
|
|
+ * 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 org.apache.commons.lang.Validate;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.command.CommandSender;
|
|
+import org.bukkit.plugin.Plugin;
|
|
+
|
|
+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 {
|
|
+
|
|
+ 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();
|
|
+ }
|
|
+ TimingsExport.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);
|
|
+ TimingsExport.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..c0d8f2016bbc0257c8eb4888508e8f50d7c4790f
|
|
--- /dev/null
|
|
+++ b/src/main/java/co/aikar/timings/TimingsCommand.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 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 ("cost".equals(arg)) {
|
|
+ sender.sendMessage("Timings cost: " + TimingsExport.getCost());
|
|
+ } 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/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..93d5a3f97a1b2b3a1cd2731d48e8ebd01d29aa91
|
|
--- /dev/null
|
|
+++ b/src/main/java/co/aikar/timings/TimingsExport.java
|
|
@@ -0,0 +1,355 @@
|
|
+/*
|
|
+ * 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.Lists;
|
|
+import com.google.common.collect.Sets;
|
|
+import org.apache.commons.lang.StringUtils;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.ChatColor;
|
|
+import org.bukkit.Material;
|
|
+import org.bukkit.command.CommandSender;
|
|
+import org.bukkit.configuration.ConfigurationSection;
|
|
+import org.bukkit.configuration.MemorySection;
|
|
+import org.bukkit.entity.EntityType;
|
|
+import org.json.simple.JSONObject;
|
|
+import org.json.simple.JSONValue;
|
|
+
|
|
+import java.io.ByteArrayOutputStream;
|
|
+import java.io.IOException;
|
|
+import java.io.InputStream;
|
|
+import java.io.OutputStream;
|
|
+import java.lang.management.ManagementFactory;
|
|
+import java.lang.management.RuntimeMXBean;
|
|
+import java.net.HttpURLConnection;
|
|
+import java.net.InetAddress;
|
|
+import java.net.URL;
|
|
+import java.util.List;
|
|
+import java.util.Map;
|
|
+import java.util.Set;
|
|
+import java.util.logging.Level;
|
|
+import java.util.zip.GZIPOutputStream;
|
|
+
|
|
+import static co.aikar.timings.TimingsManager.HISTORY;
|
|
+import static co.aikar.util.JSONUtil.appendObjectData;
|
|
+import static co.aikar.util.JSONUtil.createObject;
|
|
+import static co.aikar.util.JSONUtil.pair;
|
|
+import static co.aikar.util.JSONUtil.toArray;
|
|
+import static co.aikar.util.JSONUtil.toArrayMapper;
|
|
+import static co.aikar.util.JSONUtil.toObjectMapper;
|
|
+
|
|
+@SuppressWarnings({"rawtypes", "SuppressionAnnotation"})
|
|
+class TimingsExport extends Thread {
|
|
+
|
|
+ private final TimingsReportListener listeners;
|
|
+ private final Map out;
|
|
+ private final TimingHistory[] history;
|
|
+ private static long lastReport = 0;
|
|
+ final static List<CommandSender> requestingReport = Lists.newArrayList();
|
|
+
|
|
+ private TimingsExport(TimingsReportListener listeners, Map out, TimingHistory[] history) {
|
|
+ super("Timings paste thread");
|
|
+ this.listeners = listeners;
|
|
+ this.out = out;
|
|
+ this.history = history;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Checks if any pending reports are being requested, and builds one if needed.
|
|
+ */
|
|
+ static void reportTimings() {
|
|
+ if (requestingReport.isEmpty()) {
|
|
+ return;
|
|
+ }
|
|
+ TimingsReportListener listeners = new TimingsReportListener(requestingReport);
|
|
+ listeners.addConsoleIfNeeded();
|
|
+
|
|
+ requestingReport.clear();
|
|
+ long now = System.currentTimeMillis();
|
|
+ final long lastReportDiff = now - lastReport;
|
|
+ if (lastReportDiff < 60000) {
|
|
+ listeners.sendMessage(ChatColor.RED + "Please wait at least 1 minute in between Timings reports. (" + (int)((60000 - lastReportDiff) / 1000) + " seconds)");
|
|
+ listeners.done();
|
|
+ return;
|
|
+ }
|
|
+ final long lastStartDiff = now - TimingsManager.timingStart;
|
|
+ if (lastStartDiff < 180000) {
|
|
+ listeners.sendMessage(ChatColor.RED + "Please wait at least 3 minutes before generating a Timings report. Unlike Timings v1, v2 benefits from longer timings and is not as useful with short timings. (" + (int)((180000 - lastStartDiff) / 1000) + " seconds)");
|
|
+ listeners.done();
|
|
+ return;
|
|
+ }
|
|
+ listeners.sendMessage(ChatColor.GREEN + "Preparing Timings Report...");
|
|
+ lastReport = now;
|
|
+ Map parent = createObject(
|
|
+ // Get some basic system details about the server
|
|
+ pair("version", Bukkit.getVersion()),
|
|
+ pair("maxplayers", Bukkit.getMaxPlayers()),
|
|
+ pair("start", TimingsManager.timingStart / 1000),
|
|
+ pair("end", System.currentTimeMillis() / 1000),
|
|
+ pair("sampletime", (System.currentTimeMillis() - TimingsManager.timingStart) / 1000)
|
|
+ );
|
|
+ if (!TimingsManager.privacy) {
|
|
+ appendObjectData(parent,
|
|
+ pair("server", Bukkit.getUnsafe().getTimingsServerName()),
|
|
+ pair("motd", Bukkit.getServer().getMotd()),
|
|
+ pair("online-mode", Bukkit.getServer().getOnlineMode()),
|
|
+ pair("icon", Bukkit.getServer().getServerIcon().getData())
|
|
+ );
|
|
+ }
|
|
+
|
|
+ final Runtime runtime = Runtime.getRuntime();
|
|
+ RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
|
|
+
|
|
+ parent.put("system", createObject(
|
|
+ pair("timingcost", getCost()),
|
|
+ pair("name", System.getProperty("os.name")),
|
|
+ pair("version", System.getProperty("os.version")),
|
|
+ pair("jvmversion", System.getProperty("java.version")),
|
|
+ pair("arch", System.getProperty("os.arch")),
|
|
+ pair("maxmem", runtime.maxMemory()),
|
|
+ pair("cpu", runtime.availableProcessors()),
|
|
+ pair("runtime", ManagementFactory.getRuntimeMXBean().getUptime()),
|
|
+ pair("flags", StringUtils.join(runtimeBean.getInputArguments(), " ")),
|
|
+ pair("gc", toObjectMapper(ManagementFactory.getGarbageCollectorMXBeans(), input -> pair(input.getName(), toArray(input.getCollectionCount(), input.getCollectionTime()))))
|
|
+ )
|
|
+ );
|
|
+
|
|
+ Set<Material> tileEntityTypeSet = Sets.newHashSet();
|
|
+ Set<EntityType> entityTypeSet = Sets.newHashSet();
|
|
+
|
|
+ int size = HISTORY.size();
|
|
+ TimingHistory[] history = new TimingHistory[size + 1];
|
|
+ int i = 0;
|
|
+ for (TimingHistory timingHistory : HISTORY) {
|
|
+ tileEntityTypeSet.addAll(timingHistory.tileEntityTypeSet);
|
|
+ entityTypeSet.addAll(timingHistory.entityTypeSet);
|
|
+ history[i++] = timingHistory;
|
|
+ }
|
|
+
|
|
+ history[i] = new TimingHistory(); // Current snapshot
|
|
+ tileEntityTypeSet.addAll(history[i].tileEntityTypeSet);
|
|
+ entityTypeSet.addAll(history[i].entityTypeSet);
|
|
+
|
|
+
|
|
+ Map handlers = createObject();
|
|
+ Map groupData;
|
|
+ synchronized (TimingIdentifier.GROUP_MAP) {
|
|
+ for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) {
|
|
+ synchronized (group.handlers) {
|
|
+ for (TimingHandler id : group.handlers) {
|
|
+
|
|
+ if (!id.isTimed() && !id.isSpecial()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ String name = id.identifier.name;
|
|
+ if (name.startsWith("##")) {
|
|
+ name = name.substring(3);
|
|
+ }
|
|
+ handlers.put(id.id, toArray(
|
|
+ group.id,
|
|
+ name
|
|
+ ));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ groupData = toObjectMapper(
|
|
+ TimingIdentifier.GROUP_MAP.values(), group -> pair(group.id, group.name));
|
|
+ }
|
|
+
|
|
+ parent.put("idmap", createObject(
|
|
+ pair("groups", groupData),
|
|
+ pair("handlers", handlers),
|
|
+ pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), input -> pair(input.getValue(), input.getKey()))),
|
|
+ pair("tileentity",
|
|
+ toObjectMapper(tileEntityTypeSet, input -> pair(input.ordinal(), input.name()))),
|
|
+ pair("entity",
|
|
+ toObjectMapper(entityTypeSet, input -> pair(input.ordinal(), input.name())))
|
|
+ ));
|
|
+
|
|
+ // Information about loaded plugins
|
|
+
|
|
+ parent.put("plugins", toObjectMapper(Bukkit.getPluginManager().getPlugins(),
|
|
+ plugin -> pair(plugin.getName(), createObject(
|
|
+ pair("version", plugin.getDescription().getVersion()),
|
|
+ pair("description", String.valueOf(plugin.getDescription().getDescription()).trim()),
|
|
+ pair("website", plugin.getDescription().getWebsite()),
|
|
+ pair("authors", StringUtils.join(plugin.getDescription().getAuthors(), ", "))
|
|
+ ))));
|
|
+
|
|
+
|
|
+
|
|
+ // Information on the users Config
|
|
+
|
|
+ parent.put("config", createObject(
|
|
+ pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)),
|
|
+ pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)),
|
|
+ pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null))
|
|
+ ));
|
|
+
|
|
+ new TimingsExport(listeners, parent, history).start();
|
|
+ }
|
|
+
|
|
+ static long getCost() {
|
|
+ // Benchmark the users System.nanotime() for cost basis
|
|
+ int passes = 100;
|
|
+ TimingHandler SAMPLER1 = Timings.ofSafe("Timings Sampler 1");
|
|
+ TimingHandler SAMPLER2 = Timings.ofSafe("Timings Sampler 2");
|
|
+ TimingHandler SAMPLER3 = Timings.ofSafe("Timings Sampler 3");
|
|
+ TimingHandler SAMPLER4 = Timings.ofSafe("Timings Sampler 4");
|
|
+ TimingHandler SAMPLER5 = Timings.ofSafe("Timings Sampler 5");
|
|
+ TimingHandler SAMPLER6 = Timings.ofSafe("Timings Sampler 6");
|
|
+
|
|
+ long start = System.nanoTime();
|
|
+ for (int i = 0; i < passes; i++) {
|
|
+ SAMPLER1.startTiming();
|
|
+ SAMPLER2.startTiming();
|
|
+ SAMPLER3.startTiming();
|
|
+ SAMPLER3.stopTiming();
|
|
+ SAMPLER4.startTiming();
|
|
+ SAMPLER5.startTiming();
|
|
+ SAMPLER6.startTiming();
|
|
+ SAMPLER6.stopTiming();
|
|
+ SAMPLER5.stopTiming();
|
|
+ SAMPLER4.stopTiming();
|
|
+ SAMPLER2.stopTiming();
|
|
+ SAMPLER1.stopTiming();
|
|
+ }
|
|
+ long timingsCost = (System.nanoTime() - start) / passes / 6;
|
|
+ SAMPLER1.reset(true);
|
|
+ SAMPLER2.reset(true);
|
|
+ SAMPLER3.reset(true);
|
|
+ SAMPLER4.reset(true);
|
|
+ SAMPLER5.reset(true);
|
|
+ SAMPLER6.reset(true);
|
|
+ return timingsCost;
|
|
+ }
|
|
+
|
|
+ private static JSONObject mapAsJSON(ConfigurationSection config, String parentKey) {
|
|
+
|
|
+ JSONObject object = new JSONObject();
|
|
+ for (String key : config.getKeys(false)) {
|
|
+ String fullKey = (parentKey != null ? parentKey + "." + key : key);
|
|
+ if (fullKey.equals("database") || fullKey.equals("settings.bungeecord-addresses") || TimingsManager.hiddenConfigs.contains(fullKey)) {
|
|
+ continue;
|
|
+ }
|
|
+ final Object val = config.get(key);
|
|
+
|
|
+ object.put(key, valAsJSON(val, fullKey));
|
|
+ }
|
|
+ return object;
|
|
+ }
|
|
+
|
|
+ private static Object valAsJSON(Object val, final String parentKey) {
|
|
+ if (!(val instanceof MemorySection)) {
|
|
+ if (val instanceof List) {
|
|
+ Iterable<Object> v = (Iterable<Object>) val;
|
|
+ return toArrayMapper(v, input -> valAsJSON(input, parentKey));
|
|
+ } else {
|
|
+ return String.valueOf(val);
|
|
+ }
|
|
+ } else {
|
|
+ return mapAsJSON((ConfigurationSection) val, parentKey);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void run() {
|
|
+ out.put("data", toArrayMapper(history, TimingHistory::export));
|
|
+
|
|
+
|
|
+ String response = null;
|
|
+ String timingsURL = null;
|
|
+ try {
|
|
+ HttpURLConnection con = (HttpURLConnection) new URL("http://timings.aikar.co/post").openConnection();
|
|
+ con.setDoOutput(true);
|
|
+ String hostName = "BrokenHost";
|
|
+ try {
|
|
+ hostName = InetAddress.getLocalHost().getHostName();
|
|
+ } catch (Exception ignored) {}
|
|
+ con.setRequestProperty("User-Agent", "Paper/" + Bukkit.getUnsafe().getTimingsServerName() + "/" + hostName);
|
|
+ con.setRequestMethod("POST");
|
|
+ con.setInstanceFollowRedirects(false);
|
|
+
|
|
+ OutputStream request = new GZIPOutputStream(con.getOutputStream()) {{
|
|
+ this.def.setLevel(7);
|
|
+ }};
|
|
+
|
|
+ request.write(JSONValue.toJSONString(out).getBytes("UTF-8"));
|
|
+ request.close();
|
|
+
|
|
+ response = getResponse(con);
|
|
+
|
|
+ if (con.getResponseCode() != 302) {
|
|
+ listeners.sendMessage(
|
|
+ ChatColor.RED + "Upload Error: " + con.getResponseCode() + ": " + con.getResponseMessage());
|
|
+ listeners.sendMessage(ChatColor.RED + "Check your logs for more information");
|
|
+ if (response != null) {
|
|
+ Bukkit.getLogger().log(Level.SEVERE, response);
|
|
+ }
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ timingsURL = con.getHeaderField("Location");
|
|
+ listeners.sendMessage(ChatColor.GREEN + "View Timings Report: " + timingsURL);
|
|
+
|
|
+ if (response != null && !response.isEmpty()) {
|
|
+ Bukkit.getLogger().log(Level.INFO, "Timing Response: " + response);
|
|
+ }
|
|
+ } catch (IOException ex) {
|
|
+ listeners.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information");
|
|
+ if (response != null) {
|
|
+ Bukkit.getLogger().log(Level.SEVERE, response);
|
|
+ }
|
|
+ Bukkit.getLogger().log(Level.SEVERE, "Could not paste timings", ex);
|
|
+ } finally {
|
|
+ this.listeners.done(timingsURL);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private String getResponse(HttpURLConnection con) throws IOException {
|
|
+ InputStream is = null;
|
|
+ try {
|
|
+ is = con.getInputStream();
|
|
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
+
|
|
+ byte[] b = new byte[1024];
|
|
+ int bytesRead;
|
|
+ while ((bytesRead = is.read(b)) != -1) {
|
|
+ bos.write(b, 0, bytesRead);
|
|
+ }
|
|
+ return bos.toString();
|
|
+
|
|
+ } catch (IOException ex) {
|
|
+ listeners.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information");
|
|
+ Bukkit.getLogger().log(Level.WARNING, con.getResponseMessage(), ex);
|
|
+ return null;
|
|
+ } finally {
|
|
+ if (is != null) {
|
|
+ is.close();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
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 ae21e0f97d3d078e3b9ce9f60590c20e5012396e..755869366e7546fa8aefe7d7a1a602bab91a458c 100644
|
|
--- a/src/main/java/org/bukkit/Bukkit.java
|
|
+++ b/src/main/java/org/bukkit/Bukkit.java
|
|
@@ -618,7 +618,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 f6fb72fab398ae8ca8b746154ff3c8fcad378faf..fad4e929264e2be534d3c4a90a5d557fd6c5807b 100644
|
|
--- a/src/main/java/org/bukkit/Server.java
|
|
+++ b/src/main/java/org/bukkit/Server.java
|
|
@@ -1300,6 +1300,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..72c5501e8503aa3b5564a0467fde270d7cd93492 100644
|
|
--- a/src/main/java/org/bukkit/UnsafeValues.java
|
|
+++ b/src/main/java/org/bukkit/UnsafeValues.java
|
|
@@ -69,4 +69,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 92bafd6365313390326ea12fb815c62463f4d0fc..bfbe775fcd631b12975c0aaae8c82156b81c9614 100644
|
|
--- a/src/main/java/org/bukkit/entity/Player.java
|
|
+++ b/src/main/java/org/bukkit/entity/Player.java
|
|
@@ -1329,6 +1329,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 ec77d7be69a8213c91d05bd3beabdd4fc664afa4..c548911c4b4fad495e4b321ea47455ec65c68255 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 df8a5dcbe3345abf7be53d7ebb81d13b33c86511..50a51394f71579b71c3875e4dc8c71abc23ae246 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(); }
|
|
+
|
|
}
|