Paper/Spigot-API-Patches/0003-Timings-v2.patch
Shane Freeder 29a01de126
Updated Upstream (Bukkit/CraftBukkit/Spigot)
Upstream has released updates that appears to apply and compile correctly.
This update has not been tested by PaperMC and as with ANY update, please do your own testing

Bukkit Changes:
9a793cce Remove no longer applicable caveats to setPlayerListName
7137829e SPIGOT-4496: Undeprecate MapView.getId and make int
de33ade0 Remove some draft API designations
a35fa838 SPIGOT-4472: Add Consumer scheduler methods

CraftBukkit Changes:
8cd538e6 SPIGOT-4498: Crash on startup
b4ee04ba SPIGOT-4496: Undeprecate MapView.getId and make int
ec937d0e SPIGOT-4472: Add Consumer scheduler methods

Spigot Changes:
a1f2566f Use monotonic time for watchdog
bc4adcbf SPIGOT-4498: Crash on startup
bb387e6c Rebuild patches
2018-11-24 05:28:04 +00:00

3912 lines
142 KiB
Diff

From 3bdfef7c20a6506b0a133f7de68973203f1612ba 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 000000000..98079dc0c
--- /dev/null
+++ b/src/main/java/co/aikar/timings/FullServerTickHandler.java
@@ -0,0 +1,82 @@
+package co.aikar.timings;
+
+import static co.aikar.timings.TimingsManager.*;
+
+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);
+ }
+
+ @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()) {
+ 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;
+ CURRENT = TIMINGS_TICK;
+ TIMINGS_TICK.addDiff(diff);
+ // 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 000000000..8c43e2061
--- /dev/null
+++ b/src/main/java/co/aikar/timings/NullTimingHandler.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+public final class NullTimingHandler implements Timing {
+ @Override
+ public Timing startTiming() {
+ return this;
+ }
+
+ @Override
+ public void stopTiming() {
+
+ }
+
+ @Override
+ public Timing startTimingIfSync() {
+ return this;
+ }
+
+ @Override
+ public void stopTimingIfSync() {
+
+ }
+
+ @Override
+ public void abort() {
+
+ }
+
+ @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 000000000..feddcdbd4
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimedEventExecutor.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+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(EventExecutor executor, Plugin plugin, Method method, 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(Listener listener, Event event) throws EventException {
+ if (event.isAsynchronous() || !Timings.timingsEnabled || !Bukkit.isPrimaryThread()) {
+ executor.execute(listener, event);
+ return;
+ }
+ timings.startTiming();
+ executor.execute(listener, event);
+ timings.stopTiming();
+ }
+}
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 000000000..b2260104c
--- /dev/null
+++ b/src/main/java/co/aikar/timings/Timing.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+/**
+ * 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
+ */
+ 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
+ */
+ 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();
+
+ /**
+ * Stops timing and disregards current timing data.
+ */
+ void abort();
+
+ /**
+ * Used internally to get the actual backing Handler in the case of delegated Handlers
+ *
+ * @return TimingHandler
+ */
+ 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 000000000..f222d6b7d
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingData.java
@@ -0,0 +1,120 @@
+/*
+ * 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 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);
+ }
+
+ 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 000000000..521c985e6
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingHandler.java
@@ -0,0 +1,200 @@
+/*
+ * 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 org.bukkit.Bukkit;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+
+class TimingHandler implements Timing {
+
+ private static AtomicInteger idPool = new AtomicInteger(1);
+ final int id = idPool.getAndIncrement();
+
+ final String name;
+ private final boolean verbose;
+
+ private final Int2ObjectOpenHashMap<TimingData> children = new LoadingIntMap<>(TimingData::new);
+
+ final TimingData record;
+ private final TimingHandler groupHandler;
+
+ private long start = 0;
+ private int timingDepth = 0;
+ private boolean added;
+ private boolean timed;
+ private boolean enabled;
+ private TimingHandler parent;
+
+ TimingHandler(TimingIdentifier id) {
+ if (id.name.startsWith("##")) {
+ verbose = true;
+ this.name = id.name.substring(3);
+ } else {
+ this.name = id.name;
+ verbose = false;
+ }
+
+ 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);
+ }
+ }
+
+ @Override
+ public Timing startTimingIfSync() {
+ startTiming();
+ return this;
+ }
+
+ @Override
+ public void stopTimingIfSync() {
+ stopTiming();
+ }
+
+ public Timing startTiming() {
+ if (enabled && Bukkit.isPrimaryThread() && ++timingDepth == 1) {
+ start = System.nanoTime();
+ parent = TimingsManager.CURRENT;
+ TimingsManager.CURRENT = this;
+ }
+ return this;
+ }
+
+ public void stopTiming() {
+ if (enabled && timingDepth > 0 && Bukkit.isPrimaryThread() && --timingDepth == 0 && start != 0) {
+ addDiff(System.nanoTime() - start);
+ start = 0;
+ }
+ }
+
+ @Override
+ public void abort() {
+ if (enabled && timingDepth > 0) {
+ start = 0;
+ }
+ }
+
+ void addDiff(long diff) {
+ if (TimingsManager.CURRENT == this) {
+ TimingsManager.CURRENT = 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);
+ groupHandler.children.get(id).add(diff);
+ }
+ }
+
+ /**
+ * Reset this timer, setting all values to zero.
+ *
+ * @param full
+ */
+ void reset(boolean full) {
+ record.reset();
+ if (full) {
+ timed = false;
+ }
+ start = 0;
+ timingDepth = 0;
+ added = false;
+ children.clear();
+ checkEnabled();
+ }
+
+ @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;
+ }
+
+ 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 000000000..28d0954a3
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingHistory.java
@@ -0,0 +1,345 @@
+/*
+ * 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 co.aikar.util.JSONUtil;
+import com.google.common.base.Function;
+import com.google.common.collect.Maps;
+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 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>() {
+ @Override
+ public Integer apply(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>() {
+ @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>() {
+ @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>() {
+ @Override
+ public JSONPair apply(Map.Entry<EntityType, Counter> entry) {
+ entityTypeSet.add(entry.getKey());
+ return pair(
+ String.valueOf(entry.getKey().getTypeId()),
+ entry.getValue().count()
+ );
+ }
+ }
+ ),
+ toObjectMapper(input.tileEntityCounts.entrySet(),
+ new Function<Map.Entry<Material, Counter>, JSONPair>() {
+ @Override
+ public JSONPair apply(Map.Entry<Material, Counter> entry) {
+ tileEntityTypeSet.add(entry.getKey());
+ return pair(
+ String.valueOf(entry.getKey().getId()),
+ entry.getValue().count()
+ );
+ }
+ }
+ )
+ );
+ }
+ })
+ );
+ }
+ });
+ }
+ static class RegionData {
+ final RegionId regionId;
+ @SuppressWarnings("Guava")
+ static Function<RegionId, RegionData> LOADER = new Function<RegionId, RegionData>() {
+ @Override
+ public RegionData apply(RegionId id) {
+ return new RegionData(id);
+ }
+ };
+ RegionData(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;
+ }
+
+ 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>() {
+ @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>() {
+ @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();
+
+ 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 000000000..0e114eb32
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingHistoryEntry.java
@@ -0,0 +1,55 @@
+/*
+ * 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 static co.aikar.util.JSONUtil.toArrayMapper;
+
+class TimingHistoryEntry {
+ final TimingData data;
+ private final TimingData[] children;
+
+ TimingHistoryEntry(TimingHandler handler) {
+ this.data = handler.record.clone();
+ children = handler.cloneChildren();
+ }
+
+ List<Object> export() {
+ List<Object> result = data.export();
+ if (children.length > 0) {
+ result.add(
+ toArrayMapper(children, new Function<TimingData, Object>() {
+ @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 000000000..a7f1f44d7
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingIdentifier.java
@@ -0,0 +1,106 @@
+/*
+ * 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 co.aikar.util.MRUMapCache;
+
+import java.util.ArrayDeque;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * <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(String group, String name, 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();
+ }
+
+ static TimingGroup getGroup(String groupName) {
+ if (groupName == null) {
+ 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;
+ }
+
+ static class TimingGroup {
+
+ private static AtomicInteger idPool = new AtomicInteger(1);
+ final int id = idPool.getAndIncrement();
+
+ final String name;
+ ArrayDeque<TimingHandler> handlers = new ArrayDeque<TimingHandler>(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 000000000..f907649ba
--- /dev/null
+++ b/src/main/java/co/aikar/timings/Timings.java
@@ -0,0 +1,284 @@
+/*
+ * 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;
+
+@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
+ */
+ public static Timing of(Plugin plugin, 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
+ */
+ public static Timing of(Plugin plugin, String name, 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
+ */
+ public static Timing ofStart(Plugin plugin, 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
+ */
+ public static Timing ofStart(Plugin plugin, String name, 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(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(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
+ =================
+ */
+
+ static TimingHandler ofSafe(String name) {
+ return ofSafe(null, name, null);
+ }
+
+ static Timing ofSafe(Plugin plugin, 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);
+ }
+
+ static TimingHandler ofSafe(String name, Timing groupHandler) {
+ return ofSafe(null, name, groupHandler);
+ }
+
+ static TimingHandler ofSafe(String groupName, String name, 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 000000000..56b10e898
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingsCommand.java
@@ -0,0 +1,119 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package co.aikar.timings;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.commons.lang.Validate;
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.defaults.BukkitCommand;
+import org.bukkit.util.StringUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+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(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(CommandSender sender, String currentAlias, 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;
+ }
+
+ @Override
+ public List<String> tabComplete(CommandSender sender, String alias, 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 000000000..df7f42595
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingsExport.java
@@ -0,0 +1,342 @@
+/*
+ * 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.getServerName()),
+ 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();
+ for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) {
+ for (TimingHandler id : group.handlers) {
+ if (!id.isTimed() && !id.isSpecial()) {
+ continue;
+ }
+ handlers.put(id.id, toArray(
+ group.id,
+ id.name
+ ));
+ }
+ }
+
+ parent.put("idmap", createObject(
+ pair("groups", toObjectMapper(
+ TimingIdentifier.GROUP_MAP.values(), group -> pair(group.id, group.name))),
+ pair("handlers", handlers),
+ pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), input -> pair(input.getValue(), input.getKey()))),
+ pair("tileentity",
+ toObjectMapper(tileEntityTypeSet, input -> pair(input.getId(), input.name()))),
+ pair("entity",
+ toObjectMapper(entityTypeSet, input -> pair(input.getTypeId(), 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 val.toString();
+ }
+ } 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.getServerName() + "/" + 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 000000000..f63e7033c
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingsManager.java
@@ -0,0 +1,187 @@
+/*
+ * 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.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 co.aikar.util.LoadingMap;
+
+import javax.annotation.Nullable;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+
+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 TimingHandler CURRENT;
+ 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();
+ }
+
+ static TimingHandler getHandler(String group, String name, 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
+ */
+ public static Timing getCommandTiming(String pluginName, 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
+ */
+ 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 000000000..e7c389c05
--- /dev/null
+++ b/src/main/java/co/aikar/timings/TimingsReportListener.java
@@ -0,0 +1,72 @@
+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;
+
+@SuppressWarnings("WeakerAccess")
+public class TimingsReportListener implements MessageCommandSender {
+ private final List<CommandSender> senders;
+ private final Runnable onDone;
+ private String timingsURL;
+
+ public TimingsReportListener(CommandSender senders) {
+ this(senders, null);
+ }
+ public TimingsReportListener(CommandSender sender, Runnable onDone) {
+ this(Lists.newArrayList(sender), onDone);
+ }
+ public TimingsReportListener(List<CommandSender> senders) {
+ this(senders, null);
+ }
+ public TimingsReportListener(List<CommandSender> senders, Runnable onDone) {
+ Validate.notNull(senders);
+ Validate.notEmpty(senders);
+
+ this.senders = Lists.newArrayList(senders);
+ this.onDone = onDone;
+ }
+
+ public String getTimingsURL() {
+ return timingsURL;
+ }
+
+ public void done() {
+ done(null);
+ }
+
+ public void done(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(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 000000000..5edaba128
--- /dev/null
+++ b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+class UnsafeTimingHandler extends TimingHandler {
+
+ UnsafeTimingHandler(TimingIdentifier id) {
+ super(id);
+ }
+
+ private static void checkThread() {
+ if (!Bukkit.isPrimaryThread()) {
+ throw new IllegalStateException("Calling Timings from Async Operation");
+ }
+ }
+
+ @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 000000000..23ac07f2c
--- /dev/null
+++ b/src/main/java/co/aikar/util/Counter.java
@@ -0,0 +1,35 @@
+package co.aikar.util;
+
+import com.google.common.collect.ForwardingMap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Counter <T> extends ForwardingMap<T, Long> {
+ private final Map<T, Long> counts = new HashMap<>();
+
+ public long decrement(T key) {
+ return increment(key, -1);
+ }
+ public long increment(T key) {
+ return increment(key, 1);
+ }
+ public long decrement(T key, long amount) {
+ return decrement(key, -amount);
+ }
+ public long increment(T key, long amount) {
+ Long count = this.getCount(key);
+ count += amount;
+ this.counts.put(key, count);
+ return count;
+ }
+
+ public long getCount(T key) {
+ return this.counts.getOrDefault(key, 0L);
+ }
+
+ @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 000000000..962749750
--- /dev/null
+++ b/src/main/java/co/aikar/util/JSONUtil.java
@@ -0,0 +1,129 @@
+package co.aikar.util;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+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
+ */
+ public static JSONPair pair(String key, Object obj) {
+ return new JSONPair(key, obj);
+ }
+
+ public static JSONPair pair(long key, 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
+ */
+ public static Map createObject(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
+ */
+ public static Map appendObjectData(Map parent, 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
+ */
+ public static List toArray(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
+ */
+ public static <E> List toArrayMapper(E[] collection, Function<E, Object> mapper) {
+ return toArrayMapper(Lists.newArrayList(collection), mapper);
+ }
+
+ public static <E> List toArrayMapper(Iterable<E> collection, 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
+ */
+ public static <E> Map toObjectMapper(E[] collection, Function<E, JSONPair> mapper) {
+ return toObjectMapper(Lists.newArrayList(collection), mapper);
+ }
+
+ public static <E> Map toObjectMapper(Iterable<E> collection, 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(String key, 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 000000000..24eae4bea
--- /dev/null
+++ b/src/main/java/co/aikar/util/LoadingIntMap.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+/**
+ * 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(Function<Integer, V> loader) {
+ super();
+ this.loader = loader;
+ }
+
+ public LoadingIntMap(int expectedSize, Function<Integer, V> loader) {
+ super(expectedSize);
+ this.loader = loader;
+ }
+
+ public LoadingIntMap(int expectedSize, float loadFactor, Function<Integer, V> loader) {
+ super(expectedSize, loadFactor);
+ this.loader = loader;
+ }
+
+
+ @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> {
+ @Override
+ public T apply(Object input) {
+ return apply();
+ }
+
+ 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 000000000..9a4f9dca8
--- /dev/null
+++ b/src/main/java/co/aikar/util/LoadingMap.java
@@ -0,0 +1,339 @@
+/*
+ * 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.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;
+
+/**
+ * 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(Map<K, V> backingMap, 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
+ */
+ public static <K, V> Map<K, V> of(Map<K, V> backingMap, 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()
+ */
+ public static <K, V> Map<K, V> newAutoMap(Map<K, V> backingMap, final Class<? extends K> keyClass,
+ 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()
+ */
+ public static <K, V> Map<K, V> newAutoMap(Map<K, V> backingMap,
+ 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()
+ */
+ public static <K, V> Map<K, V> newHashAutoMap(final Class<? extends K> keyClass, 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()
+ */
+ public static <K, V> Map<K, V> newHashAutoMap(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()
+ */
+ public static <K, V> Map<K, V> newHashAutoMap(final Class<? extends K> keyClass, 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()
+ */
+ public static <K, V> Map<K, V> newHashAutoMap(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
+ */
+ public static <K, V> Map<K, V> newHashMap(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
+ */
+ public static <K, V> Map<K, V> newHashMap(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
+ */
+ public static <K, V> Map<K, V> newHashMap(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
+ */
+ public static <K, V> Map<K, V> newIdentityHashMap(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
+ */
+ public static <K, V> Map<K, V> newIdentityHashMap(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(Object key) {return backingMap.containsKey(key);}
+
+ @Override
+ public boolean containsValue(Object value) {return backingMap.containsValue(value);}
+
+ @Override
+ public V get(Object key) {
+ return backingMap.computeIfAbsent((K) key, loader);
+ }
+
+ public V put(K key, V value) {return backingMap.put(key, value);}
+
+ @Override
+ public V remove(Object key) {return backingMap.remove(key);}
+
+ public void putAll(Map<? extends K, ? extends V> m) {backingMap.putAll(m);}
+
+ @Override
+ public void clear() {backingMap.clear();}
+
+ @Override
+ public Set<K> keySet() {return backingMap.keySet();}
+
+ @Override
+ public Collection<V> values() {return backingMap.values();}
+
+ @Override
+ public boolean equals(Object o) {return backingMap.equals(o);}
+
+ @Override
+ public int hashCode() {return backingMap.hashCode();}
+
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ return backingMap.entrySet();
+ }
+
+ 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(Class<? extends K> keyClass, 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));
+ }
+ }
+
+ @Override
+ public V apply(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> {
+ @Override
+ public T apply(Object input) {
+ return apply();
+ }
+
+ 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 000000000..df592d855
--- /dev/null
+++ b/src/main/java/co/aikar/util/MRUMapCache.java
@@ -0,0 +1,102 @@
+/*
+ * 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;
+
+/**
+ * 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(final Map<K, V> backingMap) {
+ this.backingMap = backingMap;
+ }
+
+ public int size() {return backingMap.size();}
+
+ public boolean isEmpty() {return backingMap.isEmpty();}
+
+ public boolean containsKey(Object key) {
+ return key != null && key.equals(cacheKey) || backingMap.containsKey(key);
+ }
+
+ public boolean containsValue(Object value) {
+ return value != null && value == cacheValue || backingMap.containsValue(value);
+ }
+
+ public V get(Object key) {
+ if (cacheKey != null && cacheKey.equals(key)) {
+ return cacheValue;
+ }
+ cacheKey = key;
+ return cacheValue = backingMap.get(key);
+ }
+
+ public V put(K key, V value) {
+ cacheKey = key;
+ return cacheValue = backingMap.put(key, value);
+ }
+
+ public V remove(Object key) {
+ if (key != null && key.equals(cacheKey)) {
+ cacheKey = null;
+ }
+ return backingMap.remove(key);
+ }
+
+ public void putAll(Map<? extends K, ? extends V> m) {backingMap.putAll(m);}
+
+ public void clear() {
+ cacheKey = null;
+ cacheValue = null;
+ backingMap.clear();
+ }
+
+ public Set<K> keySet() {return backingMap.keySet();}
+
+ public Collection<V> values() {return backingMap.values();}
+
+ 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
+ */
+ public static <K, V> Map<K, V> of(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 2ce174401..7fdd1a084 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -568,7 +568,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 63392d5e7..0e7a81c88 100644
--- a/src/main/java/org/bukkit/Server.java
+++ b/src/main/java/org/bukkit/Server.java
@@ -1150,12 +1150,27 @@ public interface Server extends PluginMessageRecipient {
// Spigot start
public class Spigot
{
-
+ @Deprecated
public org.bukkit.configuration.file.YamlConfiguration getConfig()
{
throw new UnsupportedOperationException( "Not supported yet." );
}
+ public org.bukkit.configuration.file.YamlConfiguration getBukkitConfig()
+ {
+ throw new UnsupportedOperationException( "Not supported yet." );
+ }
+
+ public org.bukkit.configuration.file.YamlConfiguration getSpigotConfig()
+ {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ public org.bukkit.configuration.file.YamlConfiguration getPaperConfig()
+ {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
/**
* Sends the component to the player
*
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 000000000..fd452bce4
--- /dev/null
+++ b/src/main/java/org/bukkit/command/BufferedCommandSender.java
@@ -0,0 +1,18 @@
+package org.bukkit.command;
+
+public class BufferedCommandSender implements MessageCommandSender {
+ private final StringBuffer buffer = new StringBuffer();
+ @Override
+ public void sendMessage(String message) {
+ buffer.append(message);
+ buffer.append("\n");
+ }
+
+ 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 4c931f186..4a479627d 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; // Spigot
+ public String getTimingName() {return getName();} // Spigot
protected Command(String name) {
this(name, "", "/" + name, new ArrayList<String>());
@@ -47,7 +48,6 @@ public abstract class Command {
this.usageMessage = usageMessage;
this.aliases = aliases;
this.activeAliases = new ArrayList<String>(aliases);
- this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot
}
/**
@@ -236,7 +236,6 @@ public abstract class Command {
public boolean setLabel(String name) {
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 502578837..9c80f4640 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(String alias, String[] formatStrings) {
super(alias);
+ timings = co.aikar.timings.TimingsManager.getCommandTiming("minecraft", this); // Spigot
this.formatStrings = formatStrings;
}
@@ -113,6 +114,9 @@ public class FormattedCommandAlias extends Command {
return formatString;
}
+ @Override // Spigot
+ public String getTimingName() {return "Command Forwarder - " + super.getTimingName();} // Spigot
+
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 000000000..5527e7c86
--- /dev/null
+++ b/src/main/java/org/bukkit/command/MessageCommandSender.java
@@ -0,0 +1,105 @@
+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;
+
+/**
+ * For when all you care about is just messaging
+ */
+public interface MessageCommandSender extends CommandSender {
+
+ @Override
+ default void sendMessage(String[] messages) {
+ for (String message : messages) {
+ sendMessage(message);
+ }
+ }
+
+ @Override
+ default Server getServer() {
+ return Bukkit.getServer();
+ }
+
+ @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(String name) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean isPermissionSet(Permission perm) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean hasPermission(String name) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default boolean hasPermission(Permission perm) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default PermissionAttachment addAttachment(Plugin plugin) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default PermissionAttachment addAttachment(Plugin plugin, int ticks) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default void removeAttachment(PermissionAttachment attachment) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default void recalculatePermissions() {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ default Set<PermissionAttachmentInfo> getEffectivePermissions() {
+ throw new NotImplementedException();
+ }
+
+ @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 d9183db2f..43e52a77a 100644
--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java
+++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java
@@ -29,7 +29,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")); // Spigot
}
public void setFallbackCommands() {
@@ -58,6 +58,7 @@ public class SimpleCommandMap implements CommandMap {
* {@inheritDoc}
*/
public boolean register(String label, String fallbackPrefix, Command command) {
+ command.timings = co.aikar.timings.TimingsManager.getCommandTiming(fallbackPrefix, command); // Spigot
label = label.toLowerCase(java.util.Locale.ENGLISH).trim();
fallbackPrefix = fallbackPrefix.toLowerCase(java.util.Locale.ENGLISH).trim();
boolean registered = register(label, command, false, fallbackPrefix);
@@ -133,6 +134,12 @@ 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
// Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false)
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 bba914d7f..000000000
--- a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java
+++ /dev/null
@@ -1,253 +0,0 @@
-package org.bukkit.command.defaults;
-
-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 com.google.common.collect.ImmutableList;
-
-// Spigot start
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.util.logging.Level;
-
-import org.bukkit.command.RemoteConsoleCommandSender;
-import org.bukkit.plugin.SimplePluginManager;
-import org.spigotmc.CustomTimingsHandler;
-// 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(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(CommandSender sender, 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(CommandSender sender, String currentAlias, String[] args) {
- if (!testPermission(sender)) return true;
- if (args.length < 1) { // Spigot
- sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage);
- return false;
- }
- if (true) { executeSpigotTimings(sender, args); return true; } // Spigot
- 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;
- }
-
- @Override
- public List<String> tabComplete(CommandSender sender, String alias, 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(CommandSender sender, 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 5054276ff..0993f31b1 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
@@ -1575,6 +1575,11 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
public void sendMessage(net.md_5.bungee.api.ChatMessageType position, net.md_5.bungee.api.chat.BaseComponent... components) {
throw new UnsupportedOperationException("Not supported yet.");
}
+
+ public int getPing()
+ {
+ throw new UnsupportedOperationException( "Not supported yet." );
+ }
}
@Override
diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
index 8b130abb7..80c152ba9 100644
--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
@@ -293,7 +293,6 @@ public final class SimplePluginManager implements PluginManager {
}
}
- org.bukkit.command.defaults.TimingsCommand.timingStart = System.nanoTime(); // Spigot
return result.toArray(new Plugin[result.size()]);
}
@@ -330,7 +329,7 @@ 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); // Spigot
}
return result;
@@ -356,7 +355,7 @@ public final class SimplePluginManager implements PluginManager {
* @return Plugin if it exists, otherwise null
*/
public synchronized Plugin getPlugin(String name) {
- return lookupNames.get(name.replace(' ', '_'));
+ return lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Spigot
}
public synchronized Plugin[] getPlugins() {
@@ -554,7 +553,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); // Spigot
+ if (false) { // Spigot - RL handles useTimings check now
getEventListeners(event).register(new TimedRegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
} else {
getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
@@ -730,7 +730,7 @@ public final class SimplePluginManager implements PluginManager {
}
public boolean useTimings() {
- return useTimings;
+ return co.aikar.timings.Timings.isTimingsEnabled(); // Spigot
}
/**
@@ -739,6 +739,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); // Spigot
}
}
diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
index 113b899cc..81b428222 100644
--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
@@ -40,7 +40,6 @@ import org.bukkit.plugin.PluginLoader;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.plugin.TimedRegisteredListener;
import org.bukkit.plugin.UnknownDependencyException;
-import org.spigotmc.CustomTimingsHandler; // Spigot
import org.yaml.snakeyaml.error.YAMLException;
/**
@@ -51,7 +50,6 @@ public final class JavaPluginLoader implements PluginLoader {
private final Pattern[] fileFilters = new Pattern[] { Pattern.compile("\\.jar$"), };
private final Map<String, Class<?>> classes = new java.util.concurrent.ConcurrentHashMap<String, Class<?>>(); // Spigot
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
@@ -291,26 +289,20 @@ 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() { // Spigot
public void execute(Listener listener, Event event) throws EventException {
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); // Spigot
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 af8af8ef0..44093af29 100644
--- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
@@ -23,7 +23,8 @@ import org.bukkit.plugin.PluginDescriptionFile;
/**
* 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 java.util.concurrent.ConcurrentHashMap<String, Class<?>>(); // Spigot
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 5ca863b36..048047067 100644
--- a/src/main/java/org/bukkit/util/CachedServerIcon.java
+++ b/src/main/java/org/bukkit/util/CachedServerIcon.java
@@ -12,4 +12,6 @@ import org.bukkit.event.server.ServerListPingEvent;
* @see Server#loadServerIcon(java.io.File)
* @see ServerListPingEvent#setServerIcon(CachedServerIcon)
*/
-public interface CachedServerIcon {}
+public interface CachedServerIcon {
+ public String getData(); // Spigot
+}
diff --git a/src/main/java/org/spigotmc/CustomTimingsHandler.java b/src/main/java/org/spigotmc/CustomTimingsHandler.java
index 8d982974e..e9f76006e 100644
--- a/src/main/java/org/spigotmc/CustomTimingsHandler.java
+++ b/src/main/java/org/spigotmc/CustomTimingsHandler.java
@@ -1,165 +1,105 @@
+/*
+ * 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 org.bukkit.command.defaults.TimingsCommand;
-import org.bukkit.event.HandlerList;
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.AuthorNagException;
import org.bukkit.plugin.Plugin;
-import org.bukkit.plugin.RegisteredListener;
-import org.bukkit.plugin.TimedRegisteredListener;
-import java.io.PrintStream;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
+import co.aikar.timings.Timing;
+import co.aikar.timings.Timings;
+import co.aikar.timings.TimingsManager;
-import org.bukkit.Bukkit;
-import org.bukkit.World;
+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
-{
+@Deprecated
+public final class CustomTimingsHandler {
+ private final Timing handler;
+ private static Boolean sunReflectAvailable;
+ private static Method getCallerClass;
- 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;
+ public CustomTimingsHandler(String name) {
+ if (sunReflectAvailable == null) {
+ String javaVer = System.getProperty("java.version");
+ String[] elements = javaVer.split("\\.");
- public CustomTimingsHandler(String name)
- {
- this( name, null );
- }
-
- public CustomTimingsHandler(String name, 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
- */
- public static void printTimings(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(); }
+
}
--
2.19.1